Skip to content

Commit 82b1b06

Browse files
committed
refactor(multiple): migrate Listbox and Tabs to ClickEventManager
1 parent 1dc6b44 commit 82b1b06

File tree

16 files changed

+220
-64
lines changed

16 files changed

+220
-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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
* An event manager that is specialized for handling click events.
20+
*
21+
* This manager should ONLY be used to handle pointer events. It explicitly
22+
* filters out simulated click events generated by browsers when Enter or Space
23+
* keys are pressed, to avoid concurrent logic or overwriting selection state
24+
* when handling keyboard activation explicitly via KeyboardEventManager.
25+
*/
26+
export class ClickEventManager<T extends PointerEvent> extends EventManager<T> {
27+
options: EventHandlerOptions = {
28+
preventDefault: false,
29+
stopPropagation: false,
30+
};
31+
32+
/**
33+
* Configures this event manager to handle events with a specific modifier combination.
34+
*/
35+
on(modifiers: ModifierInputs, handler: EventHandler<T>): this;
36+
37+
/**
38+
* Configures this event manager to handle events with no modifiers.
39+
*
40+
* @param handler The handler function
41+
*/
42+
on(handler: EventHandler<T>): this;
43+
44+
on(...args: unknown[]) {
45+
const {handler, modifiers} = this._normalizeInputs(...args);
46+
47+
this.configs.push({
48+
handler,
49+
matcher: event => this._isMatch(event, modifiers),
50+
...this.options,
51+
});
52+
return this;
53+
}
54+
55+
private _normalizeInputs(...args: unknown[]) {
56+
if (args.length === 2) {
57+
return {
58+
modifiers: args[0] as ModifierInputs,
59+
handler: args[1] as EventHandler<T>,
60+
};
61+
}
62+
63+
return {
64+
modifiers: Modifier.None,
65+
handler: args[0] as EventHandler<T>,
66+
};
67+
}
68+
69+
_isMatch(event: T, modifiers: ModifierInputs) {
70+
return this._isRealClick(event) && hasModifiers(event, modifiers);
71+
}
72+
73+
/**
74+
* Checks if the event is a "real" pointer click.
75+
*
76+
* Real clicks typically have a non-zero detail count (click count) and
77+
* a valid pointerType (e.g. 'mouse' or 'touch').
78+
*/
79+
private _isRealClick(event: T): boolean {
80+
if (event.detail === 0) return false;
81+
if (event.clientX === 0 && event.clientY === 0) return false;
82+
if (!event.pointerType) return false;
83+
return true;
84+
}
85+
}

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)