import * as React from 'react'; import PropTypes from 'prop-types'; import Autocomplete from '@mui/material/Autocomplete'; import CircularProgress from '@mui/material/CircularProgress'; import TextField from '@mui/material/TextField'; import { QueryClient, QueryClientProvider, useInfiniteQuery, } from '@tanstack/react-query'; import { useEventCallback, useForkRef } from '@mui/material/utils'; import useTimeout from '@mui/utils/useTimeout'; import { useVirtualizer } from '@tanstack/react-virtual'; import { fetchMovies, getMovieLabel, normalizeMovieQuery } from './server'; const ITEM_HEIGHT_PX = 36; const MAX_LISTBOX_HEIGHT_PX = 8 * ITEM_HEIGHT_PX; const OVERSCAN = 5; const PREFETCH_WITHIN_ITEMS = 5; const INPUT_DEBOUNCE_MS = 200; // Autocomplete invokes `renderOption(props, option)` for every option that // would be rendered. Returning this tuple lets the virtual listbox own layout // and mount only the rows that are visible. /** Props added to the Autocomplete listbox slot for infinite loading and virtualization. */ /** * Virtualized Autocomplete listbox. * It mounts only visible options and triggers pagination as the rendered range approaches the end. */ const VirtualListbox = React.forwardRef( function VirtualListbox(props, forwardedRef) { const { children, onReachEnd, resetScrollKey, virtualizerRef, style, ...listboxProps } = props; const items = children; // One DOM node must serve both Autocomplete's listbox ref and the virtualizer's // scroll observer, so merge the forwarded ref with the local ref. const scrollContainerRef = React.useRef(null); const setScrollContainerRef = useForkRef(scrollContainerRef, forwardedRef); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => scrollContainerRef.current, estimateSize: () => ITEM_HEIGHT_PX, overscan: OVERSCAN, // Avoids forcing synchronous updates while Autocomplete is rendering. useFlushSync: false, }); React.useEffect(() => { virtualizerRef.current = virtualizer; return () => { if (virtualizerRef.current === virtualizer) { virtualizerRef.current = null; } }; }, [virtualizer, virtualizerRef]); React.useEffect(() => { scrollContainerRef.current?.scrollTo({ top: 0 }); virtualizer.scrollToOffset(0); }, [resetScrollKey, virtualizer]); const virtualItems = virtualizer.getVirtualItems(); const lastRenderedIndex = virtualItems[virtualItems.length - 1]?.index ?? -1; // Trigger pagination from the virtualizer's rendered range, not raw scroll // offsets, so overscan and keyboard scrolling behave consistently. React.useEffect(() => { if ( items.length > 0 && lastRenderedIndex >= items.length - PREFETCH_WITHIN_ITEMS ) { onReachEnd(); } }, [lastRenderedIndex, items.length, onReachEnd]); return (