From c16a2aacc705315f2bbf28c185ac9f848ddd3d2d Mon Sep 17 00:00:00 2001 From: sogav21 <223452947+sogav21@users.noreply.github.com> Date: Wed, 4 Nov 2026 21:29:45 +0200 Subject: [PATCH 1/2] Keep focus on hosts with an empty shadow root --- specs/Modal.helpers.spec.js | 41 +++++++++++++++++++++++++++++++++++++ src/helpers/scopeTab.js | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/specs/Modal.helpers.spec.js b/specs/Modal.helpers.spec.js index 9f073452..8ca11838 100644 --- a/specs/Modal.helpers.spec.js +++ b/specs/Modal.helpers.spec.js @@ -2,9 +2,50 @@ import "should"; import "@webcomponents/custom-elements/src/native-shim"; import tabbable from "../src/helpers/tabbable"; +import scopeTab from "../src/helpers/scopeTab"; import "sinon"; export default () => { + describe("scopeTab with shadow roots", () => { + it("handles a focused host with an empty shadow root", () => { + const container = document.createElement("div"); + const host = document.createElement("div"); + host.tabIndex = 0; + host.attachShadow({ mode: "open" }); + container.appendChild(host); + container.appendChild(document.createElement("button")); + document.body.appendChild(container); + try { + host.focus(); + document.activeElement.should.equal(host); + (host.shadowRoot.activeElement === null).should.equal(true); + scopeTab(container, { shiftKey: false, preventDefault() {} }); + } finally { + document.body.removeChild(container); + } + }); + + it("wraps focus from a nested shadow root back to the first element", () => { + const container = document.createElement("div"); + const first = document.createElement("button"); + const host = document.createElement("div"); + const last = document.createElement("button"); + host.attachShadow({ mode: "open" }).appendChild(last); + container.appendChild(first); + container.appendChild(host); + document.body.appendChild(container); + try { + last.focus(); + host.shadowRoot.activeElement.should.equal(last); + let prevented = false; + scopeTab(container, { shiftKey: false, preventDefault() { prevented = true; } }); + prevented.should.equal(true); + document.activeElement.should.equal(first); + } finally { + document.body.removeChild(container); + } + }); + }); describe("tabbable", () => { describe("without tabbable descendents", () => { it("returns an empty array", () => { diff --git a/src/helpers/scopeTab.js b/src/helpers/scopeTab.js index 1e24378a..c8220497 100644 --- a/src/helpers/scopeTab.js +++ b/src/helpers/scopeTab.js @@ -1,7 +1,7 @@ import findTabbable from "./tabbable"; function getActiveElement(el = document) { - return el.activeElement.shadowRoot + return el.activeElement.shadowRoot && el.activeElement.shadowRoot.activeElement ? getActiveElement(el.activeElement.shadowRoot) : el.activeElement; } From 48a38733ae71d40d843e7515dfa539f95a47152d Mon Sep 17 00:00:00 2001 From: sogav21 <223452947+sogav21@users.noreply.github.com> Date: Wed, 4 Nov 2026 21:33:09 +0200 Subject: [PATCH 2/2] Format the shadow root focus guard --- src/helpers/scopeTab.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/scopeTab.js b/src/helpers/scopeTab.js index c8220497..19f44e0e 100644 --- a/src/helpers/scopeTab.js +++ b/src/helpers/scopeTab.js @@ -1,7 +1,8 @@ import findTabbable from "./tabbable"; function getActiveElement(el = document) { - return el.activeElement.shadowRoot && el.activeElement.shadowRoot.activeElement + return el.activeElement.shadowRoot && + el.activeElement.shadowRoot.activeElement ? getActiveElement(el.activeElement.shadowRoot) : el.activeElement; }