Repository navigation
bug(aria/combobox): keys pressed back to back are lost; only the last one reaches the popup #33973
Description
Is this a regression?
- Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was
No response
Description
While the popup is open, ComboboxPattern relays navigation and typeahead keys to the popup through a signal: the key handler does this.keyboardEventRelay.set(e), and an effect later re-dispatches the event on the popup's control target. If a second key arrives before the effect runs, the signal is overwritten and the first key is never relayed.
Effects:
- Down, Down, Down pressed quickly moves the active option once.
- Typeahead loses characters: with options
item,item-2,item-3, typingitem-3with no delay between keys lands onitem; with 60 ms between keys it lands onitem-3. - Test automation and tools that send keys in a burst (and some assistive technology input) see wrong results. Seen in Chromium through Playwright, and in unit tests.
Related: #33361 (same relay signal, different symptom: a stale event replayed on reopen).
Reproduction
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Combobox, ComboboxPopup, ComboboxWidget } from '@angular/aria/combobox';
import { Listbox, Option } from '@angular/aria/listbox';
@Component({
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option],
template: `
<div ngCombobox #combobox="ngCombobox" tabindex="0">Pick one</div>
<ng-template ngComboboxPopup [combobox]="combobox">
<div ngListbox ngComboboxWidget #listbox="ngListbox" focusMode="activedescendant"
[activeDescendant]="listbox.activeDescendant()">
@for (o of options; track o) {
<div ngOption [value]="o" [label]="o">{{ o }}</div>
}
</div>
</ng-template>
`,
})
class Repro {
options = ['alpha', 'bravo', 'charlie', 'delta'];
}
const key = (el: Element, k: string) =>
el.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true }));
it('handles each key pressed back to back', async () => {
const fixture = TestBed.createComponent(Repro);
await fixture.whenStable();
const combobox = fixture.nativeElement.querySelector('[ngCombobox]');
combobox.focus();
key(combobox, 'ArrowDown'); // open
await fixture.whenStable();
key(combobox, 'Home');
await fixture.whenStable();
key(combobox, 'ArrowDown');
key(combobox, 'ArrowDown');
key(combobox, 'ArrowDown');
await fixture.whenStable();
const id = combobox.getAttribute('aria-activedescendant');
expect(fixture.nativeElement.querySelector(`[id="${id}"]`).textContent.trim())
.toBe('delta'); // actual: 'bravo'
});Expected Behavior
Each key event is handled, in order. Three Down presses move three options.
Actual Behavior
Only the last key since the effect last ran is relayed. Three Down presses move one option ("bravo" in place of "delta").
A possible fix: relay each event synchronously from the key handler (or queue them), not through a single-value signal.
Environment
- Angular: 22.2.1
- CDK/Material: @angular/cdk 22.2.3, @angular/aria 22.2.3 (also in 22.3.0-next.2 and
main) - Browser(s): Chromium (Playwright); Vitest with happy-dom
- Operating System (e.g. Windows, macOS, Ubuntu): macOS
Activity