UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

95 lines (78 loc) 3.19 kB
import React__default from 'react'; import useSafeTimeout from '../hooks/useSafeTimeout.js'; import { getAccessibleName } from './shared.js'; function useTypeahead({ containerRef, onFocusChange }) { const searchValue = React__default.useRef(''); const timeoutRef = React__default.useRef(0); const onFocusChangeRef = React__default.useRef(onFocusChange); const { safeSetTimeout, safeClearTimeout } = useSafeTimeout(); // Update the ref when the callback changes React__default.useEffect(() => { onFocusChangeRef.current = onFocusChange; }, [onFocusChange]); // Focus the closest element that matches the search value const focusSearchValue = React__default.useCallback(searchValue => { // Don't change focus if the search value is empty if (!searchValue) return; if (!containerRef.current) return; const container = containerRef.current; // Get focusable elements const elements = Array.from(container.querySelectorAll('[role="treeitem"]')); // Get the index of active element const activeIndex = elements.findIndex(element => element === document.activeElement); // Wrap the array elements such that the active descendant is at the beginning let sortedElements = wrapArray(elements, activeIndex); // Remove the active descendant from the beginning of the array // when the user initiates a new search if (searchValue.length === 1) { sortedElements = sortedElements.slice(1); } // Find the first element that matches the search value const nextElement = sortedElements.find(element => { const name = getAccessibleName(element).toLowerCase(); return name.startsWith(searchValue.toLowerCase()); }); // If a match is found, focus it if (nextElement) { onFocusChangeRef.current(nextElement); } }, [containerRef]); // Update the search value when the user types React__default.useEffect(() => { if (!containerRef.current) return; const container = containerRef.current; function onKeyDown(event) { // Ignore key presses that don't produce a character value if (!event.key || event.key.length > 1) return; // Ignore key presses that occur with a modifier if (event.ctrlKey || event.altKey || event.metaKey) return; // Update the existing search value with the new key press searchValue.current += event.key; focusSearchValue(searchValue.current); // Reset the timeout safeClearTimeout(timeoutRef.current); timeoutRef.current = safeSetTimeout(() => searchValue.current = '', 300); // Prevent default behavior event.preventDefault(); event.stopPropagation(); } container.addEventListener('keydown', onKeyDown); return () => container.removeEventListener('keydown', onKeyDown); }, [containerRef, focusSearchValue, safeClearTimeout, safeSetTimeout]); } /** * Wraps an array around itself at a given start index * * @example * wrapArray(['a', 'b', 'c', 'd'], 2) // ['c', 'd', 'a', 'b'] */ function wrapArray(array, startIndex) { return array.map((_, index) => array[(startIndex + index) % array.length]); } export { useTypeahead };