Skip to content

Commit 5635e33

Browse files
Merge pull request #102 from henrycatalinismith/selected-id
Automatically set an ID on active combobox options
2 parents 4892659 + b17e234 commit 5635e33

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

src/index.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default class Combobox {
3131
tabInsertsSuggestions: boolean
3232
firstOptionSelectionMode: FirstOptionSelectionMode
3333
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
34+
didAutoAssignLastSelectedId: boolean
3435

3536
constructor(
3637
input: HTMLTextAreaElement | HTMLInputElement,
@@ -44,6 +45,7 @@ export default class Combobox {
4445
this.scrollIntoViewOptions = scrollIntoViewOptions ?? {block: 'nearest', inline: 'nearest'}
4546

4647
this.isComposing = false
48+
this.didAutoAssignLastSelectedId = false
4749

4850
if (!list.id) {
4951
list.id = `combobox-${Math.random().toString().slice(2, 6)}`
@@ -126,11 +128,21 @@ export default class Combobox {
126128
el.removeAttribute('data-combobox-option-default')
127129

128130
if (target === el) {
129-
this.input.setAttribute('aria-activedescendant', target.id)
131+
if (!target.id && !document.getElementById(`${this.list.id}-selected`)) {
132+
target.id = `${this.list.id}-selected`
133+
this.didAutoAssignLastSelectedId = true
134+
}
135+
if (target.id) {
136+
this.input.setAttribute('aria-activedescendant', target.id)
137+
}
130138
target.setAttribute('aria-selected', 'true')
131139
fireSelectEvent(target)
132140
target.scrollIntoView(this.scrollIntoViewOptions)
133141
} else {
142+
if (el.id === `${this.list.id}-selected` && this.didAutoAssignLastSelectedId) {
143+
el.removeAttribute('id')
144+
this.didAutoAssignLastSelectedId = false
145+
}
134146
el.removeAttribute('aria-selected')
135147
}
136148
}
@@ -141,6 +153,10 @@ export default class Combobox {
141153
for (const el of this.list.querySelectorAll('[aria-selected="true"], [data-combobox-option-default="true"]')) {
142154
el.removeAttribute('aria-selected')
143155
el.removeAttribute('data-combobox-option-default')
156+
if (el.id === `${this.list.id}-selected` && this.didAutoAssignLastSelectedId) {
157+
el.removeAttribute('id')
158+
this.didAutoAssignLastSelectedId = false
159+
}
144160
}
145161
}
146162

test/test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,4 +410,71 @@ describe('combobox-nav', function () {
410410
})
411411
})
412412
})
413+
414+
describe('with missing IDs on options', function () {
415+
let input
416+
let list
417+
beforeEach(function () {
418+
document.body.innerHTML = `
419+
<input type="text">
420+
<ul role="listbox" id="list-id">
421+
<li role="option">Baymax</li>
422+
<li id="hubot" role="option">Hubot</li>
423+
<li role="option">R2-D2</li>
424+
</ul>
425+
`
426+
input = document.querySelector('input')
427+
list = document.querySelector('ul')
428+
})
429+
430+
afterEach(function () {
431+
document.body.innerHTML = ''
432+
})
433+
434+
it('automatically adds and removes option IDs when needed for aria-activedescendant', function () {
435+
const combobox = new Combobox(input, list)
436+
combobox.start()
437+
assert.equal(input.getAttribute('aria-expanded'), 'true')
438+
439+
press(input, 'ArrowDown')
440+
assert.equal(input.getAttribute('aria-activedescendant'), 'list-id-selected')
441+
assert.equal(list.children[0].getAttribute('id'), 'list-id-selected')
442+
assert.equal(list.children[1].getAttribute('id'), 'hubot')
443+
assert.equal(list.children[2].getAttribute('id'), null)
444+
445+
press(input, 'ArrowDown')
446+
assert.equal(input.getAttribute('aria-activedescendant'), 'hubot')
447+
assert.equal(list.children[0].getAttribute('id'), null)
448+
assert.equal(list.children[1].getAttribute('id'), 'hubot')
449+
assert.equal(list.children[2].getAttribute('id'), null)
450+
451+
press(input, 'ArrowDown')
452+
assert.equal(input.getAttribute('aria-activedescendant'), 'list-id-selected')
453+
assert.equal(list.children[0].getAttribute('id'), null)
454+
assert.equal(list.children[1].getAttribute('id'), 'hubot')
455+
assert.equal(list.children[2].getAttribute('id'), 'list-id-selected')
456+
457+
press(input, 'Escape')
458+
assert.equal(input.getAttribute('aria-activedescendant'), null)
459+
assert.equal(list.children[0].getAttribute('id'), null)
460+
assert.equal(list.children[1].getAttribute('id'), 'hubot')
461+
assert.equal(list.children[2].getAttribute('id'), null)
462+
})
463+
464+
it('avoids collisions with existing IDs when automatically adding option IDs', function () {
465+
const div = document.createElement('div')
466+
div.setAttribute('id', 'list-id-selected')
467+
document.body.appendChild(div)
468+
469+
const combobox = new Combobox(input, list)
470+
combobox.start()
471+
assert.equal(input.getAttribute('aria-expanded'), 'true')
472+
473+
press(input, 'ArrowDown')
474+
assert.equal(input.getAttribute('aria-activedescendant'), null)
475+
assert.equal(list.children[0].getAttribute('id'), null)
476+
assert.equal(list.children[1].getAttribute('id'), 'hubot')
477+
assert.equal(list.children[2].getAttribute('id'), null)
478+
})
479+
})
413480
})

0 commit comments

Comments
 (0)