Skip to content

Commit f84a6b2

Browse files
committed
refactor(multiple): stabilize default state upon any type of interaction
1 parent 1dc6b44 commit f84a6b2

File tree

25 files changed

+438
-93
lines changed

25 files changed

+438
-93
lines changed

goldens/aria/listbox/index.api.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ export class Listbox<V> {
1717
readonly id: _angular_core.InputSignal<string>;
1818
protected items: _angular_core.Signal<OptionPattern<V>[]>;
1919
multi: _angular_core.InputSignalWithTransform<boolean, unknown>;
20-
// (undocumented)
21-
_onFocus(): void;
2220
orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
2321
readonly _pattern: ListboxPattern<V>;
2422
readonly: _angular_core.InputSignalWithTransform<boolean, unknown>;

goldens/aria/private/index.api.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
174174
expandKey: SignalLike<"ArrowLeft" | "ArrowRight">;
175175
first(): void;
176176
firstMatch: SignalLike<T | undefined>;
177-
hasBeenFocused: WritableSignalLike<boolean>;
177+
hasBeenInteracted: WritableSignalLike<boolean>;
178178
hasPopup: SignalLike<"listbox" | "tree" | "grid" | "dialog" | null>;
179179
highlight(): void;
180180
highlightedItem: WritableSignalLike<T | undefined>;
@@ -398,7 +398,7 @@ export class GridPattern {
398398
readonly dragging: WritableSignalLike<boolean>;
399399
focusEffect(): void;
400400
readonly gridBehavior: Grid<GridCellPattern>;
401-
readonly hasBeenFocused: WritableSignalLike<boolean>;
401+
readonly hasBeenInteracted: WritableSignalLike<boolean>;
402402
// (undocumented)
403403
readonly inputs: GridInputs;
404404
readonly isFocused: WritableSignalLike<boolean>;
@@ -455,13 +455,16 @@ export class ListboxPattern<V> {
455455
followFocus: SignalLike<boolean>;
456456
// (undocumented)
457457
protected _getItem(e: PointerEvent): OptionPattern<V> | undefined;
458+
readonly hasBeenInteracted: WritableSignalLike<boolean>;
458459
// (undocumented)
459460
readonly inputs: ListboxInputs<V>;
460461
keydown: SignalLike<KeyboardEventManager<KeyboardEvent>>;
461462
// (undocumented)
462463
listBehavior: List<OptionPattern<V>, V>;
463464
multi: SignalLike<boolean>;
464465
nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
466+
// (undocumented)
467+
onFocusIn(): void;
465468
onKeydown(event: KeyboardEvent): void;
466469
// (undocumented)
467470
onPointerdown(event: PointerEvent): void;
@@ -470,6 +473,7 @@ export class ListboxPattern<V> {
470473
prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
471474
readonly: SignalLike<boolean>;
472475
setDefaultState(): void;
476+
setDefaultStateEffect(): void;
473477
setsize: SignalLike<number>;
474478
tabIndex: SignalLike<-1 | 0>;
475479
typeaheadRegexp: RegExp;
@@ -493,7 +497,7 @@ export class MenuBarPattern<V> {
493497
goto(item: MenuItemPattern<V>, opts?: {
494498
focusElement?: boolean;
495499
}): void;
496-
hasBeenFocused: WritableSignalLike<boolean>;
500+
hasBeenInteracted: WritableSignalLike<boolean>;
497501
// (undocumented)
498502
readonly inputs: MenuBarInputs<V>;
499503
isFocused: WritableSignalLike<boolean>;
@@ -507,6 +511,7 @@ export class MenuBarPattern<V> {
507511
onMouseOver(event: MouseEvent): void;
508512
prev(): void;
509513
setDefaultState(): void;
514+
setDefaultStateEffect(): void;
510515
tabIndex: () => 0 | -1;
511516
typeaheadRegexp: RegExp;
512517
}
@@ -539,7 +544,7 @@ export class MenuItemPattern<V> implements ListItem<V> {
539544
element: SignalLike<HTMLElement | undefined>;
540545
expanded: SignalLike<boolean | null>;
541546
_expanded: WritableSignalLike<boolean>;
542-
hasBeenFocused: WritableSignalLike<boolean>;
547+
hasBeenInteracted: WritableSignalLike<boolean>;
543548
hasPopup: SignalLike<boolean>;
544549
id: SignalLike<string>;
545550
index: SignalLike<number>;
@@ -572,8 +577,8 @@ export class MenuPattern<V> {
572577
dynamicSpaceKey: SignalLike<"" | " ">;
573578
expand(): void;
574579
first(): void;
575-
hasBeenFocused: WritableSignalLike<boolean>;
576580
hasBeenHovered: WritableSignalLike<boolean>;
581+
hasBeenInteracted: WritableSignalLike<boolean>;
577582
id: SignalLike<string>;
578583
// (undocumented)
579584
readonly inputs: MenuInputs<V>;
@@ -593,6 +598,7 @@ export class MenuPattern<V> {
593598
role: () => string;
594599
root: SignalLike<MenuTriggerPattern<V> | MenuBarPattern<V> | MenuPattern<V> | undefined>;
595600
setDefaultState(): void;
601+
setDefaultStateEffect(): void;
596602
shouldFocus: SignalLike<boolean>;
597603
submit(item?: MenuItemPattern<V> | undefined): void;
598604
tabIndex: () => 0 | -1;
@@ -617,7 +623,7 @@ export class MenuTriggerPattern<V> {
617623
}): void;
618624
disabled: () => boolean;
619625
expanded: WritableSignalLike<boolean>;
620-
hasBeenFocused: WritableSignalLike<boolean>;
626+
hasBeenInteracted: WritableSignalLike<boolean>;
621627
hasPopup: () => boolean;
622628
// (undocumented)
623629
readonly inputs: MenuTriggerInputs<V>;
@@ -684,11 +690,13 @@ export class TabListPattern {
684690
readonly expansionBehavior: ListExpansion;
685691
readonly focusBehavior: ListFocus<TabPattern>;
686692
readonly followFocus: SignalLike<boolean>;
693+
readonly hasBeenInteracted: WritableSignalLike<boolean>;
687694
// (undocumented)
688695
readonly inputs: TabListInputs;
689696
readonly keydown: SignalLike<KeyboardEventManager<KeyboardEvent>>;
690697
readonly navigationBehavior: ListNavigation<TabPattern>;
691698
readonly nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
699+
onFocusIn(): void;
692700
onKeydown(event: KeyboardEvent): void;
693701
onPointerdown(event: PointerEvent): void;
694702
open(value: string): boolean;
@@ -698,6 +706,7 @@ export class TabListPattern {
698706
readonly prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
699707
readonly selectedTab: WritableSignalLike<TabPattern | undefined>;
700708
setDefaultState(): void;
709+
setDefaultStateEffect(): void;
701710
readonly tabIndex: SignalLike<0 | -1>;
702711
}
703712

@@ -751,17 +760,21 @@ export class ToolbarPattern<V> {
751760
readonly activeDescendant: SignalLike<string | undefined>;
752761
readonly activeItem: () => ToolbarWidgetPattern<V> | undefined;
753762
readonly disabled: SignalLike<boolean>;
763+
readonly hasBeenInteracted: WritableSignalLike<boolean>;
754764
// (undocumented)
755765
readonly inputs: ToolbarInputs<V>;
756766
readonly listBehavior: List<ToolbarWidgetPattern<V>, V>;
757767
onClick(event: MouseEvent): void;
768+
// (undocumented)
769+
onFocusIn(): void;
758770
onKeydown(event: KeyboardEvent): void;
759771
// (undocumented)
760772
onPointerdown(event: PointerEvent): void;
761773
readonly orientation: SignalLike<'vertical' | 'horizontal'>;
762774
// (undocumented)
763775
select(): void;
764776
setDefaultState(): void;
777+
setDefaultStateEffect(): void;
765778
readonly softDisabled: SignalLike<boolean>;
766779
readonly tabIndex: SignalLike<0 | -1>;
767780
}
@@ -878,6 +891,7 @@ export class TreePattern<V> implements TreeInputs<V> {
878891
readonly followFocus: SignalLike<boolean>;
879892
protected _getItem(event: Event): TreeItemPattern<V> | undefined;
880893
goto(e: PointerEvent, opts?: SelectOptions): void;
894+
readonly hasBeenInteracted: WritableSignalLike<boolean>;
881895
readonly id: SignalLike<string>;
882896
// (undocumented)
883897
readonly inputs: TreeInputs<V>;
@@ -888,13 +902,15 @@ export class TreePattern<V> implements TreeInputs<V> {
888902
readonly multi: SignalLike<boolean>;
889903
readonly nav: SignalLike<boolean>;
890904
readonly nextKey: SignalLike<"ArrowRight" | "ArrowLeft" | "ArrowDown">;
905+
onFocusIn(): void;
891906
onKeydown(event: KeyboardEvent): void;
892907
onPointerdown(event: PointerEvent): void;
893908
readonly orientation: SignalLike<'vertical' | 'horizontal'>;
894909
pointerdown: SignalLike<PointerEventManager<PointerEvent>>;
895910
readonly prevKey: SignalLike<"ArrowUp" | "ArrowRight" | "ArrowLeft">;
896911
readonly selectionMode: SignalLike<'follow' | 'explicit'>;
897912
setDefaultState(): void;
913+
setDefaultStateEffect(): void;
898914
readonly softDisabled: SignalLike<boolean>;
899915
readonly tabIndex: SignalLike<-1 | 0>;
900916
readonly textDirection: SignalLike<'ltr' | 'rtl'>;

goldens/aria/tabs/index.api.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ export class TabList implements OnInit, OnDestroy {
4747
ngOnDestroy(): void;
4848
// (undocumented)
4949
ngOnInit(): void;
50-
// (undocumented)
51-
_onFocus(): void;
5250
open(value: string): boolean;
5351
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
5452
readonly _pattern: TabListPattern;

goldens/aria/toolbar/index.api.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ export class Toolbar<V> {
1515
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
1616
readonly element: HTMLElement;
1717
readonly _itemPatterns: _angular_core.Signal<ToolbarWidgetPattern<V>[]>;
18-
// (undocumented)
19-
_onFocus(): void;
2018
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
2119
readonly _pattern: ToolbarPattern<V>;
2220
// (undocumented)

goldens/aria/tree/index.api.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ export class Tree<V> {
2020
readonly id: _angular_core.InputSignal<string>;
2121
readonly multi: _angular_core.InputSignalWithTransform<boolean, unknown>;
2222
readonly nav: _angular_core.InputSignalWithTransform<boolean, unknown>;
23-
// (undocumented)
24-
_onFocus(): void;
2523
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
2624
readonly _pattern: TreePattern<V>;
2725
// (undocumented)

src/aria/listbox/listbox.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import {LISTBOX} from './tokens';
6464
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
6565
'(keydown)': '_pattern.onKeydown($event)',
6666
'(pointerdown)': '_pattern.onPointerdown($event)',
67-
'(focusin)': '_onFocus()',
67+
'(focusin)': '_pattern.onFocusIn()',
6868
},
6969
hostDirectives: [ComboboxPopup],
7070
providers: [{provide: LISTBOX, useExisting: Listbox}],
@@ -139,9 +139,6 @@ export class Listbox<V> {
139139
/** The Listbox UIPattern. */
140140
readonly _pattern: ListboxPattern<V>;
141141

142-
/** Whether the listbox has received focus yet. */
143-
private _hasFocused = signal(false);
144-
145142
constructor() {
146143
const inputs = {
147144
...this,
@@ -171,9 +168,7 @@ export class Listbox<V> {
171168
});
172169

173170
afterRenderEffect(() => {
174-
if (!this._hasFocused()) {
175-
this._pattern.setDefaultState();
176-
}
171+
this._pattern.setDefaultStateEffect();
177172
});
178173

179174
// Ensure that if the active item is removed from
@@ -198,10 +193,6 @@ export class Listbox<V> {
198193
});
199194
}
200195

201-
_onFocus() {
202-
this._hasFocused.set(true);
203-
}
204-
205196
scrollActiveItemIntoView(options: ScrollIntoViewOptions = {block: 'nearest'}) {
206197
this._pattern.inputs.activeItem()?.element()?.scrollIntoView(options);
207198
}

src/aria/menu/menu-bar.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,7 @@ export class MenuBar<V> {
128128
});
129129

130130
afterRenderEffect(() => {
131-
if (!this._pattern.hasBeenFocused()) {
132-
this._pattern.setDefaultState();
133-
}
131+
this._pattern.setDefaultStateEffect();
134132
});
135133
}
136134

src/aria/menu/menu.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class Menu<V> {
160160
this._deferredContentAware?.contentVisible.set(true);
161161
} else {
162162
this._deferredContentAware?.contentVisible.set(
163-
this._pattern.visible() || !!this.parent()?._pattern.hasBeenFocused(),
163+
this._pattern.visible() || !!this.parent()?._pattern.hasBeenInteracted(),
164164
);
165165
}
166166
});
@@ -177,13 +177,7 @@ export class Menu<V> {
177177
});
178178

179179
afterRenderEffect(() => {
180-
if (
181-
!this._pattern.hasBeenFocused() &&
182-
!this._pattern.hasBeenHovered() &&
183-
this._items().length
184-
) {
185-
untracked(() => this._pattern.setDefaultState());
186-
}
180+
this._pattern.setDefaultStateEffect();
187181
});
188182
}
189183

src/aria/private/combobox/combobox.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
166166
isFocused = signal(false);
167167

168168
/** Whether the combobox has ever been focused. */
169-
hasBeenFocused = signal(false);
169+
hasBeenInteracted = signal(false);
170170

171171
/** The key used to navigate to the previous item in the list. */
172172
expandKey = computed(() => (this.inputs.textDirection() === 'rtl' ? 'ArrowLeft' : 'ArrowRight'));
@@ -367,13 +367,13 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
367367

368368
/** Handles focus in events for the combobox. */
369369
onFocusIn() {
370-
if (this.inputs.alwaysExpanded() && !this.hasBeenFocused()) {
370+
if (this.inputs.alwaysExpanded() && !this.hasBeenInteracted()) {
371371
const firstSelectedItem = this.listControls()?.getSelectedItems()[0];
372372
firstSelectedItem ? this.listControls()?.focus(firstSelectedItem) : this.first();
373373
}
374374

375375
this.isFocused.set(true);
376-
this.hasBeenFocused.set(true);
376+
this.hasBeenInteracted.set(true);
377377
}
378378

379379
/** Handles focus out events for the combobox. */

src/aria/private/grid/grid.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,4 +861,53 @@ describe('Grid', () => {
861861
});
862862
});
863863
});
864+
865+
describe('#setDefaultStateEffect', () => {
866+
it('should set default state if not interacted', () => {
867+
const gridInputs = getDefaultGridInputs();
868+
const data = [{cells: [{}, {}]}, {cells: [{}, {}]}];
869+
const {grid} = createGrid(data, gridInputs);
870+
const cells = grid.cells();
871+
cells[0][1].inputs.selected.set(true); // default state will pick this since it's selected
872+
grid.setDefaultStateEffect();
873+
expect(grid.gridBehavior.focusBehavior.activeCell()).toBe(cells[0][1]); // Should set to selected cell
874+
});
875+
876+
it('should NOT set default state if keyboard interacted', () => {
877+
const gridInputs = getDefaultGridInputs();
878+
const data = [{cells: [{}, {}]}, {cells: [{}, {}]}];
879+
const {grid} = createGrid(data, gridInputs);
880+
grid.onKeydown(down()); // Interaction
881+
882+
const cells = grid.cells();
883+
cells[0][1].inputs.selected.set(true);
884+
grid.setDefaultStateEffect();
885+
expect(grid.gridBehavior.focusBehavior.activeCell()).toBeUndefined(); // Should stay undefined, meaning default state was skipped
886+
});
887+
888+
it('should NOT set default state if pointer interacted', () => {
889+
const gridInputs = getDefaultGridInputs();
890+
const data = [{cells: [{}, {}]}, {cells: [{}, {}]}];
891+
const {grid} = createGrid(data, gridInputs);
892+
const cells = grid.cells();
893+
grid.onPointerdown({target: cells[0][0].element()} as unknown as PointerEvent); // Interaction
894+
895+
cells[0][1].inputs.selected.set(true);
896+
grid.setDefaultStateEffect();
897+
expect(grid.gridBehavior.focusBehavior.activeCell()).toBe(cells[0][0]); // Should stay on interacted cell
898+
});
899+
900+
it('should NOT set default state if focus-in occurred', () => {
901+
const gridInputs = getDefaultGridInputs();
902+
const data = [{cells: [{}, {}]}, {cells: [{}, {}]}];
903+
const {grid} = createGrid(data, gridInputs);
904+
905+
grid.onFocusIn({} as FocusEvent); // Interaction
906+
907+
const cells = grid.cells();
908+
cells[0][1].inputs.selected.set(true);
909+
grid.setDefaultStateEffect();
910+
expect(grid.gridBehavior.focusBehavior.activeCell()).toBeUndefined(); // Should stay undefined due to early return
911+
});
912+
});
864913
});

0 commit comments

Comments
 (0)