Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Home and End keys #31

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
})
})
})
Loading