Skip to content

Commit 19e522f

Browse files
committed
Add support for Home and End keys
The [WAI ARIA Practices 1.1][listbox-keyboard] outlines optional support for [`Home` and `End` keys][keys] to jump to the top and bottom of the listbox, respectively. [listbox-keyboard]: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/#listbox-popup-keyboard-interaction [keys]: https://www.w3.org/TR/uievents-key/#keys-navigation Configure: Home and End navigation --- By default, continue to ignore `Home` and `End` navigation keys. When `optionalNavigationKeys: ["Home", "End"]` is passed as a configuration object, treat them as valid navigation keys.
1 parent 948dd5d commit 19e522f

File tree

3 files changed

+92
-1
lines changed

3 files changed

+92
-1
lines changed

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ These settings are available:
9494
> **Warning** Screen readers will not announce that the first item is the default. This should be announced explicitly with the use of `aria-live` status text.
9595
- `scrollIntoViewOptions?: boolean | ScrollIntoViewOptions = undefined` - When
9696
controlling the element marked `[aria-selected="true"]` with keyboard navigation, the selected element will be scrolled into the viewport by a call to [Element.scrollIntoView][]. Configure this value to control the scrolling behavior (either with a `boolean` or a [ScrollIntoViewOptions][] object.
97+
- `optionalNavigationKeys?: Array<'Home' | 'End'> = []` - When navigating the list, enable additional [Navigation Keys][], like <kbd>Home</kbd> to skip to the top of the list and <kbd>End</kbd> to skip to the bottom.
9798

9899
[Element.scrollIntoView]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
99100
[ScrollIntoViewOptions]: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#sect1
101+
[Navigation Keys]: https://www.w3.org/TR/uievents-key/#keys-home
100102

101103

102104
## Development

Diff for: src/index.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
type OptionalNavigationKeys = 'Home' | 'End'
2+
13
export type ComboboxSettings = {
24
tabInsertsSuggestions?: boolean
35
defaultFirstOption?: boolean
46
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
7+
optionalNavigationKeys?: OptionalNavigationKeys[]
58
}
69

710
export default class Combobox {
@@ -15,17 +18,19 @@ export default class Combobox {
1518
tabInsertsSuggestions: boolean
1619
defaultFirstOption: boolean
1720
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
21+
optionalNavigationKeys: OptionalNavigationKeys[]
1822

1923
constructor(
2024
input: HTMLTextAreaElement | HTMLInputElement,
2125
list: HTMLElement,
22-
{tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions}: ComboboxSettings = {},
26+
{tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions, optionalNavigationKeys}: ComboboxSettings = {},
2327
) {
2428
this.input = input
2529
this.list = list
2630
this.tabInsertsSuggestions = tabInsertsSuggestions ?? true
2731
this.defaultFirstOption = defaultFirstOption ?? false
2832
this.scrollIntoViewOptions = scrollIntoViewOptions
33+
this.optionalNavigationKeys = optionalNavigationKeys ?? []
2934

3035
this.isComposing = false
3136

@@ -154,6 +159,20 @@ function keyboardBindings(event: KeyboardEvent, combobox: Combobox) {
154159
combobox.navigate(-1)
155160
event.preventDefault()
156161
break
162+
case 'Home':
163+
if (combobox.optionalNavigationKeys.includes('Home')) {
164+
combobox.clearSelection()
165+
combobox.navigate(1)
166+
event.preventDefault()
167+
}
168+
break
169+
case 'End':
170+
if (combobox.optionalNavigationKeys.includes('End')) {
171+
combobox.clearSelection()
172+
combobox.navigate(-1)
173+
event.preventDefault()
174+
}
175+
break
157176
case 'n':
158177
if (combobox.ctrlBindings && event.ctrlKey) {
159178
combobox.navigate(1)

Diff for: test/test.js

+70
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,74 @@ describe('combobox-nav', function () {
311311
})
312312
})
313313
})
314+
315+
describe('with Home key navigation enabled', () => {
316+
let input
317+
let list
318+
let options
319+
let combobox
320+
beforeEach(() => {
321+
document.body.innerHTML = `
322+
<input type="text">
323+
<ul role="listbox" id="list-id">
324+
<li id="baymax" role="option">Baymax</li>
325+
<li><del>BB-8</del></li>
326+
<li id="hubot" role="option">Hubot</li>
327+
<li id="r2-d2" role="option">R2-D2</li>
328+
<li id="johnny-5" hidden role="option">Johnny 5</li>
329+
<li id="wall-e" role="option" aria-disabled="true">Wall-E</li>
330+
<li><a href="#link" role="option" id="link">Link</a></li>
331+
</ul>
332+
`
333+
input = document.querySelector('input')
334+
list = document.querySelector('ul')
335+
options = document.querySelectorAll('[role=option]')
336+
combobox = new Combobox(input, list, {optionalNavigationKeys: ['Home']})
337+
combobox.start()
338+
})
339+
340+
it('updates attributes on keyboard events', () => {
341+
press(input, 'ArrowDown')
342+
press(input, 'ArrowDown')
343+
344+
assert.equal(options[1].getAttribute('aria-selected'), 'true')
345+
assert.equal(input.getAttribute('aria-activedescendant'), 'hubot')
346+
347+
press(input, 'Home')
348+
assert.equal(options[0].getAttribute('aria-selected'), 'true')
349+
assert.equal(input.getAttribute('aria-activedescendant'), 'baymax')
350+
})
351+
})
352+
353+
describe('with End key navigation enabled', () => {
354+
let input
355+
let list
356+
let options
357+
let combobox
358+
beforeEach(() => {
359+
document.body.innerHTML = `
360+
<input type="text">
361+
<ul role="listbox" id="list-id">
362+
<li id="baymax" role="option">Baymax</li>
363+
<li><del>BB-8</del></li>
364+
<li id="hubot" role="option">Hubot</li>
365+
<li id="r2-d2" role="option">R2-D2</li>
366+
<li id="johnny-5" hidden role="option">Johnny 5</li>
367+
<li id="wall-e" role="option" aria-disabled="true">Wall-E</li>
368+
<li><a href="#link" role="option" id="link">Link</a></li>
369+
</ul>
370+
`
371+
input = document.querySelector('input')
372+
list = document.querySelector('ul')
373+
options = document.querySelectorAll('[role=option]')
374+
combobox = new Combobox(input, list, {optionalNavigationKeys: ['End']})
375+
combobox.start()
376+
})
377+
378+
it('updates attributes on keyboard events', () => {
379+
press(input, 'End')
380+
assert.equal(options[5].getAttribute('aria-selected'), 'true')
381+
assert.equal(input.getAttribute('aria-activedescendant'), 'link')
382+
})
383+
})
314384
})

0 commit comments

Comments
 (0)