import * as React from "react"; import { useVirtual } from "react-virtual"; import { useKBar } from "."; import { Action } from "./types"; import { usePointerMovedSinceMount } from "./utils"; const START_INDEX = 0; interface RenderParams { item: T; active: boolean; } interface KBarResultsProps { items: any[]; onRender: (params: RenderParams) => React.ReactElement; maxHeight?: number; } const KBarResults: React.FC = (props) => { const activeRef = React.useRef(null); const parentRef = React.useRef(null); // store a ref to all items so we do not have to pass // them as a dependency when setting up event listeners. const itemsRef = React.useRef(props.items); itemsRef.current = props.items; const rowVirtualizer = useVirtual({ size: itemsRef.current.length, parentRef, }); const [activeIndex, setActiveIndex] = React.useState(START_INDEX); const { query, search, currentRootActionId } = useKBar((state) => ({ search: state.searchQuery, currentRootActionId: state.currentRootActionId, })); React.useEffect(() => { const handler = (event) => { if (event.key === "ArrowUp" || (event.ctrlKey && event.key === "p")) { event.preventDefault(); setActiveIndex((index) => { let nextIndex = index > START_INDEX ? index - 1 : index; // avoid setting active index on a group if (typeof itemsRef.current[nextIndex] === "string") { if (nextIndex === 0) return index; nextIndex -= 1; } return nextIndex; }); } else if ( event.key === "ArrowDown" || (event.ctrlKey && event.key === "n") ) { event.preventDefault(); setActiveIndex((index) => { let nextIndex = index < itemsRef.current.length - 1 ? index + 1 : index; // avoid setting active index on a group if (typeof itemsRef.current[nextIndex] === "string") { if (nextIndex === itemsRef.current.length - 1) return index; nextIndex += 1; } return nextIndex; }); } else if (event.key === "Enter") { event.preventDefault(); // storing the active dom element in a ref prevents us from // having to calculate the current action to perform based // on the `activeIndex`, which we would have needed to add // as part of the dependencies array. activeRef.current?.click(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, []); // destructuring here to prevent linter warning to pass // entire rowVirtualizer in the dependencies array. const { scrollToIndex } = rowVirtualizer; React.useEffect(() => { scrollToIndex(activeIndex); }, [activeIndex, scrollToIndex]); React.useEffect(() => { // TODO(tim): fix scenario where async actions load in // and active index is reset to the first item. i.e. when // users register actions and bust the `useRegisterActions` // cache, we won't want to reset their active index as they // are navigating the list. setActiveIndex( // avoid setting active index on a group typeof props.items[START_INDEX] === "string" ? START_INDEX + 1 : START_INDEX ); }, [search, currentRootActionId, props.items]); const execute = React.useCallback( (item: RenderParams["item"]) => { if (typeof item === "string") return; if (item.perform) { item.perform(); query.toggle(); return; } query.setSearch(""); query.setCurrentRootAction(item.id); }, [query] ); const pointerMoved = usePointerMovedSinceMount(); return (
{rowVirtualizer.virtualItems.map((virtualRow) => { const item = itemsRef.current[virtualRow.index]; const handlers = { onPointerMove: () => pointerMoved && activeIndex !== virtualRow.index && setActiveIndex(virtualRow.index), onPointerDown: () => setActiveIndex(virtualRow.index), onClick: () => execute(item), }; const active = virtualRow.index === activeIndex; return (
{React.cloneElement( props.onRender({ item, active, }), { ref: virtualRow.measureRef, } )}
); })}
); }; export default KBarResults;