Skip to content

Commit

Permalink
Add support for Home and End keys
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
seanpdoyle committed Sep 25, 2023
1 parent 948dd5d commit 19e522f
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ These settings are available:
> **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.
- `scrollIntoViewOptions?: boolean | ScrollIntoViewOptions = undefined` - When
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.
- `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.

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


## Development
Expand Down
21 changes: 20 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
type OptionalNavigationKeys = 'Home' | 'End'

export type ComboboxSettings = {
tabInsertsSuggestions?: boolean
defaultFirstOption?: boolean
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
optionalNavigationKeys?: OptionalNavigationKeys[]
}

export default class Combobox {
Expand All @@ -15,17 +18,19 @@ export default class Combobox {
tabInsertsSuggestions: boolean
defaultFirstOption: boolean
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
optionalNavigationKeys: OptionalNavigationKeys[]

constructor(
input: HTMLTextAreaElement | HTMLInputElement,
list: HTMLElement,
{tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions}: ComboboxSettings = {},
{tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions, optionalNavigationKeys}: ComboboxSettings = {},
) {
this.input = input
this.list = list
this.tabInsertsSuggestions = tabInsertsSuggestions ?? true
this.defaultFirstOption = defaultFirstOption ?? false
this.scrollIntoViewOptions = scrollIntoViewOptions
this.optionalNavigationKeys = optionalNavigationKeys ?? []

this.isComposing = false

Expand Down Expand Up @@ -154,6 +159,20 @@ function keyboardBindings(event: KeyboardEvent, combobox: Combobox) {
combobox.navigate(-1)
event.preventDefault()
break
case 'Home':
if (combobox.optionalNavigationKeys.includes('Home')) {
combobox.clearSelection()
combobox.navigate(1)
event.preventDefault()
}
break
case 'End':
if (combobox.optionalNavigationKeys.includes('End')) {
combobox.clearSelection()
combobox.navigate(-1)
event.preventDefault()
}
break
case 'n':
if (combobox.ctrlBindings && event.ctrlKey) {
combobox.navigate(1)
Expand Down
70 changes: 70 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,4 +311,74 @@ describe('combobox-nav', function () {
})
})
})

describe('with Home key navigation enabled', () => {
let input
let list
let options
let combobox
beforeEach(() => {
document.body.innerHTML = `
<input type="text">
<ul role="listbox" id="list-id">
<li id="baymax" role="option">Baymax</li>
<li><del>BB-8</del></li>
<li id="hubot" role="option">Hubot</li>
<li id="r2-d2" role="option">R2-D2</li>
<li id="johnny-5" hidden role="option">Johnny 5</li>
<li id="wall-e" role="option" aria-disabled="true">Wall-E</li>
<li><a href="#link" role="option" id="link">Link</a></li>
</ul>
`
input = document.querySelector('input')
list = document.querySelector('ul')
options = document.querySelectorAll('[role=option]')
combobox = new Combobox(input, list, {optionalNavigationKeys: ['Home']})
combobox.start()
})

it('updates attributes on keyboard events', () => {
press(input, 'ArrowDown')
press(input, 'ArrowDown')

assert.equal(options[1].getAttribute('aria-selected'), 'true')
assert.equal(input.getAttribute('aria-activedescendant'), 'hubot')

press(input, 'Home')
assert.equal(options[0].getAttribute('aria-selected'), 'true')
assert.equal(input.getAttribute('aria-activedescendant'), 'baymax')
})
})

describe('with End key navigation enabled', () => {
let input
let list
let options
let combobox
beforeEach(() => {
document.body.innerHTML = `
<input type="text">
<ul role="listbox" id="list-id">
<li id="baymax" role="option">Baymax</li>
<li><del>BB-8</del></li>
<li id="hubot" role="option">Hubot</li>
<li id="r2-d2" role="option">R2-D2</li>
<li id="johnny-5" hidden role="option">Johnny 5</li>
<li id="wall-e" role="option" aria-disabled="true">Wall-E</li>
<li><a href="#link" role="option" id="link">Link</a></li>
</ul>
`
input = document.querySelector('input')
list = document.querySelector('ul')
options = document.querySelectorAll('[role=option]')
combobox = new Combobox(input, list, {optionalNavigationKeys: ['End']})
combobox.start()
})

it('updates attributes on keyboard events', () => {
press(input, 'End')
assert.equal(options[5].getAttribute('aria-selected'), 'true')
assert.equal(input.getAttribute('aria-activedescendant'), 'link')
})
})
})

0 comments on commit 19e522f

Please sign in to comment.