From 51f159bcd5fd0979c8a168fb9018eec23c7b0db8 Mon Sep 17 00:00:00 2001 From: Henry Catalini Smith Date: Sun, 29 Mar 2026 08:51:34 +0200 Subject: [PATCH 1/4] Automatically set an ID on active combobox options --- src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.ts b/src/index.ts index 35efe6e..d6d4488 100644 --- a/src/index.ts +++ b/src/index.ts @@ -126,11 +126,17 @@ export default class Combobox { el.removeAttribute('data-combobox-option-default') if (target === el) { + if (!target.id) { + target.id = `${this.list.id}-selected` + } 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`) { + el.removeAttribute('id'); + } el.removeAttribute('aria-selected') } } From 0ed23d9e27e9765db43295a0792370948c9a6707 Mon Sep 17 00:00:00 2001 From: Henry Catalini Smith Date: Wed, 29 Apr 2026 20:23:16 +0200 Subject: [PATCH 2/4] Clean up after automatically assigned IDs --- src/index.ts | 8 ++++++-- test/test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d6d4488..434f9e8 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)}` @@ -128,14 +130,16 @@ export default class Combobox { 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`) { - el.removeAttribute('id'); + if (el.id === `${this.list.id}-selected` && this.didAutoAssignLastSelectedId) { + el.removeAttribute('id') + this.didAutoAssignLastSelectedId = false } el.removeAttribute('aria-selected') } diff --git a/test/test.js b/test/test.js index a39d783..0894166 100644 --- a/test/test.js +++ b/test/test.js @@ -410,4 +410,46 @@ 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(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(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(list.children[0].getAttribute('id'), undefined) + assert.equal(list.children[1].getAttribute('id'), 'hubot') + assert.equal(list.children[2].getAttribute('id'), 'list-id-selected') + }) + }) }) From 101fb88e0bf39974a600895c54b56adf895a6708 Mon Sep 17 00:00:00 2001 From: Henry Catalini Smith Date: Fri, 1 May 2026 10:13:48 +0200 Subject: [PATCH 3/4] Add an aria-activedescendant assertion to clarify what's happening --- test/test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test.js b/test/test.js index 0894166..fdbda54 100644 --- a/test/test.js +++ b/test/test.js @@ -437,16 +437,19 @@ describe('combobox-nav', function () { 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') From 8cabac9076637ed9d5ba8448f03d07b1d6f11c0c Mon Sep 17 00:00:00 2001 From: Henry Catalini Smith Date: Fri, 1 May 2026 10:18:45 +0200 Subject: [PATCH 4/4] Clear the automatic ID in clearSelection --- src/index.ts | 4 ++++ test/test.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/index.ts b/src/index.ts index 434f9e8..e1a55e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -151,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 fdbda54..3102972 100644 --- a/test/test.js +++ b/test/test.js @@ -453,6 +453,12 @@ describe('combobox-nav', function () { 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) }) }) })