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
3 changes: 3 additions & 0 deletions goldens/aria/private/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class ComboboxPattern {
readonly ariaReadonly: _angular_core.Signal<"true" | null>;
readonly autocomplete: _angular_core.Signal<"none" | "inline" | "list" | "both">;
click: _angular_core.Signal<ClickEventManager<PointerEvent>>;
closePopupOnFocusout(): void;
readonly disabled: () => boolean;
readonly element: () => HTMLElement;
highlightEffect(): void;
Expand Down Expand Up @@ -109,6 +110,7 @@ export class ComboboxPattern {
// @public
export interface ComboboxPopupInputs {
activeDescendant: SignalLike<string | undefined>;
combobox: SignalLike<ComboboxPattern | undefined>;
controlTarget: SignalLike<HTMLElement | undefined>;
popupId: SignalLike<string | undefined>;
popupType: SignalLike<'listbox' | 'tree' | 'grid' | 'dialog'>;
Expand All @@ -118,6 +120,7 @@ export interface ComboboxPopupInputs {
export class ComboboxPopupPattern {
constructor(inputs: ComboboxPopupInputs);
readonly activeDescendant: () => string | undefined;
readonly combobox: () => ComboboxPattern | undefined;
readonly controlTarget: () => HTMLElement | undefined;
// (undocumented)
readonly inputs: ComboboxPopupInputs;
Expand Down
3 changes: 2 additions & 1 deletion src/aria/combobox/combobox-popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export class ComboboxPopup implements OnInit, OnDestroy {
readonly popupType = input<'listbox' | 'tree' | 'grid' | 'dialog'>('listbox');

/** The popup pattern. */
readonly _pattern = new ComboboxPopupPattern({
readonly _pattern: ComboboxPopupPattern = new ComboboxPopupPattern({
...this,
combobox: computed(() => this.combobox()._pattern),
});

ngOnInit() {
Expand Down
29 changes: 29 additions & 0 deletions src/aria/private/combobox/combobox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ describe('ComboboxPattern', () => {
const controlTarget = document.createElement('div');
const popupType = signal<'listbox' | 'tree' | 'grid' | 'dialog'>(inputs.popupType ?? 'listbox');

const combobox = signal<ComboboxPattern | undefined>(undefined);
const popup = new ComboboxPopupPattern({
popupType,
controlTarget: signal(controlTarget),
activeDescendant,
popupId,
combobox,
});

const pattern = new ComboboxPattern({
Expand All @@ -45,6 +47,8 @@ describe('ComboboxPattern', () => {
expandable: signal(true),
});

combobox.set(pattern);

return {
pattern,
element,
Expand Down Expand Up @@ -234,6 +238,31 @@ describe('ComboboxPattern', () => {

expect(expanded()).toBe(true);
});

it('should close when focus leaves the popup', async () => {
const {pattern, expanded, popup} = setup();
expanded.set(true);
pattern.isFocused.set(false);
popup.isFocused.set(true);

popup.onFocusout(new FocusEvent('focusout'));
await wait(100);

expect(expanded()).toBe(false);
});

it('should remain open if focus moves back to the combobox', async () => {
const {pattern, expanded, popup} = setup();
expanded.set(true);
pattern.isFocused.set(false);
popup.isFocused.set(true);

popup.onFocusout(new FocusEvent('focusout'));
pattern.onFocusin();
await wait(100);

expect(expanded()).toBe(true);
});
});

describe('Advanced Combo Keys Relay', () => {
Expand Down
15 changes: 13 additions & 2 deletions src/aria/private/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ export class ComboboxPattern {

/** Handles focus out events for the combobox. */
onFocusout() {
this.closePopupOnFocusout();
this.isFocused.set(false);
}

/** Closes the popup once focus has left both the combobox and the popup. */
closePopupOnFocusout() {
// Give focus some time to move before we check it.
setTimeout(() => {
const comboboxFocused = this.isFocused();
Expand All @@ -226,8 +232,6 @@ export class ComboboxPattern {
this.inputs.expanded.set(false);
}
});

this.isFocused.set(false);
}

/** Handles input events for the combobox. */
Expand Down Expand Up @@ -291,6 +295,9 @@ export interface ComboboxPopupInputs {

/** The ID of the popup. */
popupId: SignalLike<string | undefined>;

/** A reference to the parent combobox. */
combobox: SignalLike<ComboboxPattern | undefined>;
}

/** Controls the state of a simple combobox popup. */
Expand All @@ -307,6 +314,9 @@ export class ComboboxPopupPattern {
/** The ID of the popup. */
readonly popupId = () => this.inputs.popupId();

/** A reference to the parent combobox. */
readonly combobox = () => this.inputs.combobox();

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

Expand All @@ -323,5 +333,6 @@ export class ComboboxPopupPattern {
if (this.controlTarget()?.contains(focusTarget)) return;

this.isFocused.set(false);
this.combobox()?.closePopupOnFocusout();
}
}
Loading