Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit db220fe

Browse files
committed
poc: screen reader token feedback
1 parent 608ab91 commit db220fe

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ npm install
139139
npm test
140140
```
141141
142-
To view changes locally, run `npm run examples`.
142+
To view changes locally, run `npm run example`.
143143
144144
In `examples/index.html`, uncomment `<!--<script type="module" src="./dist/bundle.js"></script>-->` and comment out the script referencing the `unpkg` version. This allows you to use the `src` code in this repo. Otherwise, you will be pulling the latest published code, which will not reflect the local changes you are making.
145145

examples/index.html

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,25 @@
3939
</script>
4040
</head>
4141
<body>
42+
<form>
43+
<label id="robots-label" for="robot">Robots (Example with multi-tokens)</label>
44+
<!-- To enable auto-select (select first on Enter), use data-autoselect="true" -->
45+
<auto-complete src="/demo" for="items-popup" aria-labelledby="robots-label" data-autoselect="true">
46+
<input name="robot" type="text" aria-labelledby="robots-label" autofocus />
47+
<!-- if a clear button is passed in, recommended to be *before* UL elements to avoid conflicting with their blur logic -->
48+
<button id="robot-clear">x</button>
49+
<ul id="items-popup"></ul>
50+
<!-- For built-in screen-reader announcements:
51+
- Note the ID is the same as the <ul> with "feedback" appended
52+
- Also note that aria attributes will be added programmatically if they aren't set correctly
53+
-->
54+
<div id="items-popup-feedback" class="sr-only"></div>
55+
<!-- Provides feedback if entering/leaving a filter in the input -->
56+
<div id="items-popup-filter-feedback"></div>
57+
</auto-complete>
58+
<button type="submit">Save</button>
59+
</form>
60+
4261
<form>
4362
<label id="robots-label" for="robot">Robots</label>
4463
<!-- To enable auto-select (select first on Enter), use data-autoselect="true" -->
@@ -101,7 +120,8 @@
101120
document.querySelector("auto-complete#custom-fetching-method").fetchResult = async (el, url) => (await fetch(url)).text();
102121
</script>
103122

104-
<!-- <script type="module" src="./dist/bundle.js"></script>-->
105-
<script type="module" src="https://unpkg.com/@github/auto-complete-element@latest/dist/bundle.js"></script>
123+
124+
<script type="module" src="./dist/bundle.js"></script>
125+
<!-- <script type="module" src="https://unpkg.com/@github/auto-complete-element@latest/dist/bundle.js"></script> -->
106126
</body>
107127
</html>

src/autocomplete.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ export default class Autocomplete {
1212
results: HTMLElement
1313
combobox: Combobox
1414
feedback: HTMLElement | null
15+
inputFilterFeedback: HTMLElement | null
1516
autoselectEnabled: boolean
1617
clientOptions: NodeListOf<HTMLElement> | null
1718
clearButton: HTMLElement | null
19+
tokenRanges: any
1820

1921
interactingWithList: boolean
2022

@@ -29,8 +31,10 @@ export default class Autocomplete {
2931
this.results = results
3032
this.combobox = new Combobox(input, results)
3133
this.feedback = document.getElementById(`${this.results.id}-feedback`)
34+
this.inputFilterFeedback = document.getElementById(`${this.results.id}-filter-feedback`)
3235
this.autoselectEnabled = autoselectEnabled
3336
this.clearButton = document.getElementById(`${this.input.id || this.input.name}-clear`)
37+
this.tokenRanges = []
3438

3539
// check to see if there are any default options provided
3640
this.clientOptions = results.querySelectorAll('[role=option]')
@@ -41,6 +45,12 @@ export default class Autocomplete {
4145
this.feedback.setAttribute('aria-atomic', 'true')
4246
}
4347

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+
4454
// if clearButton doesn't have an accessible label, give it one
4555
if (this.clearButton && !this.clearButton.getAttribute('aria-label')) {
4656
const labelElem = document.querySelector(`label[for="${this.input.name}"]`)
@@ -129,6 +139,69 @@ export default class Autocomplete {
129139
event.stopPropagation()
130140
event.preventDefault()
131141
}
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+
}
132205
}
133206

134207
onInputFocus(): void {
@@ -163,6 +236,27 @@ export default class Autocomplete {
163236
}
164237

165238
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+
166260
if (this.feedback && this.feedback.textContent) {
167261
this.feedback.textContent = ''
168262
}

0 commit comments

Comments
 (0)