import * as React from 'react'; import TextField from '@mui/material/TextField'; import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'; import useMediaQuery from '@mui/material/useMediaQuery'; import ListSubheader from '@mui/material/ListSubheader'; import Popper from '@mui/material/Popper'; import { useTheme, styled } from '@mui/material/styles'; import { List, RowComponentProps, useListRef, ListImperativeAPI, } from 'react-window'; import Typography from '@mui/material/Typography'; const LISTBOX_PADDING = 8; // px type ItemData = Array< | { key: number; group: string; children: React.ReactNode; } | [React.ReactElement, string, number] >; function RowComponent({ index, itemData, style, }: RowComponentProps & { itemData: ItemData; }) { const dataSet = itemData[index]; const inlineStyle = { ...style, top: ((style.top as number) ?? 0) + LISTBOX_PADDING, }; if ('group' in dataSet) { return ( {dataSet.group} ); } const { key, ...optionProps } = dataSet[0]; return ( {`#${dataSet[2] + 1} - ${dataSet[1]}`} ); } // Adapter for react-window v2 const ListboxComponent = React.forwardRef< HTMLDivElement, React.HTMLAttributes & { internalListRef: React.Ref; onItemsBuilt: (optionIndexMap: Map) => void; } >(function ListboxComponent(props, ref) { const { children, internalListRef, onItemsBuilt, ...other } = props; const itemData: ItemData = []; const optionIndexMap = React.useMemo(() => new Map(), []); (children as ItemData).forEach((item) => { itemData.push(item); if ('children' in item && Array.isArray(item.children)) { itemData.push(...item.children); } }); // Map option values to their indices in the flattened array itemData.forEach((item, index) => { if (Array.isArray(item) && item[1]) { optionIndexMap.set(item[1], index); } }); React.useEffect(() => { if (onItemsBuilt) { onItemsBuilt(optionIndexMap); } }, [onItemsBuilt, optionIndexMap]); const theme = useTheme(); const smUp = useMediaQuery(theme.breakpoints.up('sm'), { noSsr: true, }); const itemCount = itemData.length; const itemSize = smUp ? 36 : 48; const getChildSize = (child: ItemData[number]) => { if (child.hasOwnProperty('group')) { return 48; } return itemSize; }; const getHeight = () => { if (itemCount > 8) { return 8 * itemSize; } return itemData.map(getChildSize).reduce((a, b) => a + b, 0); }; // Separate className for List, other props for wrapper div (ARIA, handlers) const { className, style, ...otherProps } = other; return (
getChildSize(itemData[index])} rowComponent={RowComponent} rowProps={{ itemData }} style={{ height: getHeight() + 2 * LISTBOX_PADDING, width: '100%', }} overscanCount={5} tagName="ul" />
); }); function random(length: number) { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i += 1) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; } const StyledPopper = styled(Popper)({ [`& .${autocompleteClasses.listbox}`]: { boxSizing: 'border-box', '& ul': { padding: 0, margin: 0, }, }, }); const OPTIONS = Array.from(new Array(10000)) .map(() => random(10 + Math.ceil(Math.random() * 20))) .sort((a: string, b: string) => a.toUpperCase().localeCompare(b.toUpperCase())); export default function Virtualize() { // Use react-window v2's useListRef hook for imperative API access const internalListRef = useListRef(null); const optionIndexMapRef = React.useRef>(new Map()); const handleItemsBuilt = React.useCallback( (optionIndexMap: Map) => { optionIndexMapRef.current = optionIndexMap; }, [], ); // Handle keyboard navigation by scrolling to highlighted option const handleHighlightChange = ( event: React.SyntheticEvent, option: string | null, ) => { if (option && internalListRef.current) { const index = optionIndexMapRef.current.get(option); if (index !== undefined) { internalListRef.current.scrollToRow({ index, align: 'auto' }); } } }; return ( option[0].toUpperCase()} renderInput={(params) => } renderOption={(props, option, state) => [props, option, state.index] as React.ReactNode } renderGroup={(params) => params as any} onHighlightChange={handleHighlightChange} slots={{ popper: StyledPopper, }} slotProps={{ listbox: { component: ListboxComponent, internalListRef, onItemsBuilt: handleItemsBuilt, } as any, }} /> ); }