SearchPredict.js (3285B)
1 import { 2 useEffect, useState, useRef, 3 } from '@wordpress/element' 4 import Fuse from 'fuse.js' 5 6 export default function SearchPredict({ value, setValue, list, label, touched }) { 7 const searchRef = useRef() 8 const wasFocused = useRef(false) 9 const [componentId] = useState('extendify-label' + Date.now() + Math.floor(Math.random() * 1000000)) 10 const [fuzzy, setFuzzy] = useState({}) 11 const [tempValue, setTempValue] = useState('') 12 const [visibleChoices, setVisibleChoices] = useState([]) 13 14 const updateSearch = (term) => { 15 wasFocused.current = true 16 setTempValue(term) 17 filter(term) 18 } 19 20 const filter = (term = '') => { 21 if (!term) { 22 setVisibleChoices(list) 23 } 24 const results = fuzzy.search(term) 25 if (!results || !results.length) { 26 return 27 } 28 setVisibleChoices(results.length ? results.map(t => t.item) : list) 29 } 30 31 useEffect(() => { 32 setTempValue(value) 33 }, [value]) 34 35 useEffect(() => { 36 setFuzzy(new Fuse(list, {})) 37 }, [list]) 38 39 useEffect(() => { 40 if (!list.length) { 41 return 42 } 43 const handle = (event) => { 44 if (searchRef.current.isSameNode(event.target)) { 45 return 46 } 47 if (event.target.classList.contains(`extendify-predict-${componentId}`)) { 48 return setVisibleChoices([]) 49 } 50 setVisibleChoices([]) 51 if (!list.includes(searchRef.current.value)) { 52 setValue('') 53 } 54 55 // Consider the component touched when clicked away 56 wasFocused.current && touched() 57 } 58 document.addEventListener('click', handle) 59 return () => document.removeEventListener('click', handle) 60 }, [componentId, touched, setValue, list]) 61 62 return <div className="relative max-w-md"> 63 <input 64 ref={searchRef} 65 id={componentId} 66 value={tempValue || ''} 67 onChange={(event) => updateSearch(event.target.value)} 68 onFocus={() => updateSearch('')} 69 type="text" 70 disabled={!Object.keys(fuzzy).length} 71 className="extendify-special-input button-focus text-sm h-8 min-h-0 border border-gray-900 special-input placeholder-transparent rounded-none w-full px-2 button-focus-big-green" 72 placeholder={label} /> 73 <label htmlFor={componentId} className="-top-3 bg-white absolute left-1 px-1 transition-all delay-300"> 74 {label} 75 </label> 76 {/* TODO: this could use some accessability updates like keyboard nav, etc */} 77 {visibleChoices && <div className="absolute top-100 flex flex-col w-full shadow-md bg-white overflow-x-hidden left-px divide-y max-h-64 overflow-scroll"> 78 {visibleChoices.map((cat => 79 <button 80 key={cat} 81 type="button" 82 className={`outline-none focus:bg-gray-100 bg-white text-left p-4 text-sm border-gray-300 hover:bg-gray-100 extendify-predict-${componentId}`} 83 onClick={() => { setValue(cat); setTempValue(cat)}}> 84 {cat} 85 </button> 86 ))} 87 </div>} 88 </div> 89 }