diff --git a/README.md b/README.md
index c6f6ca3..4f5a5a1 100644
--- a/README.md
+++ b/README.md
@@ -90,8 +90,11 @@ const combobox = new Combobox(input, list, {tabInsertsSuggestions: true})
These settings are available:
- `tabInsertsSuggestions: boolean = true` - Control whether the highlighted suggestion is inserted when Tab is pressed (Enter will always insert a suggestion regardless of this setting). When `true`, tab-navigation will be hijacked when open (which can have negative impacts on accessibility) but the combobox will more closely imitate a native IDE experience.
-- `defaultFirstOption: boolean = false` - If no options are selected and the user presses Enter, should the first item be inserted? If enabled, the default option can be selected and styled with `[data-combobox-option-default]` . This should be styled differently from the `aria-selected` option.
- > **Warning** Screen readers will not announce that the first item is the default. This should be announced explicitly with the use of `aria-live` status text.
+- `firstOptionSelectionMode: FirstOptionSelectionMode = 'none'` - This option dictates the default behaviour when no options have been selected yet and the user presses Enter. The following values of `FirstOptionSelectionMode` will do the following:
+ - `'none'`: Don't auto-select the first option at all.
+ - `'active'`: Place the first option in an 'active' state where it is not selected (is not the `aria-activedescendant`) but will still be applied if the user presses `Enter`. To select the second item, the user would need to press the down arrow twice. This approach allows quick application of selections without disrupting screen reader users.
+ > **Warning** Screen readers will not announce that the first item is the default. This should be announced explicitly with the use of `aria-live` status
+ - `'selected'`: Select the first item by navigating to it. This allows quick application of selections and makes it faster to select the second item, but can be disruptive or confusing for screen reader users.
- `scrollIntoViewOptions?: boolean | ScrollIntoViewOptions = undefined` - When
controlling the element marked `[aria-selected="true"]` with keyboard navigation, the selected element will be scrolled into the viewport by a call to [Element.scrollIntoView][]. Configure this value to control the scrolling behavior (either with a `boolean` or a [ScrollIntoViewOptions][] object.
diff --git a/src/index.ts b/src/index.ts
index 6a60a10..5164a8c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,25 @@
export type ComboboxSettings = {
tabInsertsSuggestions?: boolean
- defaultFirstOption?: boolean
+ /**
+ * Indicates the default behaviour for the first option when the list is shown:
+ *
+ * - `'none'`: Don't auto-select the first option at all.
+ * - `'active'`: Place the first option in an 'active' state where it is not
+ * selected (is not the `aria-activedescendant`) but will still be applied
+ * if the user presses `Enter`. To select the second item, the user would
+ * need to press the down arrow twice. This approach allows quick application
+ * of selections without disrupting screen reader users.
+ * - `'selected'`: Select the first item by navigating to it. This allows quick
+ * application of selections and makes it faster to select the second item,
+ * but can be disruptive or confusing for screen reader users.
+ */
+ firstOptionSelectionMode?: FirstOptionSelectionMode
scrollIntoViewOptions?: boolean | ScrollIntoViewOptions
}
+// Indicates the default behaviour for the first option when the list is shown.
+export type FirstOptionSelectionMode = 'none' | 'active' | 'selected'
+
export default class Combobox {
isComposing: boolean
list: HTMLElement
@@ -13,18 +29,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
@@ -77,10 +93,12 @@ export default class Combobox {
}
indicateDefaultOption(): void {
- if (this.defaultFirstOption) {
+ if (this.firstOptionSelectionMode === 'active') {
Array.from(this.list.querySelectorAll('[role="option"]:not([aria-disabled="true"])'))
.filter(visible)[0]
?.setAttribute('data-combobox-option-default', 'true')
+ } else if (this.firstOptionSelectionMode === 'selected') {
+ this.navigate(1)
}
}
@@ -123,7 +141,10 @@ export default class Combobox {
for (const el of this.list.querySelectorAll('[aria-selected="true"]')) {
el.removeAttribute('aria-selected')
}
- this.indicateDefaultOption()
+
+ if (this.firstOptionSelectionMode === 'active') {
+ this.indicateDefaultOption()
+ }
}
}
diff --git a/test/test.js b/test/test.js
index 03a82c5..3368cce 100644
--- a/test/test.js
+++ b/test/test.js
@@ -242,7 +242,7 @@ describe('combobox-nav', function () {
})
})
- describe('with defaulting to first option', function () {
+ describe('with defaulting to the first option being active', function () {
let input
let list
let options
@@ -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: 'active'})
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 the first option being selected', function () {
+ let input
+ let list
+ let combobox
+ beforeEach(function () {
+ document.body.innerHTML = `
+
+