diff --git a/src/index.ts b/src/index.ts
index 35efe6e..f4f22b3 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,21 @@ export default class Combobox {
el.removeAttribute('data-combobox-option-default')
if (target === el) {
- this.input.setAttribute('aria-activedescendant', target.id)
+ if (!target.id && !document.getElementById(`${this.list.id}-selected`)) {
+ target.id = `${this.list.id}-selected`
+ this.didAutoAssignLastSelectedId = true
+ }
+ if (target.id) {
+ 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 +153,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..429c018 100644
--- a/test/test.js
+++ b/test/test.js
@@ -410,4 +410,71 @@ describe('combobox-nav', function () {
})
})
})
+
+ describe('with missing IDs on options', function () {
+ let input
+ let list
+ beforeEach(function () {
+ document.body.innerHTML = `
+
+
+ - Baymax
+ - Hubot
+ - R2-D2
+
+ `
+ 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'), null)
+
+ press(input, 'ArrowDown')
+ assert.equal(input.getAttribute('aria-activedescendant'), 'hubot')
+ assert.equal(list.children[0].getAttribute('id'), null)
+ assert.equal(list.children[1].getAttribute('id'), 'hubot')
+ assert.equal(list.children[2].getAttribute('id'), null)
+
+ press(input, 'ArrowDown')
+ assert.equal(input.getAttribute('aria-activedescendant'), 'list-id-selected')
+ assert.equal(list.children[0].getAttribute('id'), null)
+ 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'), null)
+ assert.equal(list.children[0].getAttribute('id'), null)
+ assert.equal(list.children[1].getAttribute('id'), 'hubot')
+ assert.equal(list.children[2].getAttribute('id'), null)
+ })
+
+ it('avoids collisions with existing IDs when automatically adding option IDs', function () {
+ const div = document.createElement('div')
+ div.setAttribute('id', 'list-id-selected')
+ document.body.appendChild(div)
+
+ const combobox = new Combobox(input, list)
+ combobox.start()
+ assert.equal(input.getAttribute('aria-expanded'), 'true')
+
+ press(input, 'ArrowDown')
+ assert.equal(input.getAttribute('aria-activedescendant'), null)
+ assert.equal(list.children[0].getAttribute('id'), null)
+ assert.equal(list.children[1].getAttribute('id'), 'hubot')
+ assert.equal(list.children[2].getAttribute('id'), null)
+ })
+ })
})