This commit is contained in:
Andrew Leach 2024-02-22 09:34:01 +00:00
Родитель f7aeecf5ec
Коммит c28985b6c7
2 изменённых файлов: 76 добавлений и 6 удалений

Просмотреть файл

@ -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<HTMLElement>('[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<HTMLElement>('[aria-selected="true"]')).filter(visible)[0]
const els = Array.from(this.list.querySelectorAll<HTMLElement>('[role="option"]')).filter(visible)

Просмотреть файл

@ -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 = `
<input type="text">
<ul role="listbox" id="list-id">
<li id="baymax" role="option">Baymax</li>
<li><del>BB-8</del></li>
<li id="hubot" role="option">Hubot</li>
<li id="r2-d2" role="option">R2-D2</li>
<li id="johnny-5" hidden role="option">Johnny 5</li>
<li id="wall-e" role="option" aria-disabled="true">Wall-E</li>
<li><a href="#link" role="option" id="link">Link</a></li>
</ul>
`
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()
})
})
})
})