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
12 changes: 6 additions & 6 deletions goldens/aria/private/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ export class ComboboxListboxPattern<V> extends ListboxPattern<V> implements Comb
last: () => void;
multi: SignalLike<boolean>;
next: () => void;
onClick(_: PointerEvent): void;
onKeydown(_: KeyboardEvent): void;
onPointerdown(_: PointerEvent): void;
prev: () => void;
role: SignalLike<"listbox">;
select: (item?: OptionPattern<V>) => void;
Expand Down Expand Up @@ -450,6 +450,7 @@ export type ListboxInputs<V> = ListInputs<OptionPattern<V>, V> & {
export class ListboxPattern<V> {
constructor(inputs: ListboxInputs<V>);
activeDescendant: SignalLike<string | undefined>;
clickManager: SignalLike<ClickEventManager<PointerEvent>>;
disabled: SignalLike<boolean>;
dynamicSpaceKey: SignalLike<"" | " ">;
followFocus: SignalLike<boolean>;
Expand All @@ -462,11 +463,10 @@ export class ListboxPattern<V> {
listBehavior: List<OptionPattern<V>, V>;
multi: SignalLike<boolean>;
nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
onKeydown(event: KeyboardEvent): void;
// (undocumented)
onPointerdown(event: PointerEvent): void;
onClick(event: PointerEvent): void;
onKeydown(event: KeyboardEvent): void;
orientation: SignalLike<'vertical' | 'horizontal'>;
pointerdown: SignalLike<PointerEventManager<PointerEvent>>;
prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
readonly: SignalLike<boolean>;
setDefaultState(): void;
Expand Down Expand Up @@ -680,6 +680,7 @@ export class TabListPattern {
constructor(inputs: TabListInputs);
readonly activeDescendant: SignalLike<string | undefined>;
readonly activeTab: SignalLike<TabPattern | undefined>;
readonly clickManager: SignalLike<ClickEventManager<PointerEvent>>;
readonly disabled: SignalLike<boolean>;
readonly expansionBehavior: ListExpansion;
readonly focusBehavior: ListFocus<TabPattern>;
Expand All @@ -689,12 +690,11 @@ export class TabListPattern {
readonly keydown: SignalLike<KeyboardEventManager<KeyboardEvent>>;
readonly navigationBehavior: ListNavigation<TabPattern>;
readonly nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
onClick(event: PointerEvent): void;
onKeydown(event: KeyboardEvent): void;
onPointerdown(event: PointerEvent): void;
open(value: string): boolean;
open(tab?: TabPattern): boolean;
readonly orientation: SignalLike<'vertical' | 'horizontal'>;
readonly pointerdown: SignalLike<PointerEventManager<PointerEvent>>;
readonly prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
readonly selectedTab: WritableSignalLike<TabPattern | undefined>;
setDefaultState(): void;
Expand Down
9 changes: 8 additions & 1 deletion src/aria/listbox/listbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ describe('Listbox', () => {

const click = (index: number, eventInit?: PointerEventInit, targets?: HTMLElement[]) => {
(targets || optionElements)[index].dispatchEvent(
new PointerEvent('pointerdown', {bubbles: true, ...eventInit}),
new PointerEvent('click', {
bubbles: true,
detail: 1,
pointerType: 'mouse',
clientX: 1,
clientY: 1,
...eventInit,
}),
);
fixture.detectChanges();
};
Expand Down
2 changes: 1 addition & 1 deletion src/aria/listbox/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import {LISTBOX} from './tokens';
'[attr.aria-multiselectable]': '_pattern.multi()',
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
'(keydown)': '_pattern.onKeydown($event)',
'(pointerdown)': '_pattern.onPointerdown($event)',
'(click)': '_pattern.onClick($event)',
'(focusin)': '_onFocus()',
},
hostDirectives: [ComboboxPopup],
Expand Down
92 changes: 92 additions & 0 deletions src/aria/private/behaviors/event-manager/click-event-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @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 {
EventHandler,
EventHandlerOptions,
EventManager,
hasModifiers,
ModifierInputs,
Modifier,
} from './event-manager';

/**
* Gets whether an event could be a simulated click event.
*
* Screen readers and keyboard activation (Enter/Space) often dispatch fake click
* events. We distinguish them by checking if `event.detail` is zero or if
* `event.pointerType` is missing.
*/
export function isFakeClick(event: PointerEvent): boolean {
return event.detail === 0 || !event.pointerType;
}

/**
* Gets whether an event is a programmatic click (e.g. triggered by .click() or .dispatchEvent()).
* Programmatic events are untrusted.
*/
export function isProgrammaticClick(event: Event): boolean {
return !event.isTrusted;
}

/**
* An event manager that is specialized for handling click events.
*
* This manager should ONLY be used to handle click events. It explicitly
* filters out simulated click events generated by browsers when Enter or Space
* keys are pressed, to avoid concurrent logic or overwriting selection state
* when handling keyboard activation explicitly via KeyboardEventManager.
*/
export class ClickEventManager<T extends PointerEvent> extends EventManager<T> {
options: EventHandlerOptions = {
preventDefault: false,
stopPropagation: false,
};

/**
* Configures this event manager to handle events with a specific modifier combination.
*/
on(modifiers: ModifierInputs, handler: EventHandler<T>): this;

/**
* Configures this event manager to handle events with no modifiers.
*
* @param handler The handler function
*/
on(handler: EventHandler<T>): this;

on(...args: unknown[]) {
const {handler, modifiers} = this._normalizeInputs(...args);

this.configs.push({
handler,
matcher: event => this._isMatch(event, modifiers),
...this.options,
});
return this;
}

private _normalizeInputs(...args: unknown[]) {
if (args.length === 2) {
return {
modifiers: args[0] as ModifierInputs,
handler: args[1] as EventHandler<T>,
};
}

return {
modifiers: Modifier.None,
handler: args[0] as EventHandler<T>,
};
}

_isMatch(event: T, modifiers: ModifierInputs) {
const isAllowed = isProgrammaticClick(event) || !isFakeClick(event);
return isAllowed && hasModifiers(event, modifiers);
}
}
2 changes: 1 addition & 1 deletion src/aria/private/listbox/combobox-listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class ComboboxListboxPattern<V>
override onKeydown(_: KeyboardEvent): void {}

/** Noop. The combobox handles pointerdown events. */
override onPointerdown(_: PointerEvent): void {}
override onClick(_: PointerEvent): void {}

/** Noop. The combobox controls the open state. */
override setDefaultState(): void {}
Expand Down
Loading
Loading