Skip to content

Commit d2c3bb9

Browse files
authored
fix(cdk-experimental/ui-patterns): focus list when using active desce… (angular#31756)
* fix(cdk-experimental/ui-patterns): focus list when using active descendant * If a list is using active descendant to track focus, calling focus() on a list item should send focus to the parent list element. * fixup! fix(cdk-experimental/ui-patterns): focus list when using active descendant
1 parent 29f0bb2 commit d2c3bb9

File tree

16 files changed

+65
-3
lines changed

16 files changed

+65
-3
lines changed

src/cdk-experimental/accordion/accordion.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ export class CdkAccordionTrigger {
150150
},
151151
})
152152
export class CdkAccordionGroup {
153+
/** A reference to the group element. */
154+
private readonly _elementRef = inject(ElementRef);
155+
153156
/** The CdkAccordionTriggers nested inside this group. */
154157
protected readonly _triggers = contentChildren(CdkAccordionTrigger, {descendants: true});
155158

@@ -184,6 +187,7 @@ export class CdkAccordionGroup {
184187
expandedIds: this.value,
185188
// TODO(ok7sai): Investigate whether an accordion should support horizontal mode.
186189
orientation: () => 'vertical',
190+
element: () => this._elementRef.nativeElement,
187191
});
188192

189193
constructor() {

src/cdk-experimental/listbox/listbox.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ import {_IdGenerator} from '@angular/cdk/a11y';
5555
},
5656
})
5757
export class CdkListbox<V> {
58+
/** A reference to the listbox element. */
59+
private readonly _elementRef = inject(ElementRef);
60+
5861
/** The directionality (LTR / RTL) context for the application (or a subtree of it). */
5962
private readonly _directionality = inject(Directionality);
6063

@@ -105,6 +108,7 @@ export class CdkListbox<V> {
105108
items: this.items,
106109
activeItem: signal(undefined),
107110
textDirection: this.textDirection,
111+
element: () => this._elementRef.nativeElement,
108112
});
109113

110114
/** Whether the listbox has received focus yet. */

src/cdk-experimental/radio-group/radio-group.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export function mapSignal<T, V>(
9393
},
9494
})
9595
export class CdkRadioGroup<V> {
96+
/** A reference to the radio group element. */
97+
private readonly _elementRef = inject(ElementRef);
98+
9699
/** The CdkRadioButtons nested inside of the CdkRadioGroup. */
97100
private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true});
98101

@@ -140,6 +143,7 @@ export class CdkRadioGroup<V> {
140143
activeItem: signal(undefined),
141144
textDirection: this.textDirection,
142145
toolbar: this._toolbarPattern,
146+
element: () => this._elementRef.nativeElement,
143147
focusMode: this._toolbarPattern()?.inputs.focusMode ?? this.focusMode,
144148
skipDisabled: this._toolbarPattern()?.inputs.skipDisabled ?? this.skipDisabled,
145149
});

src/cdk-experimental/tabs/tabs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ export class CdkTabs {
130130
},
131131
})
132132
export class CdkTabList implements OnInit, OnDestroy {
133+
/** A reference to the tab list element. */
134+
private readonly _elementRef = inject(ElementRef);
135+
133136
/** The parent CdkTabs. */
134137
private readonly _cdkTabs = inject(CdkTabs);
135138

@@ -174,6 +177,7 @@ export class CdkTabList implements OnInit, OnDestroy {
174177
items: this.tabs,
175178
value: this._selection,
176179
activeItem: signal(undefined),
180+
element: () => this._elementRef.nativeElement,
177181
});
178182

179183
/** Whether the tree has received focus yet. */

src/cdk-experimental/toolbar/toolbar.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ function sortDirectives(a: HasElement, b: HasElement) {
8080
},
8181
})
8282
export class CdkToolbar<V> {
83+
/** A reference to the toolbar element. */
84+
private readonly _elementRef = inject(ElementRef);
85+
8386
/** The CdkTabList nested inside of the container. */
8487
private readonly _cdkWidgets = signal(new Set<CdkRadioButtonInterface<V> | CdkToolbarWidget>());
8588

@@ -109,6 +112,7 @@ export class CdkToolbar<V> {
109112
activeItem: signal(undefined),
110113
textDirection: this.textDirection,
111114
focusMode: signal('roving'),
115+
element: () => this._elementRef.nativeElement,
112116
});
113117

114118
/** Whether the toolbar has received focus yet. */

src/cdk-experimental/tree/tree.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ function sortDirectives(a: HasElement, b: HasElement) {
7676
},
7777
})
7878
export class CdkTree<V> {
79+
/** A reference to the tree element. */
80+
private readonly _elementRef = inject(ElementRef);
81+
7982
/** All CdkTreeItem instances within this tree. */
8083
private readonly _unorderedItems = signal(new Set<CdkTreeItem<V>>());
8184

@@ -124,6 +127,7 @@ export class CdkTree<V> {
124127
[...this._unorderedItems()].sort(sortDirectives).map(item => item.pattern),
125128
),
126129
activeItem: signal(undefined),
130+
element: () => this._elementRef.nativeElement,
127131
});
128132

129133
/** Whether the tree has received focus yet. */

src/cdk-experimental/ui-patterns/accordion/accordion.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe('Accordion Pattern', () => {
6868
expandedIds: signal<string[]>([]),
6969
skipDisabled: signal(true),
7070
wrap: signal(true),
71+
element: signal(document.createElement('div')),
7172
};
7273
groupPattern = new AccordionGroupPattern(groupInputs);
7374

src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function getListFocus(inputs: TestInputs = {}): ListFocus<ListFocusItem>
2424
disabled: signal(false),
2525
skipDisabled: signal(false),
2626
focusMode: signal('roving'),
27+
element: signal({focus: () => {}} as HTMLElement),
2728
items: items,
2829
...inputs,
2930
});
@@ -98,6 +99,12 @@ describe('List Focus', () => {
9899
focusManager.inputs.activeItem.set(focusManager.inputs.items()[1]);
99100
expect(focusManager.getActiveDescendant()).toBe(focusManager.inputs.items()[1].id());
100101
});
102+
103+
it('should focus the list element when focusing an item', () => {
104+
const focusSpy = spyOn(focusManager.inputs.element()!, 'focus');
105+
focusManager.focus(focusManager.inputs.items()[1]);
106+
expect(focusSpy).toHaveBeenCalled();
107+
});
101108
});
102109

103110
describe('#isFocusable', () => {

src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export interface ListFocusInputs<T extends ListFocusItem> {
4040

4141
/** Whether disabled items in the list should be skipped when navigating. */
4242
skipDisabled: SignalLike<boolean>;
43+
44+
element: SignalLike<HTMLElement | undefined>;
4345
}
4446

4547
/** Controls focus for a list of items. */
@@ -103,9 +105,7 @@ export class ListFocus<T extends ListFocusItem> {
103105
this.prevActiveItem.set(this.inputs.activeItem());
104106
this.inputs.activeItem.set(item);
105107

106-
if (this.inputs.focusMode() === 'roving') {
107-
item.element().focus();
108-
}
108+
this.inputs.focusMode() === 'roving' ? item.element().focus() : this.inputs.element()?.focus();
109109

110110
return true;
111111
}

src/cdk-experimental/ui-patterns/behaviors/list/list.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('List Behavior', () => {
3030
multi: inputs.multi ?? signal(false),
3131
textDirection: inputs.textDirection ?? signal('ltr'),
3232
orientation: inputs.orientation ?? signal('vertical'),
33+
element: signal({focus: () => {}} as HTMLElement),
3334
focusMode: inputs.focusMode ?? signal('roving'),
3435
skipDisabled: inputs.skipDisabled ?? signal(true),
3536
selectionMode: signal('explicit'),

0 commit comments

Comments
 (0)