import { useEffect, useRef, useState } from 'react'; import { Menu, MenuContent, MenuItem, MenuList, Popper, SearchInput } from '@patternfly/react-core'; import { words } from '../words'; export const SearchInputAutocomplete: React.FunctionComponent = () => { const [value, setValue] = useState(''); const [hint, setHint] = useState(''); const [autocompleteOptions, setAutocompleteOptions] = useState([]); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); const searchInputRef = useRef(null); const autocompleteRef = useRef(null); const onClear = () => { setValue(''); }; const onChange = (_event, newValue) => { if ( newValue !== '' && searchInputRef && searchInputRef.current && searchInputRef.current.contains(document.activeElement) ) { setIsAutocompleteOpen(true); // When the value of the search input changes, build a list of no more than 10 autocomplete options. // Options which start with the search input value are listed first, followed by options which contain // the search input value. let options = words .filter((option) => option.startsWith(newValue.toLowerCase())) .map((option) => ( {option} )); if (options.length > 10) { options = options.slice(0, 10); } else { options = [ ...options, ...words .filter((option) => !option.startsWith(newValue.toLowerCase()) && option.includes(newValue.toLowerCase())) .map((option) => ( {option} )) ].slice(0, 10); } // The hint is set whenever there is only one autocomplete option left. setHint(options.length === 1 ? options[0].props.itemId : ''); // The menu is hidden if there are no options setIsAutocompleteOpen(options.length > 0); setAutocompleteOptions(options); } else { setIsAutocompleteOpen(false); } setValue(newValue); }; // Whenever an autocomplete option is selected, set the search input value, close the menu, and put the browser // focus back on the search input const onSelect = (e, itemId) => { e.stopPropagation(); setValue(itemId); setIsAutocompleteOpen(false); searchInputRef.current.focus(); }; const handleMenuKeys = (event) => { // If there is a hint while the browser focus is on the search input, tab or right arrow will 'accept' the hint value // and set it as the search input value if (hint && (event.key === 'Tab' || event.key === 'ArrowRight') && searchInputRef.current === event.target) { setValue(hint); setHint(''); setIsAutocompleteOpen(false); if (event.key === 'ArrowRight') { event.preventDefault(); } // if the autocomplete is open and the browser focus is on the search input, } else if (isAutocompleteOpen && searchInputRef.current && searchInputRef.current === event.target) { // the escape key closes the autocomplete menu and keeps the focus on the search input. if (event.key === 'Escape') { setIsAutocompleteOpen(false); searchInputRef.current.focus(); // the up and down arrow keys move browser focus into the autocomplete menu } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { const firstElement = autocompleteRef.current.querySelector('li > button:not(:disabled)'); firstElement && firstElement.focus(); event.preventDefault(); // by default, the up and down arrow keys scroll the window // the tab, enter, and space keys will close the menu, and the tab key will move browser // focus forward one element (by default) } else if (event.key === 'Tab' || event.key === 'Enter' || event.key === ' ') { setIsAutocompleteOpen(false); if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); } } // If the autocomplete is open and the browser focus is in the autocomplete menu // hitting tab will close the autocomplete and but browser focus back on the search input. } else if (isAutocompleteOpen && autocompleteRef.current.contains(event.target) && event.key === 'Tab') { event.preventDefault(); setIsAutocompleteOpen(false); searchInputRef.current.focus(); } }; // The autocomplete menu should close if the user clicks outside the menu. const handleClickOutside = (event) => { if ( isAutocompleteOpen && autocompleteRef && autocompleteRef.current && !autocompleteRef.current.contains(event.target) ) { setIsAutocompleteOpen(false); } }; useEffect(() => { window.addEventListener('keydown', handleMenuKeys); window.addEventListener('click', handleClickOutside); return () => { window.removeEventListener('keydown', handleMenuKeys); window.removeEventListener('click', handleClickOutside); }; }, [isAutocompleteOpen, hint, searchInputRef.current]); const searchInput = ( ); const autocomplete = ( {autocompleteOptions} ); return ( document.querySelector('#autocomplete-search')} /> ); };