diff --git a/src/index.ts b/src/index.ts index 35efe6e..e1a55e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ export default class Combobox { tabInsertsSuggestions: boolean firstOptionSelectionMode: FirstOptionSelectionMode scrollIntoViewOptions?: boolean | ScrollIntoViewOptions + didAutoAssignLastSelectedId: boolean constructor( input: HTMLTextAreaElement | HTMLInputElement, @@ -44,6 +45,7 @@ export default class Combobox { this.scrollIntoViewOptions = scrollIntoViewOptions ?? {block: 'nearest', inline: 'nearest'} this.isComposing = false + this.didAutoAssignLastSelectedId = false if (!list.id) { list.id = `combobox-${Math.random().toString().slice(2, 6)}` @@ -126,11 +128,19 @@ export default class Combobox { el.removeAttribute('data-combobox-option-default') if (target === el) { + if (!target.id) { + target.id = `${this.list.id}-selected` + this.didAutoAssignLastSelectedId = true + } this.input.setAttribute('aria-activedescendant', target.id) target.setAttribute('aria-selected', 'true') fireSelectEvent(target) target.scrollIntoView(this.scrollIntoViewOptions) } else { + if (el.id === `${this.list.id}-selected` && this.didAutoAssignLastSelectedId) { + el.removeAttribute('id') + this.didAutoAssignLastSelectedId = false + } el.removeAttribute('aria-selected') } } @@ -141,6 +151,10 @@ export default class Combobox { for (const el of this.list.querySelectorAll('[aria-selected="true"], [data-combobox-option-default="true"]')) { el.removeAttribute('aria-selected') el.removeAttribute('data-combobox-option-default') + if (el.id === `${this.list.id}-selected` && this.didAutoAssignLastSelectedId) { + el.removeAttribute('id') + this.didAutoAssignLastSelectedId = false + } } } diff --git a/test/test.js b/test/test.js index a39d783..3102972 100644 --- a/test/test.js +++ b/test/test.js @@ -410,4 +410,55 @@ describe('combobox-nav', function () { }) }) }) + + describe('with missing IDs on options', function () { + let input + let list + beforeEach(function () { + document.body.innerHTML = ` + + + ` + input = document.querySelector('input') + list = document.querySelector('ul') + }) + + afterEach(function () { + document.body.innerHTML = '' + }) + + it('automatically adds and removes option IDs when needed for aria-activedescendant', function () { + const combobox = new Combobox(input, list) + combobox.start() + assert.equal(input.getAttribute('aria-expanded'), 'true') + + press(input, 'ArrowDown') + assert.equal(input.getAttribute('aria-activedescendant'), 'list-id-selected') + assert.equal(list.children[0].getAttribute('id'), 'list-id-selected') + assert.equal(list.children[1].getAttribute('id'), 'hubot') + assert.equal(list.children[2].getAttribute('id'), undefined) + + press(input, 'ArrowDown') + assert.equal(input.getAttribute('aria-activedescendant'), 'hubot') + assert.equal(list.children[0].getAttribute('id'), undefined) + assert.equal(list.children[1].getAttribute('id'), 'hubot') + assert.equal(list.children[2].getAttribute('id'), undefined) + + press(input, 'ArrowDown') + assert.equal(input.getAttribute('aria-activedescendant'), 'list-id-selected') + assert.equal(list.children[0].getAttribute('id'), undefined) + assert.equal(list.children[1].getAttribute('id'), 'hubot') + assert.equal(list.children[2].getAttribute('id'), 'list-id-selected') + + press(input, 'Escape') + assert.equal(input.getAttribute('aria-activedescendant'), undefined) + assert.equal(list.children[0].getAttribute('id'), undefined) + assert.equal(list.children[1].getAttribute('id'), 'hubot') + assert.equal(list.children[2].getAttribute('id'), undefined) + }) + }) })