@@ -33,6 +33,7 @@ const SearchableSelect = React.forwardRef<
3333 const [ dropdownPosition , setDropdownPosition ] = useState < "left" | "right" > (
3434 "left"
3535 ) ;
36+ const [ highlightedIndex , setHighlightedIndex ] = useState ( - 1 ) ;
3637 const containerRef = useRef < HTMLDivElement > ( null ) ;
3738 const inputRef = useRef < HTMLInputElement > ( null ) ;
3839
@@ -43,6 +44,7 @@ const SearchableSelect = React.forwardRef<
4344 option . value . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
4445 ) ;
4546 setFilteredOptions ( filtered ) ;
47+ setHighlightedIndex ( - 1 ) ; // Reset highlighted index when options change
4648 } , [ searchTerm , options ] ) ;
4749
4850 useEffect ( ( ) => {
@@ -65,6 +67,37 @@ const SearchableSelect = React.forwardRef<
6567 onChange ( optionValue ) ;
6668 setIsOpen ( false ) ;
6769 setSearchTerm ( "" ) ;
70+ setHighlightedIndex ( - 1 ) ;
71+ } ;
72+
73+ const handleKeyDown = ( e : React . KeyboardEvent ) => {
74+ if ( ! isOpen ) return ;
75+
76+ switch ( e . key ) {
77+ case "ArrowDown" :
78+ e . preventDefault ( ) ;
79+ setHighlightedIndex ( ( prev ) =>
80+ prev < filteredOptions . length - 1 ? prev + 1 : 0
81+ ) ;
82+ break ;
83+ case "ArrowUp" :
84+ e . preventDefault ( ) ;
85+ setHighlightedIndex ( ( prev ) =>
86+ prev > 0 ? prev - 1 : filteredOptions . length - 1
87+ ) ;
88+ break ;
89+ case "Enter" :
90+ e . preventDefault ( ) ;
91+ if ( highlightedIndex >= 0 && filteredOptions [ highlightedIndex ] ) {
92+ handleSelect ( filteredOptions [ highlightedIndex ] . value ) ;
93+ }
94+ break ;
95+ case "Escape" :
96+ setIsOpen ( false ) ;
97+ setSearchTerm ( "" ) ;
98+ setHighlightedIndex ( - 1 ) ;
99+ break ;
100+ }
68101 } ;
69102
70103 const calculateDropdownPosition = ( ) => {
@@ -100,6 +133,16 @@ const SearchableSelect = React.forwardRef<
100133 < div
101134 ref = { ref }
102135 onClick = { handleToggle }
136+ onKeyDown = { ( e ) => {
137+ if ( e . key === "Enter" || e . key === " " ) {
138+ e . preventDefault ( ) ;
139+ handleToggle ( ) ;
140+ }
141+ } }
142+ tabIndex = { 0 }
143+ role = "combobox"
144+ aria-expanded = { isOpen }
145+ aria-haspopup = "listbox"
103146 className = { `
104147 ${ commonStyles . input . base }
105148 ${ commonStyles . input . size [ size ] }
@@ -130,45 +173,58 @@ const SearchableSelect = React.forwardRef<
130173
131174 { isOpen && (
132175 < div
133- className = { `absolute z-50 w-max min-w-full mt-1 bg-white border border-gray-200 rounded-md max-h-60 overflow-auto ${
176+ className = { `absolute z-50 w-max min-w-full mt-1 ${
177+ commonStyles . input . base
178+ } rounded-md max-h-60 overflow-auto ${
134179 dropdownPosition === "right" ? "right-0" : "left-0"
135180 } `}
136181 style = { {
137182 maxWidth : `min(calc(100vw - 2rem), 500px)` ,
138183 right : dropdownPosition === "right" ? "0" : undefined ,
139184 left : dropdownPosition === "left" ? "0" : undefined ,
185+ boxShadow : commonStyles . input . shadow ,
140186 } }
141187 >
142- < div className = "p-2 border-b border-gray-200" >
188+ < div
189+ className = { `p-2 border-b border-gray-200 rounded-t-md` }
190+ style = { { boxShadow : commonStyles . input . shadow } }
191+ >
143192 < input
144193 ref = { inputRef }
145194 type = "text"
146195 value = { searchTerm }
147196 onChange = { ( e ) => setSearchTerm ( e . target . value ) }
197+ onKeyDown = { handleKeyDown }
148198 placeholder = "Search..."
149199 className = { `${ commonStyles . input . base } ${ commonStyles . input . size . sm } w-full min-w-48` }
150200 style = { { boxShadow : commonStyles . input . shadow } }
151- onKeyDown = { ( e ) => {
152- if ( e . key === "Escape" ) {
153- setIsOpen ( false ) ;
154- setSearchTerm ( "" ) ;
155- }
156- } }
201+ role = "searchbox"
202+ aria-label = "Search options"
157203 />
158204 </ div >
159- < div className = "max-h-48 overflow-auto" >
205+ < div
206+ className = "max-h-48 overflow-auto"
207+ role = "listbox"
208+ aria-label = "Options"
209+ >
160210 { filteredOptions . length > 0 ? (
161- filteredOptions . map ( ( option ) => (
211+ filteredOptions . map ( ( option , index ) => (
162212 < div
163213 key = { option . value }
164214 onClick = { ( ) => handleSelect ( option . value ) }
165- className = "px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 text-gray-700 border-b border-gray-100 last:border-b-0"
215+ onMouseEnter = { ( ) => setHighlightedIndex ( index ) }
216+ className = { `px-3 py-2 text-xs font-medium cursor-pointer hover:bg-gray-100 text-gray-700 border-b border-gray-100 last:border-b-0 ${
217+ highlightedIndex === index ? "bg-gray-100" : ""
218+ } `}
219+ role = "option"
220+ aria-selected = { highlightedIndex === index }
221+ tabIndex = { - 1 }
166222 >
167223 { option . label }
168224 </ div >
169225 ) )
170226 ) : (
171- < div className = "px-3 py-2 text-sm text-gray-500" >
227+ < div className = "px-3 py-2 text-xs font-medium text-gray-500" >
172228 No options found
173229 </ div >
174230 ) }
0 commit comments