Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ combobox.destroy()

## Events

A bubbling `combobox-commit` event is fired on the list element when an option is selected via keyboard or click.
A bubbling `combobox-commit` event is fired on the list element when an option is selected via keyboard or mouse. Mouse selection is observed on `mousedown` so the event still fires if the input blurs before `click` (for example when a `blur` handler hides the list).

For example, autocomplete when an option is selected:

Expand Down
23 changes: 23 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default class Combobox {
this.input.addEventListener('compositionend', this.compositionEventHandler)
this.input.addEventListener('input', this.inputHandler)
;(this.input as HTMLElement).addEventListener('keydown', this.keyboardEventHandler)
this.list.addEventListener('mousedown', commitWithElement)
this.list.addEventListener('click', commitWithElement)
this.resetSelection()
}
Expand All @@ -91,6 +92,7 @@ export default class Combobox {
this.input.removeEventListener('compositionend', this.compositionEventHandler)
this.input.removeEventListener('input', this.inputHandler)
;(this.input as HTMLElement).removeEventListener('keydown', this.keyboardEventHandler)
this.list.removeEventListener('mousedown', commitWithElement)
this.list.removeEventListener('click', commitWithElement)
}

Expand Down Expand Up @@ -211,11 +213,32 @@ function keyboardBindings(event: KeyboardEvent, combobox: Combobox) {
}
}

// Set when a mousedown has already committed an option, so the mouse click that
// follows does not commit it a second time. That click may never arrive, for example
// when the pointer leaves the option before the button is released, so only a mouse
// click (detail > 0) is suppressed: keyboard and programmatic clicks (detail 0)
// always commit, and the next click anywhere in the document clears the state.
let committedOnMousedown: Element | null = null

function clearMousedownCommit(): void {
committedOnMousedown = null
}

function commitWithElement(event: MouseEvent) {
if (!(event.target instanceof Element)) return
const target = event.target.closest('[role="option"]')
if (!target) return
if (target.getAttribute('aria-disabled') === 'true') return

if (event.type === 'mousedown') {
if (event.button !== 0) return
committedOnMousedown = target
target.ownerDocument.addEventListener('click', clearMousedownCommit, {once: true})
} else if (event.detail > 0 && committedOnMousedown === target) {
committedOnMousedown = null
return
}

fireCommitEvent(target, {event})
}

Expand Down
57 changes: 56 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ function press(input, key, ctrlKey) {
}

function click(element) {
element.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))
element.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))
element.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, detail: 1}))
}

describe('combobox-nav', function () {
Expand Down Expand Up @@ -164,6 +165,60 @@ describe('combobox-nav', function () {
assert.equal(expectedTargets[1], 'baymax')
})

it('fires a commit event on a direct option.click()', function () {
const expectedTargets = []

document.addEventListener('combobox-commit', function ({target}) {
expectedTargets.push(target.id)
})

document.getElementById('hubot').click()

assert.equal(expectedTargets.length, 1)
assert.equal(expectedTargets[0], 'hubot')
})

it('fires a commit event on option.click() after a mousedown with no click', function () {
const expectedTargets = []

document.addEventListener('combobox-commit', function ({target}) {
expectedTargets.push(target.id)
})

// A mousedown that no click follows, for example when the window loses focus.
const option = document.getElementById('hubot')
option.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))

option.click()

assert.equal(expectedTargets.length, 2)
assert.equal(expectedTargets[0], 'hubot')
assert.equal(expectedTargets[1], 'hubot')
})

it('fires commit before a blur handler can stop the combobox', function () {
const expectedTargets = []

document.addEventListener('combobox-commit', function ({target}) {
expectedTargets.push(target.id)
})

input.addEventListener('blur', function () {
list.hidden = true
combobox.clearSelection()
combobox.stop()
})

const option = document.getElementById('hubot')
option.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}))
input.dispatchEvent(new FocusEvent('blur'))
option.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, cancelable: true}))
option.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true, detail: 1}))

assert.equal(expectedTargets.length, 1)
assert.equal(expectedTargets[0], 'hubot')
})

it('fires select events on navigating', function () {
const expectedTargets = []

Expand Down
Loading