Skip to content

Commit

Permalink
Add new option
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Leach committed Feb 22, 2024
1 parent f7aeecf commit c28985b
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 6 deletions.
20 changes: 15 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
62 changes: 61 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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()
})
})
})
})

0 comments on commit c28985b

Please sign in to comment.