@@ -12,9 +12,11 @@ export default class Autocomplete {
12
12
results : HTMLElement
13
13
combobox : Combobox
14
14
feedback : HTMLElement | null
15
+ inputFilterFeedback : HTMLElement | null
15
16
autoselectEnabled : boolean
16
17
clientOptions : NodeListOf < HTMLElement > | null
17
18
clearButton : HTMLElement | null
19
+ tokenRanges : any
18
20
19
21
interactingWithList : boolean
20
22
@@ -29,8 +31,10 @@ export default class Autocomplete {
29
31
this . results = results
30
32
this . combobox = new Combobox ( input , results )
31
33
this . feedback = document . getElementById ( `${ this . results . id } -feedback` )
34
+ this . inputFilterFeedback = document . getElementById ( `${ this . results . id } -filter-feedback` )
32
35
this . autoselectEnabled = autoselectEnabled
33
36
this . clearButton = document . getElementById ( `${ this . input . id || this . input . name } -clear` )
37
+ this . tokenRanges = [ ]
34
38
35
39
// check to see if there are any default options provided
36
40
this . clientOptions = results . querySelectorAll ( '[role=option]' )
@@ -41,6 +45,12 @@ export default class Autocomplete {
41
45
this . feedback . setAttribute ( 'aria-atomic' , 'true' )
42
46
}
43
47
48
+ // make sure feedback has all required aria attributes
49
+ if ( this . inputFilterFeedback ) {
50
+ this . inputFilterFeedback . setAttribute ( 'aria-live' , 'polite' )
51
+ this . inputFilterFeedback . setAttribute ( 'aria-atomic' , 'true' )
52
+ }
53
+
44
54
// if clearButton doesn't have an accessible label, give it one
45
55
if ( this . clearButton && ! this . clearButton . getAttribute ( 'aria-label' ) ) {
46
56
const labelElem = document . querySelector ( `label[for="${ this . input . name } "]` )
@@ -129,6 +139,69 @@ export default class Autocomplete {
129
139
event . stopPropagation ( )
130
140
event . preventDefault ( )
131
141
}
142
+
143
+ if ( event . key === 'ArrowLeft' || event . key === 'ArrowRight' ) {
144
+ const inputValue : string = this . input . value
145
+ const cursorLocation : number = this . input . selectionStart || 0
146
+ const tokens : string [ ] = inputValue . split ( ' ' )
147
+ const screenReaderFeedbackString = ( action : string , filter : string , value : string ) =>
148
+ `${ action } the ${ filter } filter with ${ value } value`
149
+
150
+ for ( let i = 0 ; i < this . tokenRanges . length ; i ++ ) {
151
+ const currentTokenRange = this . tokenRanges [ i ]
152
+ // Early return if the token does not include a `:` denoting a filter
153
+ if ( ! tokens [ i ] . includes ( ':' ) ) return
154
+
155
+ if (
156
+ ( event . key === 'ArrowRight' &&
157
+ cursorLocation >= currentTokenRange [ 0 ] &&
158
+ cursorLocation < currentTokenRange [ 1 ] ) ||
159
+ ( event . key === 'ArrowLeft' &&
160
+ cursorLocation >= currentTokenRange [ 0 ] &&
161
+ cursorLocation - 1 < currentTokenRange [ 1 ] )
162
+ ) {
163
+ const currentRangeValue : string | undefined = tokens [ i ]
164
+ let action = 'Entering'
165
+
166
+ if (
167
+ ( cursorLocation + 1 === currentTokenRange [ 1 ] && event . key === 'ArrowRight' ) ||
168
+ ( cursorLocation - 1 <= currentTokenRange [ 0 ] && event . key === 'ArrowLeft' )
169
+ ) {
170
+ action = 'Leaving'
171
+ }
172
+
173
+ const [ filter , value ] = currentRangeValue . split ( ':' )
174
+ // Only do this one time until it changes
175
+ if (
176
+ this . inputFilterFeedback &&
177
+ this . inputFilterFeedback . textContent !== screenReaderFeedbackString ( action , filter , value )
178
+ ) {
179
+ this . inputFilterFeedback . textContent = screenReaderFeedbackString ( action , filter , value )
180
+ }
181
+ }
182
+ }
183
+ /** Scenarios:
184
+ - user presses left arrow
185
+ - enters filter
186
+ - leaves filter
187
+ - doesn't enter or leave a filter
188
+ - user presses right arrow
189
+ - enters filter
190
+ - leaves filter
191
+ - doesn't enter or leave a filter
192
+
193
+ Assumptions:
194
+ - user will never enter or leave a filter from another filter
195
+ - a filter and value are separated by a `:`
196
+
197
+ // user input samples
198
+ `repo:github/accessibility design`
199
+ `is:issue assignee:@lindseywild is:open`
200
+ `accessibility`
201
+ `is:pr interactions:>2000`
202
+ `language:swift closed:>2014-06-11`
203
+ */
204
+ }
132
205
}
133
206
134
207
onInputFocus ( ) : void {
@@ -163,6 +236,27 @@ export default class Autocomplete {
163
236
}
164
237
165
238
onInputChange ( ) : void {
239
+ const inputValue : string = this . input . value
240
+ const tokens : string [ ] = inputValue . split ( ' ' )
241
+
242
+ // Reset tokens array
243
+ this . tokenRanges = [ ]
244
+
245
+ // Creates an array of indecies
246
+ tokens . map ( ( token , index ) => {
247
+ let startIndex = 0
248
+ let endIndex = token . length
249
+
250
+ if ( index > 0 ) {
251
+ // Gets previous array's end value
252
+ const previousValue = this . tokenRanges . at ( - 1 ) [ 1 ]
253
+ startIndex = previousValue + 1
254
+ endIndex = startIndex + token . length
255
+ }
256
+
257
+ this . tokenRanges . push ( [ startIndex , endIndex ] )
258
+ } )
259
+
166
260
if ( this . feedback && this . feedback . textContent ) {
167
261
this . feedback . textContent = ''
168
262
}
0 commit comments