diff --git a/README.md b/README.md index c6f6ca3..67e4520 100644 --- a/README.md +++ b/README.md @@ -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 Home to skip to the top of the list and End 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 diff --git a/src/index.ts b/src/index.ts index 74b7f10..514d11e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ +type OptionalNavigationKeys = 'Home' | 'End' + export type ComboboxSettings = { tabInsertsSuggestions?: boolean defaultFirstOption?: boolean scrollIntoViewOptions?: boolean | ScrollIntoViewOptions + optionalNavigationKeys?: OptionalNavigationKeys[] } export default class Combobox { @@ -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 @@ -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) diff --git a/test/test.js b/test/test.js index 03a82c5..14607bb 100644 --- a/test/test.js +++ b/test/test.js @@ -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 = 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 = 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') + }) + }) })