diff --git a/src/cdk-experimental/accordion/accordion.ts b/src/cdk-experimental/accordion/accordion.ts index b5a877ce883c..6509e73e72b9 100644 --- a/src/cdk-experimental/accordion/accordion.ts +++ b/src/cdk-experimental/accordion/accordion.ts @@ -150,6 +150,9 @@ export class CdkAccordionTrigger { }, }) export class CdkAccordionGroup { + /** A reference to the group element. */ + private readonly _elementRef = inject(ElementRef); + /** The CdkAccordionTriggers nested inside this group. */ protected readonly _triggers = contentChildren(CdkAccordionTrigger, {descendants: true}); @@ -184,6 +187,7 @@ export class CdkAccordionGroup { expandedIds: this.value, // TODO(ok7sai): Investigate whether an accordion should support horizontal mode. orientation: () => 'vertical', + element: () => this._elementRef.nativeElement, }); constructor() { diff --git a/src/cdk-experimental/listbox/listbox.ts b/src/cdk-experimental/listbox/listbox.ts index 98bd4e742446..16a74daea9f2 100644 --- a/src/cdk-experimental/listbox/listbox.ts +++ b/src/cdk-experimental/listbox/listbox.ts @@ -55,6 +55,9 @@ import {_IdGenerator} from '@angular/cdk/a11y'; }, }) export class CdkListbox { + /** A reference to the listbox element. */ + private readonly _elementRef = inject(ElementRef); + /** The directionality (LTR / RTL) context for the application (or a subtree of it). */ private readonly _directionality = inject(Directionality); @@ -105,6 +108,7 @@ export class CdkListbox { items: this.items, activeItem: signal(undefined), textDirection: this.textDirection, + element: () => this._elementRef.nativeElement, }); /** Whether the listbox has received focus yet. */ diff --git a/src/cdk-experimental/radio-group/radio-group.ts b/src/cdk-experimental/radio-group/radio-group.ts index ba9586a83a7f..a5fc07124ed0 100644 --- a/src/cdk-experimental/radio-group/radio-group.ts +++ b/src/cdk-experimental/radio-group/radio-group.ts @@ -93,6 +93,9 @@ export function mapSignal( }, }) export class CdkRadioGroup { + /** A reference to the radio group element. */ + private readonly _elementRef = inject(ElementRef); + /** The CdkRadioButtons nested inside of the CdkRadioGroup. */ private readonly _cdkRadioButtons = contentChildren(CdkRadioButton, {descendants: true}); @@ -140,6 +143,7 @@ export class CdkRadioGroup { activeItem: signal(undefined), textDirection: this.textDirection, toolbar: this._toolbarPattern, + element: () => this._elementRef.nativeElement, focusMode: this._toolbarPattern()?.inputs.focusMode ?? this.focusMode, skipDisabled: this._toolbarPattern()?.inputs.skipDisabled ?? this.skipDisabled, }); diff --git a/src/cdk-experimental/tabs/tabs.ts b/src/cdk-experimental/tabs/tabs.ts index f1d75e982c50..3c1743461dbc 100644 --- a/src/cdk-experimental/tabs/tabs.ts +++ b/src/cdk-experimental/tabs/tabs.ts @@ -130,6 +130,9 @@ export class CdkTabs { }, }) export class CdkTabList implements OnInit, OnDestroy { + /** A reference to the tab list element. */ + private readonly _elementRef = inject(ElementRef); + /** The parent CdkTabs. */ private readonly _cdkTabs = inject(CdkTabs); @@ -174,6 +177,7 @@ export class CdkTabList implements OnInit, OnDestroy { items: this.tabs, value: this._selection, activeItem: signal(undefined), + element: () => this._elementRef.nativeElement, }); /** Whether the tree has received focus yet. */ diff --git a/src/cdk-experimental/toolbar/toolbar.ts b/src/cdk-experimental/toolbar/toolbar.ts index 4e7433bacbf0..884bfc487682 100644 --- a/src/cdk-experimental/toolbar/toolbar.ts +++ b/src/cdk-experimental/toolbar/toolbar.ts @@ -80,6 +80,9 @@ function sortDirectives(a: HasElement, b: HasElement) { }, }) export class CdkToolbar { + /** A reference to the toolbar element. */ + private readonly _elementRef = inject(ElementRef); + /** The CdkTabList nested inside of the container. */ private readonly _cdkWidgets = signal(new Set | CdkToolbarWidget>()); @@ -109,6 +112,7 @@ export class CdkToolbar { activeItem: signal(undefined), textDirection: this.textDirection, focusMode: signal('roving'), + element: () => this._elementRef.nativeElement, }); /** Whether the toolbar has received focus yet. */ diff --git a/src/cdk-experimental/tree/tree.ts b/src/cdk-experimental/tree/tree.ts index 3470f169b182..982809aea3e8 100644 --- a/src/cdk-experimental/tree/tree.ts +++ b/src/cdk-experimental/tree/tree.ts @@ -76,6 +76,9 @@ function sortDirectives(a: HasElement, b: HasElement) { }, }) export class CdkTree { + /** A reference to the tree element. */ + private readonly _elementRef = inject(ElementRef); + /** All CdkTreeItem instances within this tree. */ private readonly _unorderedItems = signal(new Set>()); @@ -124,6 +127,7 @@ export class CdkTree { [...this._unorderedItems()].sort(sortDirectives).map(item => item.pattern), ), activeItem: signal(undefined), + element: () => this._elementRef.nativeElement, }); /** Whether the tree has received focus yet. */ diff --git a/src/cdk-experimental/ui-patterns/accordion/accordion.spec.ts b/src/cdk-experimental/ui-patterns/accordion/accordion.spec.ts index 37e102999e2c..ccbb5181ceb0 100644 --- a/src/cdk-experimental/ui-patterns/accordion/accordion.spec.ts +++ b/src/cdk-experimental/ui-patterns/accordion/accordion.spec.ts @@ -68,6 +68,7 @@ describe('Accordion Pattern', () => { expandedIds: signal([]), skipDisabled: signal(true), wrap: signal(true), + element: signal(document.createElement('div')), }; groupPattern = new AccordionGroupPattern(groupInputs); diff --git a/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.spec.ts index f115a3eed34d..cb9cce52279f 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.spec.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.spec.ts @@ -24,6 +24,7 @@ export function getListFocus(inputs: TestInputs = {}): ListFocus disabled: signal(false), skipDisabled: signal(false), focusMode: signal('roving'), + element: signal({focus: () => {}} as HTMLElement), items: items, ...inputs, }); @@ -98,6 +99,12 @@ describe('List Focus', () => { focusManager.inputs.activeItem.set(focusManager.inputs.items()[1]); expect(focusManager.getActiveDescendant()).toBe(focusManager.inputs.items()[1].id()); }); + + it('should focus the list element when focusing an item', () => { + const focusSpy = spyOn(focusManager.inputs.element()!, 'focus'); + focusManager.focus(focusManager.inputs.items()[1]); + expect(focusSpy).toHaveBeenCalled(); + }); }); describe('#isFocusable', () => { diff --git a/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.ts b/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.ts index 57f9b17bde0e..9515e29a30ee 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/list-focus/list-focus.ts @@ -40,6 +40,8 @@ export interface ListFocusInputs { /** Whether disabled items in the list should be skipped when navigating. */ skipDisabled: SignalLike; + + element: SignalLike; } /** Controls focus for a list of items. */ @@ -103,9 +105,7 @@ export class ListFocus { this.prevActiveItem.set(this.inputs.activeItem()); this.inputs.activeItem.set(item); - if (this.inputs.focusMode() === 'roving') { - item.element().focus(); - } + this.inputs.focusMode() === 'roving' ? item.element().focus() : this.inputs.element()?.focus(); return true; } diff --git a/src/cdk-experimental/ui-patterns/behaviors/list/list.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/list/list.spec.ts index 06f234cb42b9..d9fdac5eaff8 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/list/list.spec.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/list/list.spec.ts @@ -30,6 +30,7 @@ describe('List Behavior', () => { multi: inputs.multi ?? signal(false), textDirection: inputs.textDirection ?? signal('ltr'), orientation: inputs.orientation ?? signal('vertical'), + element: signal({focus: () => {}} as HTMLElement), focusMode: inputs.focusMode ?? signal('roving'), skipDisabled: inputs.skipDisabled ?? signal(true), selectionMode: signal('explicit'), diff --git a/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts b/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts index 33cd25335b6d..2c2ecde4793f 100644 --- a/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts +++ b/src/cdk-experimental/ui-patterns/listbox/listbox.spec.ts @@ -45,6 +45,7 @@ describe('Listbox Pattern', () => { textDirection: inputs.textDirection ?? signal('ltr'), orientation: inputs.orientation ?? signal('vertical'), selectionMode: inputs.selectionMode ?? signal('explicit'), + element: signal(document.createElement('div')), }); } diff --git a/src/cdk-experimental/ui-patterns/radio-group/radio.spec.ts b/src/cdk-experimental/ui-patterns/radio-group/radio.spec.ts index 1197b1699d5b..1230d642e4c6 100644 --- a/src/cdk-experimental/ui-patterns/radio-group/radio.spec.ts +++ b/src/cdk-experimental/ui-patterns/radio-group/radio.spec.ts @@ -33,6 +33,7 @@ describe('RadioGroup Pattern', () => { items: inputs.items, value: inputs.value ?? signal([]), activeItem: signal(undefined), + element: signal(document.createElement('div')), readonly: inputs.readonly ?? signal(false), disabled: inputs.disabled ?? signal(false), skipDisabled: inputs.skipDisabled ?? signal(true), diff --git a/src/cdk-experimental/ui-patterns/tabs/tabs.spec.ts b/src/cdk-experimental/ui-patterns/tabs/tabs.spec.ts index 852fe3b5b0c3..6e7d381480a7 100644 --- a/src/cdk-experimental/ui-patterns/tabs/tabs.spec.ts +++ b/src/cdk-experimental/ui-patterns/tabs/tabs.spec.ts @@ -66,6 +66,7 @@ describe('Tabs Pattern', () => { skipDisabled: signal(true), items: signal([]), value: signal(['tab-1']), + element: signal(document.createElement('div')), }; tabListPattern = new TabListPattern(tabListInputs); diff --git a/src/cdk-experimental/ui-patterns/toolbar/toolbar.spec.ts b/src/cdk-experimental/ui-patterns/toolbar/toolbar.spec.ts index d6fc4b25ea1e..b4e8903efdba 100644 --- a/src/cdk-experimental/ui-patterns/toolbar/toolbar.spec.ts +++ b/src/cdk-experimental/ui-patterns/toolbar/toolbar.spec.ts @@ -51,6 +51,7 @@ describe('Toolbar Pattern', () => { textDirection: inputs.textDirection ?? signal('ltr'), orientation: inputs.orientation ?? signal('vertical'), toolbar: inputs.toolbar ?? signal(undefined), + element: signal(document.createElement('div')), }); } @@ -92,6 +93,7 @@ describe('Toolbar Pattern', () => { textDirection: inputs.textDirection ?? signal('ltr'), orientation: inputs.orientation ?? signal('horizontal'), wrap: inputs.wrap ?? signal(false), + element: signal(document.createElement('div')), }); } diff --git a/src/cdk-experimental/ui-patterns/tree/tree.spec.ts b/src/cdk-experimental/ui-patterns/tree/tree.spec.ts index bc7c66d706e8..24e3808ef5d0 100644 --- a/src/cdk-experimental/ui-patterns/tree/tree.spec.ts +++ b/src/cdk-experimental/ui-patterns/tree/tree.spec.ts @@ -149,6 +149,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -201,6 +202,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(true), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -246,6 +248,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -430,6 +433,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -488,6 +492,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -552,6 +557,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -710,6 +716,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -860,6 +867,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -900,6 +908,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -944,6 +953,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -992,6 +1002,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -1074,6 +1085,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); @@ -1236,6 +1248,7 @@ describe('Tree Pattern', () => { wrap: signal(false), nav: signal(false), currentType: signal('page'), + element: signal(document.createElement('div')), }; }); diff --git a/src/dev-app/common-classes.css b/src/dev-app/common-classes.css index 5036839dc7c1..a4a7de05e811 100644 --- a/src/dev-app/common-classes.css +++ b/src/dev-app/common-classes.css @@ -60,6 +60,17 @@ border-radius: var(--mat-sys-corner-extra-small); } +[aria-disabled='true']:focus-within, +[aria-activedescendant]:focus-within { + outline: var(--mat-sys-primary) solid 1px; +} + +[aria-disabled='true'] .example-selectable:focus-within, +[aria-activedescendant] .example-selectable:focus-within { + outline-color: transparent; + border-radius: 0; +} + [aria-disabled='true'] .example-selectable[aria-selected='true'], .example-selectable[aria-disabled='true'][aria-selected='true'], [aria-disabled='true'] .example-selectable[aria-checked='true'],