Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
id: Virtual select list
section: extensions
source: react
sourceLink: https://github.com/patternfly/react-virtualized-extension
propComponents: ['VirtualSelectList']
---

Note: React Virtualized Extension lives in its own package at [`@patternfly/react-virtualized-extension`](https://www.npmjs.com/package/@patternfly/react-virtualized-extension)!

import { VirtualSelectList } from '@patternfly/react-virtualized-extension';
import { Select, SelectOption, MenuToggle, TextInputGroup, TextInputGroupMain, TextInputGroupUtilities, Button, Badge } from '@patternfly/react-core';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { useState, useMemo, useRef } from 'react';

## About

VirtualSelectList is a virtualized replacement for PatternFly's SelectList component. It renders only the visible items in a scrollable dropdown, enabling Select menus with thousands of options without DOM performance degradation.

Use VirtualSelectList as a drop-in replacement for `<SelectList>` inside a composable `<Select>`. The `itemRenderer` callback receives positioning styles that must be applied to each rendered `<SelectOption>`.

## Examples

### Basic

```js file="./VirtualSelectListBasic.tsx"

```

### Typeahead with filtering

```js file="./VirtualSelectListTypeahead.tsx"

```

### Multi-select

```js file="./VirtualSelectListMulti.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useState, FunctionComponent } from 'react';
import { Select, SelectOption, MenuToggle, MenuToggleElement } from '@patternfly/react-core';
import { VirtualSelectList } from '@patternfly/react-virtualized-extension';

export const VirtualSelectListBasicExample: FunctionComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState<string>();

const items: string[] = [];
for (let i = 0; i < 10000; i++) {
items.push(`Option ${i}`);
}

const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
setSelected(value as string);
setIsOpen(false);
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={() => setIsOpen((prev) => !prev)}
isExpanded={isOpen}
style={{ width: '200px' } as React.CSSProperties}
>
{selected || 'Select an option'}
</MenuToggle>
);

return (
<Select
isOpen={isOpen}
selected={selected}
onSelect={onSelect}
onOpenChange={setIsOpen}
toggle={toggle}
isScrollable
>
<VirtualSelectList
rowCount={items.length}
rowHeight={36}
maxHeight={300}
aria-label="Basic virtualized select"
rowRenderer={({ index, style, key }) => (
<SelectOption key={key} style={style} value={items[index]} isSelected={selected === items[index]}>
{items[index]}
</SelectOption>
)}
/>
</Select>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useState, FunctionComponent } from 'react';
import {
Select,
SelectOption,
MenuToggle,
MenuToggleElement,
Badge
} from '@patternfly/react-core';
import { VirtualSelectList } from '@patternfly/react-virtualized-extension';

export const VirtualSelectListMultiExample: FunctionComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedItems, setSelectedItems] = useState<string[]>([]);

const items: string[] = [];
for (let i = 0; i < 1000; i++) {
items.push(`Option ${i}`);
}

const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
const val = value as string;
setSelectedItems((prev) =>
prev.includes(val) ? prev.filter((item) => item !== val) : [...prev, val]
);
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={() => setIsOpen((prev) => !prev)}
isExpanded={isOpen}
style={{ width: '300px' } as React.CSSProperties}
>
{selectedItems.length > 0 ? (
<>
{selectedItems.length} selected
<Badge isRead className="pf-v6-u-ml-sm">
{selectedItems.length}
</Badge>
</>
) : (
'Select options'
)}
</MenuToggle>
);

return (
<Select
isOpen={isOpen}
selected={selectedItems}
onSelect={onSelect}
onOpenChange={setIsOpen}
toggle={toggle}
isScrollable
>
<VirtualSelectList
rowCount={items.length}
rowHeight={36}
maxHeight={300}
isAriaMultiselectable
aria-label="Multi-select virtualized list"
rowRenderer={({ index, style, key }) => (
<SelectOption
key={key}
style={style}
value={items[index]}
hasCheckbox
isSelected={selectedItems.includes(items[index])}
>
{items[index]}
</SelectOption>
)}
/>
</Select>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useState, useMemo, useRef, FunctionComponent } from 'react';
import {
Select,
SelectOption,
MenuToggle,
MenuToggleElement,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
Button
} from '@patternfly/react-core';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { VirtualSelectList, VirtualSelectListRef } from '@patternfly/react-virtualized-extension';

const allItems: string[] = [];
for (let i = 0; i < 10000; i++) {
allItems.push(`Option ${i}`);
}

export const VirtualSelectListTypeaheadExample: FunctionComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState<string>();
const [filterValue, setFilterValue] = useState('');
const listRef = useRef<VirtualSelectListRef>(null);

const filteredItems = useMemo(
() =>
filterValue
? allItems.filter((item) => item.toLowerCase().includes(filterValue.toLowerCase()))
: allItems,
[filterValue]
);

const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
setSelected(value as string);
setFilterValue('');
setIsOpen(false);
};

const onInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
setFilterValue(value);
if (!isOpen) {
setIsOpen(true);
}
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
variant="typeahead"
onClick={() => setIsOpen((prev) => !prev)}
isExpanded={isOpen}
isFullWidth
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={filterValue || selected || ''}
onClick={() => setIsOpen(true)}
onChange={onInputChange}
autoComplete="off"
placeholder="Search from 10,000 options..."
/>
{(filterValue || selected) && (
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={() => {
setSelected(undefined);
setFilterValue('');
}}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
)}
</TextInputGroup>
</MenuToggle>
);

return (
<Select
isOpen={isOpen}
selected={selected}
onSelect={onSelect}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
setFilterValue('');
}
}}
toggle={toggle}
isScrollable
>
<VirtualSelectList
ref={listRef}
rowCount={filteredItems.length}
rowHeight={36}
maxHeight={300}
aria-label="Typeahead virtualized select"
noRowsRenderer={() => (
<li className="pf-v6-c-menu__list-item pf-m-disabled" role="option" aria-disabled="true">
No results found for &quot;{filterValue}&quot;
</li>
)}
rowRenderer={({ index, style, key }) => (
<SelectOption key={key} style={style} value={filteredItems[index]} isSelected={selected === filteredItems[index]}>
{filteredItems[index]}
</SelectOption>
)}
/>
</Select>
);
};
10 changes: 8 additions & 2 deletions packages/module/src/components/Virtualized/VirtualGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ export interface VirtualGridProps {

/** Inner Scroll Container element to render */
innerScrollContainerComponent?: string | ComponentType<any>;

/** Optional custom CSS class name for the inner scroll container.
* Defaults to 'pf-v6-c-table__tbody' for backward compatibility with table-based usage.
*/
innerScrollContainerClassName?: string;
}

interface InstanceProps {
Expand Down Expand Up @@ -920,7 +925,8 @@ export class VirtualGrid extends Component<VirtualGridProps, VirtualGridState> {
tabIndex,
width,
scrollContainerComponent,
innerScrollContainerComponent
innerScrollContainerComponent,
innerScrollContainerClassName
} = this.props;
const { instanceProps, needToResetStyleCache } = this.state;

Expand Down Expand Up @@ -1008,7 +1014,7 @@ export class VirtualGrid extends Component<VirtualGridProps, VirtualGridState> {
let innerScrollContainer = null;
if (childrenToDisplay.length > 0) {
const innerScrollContainerProps = {
className: 'ReactVirtualized__VirtualGrid__innerScrollContainer pf-v6-c-table__tbody',
className: css('ReactVirtualized__VirtualGrid__innerScrollContainer', innerScrollContainerClassName ?? 'pf-v6-c-table__tbody'),
key: 'ReactVirtualized__VirtualGrid__innerScrollContainer',
role: containerRole,
style: {
Expand Down
Loading