1
1
import {
2
2
Box ,
3
3
Flex ,
4
+ FormControl ,
5
+ FormLabel ,
4
6
Input ,
7
+ InputGroup ,
8
+ InputRightElement ,
9
+ Kbd ,
5
10
Menu ,
6
- MenuItem as ChakraMenuItem ,
7
11
MenuList ,
8
12
type MenuListProps ,
9
13
type MenuProps ,
10
14
Text ,
15
+ type UseDisclosureReturn ,
16
+ useEventListener ,
11
17
} from "@chakra-ui/react"
12
18
13
19
import { Button } from "@/components/Buttons"
@@ -21,34 +27,45 @@ type LanguagePickerProps = Omit<MenuListProps, "children"> & {
21
27
children : React . ReactNode
22
28
placement : MenuProps [ "placement" ]
23
29
handleClose ?: ( ) => void
30
+ menuState ?: UseDisclosureReturn
24
31
}
25
32
26
33
const LanguagePicker = ( {
27
34
children,
28
35
placement,
29
36
handleClose,
37
+ menuState,
30
38
...props
31
39
} : LanguagePickerProps ) => {
32
- const {
33
- t,
34
- disclosure,
35
- inputRef,
36
- firstItemRef,
37
- filterValue,
38
- setFilterValue,
39
- filteredNames,
40
- } = useLanguagePicker ( handleClose )
41
-
40
+ const { t, refs, disclosure, filterValue, setFilterValue, filteredNames } =
41
+ useLanguagePicker ( handleClose , menuState )
42
+ const { inputRef, firstItemRef, noResultsRef, footerRef } = refs
42
43
const { onClose } = disclosure
43
44
45
+ /**
46
+ * Adds a keydown event listener to focus filter input (\).
47
+ * @param {string } event - The keydown event.
48
+ */
49
+ useEventListener ( "keydown" , ( e ) => {
50
+ if ( e . key !== "\\" ) return
51
+ e . preventDefault ( )
52
+ inputRef . current ?. focus ( )
53
+ } )
54
+
44
55
return (
45
- < Menu isLazy placement = { placement } closeOnSelect = { false } { ...disclosure } >
56
+ < Menu isLazy placement = { placement } autoSelect = { false } { ...disclosure } >
46
57
{ children }
47
58
< MenuList
48
59
position = "relative"
49
60
overflow = "auto"
50
61
borderRadius = "base"
51
62
py = "0"
63
+ onKeyDown = { ( e ) => {
64
+ if ( e . key === "Tab" || e . key === "\\" ) {
65
+ e . preventDefault ( )
66
+ ; ( e . shiftKey ? inputRef : footerRef ) . current ?. focus ( )
67
+ }
68
+ } }
52
69
{ ...props }
53
70
>
54
71
{ /* Mobile Close bar */ }
@@ -79,63 +96,100 @@ const LanguagePicker = ({
79
96
bg = "background.highlight"
80
97
sx = { { "[role=menuitem]" : { py : "3" , px : "2" } } }
81
98
>
82
- < Text fontSize = "xs" color = "body.medium" >
83
- { t ( "page-languages-filter-label" ) } { " " }
84
- < Text as = "span" textTransform = "lowercase" >
85
- ({ filteredNames . length } { t ( "common:languages" ) } )
86
- </ Text >
87
- </ Text >
88
- < ChakraMenuItem
89
- onFocus = { ( ) => inputRef . current ?. focus ( ) }
90
- p = "0"
91
- bg = "transparent"
92
- position = "relative"
93
- closeOnSelect = { false }
94
- >
95
- < Input
96
- placeholder = { t ( "page-languages-filter-placeholder" ) }
97
- value = { filterValue }
98
- onChange = { ( e ) => setFilterValue ( e . target . value ) }
99
- ref = { inputRef }
100
- h = "8"
101
- mt = "1"
102
- mb = "2"
103
- bg = "background.base"
104
- color = "body.base"
105
- onKeyDown = { ( e ) => {
106
- // Navigate to first result on enter
107
- if ( e . key === "Enter" ) {
99
+ < FormControl >
100
+ < FormLabel fontSize = "xs" color = "body.medium" >
101
+ { t ( "page-languages-filter-label" ) } { " " }
102
+ < Text as = "span" textTransform = "lowercase" >
103
+ ({ filteredNames . length } { t ( "common:languages" ) } )
104
+ </ Text >
105
+ </ FormLabel >
106
+ < InputGroup >
107
+ < Input
108
+ type = "search"
109
+ autoComplete = "off"
110
+ placeholder = { t ( "page-languages-filter-placeholder" ) }
111
+ value = { filterValue }
112
+ onChange = { ( e ) => setFilterValue ( e . target . value ) }
113
+ onBlur = { ( e ) => {
114
+ if ( e . relatedTarget ?. tagName . toLowerCase ( ) === "div" ) {
115
+ e . currentTarget . focus ( )
116
+ }
117
+ } }
118
+ ref = { inputRef }
119
+ h = "8"
120
+ mt = "1"
121
+ mb = "2"
122
+ bg = "background.base"
123
+ color = "body.base"
124
+ onKeyDown = { ( e ) => {
125
+ // Navigate to first result on enter
126
+ if ( e . key === "Enter" ) {
127
+ e . preventDefault ( )
128
+ firstItemRef . current ?. click ( )
129
+ }
130
+ // If Tab/ArrowDown, focus on first item if available, NoResults link otherwise
131
+ if ( e . key === "Tab" || e . key === "ArrowDown" ) {
132
+ e . preventDefault ( )
133
+ ; ( filteredNames . length === 0
134
+ ? noResultsRef
135
+ : firstItemRef
136
+ ) . current ?. focus ( )
137
+ e . stopPropagation ( )
138
+ }
139
+ } }
140
+ />
141
+ < InputRightElement
142
+ hideBelow = "lg" // TODO: Confirm breakpoint after nav-menu PR merged
143
+ cursor = "text"
144
+ >
145
+ < Kbd
146
+ fontSize = "sm"
147
+ lineHeight = "none"
148
+ me = "2"
149
+ p = "1"
150
+ py = "0.5"
151
+ ms = "auto"
152
+ border = "1px"
153
+ borderColor = "disabled"
154
+ color = "disabled"
155
+ rounded = "base"
156
+ >
157
+ \
158
+ </ Kbd >
159
+ </ InputRightElement >
160
+ </ InputGroup >
161
+
162
+ { filteredNames . map ( ( displayInfo , index ) => (
163
+ < MenuItem
164
+ key = { "item-" + displayInfo . localeOption }
165
+ displayInfo = { displayInfo }
166
+ ref = { index === 0 ? firstItemRef : null }
167
+ onKeyDown = { ( e ) => {
168
+ if ( e . key !== "\\" ) return
108
169
e . preventDefault ( )
109
- firstItemRef . current ?. click ( )
170
+ inputRef . current ?. focus ( )
171
+ } }
172
+ onClick = { ( ) =>
173
+ onClose ( {
174
+ eventAction : "Locale chosen" ,
175
+ eventName : displayInfo . localeOption ,
176
+ } )
110
177
}
111
- } }
112
- />
113
- </ ChakraMenuItem >
178
+ />
179
+ ) ) }
114
180
115
- { filteredNames . map ( ( displayInfo , index ) => (
116
- < MenuItem
117
- key = { "item-" + displayInfo . localeOption }
118
- displayInfo = { displayInfo }
119
- ref = { index === 0 ? firstItemRef : null }
120
- onClick = { ( ) =>
121
- onClose ( {
122
- eventAction : "Locale chosen" ,
123
- eventName : displayInfo . localeOption ,
124
- } )
125
- }
126
- />
127
- ) ) }
128
-
129
- { filteredNames . length === 0 && (
130
- < NoResultsCallout
131
- onClose = { ( ) =>
132
- onClose ( {
133
- eventAction : "Translation program link (no results)" ,
134
- eventName : "/contributing/translation-program" ,
135
- } )
136
- }
137
- />
138
- ) }
181
+ { filteredNames . length === 0 && (
182
+ < NoResultsCallout
183
+ ref = { noResultsRef }
184
+ onClose = { ( ) =>
185
+ onClose ( {
186
+ eventAction : "Translation program link (no results)" ,
187
+ eventName : "/contributing/translation-program" ,
188
+ } )
189
+ }
190
+ />
191
+ ) }
192
+ </ FormControl >
139
193
</ Box >
140
194
141
195
{ /* Footer callout */ }
@@ -151,6 +205,7 @@ const LanguagePicker = ({
151
205
< Text fontSize = "xs" textAlign = "center" color = "body.base" >
152
206
{ t ( "page-languages-recruit-community" ) } { " " }
153
207
< BaseLink
208
+ ref = { footerRef }
154
209
href = "/contributing/translation-program"
155
210
onClick = { ( ) =>
156
211
onClose ( {
0 commit comments