Skip to content
Open
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
14 changes: 14 additions & 0 deletions src/aria/combobox/combobox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,20 @@ describe('Combobox', () => {
expect(inputElement.getAttribute('aria-activedescendant')).toBe(options[1].id);
});

it('should handle each ArrowDown pressed back to back', async () => {
await down(); // Open popup
const press = () =>
inputElement.dispatchEvent(
new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true}),
);
press();
press();
press();
await fixture.whenStable();
const options = getOptions();
expect(inputElement.getAttribute('aria-activedescendant')).toBe(options[3].id);
});

it('should navigate to the previous item on ArrowUp when open', async () => {
await down(); // Open
await down(); // Move to next item
Expand Down
55 changes: 36 additions & 19 deletions src/aria/private/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export class ComboboxPattern {
/** A relay for keyboard events to the popup. */
readonly keyboardEventRelay = signal<KeyboardEvent | undefined>(undefined);

/**
* Events waiting to be relayed to the popup. The relay signal only holds the latest event, so
* events that arrive before the relay effect runs are queued here to avoid dropping them.
*/
private _pendingRelayEvents: KeyboardEvent[] = [];

/** Whether the combobox is focused. */
readonly isFocused = signal(false);

Expand Down Expand Up @@ -137,44 +143,44 @@ export class ComboboxPattern {
.on(
'ArrowLeft',
e => {
this.keyboardEventRelay.set(e);
this._relayKeyboardEvent(e);
},
{preventDefault: this.popupType() !== 'listbox', ignoreRepeat: false},
)
.on(
'ArrowRight',
e => {
this.keyboardEventRelay.set(e);
this._relayKeyboardEvent(e);
},
{preventDefault: this.popupType() !== 'listbox', ignoreRepeat: false},
)
.on('ArrowUp', e => this.keyboardEventRelay.set(e), {ignoreRepeat: false})
.on('ArrowDown', e => this.keyboardEventRelay.set(e), {ignoreRepeat: false})
.on(Modifier.Shift, 'ArrowUp', e => this.keyboardEventRelay.set(e), {ignoreRepeat: false})
.on(Modifier.Shift, 'ArrowDown', e => this.keyboardEventRelay.set(e), {ignoreRepeat: false})
.on('Home', e => this.keyboardEventRelay.set(e))
.on('End', e => this.keyboardEventRelay.set(e))
.on('PageUp', e => this.keyboardEventRelay.set(e))
.on('PageDown', e => this.keyboardEventRelay.set(e))
.on('ArrowUp', e => this._relayKeyboardEvent(e), {ignoreRepeat: false})
.on('ArrowDown', e => this._relayKeyboardEvent(e), {ignoreRepeat: false})
.on(Modifier.Shift, 'ArrowUp', e => this._relayKeyboardEvent(e), {ignoreRepeat: false})
.on(Modifier.Shift, 'ArrowDown', e => this._relayKeyboardEvent(e), {ignoreRepeat: false})
.on('Home', e => this._relayKeyboardEvent(e))
.on('End', e => this._relayKeyboardEvent(e))
.on('PageUp', e => this._relayKeyboardEvent(e))
.on('PageDown', e => this._relayKeyboardEvent(e))
.on('Escape', () => {
if (!this.inputs.alwaysExpanded()) {
this.inputs.expanded.set(false);
}
});

if (!this.readonly()) {
manager.on('Enter', e => this.keyboardEventRelay.set(e));
manager.on('Enter', e => this._relayKeyboardEvent(e));
}

if (!this.isEditable() && !this.readonly()) {
manager
.on(' ', e => this.keyboardEventRelay.set(e))
.on([Modifier.Ctrl, Modifier.Meta], 'a', e => this.keyboardEventRelay.set(e))
.on([Modifier.Ctrl, Modifier.Meta], 'A', e => this.keyboardEventRelay.set(e))
.on(Modifier.Shift, 'Home', e => this.keyboardEventRelay.set(e))
.on(Modifier.Shift, 'End', e => this.keyboardEventRelay.set(e))
.on(' ', e => this._relayKeyboardEvent(e))
.on([Modifier.Ctrl, Modifier.Meta], 'a', e => this._relayKeyboardEvent(e))
.on([Modifier.Ctrl, Modifier.Meta], 'A', e => this._relayKeyboardEvent(e))
.on(Modifier.Shift, 'Home', e => this._relayKeyboardEvent(e))
.on(Modifier.Shift, 'End', e => this._relayKeyboardEvent(e))
.on(/^.$/, e => {
this.keyboardEventRelay.set(e);
this._relayKeyboardEvent(e);
});
}

Expand All @@ -196,6 +202,12 @@ export class ComboboxPattern {
this.value = inputs.value;
}

/** Queues a keyboard event to be relayed to the popup. */
private _relayKeyboardEvent(event: KeyboardEvent) {
this._pendingRelayEvents.push(event);
this.keyboardEventRelay.set(event);
}

/** Handles keydown events for the combobox. */
onKeydown(event: KeyboardEvent) {
if (!this.inputs.disabled()) {
Expand Down Expand Up @@ -269,14 +281,19 @@ export class ComboboxPattern {
const event = this.keyboardEventRelay();
if (event === undefined) return;

const events = this._pendingRelayEvents;
this._pendingRelayEvents = [];

// Reset isDeleting when the user navigates, so that the highlight effect can run again.
this.isDeleting.set(false);

untracked(() => {
const popup = this.inputs.popup();
if (this.isExpanded()) {
const relayedEvent = new KeyboardEvent(event.type, event);
popup?.controlTarget()?.dispatchEvent(relayedEvent);
const target = popup?.controlTarget();
for (const pendingEvent of events) {
target?.dispatchEvent(new KeyboardEvent(pendingEvent.type, pendingEvent));
}
}
});
}
Expand Down
Loading