Skip to content

Commit e01062a

Browse files
authored
refactor(multiple): migrate Listbox and Tabs to ClickEventManager (#33031)
* refactor(multiple): migrate Listbox and Tabs to ClickEventManager * fixup! refactor(multiple): migrate Listbox and Tabs to ClickEventManager * fixup! refactor(multiple): migrate Listbox and Tabs to ClickEventManager * fixup! refactor(multiple): migrate Listbox and Tabs to ClickEventManager
1 parent ab21f78 commit e01062a

File tree

16 files changed

+223
-64
lines changed

16 files changed

+223
-64
lines changed

goldens/aria/private/index.api.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ export class ComboboxListboxPattern<V> extends ListboxPattern<V> implements Comb
144144
last: () => void;
145145
multi: SignalLike<boolean>;
146146
next: () => void;
147+
onClick(_: PointerEvent): void;
147148
onKeydown(_: KeyboardEvent): void;
148-
onPointerdown(_: PointerEvent): void;
149149
prev: () => void;
150150
role: SignalLike<"listbox">;
151151
select: (item?: OptionPattern<V>) => void;
@@ -450,6 +450,7 @@ export type ListboxInputs<V> = ListInputs<OptionPattern<V>, V> & {
450450
export class ListboxPattern<V> {
451451
constructor(inputs: ListboxInputs<V>);
452452
activeDescendant: SignalLike<string | undefined>;
453+
clickManager: SignalLike<ClickEventManager<PointerEvent>>;
453454
disabled: SignalLike<boolean>;
454455
dynamicSpaceKey: SignalLike<"" | " ">;
455456
followFocus: SignalLike<boolean>;
@@ -462,11 +463,10 @@ export class ListboxPattern<V> {
462463
listBehavior: List<OptionPattern<V>, V>;
463464
multi: SignalLike<boolean>;
464465
nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
465-
onKeydown(event: KeyboardEvent): void;
466466
// (undocumented)
467-
onPointerdown(event: PointerEvent): void;
467+
onClick(event: PointerEvent): void;
468+
onKeydown(event: KeyboardEvent): void;
468469
orientation: SignalLike<'vertical' | 'horizontal'>;
469-
pointerdown: SignalLike<PointerEventManager<PointerEvent>>;
470470
prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
471471
readonly: SignalLike<boolean>;
472472
setDefaultState(): void;
@@ -680,6 +680,7 @@ export class TabListPattern {
680680
constructor(inputs: TabListInputs);
681681
readonly activeDescendant: SignalLike<string | undefined>;
682682
readonly activeTab: SignalLike<TabPattern | undefined>;
683+
readonly clickManager: SignalLike<ClickEventManager<PointerEvent>>;
683684
readonly disabled: SignalLike<boolean>;
684685
readonly expansionBehavior: ListExpansion;
685686
readonly focusBehavior: ListFocus<TabPattern>;
@@ -689,12 +690,11 @@ export class TabListPattern {
689690
readonly keydown: SignalLike<KeyboardEventManager<KeyboardEvent>>;
690691
readonly navigationBehavior: ListNavigation<TabPattern>;
691692
readonly nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
693+
onClick(event: PointerEvent): void;
692694
onKeydown(event: KeyboardEvent): void;
693-
onPointerdown(event: PointerEvent): void;
694695
open(value: string): boolean;
695696
open(tab?: TabPattern): boolean;
696697
readonly orientation: SignalLike<'vertical' | 'horizontal'>;
697-
readonly pointerdown: SignalLike<PointerEventManager<PointerEvent>>;
698698
readonly prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
699699
readonly selectedTab: WritableSignalLike<TabPattern | undefined>;
700700
setDefaultState(): void;

src/aria/listbox/listbox.spec.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ describe('Listbox', () => {
3434

3535
const click = (index: number, eventInit?: PointerEventInit, targets?: HTMLElement[]) => {
3636
(targets || optionElements)[index].dispatchEvent(
37-
new PointerEvent('pointerdown', {bubbles: true, ...eventInit}),
37+
new PointerEvent('click', {
38+
bubbles: true,
39+
detail: 1,
40+
pointerType: 'mouse',
41+
clientX: 1,
42+
clientY: 1,
43+
...eventInit,
44+
}),
3845
);
3946
fixture.detectChanges();
4047
};

src/aria/listbox/listbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import {LISTBOX} from './tokens';
6363
'[attr.aria-multiselectable]': '_pattern.multi()',
6464
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
6565
'(keydown)': '_pattern.onKeydown($event)',
66-
'(pointerdown)': '_pattern.onPointerdown($event)',
66+
'(click)': '_pattern.onClick($event)',
6767
'(focusin)': '_onFocus()',
6868
},
6969
hostDirectives: [ComboboxPopup],
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
EventHandler,
11+
EventHandlerOptions,
12+
EventManager,
13+
hasModifiers,
14+
ModifierInputs,
15+
Modifier,
16+
} from './event-manager';
17+
18+
/**
19+
* Gets whether an event could be a simulated click event.
20+
*
21+
* Screen readers and keyboard activation (Enter/Space) often dispatch fake click
22+
* events. We distinguish them by checking if `event.detail` is zero or if
23+
* `event.pointerType` is missing.
24+
*/
25+
export function isFakeClick(event: PointerEvent): boolean {
26+
return event.detail === 0 || !event.pointerType;
27+
}
28+
29+
/**
30+
* Gets whether an event is a programmatic click (e.g. triggered by .click() or .dispatchEvent()).
31+
* Programmatic events are untrusted.
32+
*/
33+
export function isProgrammaticClick(event: Event): boolean {
34+
return !event.isTrusted;
35+
}
36+
37+
/**
38+
* An event manager that is specialized for handling click events.
39+
*
40+
* This manager should ONLY be used to handle click events. It explicitly
41+
* filters out simulated click events generated by browsers when Enter or Space
42+
* keys are pressed, to avoid concurrent logic or overwriting selection state
43+
* when handling keyboard activation explicitly via KeyboardEventManager.
44+
*/
45+
export class ClickEventManager<T extends PointerEvent> extends EventManager<T> {
46+
options: EventHandlerOptions = {
47+
preventDefault: false,
48+
stopPropagation: false,
49+
};
50+
51+
/**
52+
* Configures this event manager to handle events with a specific modifier combination.
53+
*/
54+
on(modifiers: ModifierInputs, handler: EventHandler<T>): this;
55+
56+
/**
57+
* Configures this event manager to handle events with no modifiers.
58+
*
59+
* @param handler The handler function
60+
*/
61+
on(handler: EventHandler<T>): this;
62+
63+
on(...args: unknown[]) {
64+
const {handler, modifiers} = this._normalizeInputs(...args);
65+
66+
this.configs.push({
67+
handler,
68+
matcher: event => this._isMatch(event, modifiers),
69+
...this.options,
70+
});
71+
return this;
72+
}
73+
74+
private _normalizeInputs(...args: unknown[]) {
75+
if (args.length === 2) {
76+
return {
77+
modifiers: args[0] as ModifierInputs,
78+
handler: args[1] as EventHandler<T>,
79+
};
80+
}
81+
82+
return {
83+
modifiers: Modifier.None,
84+
handler: args[0] as EventHandler<T>,
85+
};
86+
}
87+
88+
_isMatch(event: T, modifiers: ModifierInputs) {
89+
const isAllowed = isProgrammaticClick(event) || !isFakeClick(event);
90+
return isAllowed && hasModifiers(event, modifiers);
91+
}
92+
}

src/aria/private/listbox/combobox-listbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class ComboboxListboxPattern<V>
5353
override onKeydown(_: KeyboardEvent): void {}
5454

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

5858
/** Noop. The combobox controls the open state. */
5959
override setDefaultState(): void {}

0 commit comments

Comments
 (0)