diff --git a/src/index.ts b/src/index.ts index 6a60a10..4aedb2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ export type ComboboxSettings = { tabInsertsSuggestions?: boolean - defaultFirstOption?: boolean + firstOptionSelectionMode?: FirstOptionSelectionMode scrollIntoViewOptions?: boolean | ScrollIntoViewOptions } +// Indicates the default behaviour for the first option when the list is shown. +export type FirstOptionSelectionMode = 'none' | 'selected' | 'focused' + export default class Combobox { isComposing: boolean list: HTMLElement @@ -13,18 +16,18 @@ export default class Combobox { inputHandler: (event: Event) => void ctrlBindings: boolean tabInsertsSuggestions: boolean - defaultFirstOption: boolean + firstOptionSelectionMode: FirstOptionSelectionMode scrollIntoViewOptions?: boolean | ScrollIntoViewOptions constructor( input: HTMLTextAreaElement | HTMLInputElement, list: HTMLElement, - {tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions}: ComboboxSettings = {}, + {tabInsertsSuggestions, firstOptionSelectionMode, scrollIntoViewOptions}: ComboboxSettings = {}, ) { this.input = input this.list = list this.tabInsertsSuggestions = tabInsertsSuggestions ?? true - this.defaultFirstOption = defaultFirstOption ?? false + this.firstOptionSelectionMode = firstOptionSelectionMode ?? 'none' this.scrollIntoViewOptions = scrollIntoViewOptions ?? {block: 'nearest', inline: 'nearest'} this.isComposing = false @@ -64,6 +67,7 @@ export default class Combobox { ;(this.input as HTMLElement).addEventListener('keydown', this.keyboardEventHandler) this.list.addEventListener('click', commitWithElement) this.indicateDefaultOption() + this.focusDefaultOptionIfNeeded() } stop(): void { @@ -77,13 +81,19 @@ export default class Combobox { } indicateDefaultOption(): void { - if (this.defaultFirstOption) { + if (this.firstOptionSelectionMode === 'selected') { Array.from(this.list.querySelectorAll('[role="option"]:not([aria-disabled="true"])')) .filter(visible)[0] ?.setAttribute('data-combobox-option-default', 'true') } } + focusDefaultOptionIfNeeded(): void { + if (this.firstOptionSelectionMode === 'focused') { + this.navigate(1) + } + } + navigate(indexDiff: -1 | 1 = 1): void { const focusEl = Array.from(this.list.querySelectorAll('[aria-selected="true"]')).filter(visible)[0] const els = Array.from(this.list.querySelectorAll('[role="option"]')).filter(visible) diff --git a/test/test.js b/test/test.js index 03a82c5..5f47f13 100644 --- a/test/test.js +++ b/test/test.js @@ -263,7 +263,7 @@ describe('combobox-nav', function () { input = document.querySelector('input') list = document.querySelector('ul') options = document.querySelectorAll('[role=option]') - combobox = new Combobox(input, list, {defaultFirstOption: true}) + combobox = new Combobox(input, list, {firstOptionSelectionMode: 'selected'}) combobox.start() }) @@ -276,6 +276,7 @@ describe('combobox-nav', function () { it('indicates first option when started', () => { assert.equal(document.querySelector('[data-combobox-option-default]'), options[0]) assert.equal(document.querySelectorAll('[data-combobox-option-default]').length, 1) + assert.equal(list.children[0].getAttribute('aria-selected'), null) }) it('indicates first option when restarted', () => { @@ -311,4 +312,63 @@ describe('combobox-nav', function () { }) }) }) + + describe('with defaulting to focusing the first option', function () { + let input + let list + let combobox + beforeEach(function () { + document.body.innerHTML = ` + +
    +
  • Baymax
  • +
  • BB-8
  • +
  • Hubot
  • +
  • R2-D2
  • + +
  • Wall-E
  • +
  • Link
  • +
+ ` + input = document.querySelector('input') + list = document.querySelector('ul') + combobox = new Combobox(input, list, {firstOptionSelectionMode: 'focused'}) + combobox.start() + }) + + afterEach(function () { + combobox.destroy() + combobox = null + document.body.innerHTML = '' + }) + + it('focuses first option when started', () => { + // Does not set the default attribute + assert.equal(document.querySelectorAll('[data-combobox-option-default]').length, 0) + // Item is correctly selected + assert.equal(list.children[0].getAttribute('aria-selected'), 'true') + }) + + it('indicates first option when restarted', () => { + combobox.stop() + combobox.start() + assert.equal(list.children[0].getAttribute('aria-selected'), 'true') + }) + + it('applies default option on Enter', () => { + let commits = 0 + document.addEventListener('combobox-commit', () => commits++) + + assert.equal(commits, 0) + press(input, 'Enter') + assert.equal(commits, 1) + }) + + it('does not error when no options are visible', () => { + assert.doesNotThrow(() => { + document.getElementById('list-id').style.display = 'none' + combobox.clearSelection() + }) + }) + }) })