Skip to content

Commit eb7960a

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

File tree

2 files changed

+73
-1
lines changed

2 files changed

+73
-1
lines changed

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Platform, _supportsShadowDom} from '../../platform';
22
import {CdkPortalOutlet, PortalModule, TemplatePortal} from '../../portal';
33
import {
4+
AfterViewInit,
45
Component,
56
TemplateRef,
67
ViewChild,
@@ -185,6 +186,24 @@ describe('FocusTrap', () => {
185186
expect(() => focusTrapInstance.focusFirstTabbableElement()).not.toThrow();
186187
expect(() => focusTrapInstance.focusLastTabbableElement()).not.toThrow();
187188
});
189+
190+
it('should find tabbable elements in shadow DOM', () => {
191+
if (!_supportsShadowDom()) {
192+
return;
193+
}
194+
195+
const fixture = TestBed.createComponent(FocusTrapWithShadowDom);
196+
fixture.detectChanges();
197+
const focusTrapInstance = fixture.componentInstance.focusTrapDirective.focusTrap;
198+
199+
// The shadow button should be found as the first tabbable element
200+
expect(focusTrapInstance.focusFirstTabbableElement()).toBe(true);
201+
expect(getActiveElement().textContent?.trim()).toBe('Shadow Button');
202+
203+
// The shadow button should also be found as the last tabbable element
204+
expect(focusTrapInstance.focusLastTabbableElement()).toBe(true);
205+
expect(getActiveElement().textContent?.trim()).toBe('Shadow Button');
206+
});
188207
});
189208

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

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)