Skip to content

Commit bff7492

Browse files
committed
fix(cdk/a11y): add Shadow DOM support to FocusTrap
1 parent d02338b commit bff7492

File tree

2 files changed

+72
-1
lines changed

2 files changed

+72
-1
lines changed

src/cdk/a11y/focus-trap/focus-trap.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,24 @@ describe('FocusTrap', () => {
185185
expect(() => focusTrapInstance.focusFirstTabbableElement()).not.toThrow();
186186
expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow();
187187
});
188+
189+
it('should find tabbable elements in shadow DOM', () => {
190+
if (!_supportsShadowDom()) {
191+
return;
192+
}
193+
194+
const fixture = TestBed.createComponent(FocusTrapWithShadowDom);
195+
fixture.detectChanges();
196+
const focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap;
197+
198+
// The shadow button should be found as the first tabbable element
199+
expect(focusTrapInstance.focusFirstTabbableElement()).toBe(true);
200+
expect(getActiveElement().textContent?.trim()).toBe('Shadow Button');
201+
202+
// The shadow button should also be found as the last tabbable element
203+
expect(focusTrapInstance.focusLastTabbableElement()).toBe(true);
204+
expect(getActiveElement().textContent?.trim()).toBe('Shadow Button');
205+
});
188206
});
189207

190208
describe('with autoCapture', () => {
@@ -448,3 +466,25 @@ class FocusTrapInsidePortal {
448466
@ViewChild('template') template: TemplateRef<any>;
449467
@ViewChild(CdkPortalOutlet) portalOutlet: CdkPortalOutlet;
450468
}
469+
470+
@Component({
471+
template: `
472+
<div cdkTrapFocus>
473+
<div #shadowHost></div>
474+
</div>
475+
`,
476+
imports: [A11yModule],
477+
})
478+
class FocusTrapWithShadowDom {
479+
@ViewChild(CdkTrapFocus) focusTrapDirective: CdkTrapFocus;
480+
@ViewChild('shadowHost', {static: true}) shadowHost: any;
481+
482+
ngAfterViewInit() {
483+
if (_supportsShadowDom()) {
484+
const shadowRoot = this.shadowHost.nativeElement.attachShadow({mode: 'open'});
485+
const shadowButton = document.createElement('button');
486+
shadowButton.textContent = 'Shadow Button';
487+
shadowRoot.appendChild(shadowButton);
488+
}
489+
}
490+
}

src/cdk/a11y/focus-trap/focus-trap.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,22 @@ export class FocusTrap {
286286
return root;
287287
}
288288

289+
// Check shadow DOM first if it exists
290+
if (root.shadowRoot) {
291+
const shadowChildren = root.shadowRoot.children;
292+
for (let i = 0; i < shadowChildren.length; i++) {
293+
const tabbableChild =
294+
shadowChildren[i].nodeType === this._document.ELEMENT_NODE
295+
? this._getFirstTabbableElement(shadowChildren[i] as HTMLElement)
296+
: null;
297+
298+
if (tabbableChild) {
299+
return tabbableChild;
300+
}
301+
}
302+
}
303+
304+
// Then check light DOM children
289305
const children = root.children;
290306

291307
for (let i = 0; i < children.length; i++) {
@@ -308,7 +324,7 @@ export class FocusTrap {
308324
return root;
309325
}
310326

311-
// Iterate in reverse DOM order.
327+
// Iterate in reverse DOM order - check light DOM children first
312328
const children = root.children;
313329

314330
for (let i = children.length - 1; i >= 0; i--) {
@@ -322,6 +338,21 @@ export class FocusTrap {
322338
}
323339
}
324340

341+
// Then check shadow DOM if it exists
342+
if (root.shadowRoot) {
343+
const shadowChildren = root.shadowRoot.children;
344+
for (let i = shadowChildren.length - 1; i >= 0; i--) {
345+
const tabbableChild =
346+
shadowChildren[i].nodeType === this._document.ELEMENT_NODE
347+
? this._getLastTabbableElement(shadowChildren[i] as HTMLElement)
348+
: null;
349+
350+
if (tabbableChild) {
351+
return tabbableChild;
352+
}
353+
}
354+
}
355+
325356
return null;
326357
}
327358

0 commit comments

Comments
 (0)