diff --git a/src/cdk/a11y/focus-trap/focus-trap.spec.ts b/src/cdk/a11y/focus-trap/focus-trap.spec.ts index 428ca86c3337..3ad929621847 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.spec.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.spec.ts @@ -185,6 +185,24 @@ describe('FocusTrap', () => { expect(() => focusTrapInstance.focusFirstTabbableElement()).not.toThrow(); expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow(); }); + + it('should find tabbable elements in shadow DOM', () => { + if (!_supportsShadowDom()) { + return; + } + + const fixture = TestBed.createComponent(FocusTrapWithShadowDom); + fixture.detectChanges(); + const focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap; + + // The shadow button should be found as the first tabbable element + expect(focusTrapInstance.focusFirstTabbableElement()).toBe(true); + expect(getActiveElement().textContent?.trim()).toBe('Shadow Button'); + + // The shadow button should also be found as the last tabbable element + expect(focusTrapInstance.focusLastTabbableElement()).toBe(true); + expect(getActiveElement().textContent?.trim()).toBe('Shadow Button'); + }); }); describe('with autoCapture', () => { @@ -448,3 +466,25 @@ class FocusTrapInsidePortal { @ViewChild('template') template: TemplateRef; @ViewChild(CdkPortalOutlet) portalOutlet: CdkPortalOutlet; } + +@Component({ + template: ` +
+
+
+ `, + imports: [A11yModule], +}) +class FocusTrapWithShadowDom { + @ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus; + @ViewChild('shadowHost', {static: true}) shadowHost: any; + + ngAfterViewInit() { + if (_supportsShadowDom()) { + const shadowRoot = this.shadowHost.nativeElement.attachShadow({mode: 'open'}); + const shadowButton = document.createElement('button'); + shadowButton.textContent = 'Shadow Button'; + shadowRoot.appendChild(shadowButton); + } + } +} diff --git a/src/cdk/a11y/focus-trap/focus-trap.ts b/src/cdk/a11y/focus-trap/focus-trap.ts index 225410192f8b..397debef55a0 100644 --- a/src/cdk/a11y/focus-trap/focus-trap.ts +++ b/src/cdk/a11y/focus-trap/focus-trap.ts @@ -286,6 +286,22 @@ export class FocusTrap { return root; } + // Check shadow DOM first if it exists + if (root.shadowRoot) { + const shadowChildren = root.shadowRoot.children; + for (let i = 0; i < shadowChildren.length; i++) { + const tabbableChild = + shadowChildren[i].nodeType === this._document.ELEMENT_NODE + ? this._getFirstTabbableElement(shadowChildren[i] as HTMLElement) + : null; + + if (tabbableChild) { + return tabbableChild; + } + } + } + + // Then check light DOM children const children = root.children; for (let i = 0; i < children.length; i++) { @@ -308,7 +324,7 @@ export class FocusTrap { return root; } - // Iterate in reverse DOM order. + // Iterate in reverse DOM order - check light DOM children first const children = root.children; for (let i = children.length - 1; i >= 0; i--) { @@ -322,6 +338,21 @@ export class FocusTrap { } } + // Then check shadow DOM if it exists + if (root.shadowRoot) { + const shadowChildren = root.shadowRoot.children; + for (let i = shadowChildren.length - 1; i >= 0; i--) { + const tabbableChild = + shadowChildren[i].nodeType === this._document.ELEMENT_NODE + ? this._getLastTabbableElement(shadowChildren[i] as HTMLElement) + : null; + + if (tabbableChild) { + return tabbableChild; + } + } + } + return null; } diff --git a/src/material/datepicker/calendar-body.html b/src/material/datepicker/calendar-body.html index 4bd24d90fa83..cf19313f24e9 100644 --- a/src/material/datepicker/calendar-body.html +++ b/src/material/datepicker/calendar-body.html @@ -14,7 +14,7 @@ } -@for (row of rows; track _trackRow(row); let rowIndex = $index) { +@for (row of rows; track _trackRow($index, row); let rowIndex = $index) {