Skip to content

Commit c28985b

Browse files
author
Andrew Leach
committed
Add new option
1 parent f7aeecf commit c28985b

File tree

2 files changed

+76
-6
lines changed

2 files changed

+76
-6
lines changed

src/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
export type ComboboxSettings = {
22
tabInsertsSuggestions?: boolean
3-
defaultFirstOption?: boolean
3+
firstOptionSelectionMode?: FirstOptionSelectionMode
44
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
55
}
66

7+
// Indicates the default behaviour for the first option when the list is shown.
8+
export type FirstOptionSelectionMode = 'none' | 'selected' | 'focused'
9+
710
export default class Combobox {
811
isComposing: boolean
912
list: HTMLElement
@@ -13,18 +16,18 @@ export default class Combobox {
1316
inputHandler: (event: Event) => void
1417
ctrlBindings: boolean
1518
tabInsertsSuggestions: boolean
16-
defaultFirstOption: boolean
19+
firstOptionSelectionMode: FirstOptionSelectionMode
1720
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
1821

1922
constructor(
2023
input: HTMLTextAreaElement | HTMLInputElement,
2124
list: HTMLElement,
22-
{tabInsertsSuggestions, defaultFirstOption, scrollIntoViewOptions}: ComboboxSettings = {},
25+
{tabInsertsSuggestions, firstOptionSelectionMode, scrollIntoViewOptions}: ComboboxSettings = {},
2326
) {
2427
this.input = input
2528
this.list = list
2629
this.tabInsertsSuggestions = tabInsertsSuggestions ?? true
27-
this.defaultFirstOption = defaultFirstOption ?? false
30+
this.firstOptionSelectionMode = firstOptionSelectionMode ?? 'none'
2831
this.scrollIntoViewOptions = scrollIntoViewOptions ?? {block: 'nearest', inline: 'nearest'}
2932

3033
this.isComposing = false
@@ -64,6 +67,7 @@ export default class Combobox {
6467
;(this.input as HTMLElement).addEventListener('keydown', this.keyboardEventHandler)
6568
this.list.addEventListener('click', commitWithElement)
6669
this.indicateDefaultOption()
70+
this.focusDefaultOptionIfNeeded()
6771
}
6872

6973
stop(): void {
@@ -77,13 +81,19 @@ export default class Combobox {
7781
}
7882

7983
indicateDefaultOption(): void {
80-
if (this.defaultFirstOption) {
84+
if (this.firstOptionSelectionMode === 'selected') {
8185
Array.from(this.list.querySelectorAll<HTMLElement>('[role="option"]:not([aria-disabled="true"])'))
8286
.filter(visible)[0]
8387
?.setAttribute('data-combobox-option-default', 'true')
8488
}
8589
}
8690

91+
focusDefaultOptionIfNeeded(): void {
92+
if (this.firstOptionSelectionMode === 'focused') {
93+
this.navigate(1)
94+
}
95+
}
96+
8797
navigate(indexDiff: -1 | 1 = 1): void {
8898
const focusEl = Array.from(this.list.querySelectorAll<HTMLElement>('[aria-selected="true"]')).filter(visible)[0]
8999
const els = Array.from(this.list.querySelectorAll<HTMLElement>('[role="option"]')).filter(visible)

test/test.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ describe('combobox-nav', function () {
263263
input = document.querySelector('input')
264264
list = document.querySelector('ul')
265265
options = document.querySelectorAll('[role=option]')
266-
combobox = new Combobox(input, list, {defaultFirstOption: true})
266+
combobox = new Combobox(input, list, {firstOptionSelectionMode: 'selected'})
267267
combobox.start()
268268
})
269269

@@ -276,6 +276,7 @@ describe('combobox-nav', function () {
276276
it('indicates first option when started', () => {
277277
assert.equal(document.querySelector('[data-combobox-option-default]'), options[0])
278278
assert.equal(document.querySelectorAll('[data-combobox-option-default]').length, 1)
279+
assert.equal(list.children[0].getAttribute('aria-selected'), null)
279280
})
280281

281282
it('indicates first option when restarted', () => {
@@ -311,4 +312,63 @@ describe('combobox-nav', function () {
311312
})
312313
})
313314
})
315+
316+
describe('with defaulting to focusing the first option', function () {
317+
let input
318+
let list
319+
let combobox
320+
beforeEach(function () {
321+
document.body.innerHTML = `
322+
<input type="text">
323+
<ul role="listbox" id="list-id">
324+
<li id="baymax" role="option">Baymax</li>
325+
<li><del>BB-8</del></li>
326+
<li id="hubot" role="option">Hubot</li>
327+
<li id="r2-d2" role="option">R2-D2</li>
328+
<li id="johnny-5" hidden role="option">Johnny 5</li>
329+
<li id="wall-e" role="option" aria-disabled="true">Wall-E</li>
330+
<li><a href="#link" role="option" id="link">Link</a></li>
331+
</ul>
332+
`
333+
input = document.querySelector('input')
334+
list = document.querySelector('ul')
335+
combobox = new Combobox(input, list, {firstOptionSelectionMode: 'focused'})
336+
combobox.start()
337+
})
338+
339+
afterEach(function () {
340+
combobox.destroy()
341+
combobox = null
342+
document.body.innerHTML = ''
343+
})
344+
345+
it('focuses first option when started', () => {
346+
// Does not set the default attribute
347+
assert.equal(document.querySelectorAll('[data-combobox-option-default]').length, 0)
348+
// Item is correctly selected
349+
assert.equal(list.children[0].getAttribute('aria-selected'), 'true')
350+
})
351+
352+
it('indicates first option when restarted', () => {
353+
combobox.stop()
354+
combobox.start()
355+
assert.equal(list.children[0].getAttribute('aria-selected'), 'true')
356+
})
357+
358+
it('applies default option on Enter', () => {
359+
let commits = 0
360+
document.addEventListener('combobox-commit', () => commits++)
361+
362+
assert.equal(commits, 0)
363+
press(input, 'Enter')
364+
assert.equal(commits, 1)
365+
})
366+
367+
it('does not error when no options are visible', () => {
368+
assert.doesNotThrow(() => {
369+
document.getElementById('list-id').style.display = 'none'
370+
combobox.clearSelection()
371+
})
372+
})
373+
})
314374
})

0 commit comments

Comments
 (0)