|
| 1 | +/** |
| 2 | + * Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/ |
| 3 | + */ |
| 4 | + |
| 5 | +import{useRef,useEffect,useState,useMemo}from"react"; |
| 6 | + |
| 7 | +import{useAppContext}from"@/contexts/AppContext"; |
| 8 | +import{useKeyboardNavigation}from"@/hooks/useKeyboardNavigation"; |
| 9 | +import{useLanguages}from"@/hooks/useLanguages"; |
| 10 | +import{LanguageType}from"@/types"; |
| 11 | +// import { configureUserSelection } from "@utils/configureUserSelection"; |
| 12 | +// import { |
| 13 | +// getLanguageDisplayLogo, |
| 14 | +// getLanguageDisplayName, |
| 15 | +// } from "@utils/languageUtils"; |
| 16 | +import{slugify}from"@/utils/slugify"; |
| 17 | + |
| 18 | +// import SubLanguageSelector from "./SubLanguageSelector"; |
| 19 | +import{useRouter}from"next/router"; |
| 20 | + |
| 21 | +constLanguageSelector=()=>{ |
| 22 | +constrouter=useRouter(); |
| 23 | + |
| 24 | +const{ selectedLanguage, selectedCategory, setSearchText}=useAppContext(); |
| 25 | +const{ fetchedLanguages, loading, error}=useLanguages(); |
| 26 | + |
| 27 | +constdropdownRef=useRef<HTMLDivElement>(null); |
| 28 | +const[isOpen,setIsOpen]=useState<boolean>(false); |
| 29 | +const[openedLanguages,setOpenedLanguages]=useState<LanguageType[]>([]); |
| 30 | + |
| 31 | +constkeyboardItems=useMemo(()=>{ |
| 32 | +returnfetchedLanguages.flatMap((lang)=> |
| 33 | +openedLanguages.map((ol)=>ol.name).includes(lang.languageName) |
| 34 | + ?[ |
| 35 | +{languageName:lang.languageName}, |
| 36 | + ...lang.subLanguages.map((sl)=>({ |
| 37 | +languageName:lang.name, |
| 38 | +subLanguageName:sl.name, |
| 39 | +})), |
| 40 | +] |
| 41 | + :[{languageName:lang.name}] |
| 42 | +); |
| 43 | +},[fetchedLanguages,openedLanguages]); |
| 44 | + |
| 45 | +constdisplayName=useMemo( |
| 46 | +()=>getLanguageDisplayName(language.name,subLanguage), |
| 47 | +[language.name,subLanguage] |
| 48 | +); |
| 49 | + |
| 50 | +constdisplayLogo=useMemo( |
| 51 | +()=>getLanguageDisplayLogo(language.name,subLanguage), |
| 52 | +[language.name,subLanguage] |
| 53 | +); |
| 54 | + |
| 55 | +consthandleToggleSubLanguage=(name:LanguageType["name"])=>{ |
| 56 | +constisAlreadyOpened=openedLanguages.some((lang)=>lang.name===name); |
| 57 | +constopenedLang=fetchedLanguages.find( |
| 58 | +(lang)=>lang.languageName===name |
| 59 | +); |
| 60 | +if(openedLang===undefined||openedLang.subLanguages.length===0){ |
| 61 | +return; |
| 62 | +} |
| 63 | + |
| 64 | +if(!isAlreadyOpened){ |
| 65 | +setOpenedLanguages((prev)=>[...prev,openedLang]); |
| 66 | +}else{ |
| 67 | +setOpenedLanguages((prev)=> |
| 68 | +prev.filter((lang)=>lang.name!==openedLang.name) |
| 69 | +); |
| 70 | +} |
| 71 | +}; |
| 72 | + |
| 73 | +/** |
| 74 | + * When setting a new language we need to ensure that a category |
| 75 | + * has been set given this new language. |
| 76 | + * Ensure that the search text is cleared. |
| 77 | + */ |
| 78 | +consthandleSelect=async(selected:LanguageType)=>{ |
| 79 | +const{ |
| 80 | +language:newLanguage, |
| 81 | +subLanguage:newSubLanguage, |
| 82 | +category:newCategory, |
| 83 | +}=awaitconfigureUserSelection({ |
| 84 | +languageName:selected.name, |
| 85 | +}); |
| 86 | + |
| 87 | +setSearchText(""); |
| 88 | +router.push(`/${slugify(newLanguage.name)}/${slugify(newCategory)}`); |
| 89 | +setIsOpen(false); |
| 90 | +setOpenedLanguages([]); |
| 91 | +}; |
| 92 | + |
| 93 | +constafterSelect=()=>{ |
| 94 | +setIsOpen(false); |
| 95 | +}; |
| 96 | + |
| 97 | +consthandleSubLanguageSelect=async( |
| 98 | +selectedLanguageName:LanguageType["name"], |
| 99 | +selectedSubLanguageName: |
| 100 | +|LanguageType["subLanguages"][number]["name"] |
| 101 | +|undefined |
| 102 | +)=>{ |
| 103 | +const{ |
| 104 | +language:newLanguage, |
| 105 | +subLanguage:newSubLanguage, |
| 106 | +category:newCategory, |
| 107 | +}=awaitconfigureUserSelection({ |
| 108 | +languageName:selectedLanguageName, |
| 109 | +subLanguageName:selectedSubLanguageName, |
| 110 | +}); |
| 111 | + |
| 112 | +setSearchText(""); |
| 113 | +router.push(`/${slugify(newLanguage.name)}/${slugify(newCategory)}`); |
| 114 | +afterSelect(); |
| 115 | +}; |
| 116 | + |
| 117 | +const{ focusedIndex, handleKeyDown, resetFocus, focusFirst}= |
| 118 | +useKeyboardNavigation({ |
| 119 | +items:keyboardItems, |
| 120 | + isOpen, |
| 121 | +toggleDropdown:(l)=>handleToggleSubLanguage(l), |
| 122 | +onSelect:(l,sl)=>handleSubLanguageSelect(l,sl), |
| 123 | +onClose:()=>setIsOpen(false), |
| 124 | +}); |
| 125 | + |
| 126 | +consthandleBlur=()=>{ |
| 127 | +setTimeout(()=>{ |
| 128 | +if( |
| 129 | +dropdownRef.current&& |
| 130 | +!dropdownRef.current.contains(document.activeElement) |
| 131 | +){ |
| 132 | +setIsOpen(false); |
| 133 | +} |
| 134 | +},0); |
| 135 | +}; |
| 136 | + |
| 137 | +consttoggleDropdown=()=>{ |
| 138 | +setIsOpen((prev)=>{ |
| 139 | +if(!prev)setTimeout(focusFirst,0); |
| 140 | +return!prev; |
| 141 | +}); |
| 142 | +}; |
| 143 | + |
| 144 | +useEffect(()=>{ |
| 145 | +if(!isOpen){ |
| 146 | +resetFocus(); |
| 147 | +} |
| 148 | +// eslint-disable-next-line react-hooks/exhaustive-deps |
| 149 | +},[isOpen]); |
| 150 | + |
| 151 | +useEffect(()=>{ |
| 152 | +if(isOpen&&focusedIndex>=0){ |
| 153 | +constelements=Array.from( |
| 154 | +document.querySelectorAll(".selector__item") |
| 155 | +)asHTMLElement[]; |
| 156 | +constfocusableElements=elements.filter( |
| 157 | +(el)=>el.getAttribute("tabIndex")!=="-1" |
| 158 | +); |
| 159 | +constelement=focusableElements[focusedIndex]; |
| 160 | +element?.focus(); |
| 161 | +} |
| 162 | +},[isOpen,focusedIndex]); |
| 163 | + |
| 164 | +if(loading){ |
| 165 | +return<p>Loading languages...</p>; |
| 166 | +} |
| 167 | + |
| 168 | +if(error){ |
| 169 | +return<p>Error fetching languages:{error}</p>; |
| 170 | +} |
| 171 | + |
| 172 | +return( |
| 173 | +<div |
| 174 | +className={`selector${isOpen ?"selector--open" :""}`} |
| 175 | +ref={dropdownRef} |
| 176 | +onBlur={handleBlur} |
| 177 | +> |
| 178 | +<button |
| 179 | +className="selector__button" |
| 180 | +aria-label="select button" |
| 181 | +aria-haspopup="listbox" |
| 182 | +aria-expanded={isOpen} |
| 183 | +onClick={toggleDropdown} |
| 184 | +> |
| 185 | +<divclassName="selector__value"> |
| 186 | +<imgsrc={displayLogo}alt=""/> |
| 187 | +<span>{displayName}</span> |
| 188 | +</div> |
| 189 | +<spanclassName="selector__arrow"/> |
| 190 | +</button> |
| 191 | +<ul |
| 192 | +className={`selector__dropdown${isOpen ?"" :" hidden"}`} |
| 193 | +role="listbox" |
| 194 | +onKeyDown={handleKeyDown} |
| 195 | +tabIndex={0} |
| 196 | +> |
| 197 | +{fetchedLanguages.map((lang,index)=> |
| 198 | +lang.subLanguages.length>0 ?( |
| 199 | +<SubLanguageSelector |
| 200 | +key={lang.name} |
| 201 | +opened={openedLanguages.includes(lang)} |
| 202 | +parentLanguage={lang} |
| 203 | +onDropdownToggle={handleToggleSubLanguage} |
| 204 | +handleParentSelect={handleSelect} |
| 205 | +afterSelect={afterSelect} |
| 206 | +/> |
| 207 | +) :( |
| 208 | +<li |
| 209 | +key={lang.name} |
| 210 | +role="option" |
| 211 | +tabIndex={0} |
| 212 | +onClick={()=>handleSelect(lang)} |
| 213 | +className={`selector__item${ |
| 214 | +language.name===lang.name ?"selected" :"" |
| 215 | +}${focusedIndex===index ?"focused" :""}`} |
| 216 | +aria-selected={language.name===lang.name} |
| 217 | +> |
| 218 | +<label> |
| 219 | +<imgsrc={lang.icon}alt=""/> |
| 220 | +<span>{lang.name}</span> |
| 221 | +</label> |
| 222 | +</li> |
| 223 | +) |
| 224 | +)} |
| 225 | +</ul> |
| 226 | +</div> |
| 227 | +); |
| 228 | +}; |
| 229 | + |
| 230 | +exportdefaultLanguageSelector; |