Skip to content

Commit 19b96ba

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

File tree

15 files changed

+212
-63
lines changed

15 files changed

+212
-63
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.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 {}

src/aria/private/listbox/listbox.spec.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,10 @@ describe('Listbox Pattern', () => {
569569
target: options[index].element(),
570570
shiftKey: mods?.shift,
571571
ctrlKey: mods?.control,
572+
detail: 1,
573+
pointerType: 'mouse',
574+
clientX: 1,
575+
clientY: 1,
572576
} as unknown as PointerEvent;
573577
}
574578

@@ -578,7 +582,7 @@ describe('Listbox Pattern', () => {
578582
multi: signal(false),
579583
selectionMode: signal('follow'),
580584
});
581-
listbox.onPointerdown(click(options, 0));
585+
listbox.onClick(click(options, 0));
582586
expect(listbox.inputs.value()).toEqual(['Apple']);
583587
});
584588
});
@@ -589,7 +593,7 @@ describe('Listbox Pattern', () => {
589593
multi: signal(false),
590594
selectionMode: signal('explicit'),
591595
});
592-
listbox.onPointerdown(click(options, 0));
596+
listbox.onClick(click(options, 0));
593597
expect(listbox.inputs.value()).toEqual(['Apple']);
594598
});
595599

@@ -599,7 +603,7 @@ describe('Listbox Pattern', () => {
599603
value: signal(['Apple']),
600604
selectionMode: signal('explicit'),
601605
});
602-
listbox.onPointerdown(click(options, 0));
606+
listbox.onClick(click(options, 0));
603607
expect(listbox.inputs.value()).toEqual([]);
604608
});
605609
});
@@ -610,7 +614,7 @@ describe('Listbox Pattern', () => {
610614
multi: signal(true),
611615
selectionMode: signal('explicit'),
612616
});
613-
listbox.onPointerdown(click(options, 0));
617+
listbox.onClick(click(options, 0));
614618
expect(listbox.inputs.value()).toEqual(['Apple']);
615619
});
616620

@@ -620,7 +624,7 @@ describe('Listbox Pattern', () => {
620624
value: signal(['Apple']),
621625
selectionMode: signal('explicit'),
622626
});
623-
listbox.onPointerdown(click(options, 0));
627+
listbox.onClick(click(options, 0));
624628
expect(listbox.inputs.value()).toEqual([]);
625629
});
626630

@@ -629,9 +633,9 @@ describe('Listbox Pattern', () => {
629633
multi: signal(true),
630634
selectionMode: signal('explicit'),
631635
});
632-
listbox.onPointerdown(click(options, 2));
636+
listbox.onClick(click(options, 2));
633637
listbox.onKeydown(shift());
634-
listbox.onPointerdown(click(options, 5, {shift: true}));
638+
listbox.onClick(click(options, 5, {shift: true}));
635639
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
636640
});
637641

@@ -640,11 +644,11 @@ describe('Listbox Pattern', () => {
640644
multi: signal(true),
641645
selectionMode: signal('explicit'),
642646
});
643-
listbox.onPointerdown(click(options, 2));
647+
listbox.onClick(click(options, 2));
644648
listbox.onKeydown(shift());
645-
listbox.onPointerdown(click(options, 5, {shift: true}));
649+
listbox.onClick(click(options, 5, {shift: true}));
646650
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
647-
listbox.onPointerdown(click(options, 0, {shift: true}));
651+
listbox.onClick(click(options, 0, {shift: true}));
648652
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
649653
});
650654
});
@@ -655,11 +659,11 @@ describe('Listbox Pattern', () => {
655659
multi: signal(true),
656660
selectionMode: signal('follow'),
657661
});
658-
listbox.onPointerdown(click(options, 0));
662+
listbox.onClick(click(options, 0));
659663
expect(listbox.inputs.value()).toEqual(['Apple']);
660-
listbox.onPointerdown(click(options, 1));
664+
listbox.onClick(click(options, 1));
661665
expect(listbox.inputs.value()).toEqual(['Apricot']);
662-
listbox.onPointerdown(click(options, 2));
666+
listbox.onClick(click(options, 2));
663667
expect(listbox.inputs.value()).toEqual(['Banana']);
664668
});
665669

@@ -668,11 +672,11 @@ describe('Listbox Pattern', () => {
668672
multi: signal(true),
669673
selectionMode: signal('follow'),
670674
});
671-
listbox.onPointerdown(click(options, 0));
675+
listbox.onClick(click(options, 0));
672676
expect(listbox.inputs.value()).toEqual(['Apple']);
673-
listbox.onPointerdown(click(options, 1, {control: true}));
677+
listbox.onClick(click(options, 1, {control: true}));
674678
expect(listbox.inputs.value()).toEqual(['Apple', 'Apricot']);
675-
listbox.onPointerdown(click(options, 2, {control: true}));
679+
listbox.onClick(click(options, 2, {control: true}));
676680
expect(listbox.inputs.value()).toEqual(['Apple', 'Apricot', 'Banana']);
677681
});
678682

@@ -681,9 +685,9 @@ describe('Listbox Pattern', () => {
681685
multi: signal(true),
682686
selectionMode: signal('follow'),
683687
});
684-
listbox.onPointerdown(click(options, 0));
688+
listbox.onClick(click(options, 0));
685689
expect(listbox.inputs.value()).toEqual(['Apple']);
686-
listbox.onPointerdown(click(options, 0, {control: true}));
690+
listbox.onClick(click(options, 0, {control: true}));
687691
expect(listbox.inputs.value()).toEqual([]);
688692
});
689693

@@ -692,9 +696,9 @@ describe('Listbox Pattern', () => {
692696
multi: signal(true),
693697
selectionMode: signal('follow'),
694698
});
695-
listbox.onPointerdown(click(options, 2));
699+
listbox.onClick(click(options, 2));
696700
listbox.onKeydown(shift());
697-
listbox.onPointerdown(click(options, 5, {shift: true}));
701+
listbox.onClick(click(options, 5, {shift: true}));
698702
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
699703
});
700704

@@ -703,11 +707,11 @@ describe('Listbox Pattern', () => {
703707
multi: signal(true),
704708
selectionMode: signal('follow'),
705709
});
706-
listbox.onPointerdown(click(options, 2));
710+
listbox.onClick(click(options, 2));
707711
listbox.onKeydown(shift());
708-
listbox.onPointerdown(click(options, 5, {shift: true}));
712+
listbox.onClick(click(options, 5, {shift: true}));
709713
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry', 'Cantaloupe']);
710-
listbox.onPointerdown(click(options, 0, {shift: true}));
714+
listbox.onClick(click(options, 0, {shift: true}));
711715
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
712716
});
713717

@@ -718,11 +722,11 @@ describe('Listbox Pattern', () => {
718722
selectionMode: signal('follow'),
719723
});
720724
options[2].disabled.set(true);
721-
listbox.onPointerdown(click(options, 0));
725+
listbox.onClick(click(options, 0));
722726
expect(listbox.inputs.value()).toEqual(['Apple']);
723727

724728
listbox.onKeydown(shift());
725-
listbox.onPointerdown(click(options, 2, {shift: true}));
729+
listbox.onClick(click(options, 2, {shift: true}));
726730
expect(listbox.inputs.value()).toEqual(['Apple', 'Apricot']);
727731
expect(listbox.inputs.activeItem()).toEqual(options[2]);
728732
});
@@ -734,11 +738,11 @@ describe('Listbox Pattern', () => {
734738
selectionMode: signal('follow'),
735739
});
736740
options[2].disabled.set(true);
737-
listbox.onPointerdown(click(options, 0));
741+
listbox.onClick(click(options, 0));
738742
expect(listbox.inputs.value()).toEqual(['Apple']);
739743
listbox.onKeydown(down({control: true}));
740744
expect(listbox.inputs.value()).toEqual(['Apple']);
741-
listbox.onPointerdown(click(options, 2));
745+
listbox.onClick(click(options, 2));
742746
expect(listbox.inputs.value()).toEqual(['Apple']);
743747
});
744748
});
@@ -748,11 +752,11 @@ describe('Listbox Pattern', () => {
748752
readonly: signal(true),
749753
selectionMode: signal('follow'),
750754
});
751-
listbox.onPointerdown(click(options, 0));
755+
listbox.onClick(click(options, 0));
752756
expect(listbox.inputs.value()).toEqual([]);
753-
listbox.onPointerdown(click(options, 1));
757+
listbox.onClick(click(options, 1));
754758
expect(listbox.inputs.value()).toEqual([]);
755-
listbox.onPointerdown(click(options, 2));
759+
listbox.onClick(click(options, 2));
756760
expect(listbox.inputs.value()).toEqual([]);
757761
});
758762

@@ -761,14 +765,14 @@ describe('Listbox Pattern', () => {
761765
multi: signal(true),
762766
selectionMode: signal('follow'),
763767
});
764-
listbox.onPointerdown(click(options, 2));
768+
listbox.onClick(click(options, 2));
765769
listbox.onKeydown(down({control: true}));
766770
listbox.onKeydown(down({control: true}));
767771

768772
listbox.onKeydown(shift());
769773
listbox.onKeydown(space({shift: true}));
770774
expect(listbox.inputs.value()).toEqual(['Banana', 'Blackberry', 'Blueberry']);
771-
listbox.onPointerdown(click(options, 0, {shift: true}));
775+
listbox.onClick(click(options, 0, {shift: true}));
772776
expect(listbox.inputs.value()).toEqual(['Banana', 'Apricot', 'Apple']);
773777
});
774778

@@ -777,12 +781,12 @@ describe('Listbox Pattern', () => {
777781
multi: signal(true),
778782
selectionMode: signal('follow'),
779783
});
780-
listbox.onPointerdown(click(options, 0));
784+
listbox.onClick(click(options, 0));
781785
expect(listbox.inputs.value()).toEqual(['Apple']);
782786
listbox.onKeydown(down({control: true}));
783787
listbox.onKeydown(down({control: true}));
784788
listbox.onKeydown(shift());
785-
listbox.onPointerdown(click(options, 4, {shift: true}));
789+
listbox.onClick(click(options, 4, {shift: true}));
786790
expect(listbox.inputs.value()).toEqual(['Apple', 'Banana', 'Blackberry', 'Blueberry']);
787791
});
788792
});

0 commit comments

Comments
 (0)