1- import { type FC , useState , useMemo , forwardRef } from 'react'
1+ import {
2+ type FC ,
3+ useState ,
4+ useMemo ,
5+ forwardRef ,
6+ useRef ,
7+ useEffect ,
8+ useCallback
9+ } from 'react'
210import { Flex , Text , Box , Button , ChainIcon } from '../../primitives/index.js'
311import { Dropdown , DropdownMenuItem } from '../../primitives/Dropdown.js'
412import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
@@ -33,7 +41,11 @@ const ChainFilterTrigger = forwardRef<
3341 cursor : 'pointer' ,
3442 backgroundColor : 'dropdown-background' ,
3543 borderRadius : 'dropdown-border-radius' ,
36- flexShrink : 0
44+ flexShrink : 0 ,
45+ '--focusColor' : 'colors.focus-color' ,
46+ _focusVisible : {
47+ boxShadow : 'inset 0 0 0 2px var(--focusColor)'
48+ }
3749 } }
3850 >
3951 { value . id ? (
@@ -81,6 +93,7 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
8193} ) => {
8294 const [ open , setOpen ] = useState ( false )
8395 const [ chainSearchInput , setChainSearchInput ] = useState ( '' )
96+ const searchInputRef = useRef < HTMLInputElement | null > ( null )
8497 const chainFuse = new Fuse ( options , {
8598 includeScore : true ,
8699 includeMatches : true ,
@@ -107,6 +120,44 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
107120 return Array . from ( uniqueChains . values ( ) )
108121 } , [ chainSearchInput , chainFuse ] )
109122
123+ useEffect ( ( ) => {
124+ if ( open ) {
125+ requestAnimationFrame ( ( ) => {
126+ searchInputRef . current ?. focus ( )
127+ } )
128+ } else {
129+ setChainSearchInput ( '' )
130+ }
131+ } , [ open ] )
132+
133+ const focusDropdownItem = useCallback ( ( position : 'first' | 'last' ) => {
134+ const container = searchInputRef . current ?. closest (
135+ '[data-chain-dropdown]'
136+ ) as HTMLElement | null
137+ if ( ! container ) return
138+
139+ const items = Array . from (
140+ container . querySelectorAll < HTMLElement > ( '[data-chain-dropdown-item]' )
141+ )
142+ if ( items . length === 0 ) return
143+
144+ const target = position === 'first' ? items [ 0 ] : items [ items . length - 1 ]
145+ target . focus ( )
146+ } , [ ] )
147+
148+ const handleSearchKeyDown = useCallback (
149+ ( event : React . KeyboardEvent < HTMLInputElement > ) => {
150+ if ( event . key === 'ArrowDown' ) {
151+ event . preventDefault ( )
152+ focusDropdownItem ( 'first' )
153+ } else if ( event . key === 'ArrowUp' ) {
154+ event . preventDefault ( )
155+ focusDropdownItem ( 'last' )
156+ }
157+ } ,
158+ [ focusDropdownItem ]
159+ )
160+
110161 return (
111162 < Dropdown
112163 open = { open }
@@ -123,11 +174,15 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
123174 }
124175 } }
125176 >
126- < Flex direction = "column" css = { { p : '2' } } >
177+ < Flex direction = "column" css = { { p : '2' } } data-chain-dropdown = "true" >
127178 < ChainSearchInput
179+ ref = { searchInputRef }
128180 value = { chainSearchInput }
129181 onChange = { setChainSearchInput }
130- onKeyDown = { ( event ) => event . stopPropagation ( ) }
182+ onKeyDown = { ( event ) => {
183+ event . stopPropagation ( )
184+ handleSearchKeyDown ( event )
185+ } }
131186 />
132187 < Flex
133188 direction = "column"
@@ -143,20 +198,25 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
143198 filteredChains . map ( ( chain ) => {
144199 const tag = 'tags' in chain ? chain . tags ?. [ 0 ] : undefined
145200 return (
146- < Flex
201+ < DropdownMenuItem
147202 key = { chain . id ?. toString ( ) ?? 'all-chains' }
148- onClick = { ( ) => {
149- setOpen ( false )
203+ data-chain-dropdown-item
204+ onSelect = { ( ) => {
150205 onSelect ( chain )
206+ setOpen ( false )
151207 setChainSearchInput ( '' )
152208 } }
153209 css = { {
154210 padding : '8px' ,
155211 borderRadius : 4 ,
156212 cursor : 'pointer' ,
157213 backgroundColor : 'modal-background' ,
214+ outline : 'none' ,
158215 _hover : {
159216 backgroundColor : 'gray3'
217+ } ,
218+ _focus : {
219+ backgroundColor : 'gray3'
160220 }
161221 } }
162222 >
@@ -166,7 +226,7 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
166226 onToggleStar = { onChainStarToggle }
167227 onAnalyticEvent = { onAnalyticEvent }
168228 />
169- </ Flex >
229+ </ DropdownMenuItem >
170230 )
171231 } )
172232 ) : (
@@ -178,12 +238,25 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
178238 < >
179239 { allChainsOption && (
180240 < DropdownMenuItem
181- onClick = { ( ) => {
241+ data-chain-dropdown-item
242+ onSelect = { ( ) => {
182243 setOpen ( false )
183244 onSelect ( allChainsOption )
184245 setChainSearchInput ( '' )
185246 } }
186- css = { { p : '2' } }
247+ css = { {
248+ padding : '8px' ,
249+ borderRadius : 4 ,
250+ cursor : 'pointer' ,
251+ backgroundColor : 'modal-background' ,
252+ outline : 'none' ,
253+ _hover : {
254+ backgroundColor : 'gray3'
255+ } ,
256+ _focus : {
257+ backgroundColor : 'gray3'
258+ }
259+ } }
187260 >
188261 < ChainFilterRow
189262 chain = { allChainsOption }
@@ -205,20 +278,25 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
205278 { starredChains . map ( ( chain : ChainFilterValue ) => {
206279 const tag = 'tags' in chain ? chain . tags ?. [ 0 ] : undefined
207280 return (
208- < Flex
281+ < DropdownMenuItem
209282 key = { chain . id ?. toString ( ) ?? 'all-chains' }
210- onClick = { ( ) => {
211- setOpen ( false )
283+ data-chain-dropdown-item
284+ onSelect = { ( ) => {
212285 onSelect ( chain )
286+ setOpen ( false )
213287 setChainSearchInput ( '' )
214288 } }
215289 css = { {
216290 padding : '8px' ,
217291 borderRadius : 4 ,
218292 cursor : 'pointer' ,
219293 backgroundColor : 'modal-background' ,
294+ outline : 'none' ,
220295 _hover : {
221296 backgroundColor : 'gray3'
297+ } ,
298+ _focus : {
299+ backgroundColor : 'gray3'
222300 }
223301 } }
224302 >
@@ -229,7 +307,7 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
229307 showStar = { false }
230308 onAnalyticEvent = { onAnalyticEvent }
231309 />
232- </ Flex >
310+ </ DropdownMenuItem >
233311 )
234312 } ) }
235313 </ >
@@ -241,20 +319,25 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
241319 { alphabeticalChains . map ( ( chain : ChainFilterValue ) => {
242320 const tag = 'tags' in chain ? chain . tags ?. [ 0 ] : undefined
243321 return (
244- < Flex
322+ < DropdownMenuItem
245323 key = { chain . id ?. toString ( ) ?? 'all-chains' }
246- onClick = { ( ) => {
247- setOpen ( false )
324+ data-chain-dropdown-item
325+ onSelect = { ( ) => {
248326 onSelect ( chain )
327+ setOpen ( false )
249328 setChainSearchInput ( '' )
250329 } }
251330 css = { {
252331 padding : '8px' ,
253332 borderRadius : 4 ,
254333 cursor : 'pointer' ,
255334 backgroundColor : 'modal-background' ,
335+ outline : 'none' ,
256336 _hover : {
257337 backgroundColor : 'gray3'
338+ } ,
339+ _focus : {
340+ backgroundColor : 'gray3'
258341 }
259342 } }
260343 >
@@ -264,7 +347,7 @@ export const CompactChainFilter: FC<CompactChainFilterProps> = ({
264347 onToggleStar = { onChainStarToggle }
265348 onAnalyticEvent = { onAnalyticEvent }
266349 />
267- </ Flex >
350+ </ DropdownMenuItem >
268351 )
269352 } ) }
270353 </ >
0 commit comments