diff --git a/src/aria/combobox/combobox.ts b/src/aria/combobox/combobox.ts index 704a70d457c2..a5bda22d7c71 100644 --- a/src/aria/combobox/combobox.ts +++ b/src/aria/combobox/combobox.ts @@ -130,7 +130,7 @@ export class Combobox extends DeferredContentAware implements OnInit { constructor() { super(); - afterRenderEffect(() => this._pattern.keyboardEventRelayEffect()); + afterRenderEffect({write: () => this._pattern.keyboardEventRelayEffect()}); afterRenderEffect(() => this._pattern.closePopupOnBlurEffect()); afterRenderEffect(() => { this.contentVisible.set(this._pattern.isExpanded()); diff --git a/src/aria/combobox/combobox.zone.spec.ts b/src/aria/combobox/combobox.zone.spec.ts new file mode 100644 index 000000000000..ab603ee12fbe --- /dev/null +++ b/src/aria/combobox/combobox.zone.spec.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {Component, computed, signal, provideZoneChangeDetection} from '@angular/core'; +import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {Combobox} from './combobox'; +import {ComboboxPopup} from './combobox-popup'; +import {ComboboxWidget} from './combobox-widget'; +import {Listbox, Option} from '../listbox'; + +describe('Combobox Zone.js integration', () => { + let fixture: ComponentFixture; + let inputElement: HTMLInputElement; + + beforeEach(async () => { + TestBed.configureTestingModule({ + providers: [provideZoneChangeDetection()], + }); + fixture = TestBed.createComponent(ComboboxListboxZoneExample); + await fixture.whenStable(); + const inputDebugElement = fixture.debugElement.query(By.directive(Combobox)); + inputElement = inputDebugElement.nativeElement as HTMLInputElement; + }); + + const focus = () => { + inputElement.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); + fixture.detectChanges(); + }; + + const keydown = (key: string) => { + focus(); + inputElement.dispatchEvent( + new KeyboardEvent('keydown', { + key, + bubbles: true, + }), + ); + fixture.detectChanges(); + }; + + function getOption(text: string): HTMLElement | null { + const options = Array.from(document.querySelectorAll('[ngoption]')) as HTMLElement[]; + return options.find(option => option.textContent?.trim() === text) || null; + } + + it('should relay ArrowDown to the listbox and update active descendant', fakeAsync(() => { + // Open the popup (sets active descendant to Alabama via default state) + keydown('ArrowDown'); + tick(); + fixture.detectChanges(); + + // Check if expanded is true + expect(inputElement.getAttribute('aria-expanded')).toBe('true'); + // Active descendant is bound to host: + const alabama = getOption('Alabama')!; + expect(inputElement.getAttribute('aria-activedescendant')).toBe(alabama.id); + + // Press ArrowDown again to move to Alaska + keydown('ArrowDown'); + tick(); + fixture.detectChanges(); + + const alaska = getOption('Alaska')!; + expect(inputElement.getAttribute('aria-activedescendant')).toBe(alaska.id); + })); +}); + +@Component({ + template: ` +
+ + +
    + @for (option of options(); track option) { +
  • {{option}}
  • + } +
+
+
+ `, + imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option], +}) +class ComboboxListboxZoneExample { + popupExpanded = signal(false); + searchString = signal(''); + value = signal([]); + options = computed(() => ['Alabama', 'Alaska', 'Arizona', 'Arkansas']); +} diff --git a/src/aria/private/combobox/combobox.ts b/src/aria/private/combobox/combobox.ts index a119779db3c7..b0833a03268f 100644 --- a/src/aria/private/combobox/combobox.ts +++ b/src/aria/private/combobox/combobox.ts @@ -260,7 +260,8 @@ export class ComboboxPattern { untracked(() => { const popup = this.inputs.popup(); if (this.isExpanded()) { - popup?.controlTarget()?.dispatchEvent(event); + const relayedEvent = new KeyboardEvent(event.type, event); + popup?.controlTarget()?.dispatchEvent(relayedEvent); } }); }