summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/shadow-dom/focus
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/shadow-dom/focus')
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html92
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html27
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html38
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html138
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html68
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html68
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html23
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html338
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html40
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html36
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html312
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html101
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html122
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html25
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html27
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html26
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html92
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html68
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html31
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html34
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html34
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html37
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html36
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html66
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html80
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html36
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html34
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html35
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html38
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html37
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html37
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html36
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html35
-rw-r--r--testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js80
34 files changed, 2327 insertions, 0 deletions
diff --git a/testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html b/testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html
new file mode 100644
index 0000000000..20456b057e
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/DocumentOrShadowRoot-activeElement.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: DocumentOrShadowRoot.activeElement</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<body>
+<script>
+function createChildAndFocus(focusParent) {
+ const focused = document.createElement("div");
+ focused.tabIndex = 0;
+ focusParent.appendChild(focused);
+ focused.focus();
+ return focused;
+}
+
+test(() => {
+ const host = document.createElement("div");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ document.body.appendChild(host);
+
+ const focused = createChildAndFocus(shadowRoot);
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, focused);
+}, "activeElement on document & shadow root when focused element is in the shadow tree");
+
+test(() => {
+ const host = document.createElement("div");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ document.body.appendChild(host);
+
+ const focused = createChildAndFocus(document.body);
+ assert_equals(document.activeElement, focused);
+ assert_equals(shadowRoot.activeElement, null);
+}, "activeElement on document & shadow root when focused element is in the document");
+
+test(() => {
+ const host = document.createElement("div");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ shadowRoot.appendChild(document.createElement("slot"));
+ document.body.appendChild(host);
+
+ // Child of |host|, will be slotted to the slot in |shadowRoot|.
+ const focused = createChildAndFocus(host);
+ assert_equals(document.activeElement, focused);
+ assert_equals(shadowRoot.activeElement, null);
+}, "activeElement on document & shadow root when focused element is slotted");
+
+test(() => {
+ const host = document.createElement("div");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ document.body.appendChild(host);
+ const neighborHost = document.createElement("div");
+ const neighborShadowRoot = neighborHost.attachShadow({ mode: "open" });
+ document.body.appendChild(neighborHost);
+
+ const focused = createChildAndFocus(shadowRoot);
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, focused);
+ assert_equals(neighborShadowRoot.activeElement, null);
+}, "activeElement on a neighboring host when focused element is in another shadow tree");
+
+test(() => {
+ const host = document.createElement("div");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ document.body.appendChild(host);
+ const nestedHost = document.createElement("div");
+ const nestedShadowRoot = nestedHost.attachShadow({ mode: "open" });
+ shadowRoot.appendChild(nestedHost);
+
+ const focused = createChildAndFocus(nestedShadowRoot);
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, nestedHost);
+ assert_equals(nestedShadowRoot.activeElement, focused);
+}, "activeElement when focused element is in a nested shadow tree");
+
+test(() => {
+ const host = document.createElement("div");
+ const shadowRoot = host.attachShadow({ mode: "open" });
+ document.body.appendChild(host);
+ const nestedHost = document.createElement("div");
+ const nestedShadowRoot = nestedHost.attachShadow({ mode: "open" });
+ shadowRoot.appendChild(nestedHost);
+
+ const focused = createChildAndFocus(shadowRoot);
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, focused);
+ assert_equals(nestedShadowRoot.activeElement, null);
+}, "activeElement when focused element is in a parent shadow tree");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html
new file mode 100644
index 0000000000..4b7fe4e50e
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/ShadowRoot-delegatesFocus.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ShadowRoot's delegatesFocus attribute</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="host1"></div>
+<div id="host2"></div>
+<div id="host3"></div>
+<script>
+test(t => {
+ const host = document.getElementById("host1");
+ const shadowRoot = host.attachShadow({mode: "closed"});
+ assert_equals(shadowRoot.delegatesFocus, false);
+}, "default delegatesFocus value");
+
+test(t => {
+ const host = document.getElementById("host2");
+ const shadowRoot = host.attachShadow({mode: "closed", delegatesFocus: false});
+ assert_equals(shadowRoot.delegatesFocus, false);
+}, "delegatesFocus set to false in init dict");
+
+test(t => {
+ const host = document.getElementById("host3");
+ const shadowRoot = host.attachShadow({mode: "closed", delegatesFocus: true});
+ assert_equals(shadowRoot.delegatesFocus, true);
+}, "delegatesFocus set to true in init dict");
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html
new file mode 100644
index 0000000000..289b554372
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/blur-on-shadow-host-delegatesFocus.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Blur on shadow host</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<div id="host">
+ <input id="slotted">
+</div>
+
+<script>
+const host = document.getElementById("host");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+
+shadowRoot.innerHTML = "<input><slot>"
+
+test(function() {
+ host.focus();
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, shadowRoot.querySelector("input"));
+ host.blur();
+ assert_equals(document.activeElement, document.body);
+ assert_equals(shadowRoot.activeElement, null);
+}, "Calling blur() on shadow host with delegatesFocus should remove the focus.");
+
+test(function() {
+ const slotted = document.getElementById("slotted");
+ slotted.focus();
+ assert_equals(document.activeElement, slotted);
+ assert_equals(shadowRoot.activeElement, null)
+ host.blur();
+ assert_equals(document.activeElement, slotted);
+ assert_equals(shadowRoot.activeElement, null)
+}, "Calling blur() on shadow host with delegatesFocus when the focus is on a slotted element should not remove the focus.");
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html
new file mode 100644
index 0000000000..0578c15582
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-click.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+ <div id="slotted">slotted</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slotted = document.getElementById("slotted");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlot = document.createElement("div");
+aboveSlot.innerText = "aboveSlot";
+const slot = document.createElement("slot");
+shadowRoot.appendChild(aboveSlot);
+shadowRoot.appendChild(slot);
+
+const elementsInFlatTreeOrder = [host, aboveSlot, slot, slotted, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+// #shadowRoot
+// <div #aboveSlot>
+// <slot #slot>
+// (slotted) <div #slotted>
+// <div #outside>
+
+function setAllTabIndex(value) {
+ setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+ removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+ removeAllTabIndex();
+ resetFocus(document);
+ resetFocus(shadowRoot);
+}
+
+test(() => {
+ resetTabIndexAndFocus();
+ setAllTabIndex(0);
+ host.click();
+ assert_equals(shadowRoot.activeElement, null);
+ assert_equals(document.activeElement, document.body);
+}, "call click() on host with delegatesFocus, all tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setAllTabIndex(0);
+ slotted.click();
+ assert_equals(shadowRoot.activeElement, null);
+ assert_equals(document.activeElement, document.body);
+}, "call click() on slotted element in delegatesFocus shadow tree, all tabindex=0");
+
+function createNestedHosts(outerDelegatesFocus, innerDelegatesFocus) {
+ // Structure:
+ // <div> outerHost
+ // <input> outerLightChild
+ // #shadowRoot outerShadow delegatesFocus=true
+ // <div> spacer
+ // <span> innerHost
+ // #shadowRoot innerShadow delegatesFocus=true/false
+ // <input> innerShadowChild
+ // <input> outerShadowChild
+ const outerHost = document.createElement('div');
+ const outerLightChild = document.createElement('input');
+ outerHost.appendChild(outerLightChild);
+ const innerHost = document.createElement('span');
+ const outerShadow = outerHost.attachShadow({mode: 'closed', delegatesFocus:outerDelegatesFocus});
+
+ const spacer = document.createElement("div");
+ spacer.style = "height: 1000px;";
+ outerShadow.appendChild(spacer);
+
+ outerShadow.appendChild(innerHost);
+ const outerShadowChild = document.createElement('input');
+ outerShadow.appendChild(outerShadowChild);
+
+ const innerShadow = innerHost.attachShadow({mode: 'closed', delegatesFocus:innerDelegatesFocus});
+ const innerShadowChild = document.createElement('input');
+ innerShadow.appendChild(innerShadowChild);
+
+ document.body.insertBefore(outerHost, document.body.firstChild);
+ return {outerHost: outerHost,
+ outerLightChild: outerLightChild,
+ outerShadow: outerShadow,
+ outerShadowChild: outerShadowChild,
+ innerHost: innerHost,
+ innerShadow: innerShadow,
+ innerShadowChild: innerShadowChild};
+}
+
+promise_test(async function() {
+ const dom = createNestedHosts(true, true);
+ await test_driver.click(dom.outerHost);
+ assert_equals(document.activeElement, dom.outerHost);
+ assert_equals(dom.outerShadow.activeElement, dom.innerHost);
+ assert_equals(dom.innerShadow.activeElement, dom.innerShadowChild);
+}, "click on the host with delegatesFocus with another host with delegatesFocus and a focusable child");
+
+promise_test(async function() {
+ const dom = createNestedHosts(true, false);
+ await test_driver.click(dom.outerHost);
+ assert_equals(document.activeElement, dom.outerHost);
+ assert_equals(dom.outerShadow.activeElement, dom.outerShadowChild);
+ assert_equals(dom.innerShadow.activeElement, null);
+}, "click on the host with delegatesFocus with another host with no delegatesFocus and a focusable child");
+
+promise_test(async function() {
+ const dom = createNestedHosts(false, true);
+ await test_driver.click(dom.outerHost);
+ assert_equals(document.activeElement, document.body);
+ assert_equals(dom.outerShadow.activeElement, null);
+ assert_equals(dom.innerShadow.activeElement, null);
+}, "click on the host with no delegatesFocus with another host with delegatesFocus and a focusable child");
+
+promise_test(async function() {
+ const dom = createNestedHosts(false, false);
+ await test_driver.click(dom.outerHost);
+ assert_equals(document.activeElement, document.body);
+ assert_equals(dom.outerShadow.activeElement, null);
+ assert_equals(dom.innerShadow.activeElement, null);
+}, "click on the host with no delegatesFocus with another host with no delegatesFocus and a focusable child");
+
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html
new file mode 100644
index 0000000000..4051db128a
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+ <div id="slotted">slotted</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slotted = document.getElementById("slotted");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlot = document.createElement("div");
+aboveSlot.innerText = "aboveSlot";
+const slot = document.createElement("slot");
+// Add an unfocusable spacer, because test_driver.click will click on the
+// center point of #host, and we don't want the click to land on #aboveSlot
+// or #slot.
+const spacer = document.createElement("div");
+spacer.style = "height: 1000px;";
+shadowRoot.appendChild(spacer);
+shadowRoot.appendChild(aboveSlot);
+shadowRoot.appendChild(slot);
+
+const elementsInFlatTreeOrder = [host, aboveSlot, spacer, slot, slotted, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+// #shadowRoot
+// <div #spacer>
+// <div #aboveSlot>
+// <slot #slot>
+// (slotted) <div #slotted>
+// <div #outside>
+
+function setAllTabIndex(value) {
+ setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+ removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+ removeAllTabIndex();
+ resetFocus(document);
+ resetFocus(shadowRoot);
+}
+
+promise_test(async () => {
+ resetTabIndexAndFocus();
+ setTabIndex([aboveSlot], 2);
+ setTabIndex([slot, slotted], 1);
+ await test_driver.click(host);
+ assert_equals(shadowRoot.activeElement, aboveSlot);
+ assert_equals(document.activeElement, host);
+}, "click on host with delegatesFocus, #aboveSlot tabindex = 2, #slot and #slotted tabindex = 1");
+
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html
new file mode 100644
index 0000000000..5f7914f2a4
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+ <div id="slotted">slotted</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slotted = document.getElementById("slotted");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlot = document.createElement("div");
+aboveSlot.innerText = "aboveSlot";
+const slot = document.createElement("slot");
+// Add an unfocusable spacer, because test_driver.click will click on the
+// center point of #host, and we don't want the click to land on #aboveSlot
+// or #slot.
+const spacer = document.createElement("div");
+spacer.style = "height: 1000px;";
+shadowRoot.appendChild(spacer);
+shadowRoot.appendChild(aboveSlot);
+shadowRoot.appendChild(slot);
+
+const elementsInFlatTreeOrder = [host, aboveSlot, spacer, slot, slotted, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+// #shadowRoot
+// <div #spacer>
+// <div #aboveSlot>
+// <slot #slot>
+// (slotted) <div #slotted>
+// <div #outside>
+
+function setAllTabIndex(value) {
+ setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+ removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+ removeAllTabIndex();
+ resetFocus(document);
+ resetFocus(shadowRoot);
+}
+
+promise_test(async () => {
+ resetTabIndexAndFocus();
+ setAllTabIndex(0);
+ removeTabIndex([spacer]);
+ await test_driver.click(host);
+ assert_equals(shadowRoot.activeElement, aboveSlot);
+ assert_equals(document.activeElement, host);
+}, "click on host with delegatesFocus, all tabindex=0 except spacer");
+
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html b/testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html
new file mode 100644
index 0000000000..f159c22164
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/delegatesFocus-tabindex-change.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+test(() => {
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+
+ const shadowRoot = host.attachShadow({mode: 'open', delegatesFocus: true});
+
+ const shadowInput = document.createElement('input');
+ shadowRoot.appendChild(shadowInput);
+
+ host.focus();
+ assert_equals(document.activeElement, host, 'The shadow host should be focused.');
+
+ host.setAttribute('tabindex', '0');
+ assert_equals(document.activeElement, host, 'The shadow host should remain focused after changing tabindex.');
+}, 'Setting tabindex on the shadow host of a focused element with delegatesFocus should not change focus.');
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html
new file mode 100644
index 0000000000..75a50b84c6
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-autofocus.html
@@ -0,0 +1,338 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="author" title="Sean Feng" href="mailto:sefeng@mozilla.com">
+<meta name="assert" content="Elements with autofocus should have high precedence over other elements for delegates focus">
+<link rel="help" href="https://github.com/whatwg/html/pull/6990">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+</head>
+
+<body>
+ <script>
+ function createShadowDOMTree() {
+ // <div #host> (delegatesFocus = true)
+ // #shadowRoot
+ // <div #firstOuterDiv>
+ // <div #innertHost>
+ // #shadowRoot
+ // <div #firstInnerDiv>
+ // <div #secondInnerDiv>
+ // <div #secondOuterDiv>
+ const host = document.createElement("div");
+ host.setAttribute("id", "host");
+ const outerRoot = host.attachShadow({mode: "open", delegatesFocus: true});
+
+ const firstOuterDiv = document.createElement("div");
+
+ const innerHost = document.createElement("div");
+ const innerRoot = innerHost.attachShadow({mode: "open"});
+ const firstInnerDiv = document.createElement("div");
+ const secondInnerDiv = document.createElement("div");
+ innerRoot.appendChild(firstInnerDiv);
+ innerRoot.appendChild(secondInnerDiv);
+
+ const secondOuterDiv = document.createElement("div");
+
+ outerRoot.appendChild(firstOuterDiv);
+ outerRoot.appendChild(innerHost);
+ outerRoot.appendChild(secondOuterDiv);
+ document.body.appendChild(host);
+ return [
+ host,
+ outerRoot,
+ firstOuterDiv,
+ secondOuterDiv,
+ innerHost,
+ innerRoot,
+ firstInnerDiv,
+ secondInnerDiv
+ ]
+ }
+
+ function resetShadowDOMTree() {
+ const host = document.getElementById("host");
+ if (host) {
+ host.remove();
+ }
+ return createShadowDOMTree();
+ }
+
+ function resetTabIndexAndFocus(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ outerRoot,
+ innerRoot
+ ) {
+ firstOuterDiv.removeAttribute("tabindex");
+ firstOuterDiv.removeAttribute("autofocus");
+
+ secondOuterDiv.removeAttribute("tabindex");
+ secondOuterDiv.removeAttribute("autofocus");
+
+ firstInnerDiv.removeAttribute("tabindex");
+ firstInnerDiv.removeAttribute("autofocus");
+
+ secondInnerDiv.removeAttribute("tabindex");
+ secondInnerDiv.removeAttribute("autofocus");
+
+ resetFocus(document);
+ resetFocus(outerRoot);
+ resetFocus(innerRoot);
+ }
+
+ function setAllTabIndexTo(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ tabIndex
+ ) {
+ firstOuterDiv.tabIndex = tabIndex;
+ secondOuterDiv.tabIndex = tabIndex;
+ firstInnerDiv.tabIndex = tabIndex;
+ secondInnerDiv.tabIndex = tabIndex;
+ }
+
+ test(function() {
+ const [
+ host,
+ outerRoot,
+ firstOuterDiv,
+ secondOuterDiv,
+ innerHost,
+ innerRoot,
+ firstInnerDiv,
+ secondInnerDiv
+ ] = resetShadowDOMTree();
+
+ resetTabIndexAndFocus(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ outerRoot,
+ innerRoot
+ );
+
+ setAllTabIndexTo(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ 0
+ );
+
+ // <div #host> (delegatesFocus = true)
+ // #shadowRoot
+ // <div tabIndex=0 #firstOuterDiv>
+ // <div #innertHost>
+ // #shadowRoot
+ // <div tabIndex=0 #firstInnerDiv>
+ // <div tabIndex=0 #secondInnerDiv>
+ // <div tabIndex=0 autofocus #secondOuterDiv>
+ secondOuterDiv.autofocus = true;
+ secondOuterDiv.setAttribute("autofocus", true);
+
+ host.focus();
+
+ assert_equals(document.activeElement, host);
+ assert_equals(outerRoot.activeElement, secondOuterDiv);
+ }, "The second input should be focused since it has autofocus");
+
+ test(function() {
+ const [
+ host,
+ outerRoot,
+ firstOuterDiv,
+ secondOuterDiv,
+ innerHost,
+ innerRoot,
+ firstInnerDiv,
+ secondInnerDiv
+ ] = resetShadowDOMTree();
+
+ resetTabIndexAndFocus(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ outerRoot,
+ innerRoot
+ );
+
+ // <div #host> (delegatesFocus = true)
+ // #shadowRoot
+ // <div #firstOuterDiv>
+ // <div #innertHost>
+ // #shadowRoot
+ // <div tabIndex=0 #firstInnerDiv>
+ // <div tabIndex=0 autofocus #secondInnerDiv>
+ // <div #secondOuterDiv>
+ firstInnerDiv.tabIndex = 0;
+ secondInnerDiv.tabIndex = 0;
+ secondInnerDiv.setAttribute("autofocus", true);
+
+ host.focus();
+ assert_equals(document.activeElement, document.body);
+ assert_equals(outerRoot.activeElement, null);
+ }, "Focus should not be delegated to the autofocus element because the inner host doesn't have delegates focus");
+
+ test(function() {
+ const [
+ host,
+ outerRoot,
+ firstOuterDiv,
+ secondOuterDiv,
+ innerHost,
+ innerRoot,
+ firstInnerDiv,
+ secondInnerDiv
+ ] = resetShadowDOMTree();
+
+ resetTabIndexAndFocus(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ outerRoot,
+ innerRoot
+ );
+
+ const newInnerHost = document.createElement("div");
+ const newInnerRoot = newInnerHost.attachShadow({mode: "open", delegatesFocus: true});
+ const newFirstInnerDiv = document.createElement("div");
+ const newSecondInnerDiv = document.createElement("div");
+ newFirstInnerDiv.setAttribute("tabIndex", 0);
+ newSecondInnerDiv.setAttribute("tabIndex", 0);
+
+ newSecondInnerDiv.setAttribute("autofocus", true);
+ newInnerRoot.appendChild(newFirstInnerDiv);
+ newInnerRoot.appendChild(newSecondInnerDiv);
+
+ // <div #host> (delegatesFocus = true)
+ // #shadowRoot
+ // <div #firstOuterDiv>
+ // <div #innertHost> (delegatesFocus = true)
+ // #shadowRoot
+ // <div tabIndex=0 #newFirstInnerDiv>
+ // <div tabIndex=0 autofocus #newSecondInnerDiv>
+ // <div #secondOuterDiv>
+ outerRoot.replaceChild(newInnerHost, innerHost);
+
+ host.focus();
+
+ assert_equals(document.activeElement, host);
+ assert_equals(outerRoot.activeElement, newInnerHost);
+ assert_equals(newInnerRoot.activeElement, newSecondInnerDiv);
+ }, "Focus should be delegated to the autofocus element when the inner host has delegates focus");
+
+ test(function() {
+ const [
+ host,
+ outerRoot,
+ firstOuterDiv,
+ secondOuterDiv,
+ innerHost,
+ innerRoot,
+ firstInnerDiv,
+ secondInnerDiv
+ ] = resetShadowDOMTree();
+
+ resetTabIndexAndFocus(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ outerRoot,
+ innerRoot
+ );
+
+ // <div #host> (delegatesFocus = true)
+ // #shadowRoot
+ // <slot>
+ // (slotted) <div autofocus tabIndex=0 #slottedAutofocus></div>
+ // <div tabIndex=0 #firstOuterDiv>
+ // <div #innertHost>
+ // #shadowRoot
+ // <div tabIndex=0 #firstInnerDiv>
+ // <div tabIndex=0 autofocus #secondInnerDiv>
+ // <div #secondOuterDiv>
+
+ const slottedAutofocus = document.createElement("div");
+ slottedAutofocus.tabIndex = 0;
+ slottedAutofocus.setAttribute("autofocus", true);
+ host.appendChild(slottedAutofocus);
+
+ const slot = document.createElement("slot");
+ outerRoot.insertBefore(slot, firstOuterDiv);
+
+ firstOuterDiv.tabIndex = 0;
+
+ host.focus();
+ assert_equals(document.activeElement, host);
+ assert_equals(outerRoot.activeElement, firstOuterDiv);
+ }, "Focus should not be delegated to the slotted elements");
+
+ test(function() {
+ const [
+ host,
+ outerRoot,
+ firstOuterDiv,
+ secondOuterDiv,
+ innerHost,
+ innerRoot,
+ firstInnerDiv,
+ secondInnerDiv
+ ] = resetShadowDOMTree();
+
+ resetTabIndexAndFocus(
+ firstOuterDiv,
+ secondOuterDiv,
+ firstInnerDiv,
+ secondInnerDiv,
+ outerRoot,
+ innerRoot
+ );
+
+ // <div #host> (delegatesFocus = true)
+ // #shadowRoot
+ // <div #firstOuterDiv>
+ // <div tabIndex=0 #firstNestedDiv>
+ // <div tabIndex=0 #secondNestedDiv>
+ // <div tabIndex=0 autofocus #thirdNestedDiv>
+ // <div #innertHost>
+ // #shadowRoot
+ // <div #firstInnerDiv>
+ // <div #secondInnerDiv>
+ // <div autofocus tabIndex=0 #secondOuterDiv>
+
+ secondInnerDiv.tabIndex = 0;
+ secondInnerDiv.setAttribute("autofocus", true);
+
+ const firstNestedDiv = document.createElement("div");
+ const secondNestedDiv = document.createElement("div");
+ const thirdNestedDiv = document.createElement("div");
+
+ firstNestedDiv.tabIndex = 0;
+ secondNestedDiv.tabIndex = 0;
+ thirdNestedDiv.tabIndex = 0;
+ thirdNestedDiv.setAttribute("autofocus", true);
+
+ firstOuterDiv.appendChild(firstNestedDiv);
+ firstNestedDiv.appendChild(secondNestedDiv);
+ secondNestedDiv.appendChild(thirdNestedDiv);
+
+ host.focus();
+
+ assert_equals(document.activeElement, host);
+ assert_equals(outerRoot.activeElement, thirdNestedDiv);
+ }, "Focus should be delegated to the nested div which has autofocus based on the tree order");
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html
new file mode 100644
index 0000000000..7a318a0200
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-click-on-shadow-host.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<link rel="author" href="mailto:dizhangg@chromium.org">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1327136">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+ <div id="host"></div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+// Add an unfocusable spacer, because test_driver.click will click on the
+// center point of #host, and we don't want the click to land on focusableDiv
+const spacer = document.createElement("div");
+spacer.style = "height: 1000px;";
+shadowRoot.appendChild(spacer);
+
+const focusableDiv = document.createElement("div");
+focusableDiv.tabIndex = 0;
+shadowRoot.appendChild(focusableDiv);
+
+promise_test(async () => {
+ assert_equals(document.activeElement, document.body);
+ // Mouse click
+ await test_driver.click(host);
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, focusableDiv);
+ assert_true(host.matches(':focus'));
+ assert_true(focusableDiv.matches(':focus'));
+ assert_false(focusableDiv.matches(':focus-visible'));
+}, ":focus should be applied to the host and the child node when the focus is moved by mouse click");
+
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html
new file mode 100644
index 0000000000..d2724f17d5
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus-nested-browsing-context.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus() on shadow host within an iframe with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<body>
+<script>
+test(() => {
+ const iframe = document.createElement("iframe");
+ document.body.appendChild(iframe);
+ iframe.addEventListener("load", () => {
+ iframe.contentDocument.body.innerHTML =
+ `<div id="host"></div>`;
+ const host = iframe.contentDocument.getElementById("host");
+ const firstInput = iframe.contentDocument.createElement("input");
+ const secondInput = iframe.contentDocument.createElement("input");
+
+ host.attachShadow({mode: 'open', delegatesFocus: true});
+ host.shadowRoot.appendChild(firstInput);
+ host.shadowRoot.appendChild(secondInput);
+
+ iframe.contentDocument.body.appendChild(host);
+
+ secondInput.focus();
+ assert_equals(host.shadowRoot.activeElement, secondInput);
+
+ // host is a shadow-including-ancestor of secondInput, so
+ // the focus should remain secondInput.
+ host.focus();
+ assert_equals(host.shadowRoot.activeElement, secondInput);
+ });
+}, "focus delegate step should not be run when the focus target is a shadow-including inclusive ancestor of the current focus.");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html
new file mode 100644
index 0000000000..99667029ad
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-method-delegatesFocus.html
@@ -0,0 +1,312 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus() on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+ <div id="slottedToSecondSlot" slot="secondSlot">slottedToSecondSlot</div>
+ <div id="slottedToFirstSlot" slot="firstSlot">slottedToFirstSlot</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slottedToSecondSlot = document.getElementById("slottedToSecondSlot");
+const slottedToFirstSlot = document.getElementById("slottedToFirstSlot");
+const outside = document.getElementById("outside");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlots = document.createElement("div");
+aboveSlots.innerText = "aboveSlots";
+const firstSlot = document.createElement("slot");
+firstSlot.name = "firstSlot";
+const secondSlot = document.createElement("slot");
+secondSlot.name = "secondSlot";
+const belowSlots = document.createElement("div");
+belowSlots.innerText = "belowSlots";
+shadowRoot.appendChild(aboveSlots);
+shadowRoot.appendChild(firstSlot);
+shadowRoot.appendChild(secondSlot);
+shadowRoot.appendChild(belowSlots);
+
+const elementsInFlatTreeOrder = [host, aboveSlots, firstSlot,
+ slottedToFirstSlot, secondSlot, slottedToSecondSlot, belowSlots, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+// #shadowRoot
+// <div #aboveSlots>
+// <slot #firstSlot>
+// (slotted) <div #slottedToFirstSlot>
+// <slot #secondSlot>
+// (slotted) <div #slottedToSecondSlot>
+// <div #belowSlots>
+// <div #outside>
+
+
+function setAllTabIndex(value) {
+ setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+ removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+ removeAllTabIndex();
+ resetFocus(document);
+ resetFocus(shadowRoot);
+}
+
+test(() => {
+ resetTabIndexAndFocus();
+ setAllTabIndex(0);
+ // Structure:
+ // <div #host> (delegatesFocus=true) tabindex=0
+ // #shadowRoot
+ // <div #aboveSlots> tabindex=0
+ // <slot #firstSlot> tabindex=0
+ // (slotted) <div #slottedToFirstSlot> tabindex=0
+ // <slot #secondSlot> tabindex=0
+ // (slotted) <div #slottedToSecondSlot> tabindex=0
+ // <div #belowSlots> tabindex=0
+ // <div #outside> tabindex=0
+ // First focusable = #aboveSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus, all tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setAllTabIndex(0);
+ setTabIndex([host], -1);
+ // First focusable = #aboveSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex =-1, all other tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setTabIndex([aboveSlots, slottedToFirstSlot, slottedToSecondSlot, belowSlots], 0);
+ // First focusable = #aboveSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & no tabindex, all other tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setAllTabIndex(-1);
+ setTabIndex([host], 0);
+ // First focusable = #aboveSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex = 0, all other tabindex=-1");
+
+test(() => {
+ resetTabIndexAndFocus();
+ removeAllTabIndex();
+ // No focusable element under #host in the flat tree.
+ host.focus();
+ assert_equals(shadowRoot.activeElement, null);
+ assert_equals(document.activeElement, document.body);
+}, "focus() on host with delegatesFocus, all without tabindex");
+
+test(() => {
+ resetTabIndexAndFocus();
+ // First focusable = #aboveSlots
+ setAllTabIndex(-1);
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus, all tabindex=-1");
+
+test(() => {
+ resetTabIndexAndFocus();
+ removeAllTabIndex();
+ setTabIndex([host, belowSlots], 0);
+ // Structure:
+ // <div #host> (delegatesFocus=true) tabindex=0
+ // #shadowRoot
+ // <div #aboveSlots>
+ // <slot #firstSlot>
+ // (slotted) <div #slottedToFirstSlot>
+ // <slot #secondSlot>
+ // (slotted) <div #slottedToSecondSlot>
+ // <div #belowSlots> tabindex=0
+ // <div #outside>
+ // First focusable = #belowSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, belowSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #belowSlots with tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ removeAllTabIndex();
+ setTabIndex([host, outside], 0);
+ // Structure:
+ // <div #host> (delegatesFocus=true) tabindex=0
+ // #shadowRoot
+ // <div #aboveSlots>
+ // <slot #firstSlot>
+ // (slotted) <div #slottedToFirstSlot>
+ // <slot #secondSlot>
+ // (slotted) <div #slottedToSecondSlot>
+ // <div #belowSlots>
+ // <div #outside> tabindex=0
+ // No focusable element under #host in the flat tree.
+ host.focus();
+ assert_equals(shadowRoot.activeElement, null);
+ assert_equals(document.activeElement, document.body);
+}, "focus() on host with delegatesFocus & tabindex=0, #outside with tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setTabIndex([host, aboveSlots, belowSlots], 0);
+ // Structure:
+ // <div #host> (delegatesFocus=true) tabindex=0
+ // #shadowRoot
+ // <div #aboveSlots> tabindex=0
+ // <slot #firstSlot>
+ // (slotted) <div #slottedToFirstSlot>
+ // <slot #secondSlot>
+ // (slotted) <div #slottedToSecondSlot>
+ // <div #belowSlots> tabindex=0
+ // <div #outside>
+ // First focusable = #aboveSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #aboveSlots and #belowSlots with tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setTabIndex([host, aboveSlots], 0);
+ setTabIndex([belowSlots], 1);
+ // Structure:
+ // <div #host> (delegatesFocus=true) tabindex=0
+ // #shadowRoot
+ // <div #aboveSlots> tabindex=0
+ // <slot #firstSlot>
+ // (slotted) <div #slottedToFirstSlot>
+ // <slot #secondSlot>
+ // (slotted) <div #slottedToSecondSlot>
+ // <div #belowSlots> tabindex=1
+ // <div #outside>
+ // First focusable = #aboveSlots
+ host.focus();
+ assert_equals(shadowRoot.activeElement, aboveSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #aboveSlots with tabindex=0 and #belowSlots with tabindex=1");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setTabIndex([host, slottedToFirstSlot, slottedToSecondSlot, belowSlots], 0);
+ // Structure:
+ // <div #host> (delegatesFocus=true) tabindex=0
+ // #shadowRoot
+ // <div #aboveSlots>
+ // <slot #firstSlot>
+ // (slotted) <div #slottedToFirstSlot> tabindex=0
+ // <slot #secondSlot>
+ // (slotted) <div #slottedToSecondSlot> tabindex=0
+ // <div #belowSlots> tabindex=0
+ // <div #outside>
+ // First focusable = #slottedToFirstSlot
+ host.focus();
+ assert_equals(shadowRoot.activeElement, belowSlots);
+ assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots with tabindex=0");
+
+test(() => {
+ resetTabIndexAndFocus();
+ setTabIndex([aboveSlots, belowSlots], 0);
+ belowSlots.focus();
+ host.focus();
+ assert_equals(shadowRoot.activeElement, belowSlots);
+}, "focus() on host with delegatesFocus and already-focused non-first shadow descendant");
+
+function createNestedHosts(innerDelegatesFocus) {
+ // Structure:
+ // <div> outerHost
+ // <input> outerLightChild
+ // #shadowRoot outerShadow delegatesFocus=true
+ // <span> innerHost
+ // #shadowRoot inneShadow delegatesFocus=true/false
+ // <input> innerShadowChild
+ // <input> outerShadowChild
+ const outerHost = document.createElement('div');
+ const outerLightChild = document.createElement('input');
+ outerHost.appendChild(outerLightChild);
+ const innerHost = document.createElement('span');
+ const outerShadow = outerHost.attachShadow({mode: 'closed', delegatesFocus:true});
+ outerShadow.appendChild(innerHost);
+ const outerShadowChild = document.createElement('input');
+ outerShadow.appendChild(outerShadowChild);
+
+ const innerShadow = innerHost.attachShadow({mode: 'closed', delegatesFocus:innerDelegatesFocus});
+ const innerShadowChild = document.createElement('input');
+ innerShadow.appendChild(innerShadowChild);
+
+ document.body.insertBefore(outerHost, document.body.firstChild);
+ return {outerHost: outerHost,
+ outerLightChild: outerLightChild,
+ outerShadow: outerShadow,
+ outerShadowChild: outerShadowChild,
+ innerHost: innerHost,
+ innerShadow: innerShadow,
+ innerShadowChild: innerShadowChild};
+}
+
+test(() => {
+ const dom = createNestedHosts(false);
+ dom.outerHost.focus();
+ assert_equals(document.activeElement, dom.outerHost);
+ assert_equals(dom.outerShadow.activeElement, dom.outerShadowChild);
+}, 'focus() on host with delegatesFocus with another host with no delegatesFocus and a focusable child');
+
+test(() => {
+ const dom = createNestedHosts(true);
+ dom.outerHost.focus();
+ assert_equals(document.activeElement, dom.outerHost);
+ assert_equals(dom.outerShadow.activeElement, dom.innerHost);
+ assert_equals(dom.innerShadow.activeElement, dom.innerShadowChild);
+}, 'focus() on host with delegatesFocus with another host with delegatesFocus and a focusable child');
+
+test(() => {
+ // Structure:
+ // <div> host
+ // #shadowRoot root delegatesFocus=true
+ // <slot>
+ // (slotted) <div>
+ // <input>
+ // <input #firstFocusable>
+ const host = document.createElement("div");
+ const slotted = document.createElement("div");
+ slotted.appendChild(document.createElement("input"));
+ host.appendChild(slotted);
+
+ const root = host.attachShadow({mode: "open", delegatesFocus: true});
+
+ const firstFocusable = document.createElement("input");
+ root.innerHTML = "<slot>";
+ root.appendChild(firstFocusable);
+
+ document.body.appendChild(host);
+
+ host.focus();
+ assert_equals(document.activeElement, host);
+ assert_equals(root.activeElement, firstFocusable);
+}, "focus() on host with delegatesFocus and slotted focusable children");
+</script>
+
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html
new file mode 100644
index 0000000000..8caea8ccda
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-method-with-delegatesFocus.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-dom.js"></script>
+
+<template id='ShadowTemplate'>
+ <ul>
+ <li tabindex='0' id='one'>One</li>
+ <li tabindex='0' id='two'>Two</li>
+ <li id='three'>Three</li>
+ </ul>
+</template>
+<template id='NoFocusableShadowTemplate'>
+ <ul>
+ <li id='one'>One</li>
+ <li id='two'>Two</li>
+ <li id='three'>Three</li>
+ </ul>
+</template>
+<body>
+<input id='input0'>
+<x-shadow id='xshadow0'></x-shadow>
+<x-shadow id='xshadow1' tabindex='0'></x-shadow>
+<x-shadow id='xshadow2' tabindex='0' delegatesFocus></x-shadow>
+<x-shadow-nofocus id='xshadow3'></x-shadow-nofocus>
+<x-shadow-nofocus id='xshadow4' tabindex='0'></x-shadow-nofocus>
+<x-shadow-nofocus id='xshadow5' tabindex='0' delegatesFocus></x-shadow-nofocus>
+</body>
+<script>
+'use strict';
+
+function registerShadow(templateId, tagName) {
+ const template = document.getElementById(templateId);
+
+ customElements.define(tagName, class extends HTMLElement {
+ connectedCallback() {
+ const delegatesFocus = this.hasAttribute('delegatesFocus');
+ this.attachShadow({mode: 'open', delegatesFocus: delegatesFocus})
+ .appendChild(document.importNode(template.content, true));
+ }
+ });
+}
+
+registerShadow('ShadowTemplate', 'x-shadow');
+registerShadow('NoFocusableShadowTemplate', 'x-shadow-nofocus');
+
+test(() => {
+ xshadow0.focus();
+ assert_equals(document.activeElement.tagName, 'BODY');
+ assert_equals(xshadow0.shadowRoot.activeElement, null);
+}, 'xshadow0 is not focusable without tabindex.');
+
+test(() => {
+ xshadow1.focus();
+ assert_equals(document.activeElement.id, 'xshadow1');
+ assert_equals(xshadow1.shadowRoot.activeElement, null);
+}, 'xshadow1 becomes focusable with tabindex.');
+
+test(() => {
+ xshadow2.focus();
+ assert_equals(document.activeElement.id, 'xshadow2');
+ assert_equals(xshadow2.shadowRoot.activeElement.id, 'one');
+}, 'on focus(), focusable xshadow2 with delegatesFocus=true delegates focus into its inner element.');
+
+test(() => {
+ xshadow2.shadowRoot.querySelector('#two').focus();
+ assert_equals(document.activeElement.id, 'xshadow2');
+ assert_equals(xshadow2.shadowRoot.activeElement.id, 'two');
+}, 'if an element within shadow is focused, focusing on shadow host should not slide focus to its inner element.');
+
+test(() => {
+ xshadow2.focus();
+ assert_equals(document.activeElement.id, 'xshadow2');
+ assert_equals(xshadow2.shadowRoot.activeElement.id, 'two');
+}, 'xshadow2.focus() shouldn\'t move focus to #one when its inner element is already focused.');
+
+test(() => {
+ // Focus outside shadow DOMs.
+ input0.focus();
+
+ // within shadow root. This is different from mouse click behavior.
+ xshadow1.shadowRoot.querySelector('#three').focus();
+ assert_equals(document.activeElement.id, 'input0');
+ xshadow2.shadowRoot.querySelector('#three').focus();
+ assert_equals(document.activeElement.id, 'input0');
+}, 'focus() inside shadow DOM should not focus its shadow host, nor focusable siblings.');
+
+test(() => {
+ xshadow3.focus();
+ assert_equals(document.activeElement.id, 'input0');
+}, 'If any element including shadow host is not focusable, focus doesn\'t change.');
+
+test(() => {
+ xshadow4.focus();
+ assert_equals(document.activeElement.id, 'xshadow4');
+ xshadow5.focus();
+ assert_equals(document.activeElement.id, 'xshadow4');
+}, 'If no element is focusable within a delegatesFocus shadow root, the host can\'t get focus regardless of host\'s tabIndex.');
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html
new file mode 100644
index 0000000000..34f8c01294
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-matches-on-shadow-host.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content=":focus should match a shadow host which contains the focused element">
+<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus">
+<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<input id="defaultFocus" autofocus>
+<div id="log"></div>
+<div id="container"></div>
+<script>
+
+let focusedDefault = false;
+function didFocusDefault() { }
+function handleFocus() {
+ if (!focusedDefault) {
+ // Use step_timeout here to avoid nested focusing steps.
+ // For example, <input id="defaultFocus" autofocus> could run scripts
+ // while it's autofocusing which may run the tests, so that the
+ // focus() usage in the tests becomes nested focusing steps.
+ step_timeout(function() {
+ testInMode('open', false);
+ testInMode('open', true);
+ testInMode('closed', false);
+ testInMode('closed', true);
+ }, 0);
+ }
+ focusedDefault = true;
+ didFocusDefault();
+}
+defaultFocus.addEventListener('focus', handleFocus);
+
+function prepare(test)
+{
+ test.add_cleanup(() => {
+ defaultFocus.focus();
+ container.textContent = '';
+ });
+ return new Promise((resolve) => {
+ if (focusedDefault)
+ resolve();
+ else
+ didFocusDefault = resolve;
+ });
+}
+
+function testInMode(mode, delegatesFocus) {
+ const modeString = `{mode:${mode}, delegatesFocus:${delegatesFocus}}`;
+ promise_test(async function () {
+ await prepare(this);
+ const host = document.createElement('div');
+ container.appendChild(host);
+ const shadowRoot = host.attachShadow({mode, delegatesFocus});
+ shadowRoot.innerHTML = '<input>';
+ assert_equals(document.activeElement, defaultFocus);
+ assert_equals(shadowRoot.activeElement, null);
+ assert_false(host.matches(':focus'));
+ }, `:focus must not match a shadow host with ${modeString} shadow root that does not contain the focused element`);
+
+ promise_test(async function () {
+ await prepare(this);
+ const host = document.createElement('div');
+ document.body.appendChild(host);
+ const shadowRoot = host.attachShadow({mode, delegatesFocus});
+ shadowRoot.innerHTML = '<input>';
+ shadowRoot.firstChild.focus();
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, shadowRoot.firstChild);
+ assert_true(host.matches(':focus'));
+ }, `:focus must match a shadow host with ${modeString} shadow root that contains the focused element`);
+
+ promise_test(async function () {
+ await prepare(this);
+ const host = document.createElement('div');
+ container.appendChild(host);
+ const shadowRoot = host.attachShadow({mode, delegatesFocus});
+ shadowRoot.innerHTML = '<slot>';
+ host.innerHTML = '<input>';
+ host.firstChild.focus();
+ assert_equals(document.activeElement, host.firstChild);
+ assert_equals(shadowRoot.activeElement, null);
+ assert_false(host.matches(':focus'));
+ }, `:focus must not match a shadow host with ${modeString} shadow root contains the focused element assigned to a slot`);
+
+ promise_test(async function() {
+ await prepare(this);
+ const host1 = document.body.appendChild(document.createElement('div'));
+ const shadowRoot1 = host1.attachShadow({mode, delegatesFocus});
+ const host2 = shadowRoot1.appendChild(document.createElement('div'));
+ const shadowRoot2 = host2.attachShadow({mode, delegatesFocus});
+ shadowRoot2.innerHTML = '<input>';
+ shadowRoot2.firstChild.focus();
+ assert_equals(document.activeElement, host1);
+ assert_equals(shadowRoot1.activeElement, host2);
+ assert_equals(shadowRoot2.activeElement, shadowRoot2.firstChild);
+ assert_true(host1.matches(':focus'));
+ assert_true(host2.matches(':focus'));
+ }, `:focus must match all shadow hosts which are ancestors of a foccused element; ${modeString}`);
+
+ promise_test(async function() {
+ await prepare(this);
+ const host = document.body.appendChild(document.createElement('div'));
+ const shadowRoot = host.attachShadow({mode, delegatesFocus});
+ shadowRoot.innerHTML = '<input>';
+ const input = shadowRoot.firstChild;
+ const outer = document.body.appendChild(document.createElement('div'));
+
+ assert_false(host.matches(':focus'));
+ input.focus();
+ assert_true(host.matches(':focus'));
+ outer.appendChild(input);
+ assert_false(host.matches(':focus'));
+ }, `:focus behavior on tree structure changes; ${modeString}`);
+}
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html
new file mode 100644
index 0000000000..ba900d6a6d
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content=":focus should match a shadow host which contains the focused element">
+<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus">
+<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432">
+<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht">
+</head>
+<body>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="host"></div>
+<style>
+#host { background: red; width: 100px; height: 100px; }
+#host:focus { background: green; }
+</style>
+<script>
+
+const shadowRoot = host.attachShadow({mode: 'closed'});
+shadowRoot.innerHTML = '<div tabindex="0" style="outline: none;"></div>';
+shadowRoot.firstChild.focus();
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html
new file mode 100644
index 0000000000..f119008ee3
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-2.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content=":focus should match a shadow host which contains the focused element">
+<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus">
+<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432">
+<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht">
+<link rel="stylesheet" type="text/css" href="/fonts/ahem.css">
+</head>
+<body>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="host"><span>FAIL</span></div>
+<style>
+#host { background: green; width: 100px; height: 100px; }
+#host span { background: red; font: 10px/1 Ahem; }
+#host:focus span { background: green; color: green; }
+</style>
+<script>
+
+const shadowRoot = host.attachShadow({mode: 'closed'});
+shadowRoot.innerHTML = '<div tabindex="0" style="outline: none;"><slot></slot></div>';
+shadowRoot.firstChild.focus();
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html
new file mode 100644
index 0000000000..e72d1b39d4
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-pseudo-on-shadow-host-3.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content=":focus should not match a shadow host if the focused element is a slotted content">
+<link rel="help" href="https://html.spec.whatwg.org/#element-has-the-focus">
+<link rel="help=" href="https://bugs.webkit.org/show_bug.cgi?id=202432">
+<link rel="match" href="/css/reference/ref-filled-green-100px-square.xht">
+</head>
+<body>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div id="host"><div id="target" tabindex="0"></div></div>
+<style>
+#host { background: green; width: 100px; height: 100px; }
+#host:focus #target { background: red; width: 100px; height: 100px; }
+#target { outline: none; }
+</style>
+<script>
+
+const shadowRoot = host.attachShadow({mode: 'closed'});
+shadowRoot.innerHTML = '<slot></slot>';
+target.focus();
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html
new file mode 100644
index 0000000000..bbc1346a46
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-selector-delegatesFocus.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>CSS Test (Selectors): :focus behavior with shadow hosts & delegatesFocus </title>
+ <link rel="author" title="Rakina Zata Amni" href="rakina@chromium.org" />
+ <link rel="help" href="https://html.spec.whatwg.org/multipage/semantics-other.html#selector-focus" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/shadow-utils.js"></script>
+</head>
+
+<body>
+<input>
+
+<script>
+function createFocusableDiv() {
+ const div = document.createElement("div");
+ div.innerText = "foo";
+ div.tabIndex = 0;
+ return div;
+}
+
+function createShadowHost(delegatesFocus, container) {
+ const host = document.createElement("div");
+ host.attachShadow({ mode: "open", delegatesFocus: delegatesFocus });
+ container.appendChild(host);
+ return host;
+}
+
+const delegatesFocusValues = [true, false];
+
+for (const delegatesFocus of delegatesFocusValues) {
+ test(() => {
+ resetFocus();
+ const host = createShadowHost(delegatesFocus, document.body);
+ const shadowChild = createFocusableDiv();
+ host.shadowRoot.appendChild(shadowChild);
+
+ shadowChild.focus();
+ assert_true(shadowChild.matches(":focus"), "element in shadow tree matches :focus");
+ assert_true(host.matches(":focus"), "host matches :focus");
+ }, `:focus applies to host with delegatesFocus=${delegatesFocus} when the shadow root's descendant has focus`);
+
+ test(() => {
+ resetFocus();
+ const host = createShadowHost(delegatesFocus, document.body);
+ const slotted = createFocusableDiv();
+ host.shadowRoot.appendChild(document.createElement("slot"));
+ host.appendChild(slotted);
+
+ slotted.focus();
+ assert_true(slotted.matches(":focus"), "slotted element matches :focus");
+ assert_false(host.matches(":focus"), "host matches :focus");
+ }, `:focus does not apply to host with delegatesFocus=${delegatesFocus} when slotted element has focus`);
+
+ for (const nestedDelegatesFocus of delegatesFocusValues) {
+ test(() => {
+ resetFocus();
+ const host = createShadowHost(delegatesFocus, document.body);
+ const nestedHost = createShadowHost(nestedDelegatesFocus, host.shadowRoot);
+ const nestedShadowChild = createFocusableDiv();
+ nestedHost.shadowRoot.appendChild(nestedShadowChild);
+ nestedShadowChild.focus();
+ assert_true(nestedShadowChild.matches(":focus"), "element in nested shadow tree matches :focus");
+ assert_true(nestedHost.matches(":focus"), "host of nested shadow tree matches focus");
+ assert_true(host.matches(":focus"), "topmost host matches focus");
+ }, `:focus applies to host with delegatesFocus=${delegatesFocus} when an element in a nested shadow tree with delegatesFocus=${nestedDelegatesFocus} is focused`);
+
+ test(() => {
+ resetFocus();
+ const host = createShadowHost(delegatesFocus, document.body);
+ const nestedHost = createShadowHost(nestedDelegatesFocus, host.shadowRoot);
+ const nestedShadowChild = createFocusableDiv();
+ nestedHost.shadowRoot.appendChild(nestedShadowChild);
+ // All nested shadow hosts should has :focus applied
+ nestedShadowChild.focus();
+
+ const elementOutsideOfShadowDOM = document.querySelector("input");
+ // Move the focus to an element which is outside of the nested
+ // shadow DOM trees
+ elementOutsideOfShadowDOM.focus();
+
+ assert_false(nestedShadowChild.matches(":focus"), "element in nested shadow tree doesn't matche :focus");
+ assert_false(nestedHost.matches(":focus"), "host of nested shadow tree doesn't match focus");
+ assert_false(host.matches(":focus"), "topmost host matches focus");
+ assert_true(elementOutsideOfShadowDOM.matches(":focus"), "The element outside of shadow dom matches :focus");
+ }, `:focus should be removed from hosts with delegatesFocus=${delegatesFocus} when none of the elements in a nested shadow tree with delegatesFocus=${nestedDelegatesFocus} is focused`);
+ }
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html b/testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html
new file mode 100644
index 0000000000..40f1b01f66
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-shadowhost-display-none.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#host:focus { display: none; }
+</style>
+<div id='sandbox'></div>
+<script>
+'use strict';
+
+// Check if shadow host with display:none CSS rule for :focus works. crbug.com/482830
+
+var host;
+var root;
+var input;
+
+function setupShadowDOM(delegatesFocus) {
+ sandbox.innerHTML = '';
+ host = sandbox.appendChild(document.createElement('div'));
+ host.id = 'host';
+
+ root = host.attachShadow({mode: 'open', delegatesFocus: delegatesFocus});
+ input = document.createElement('input');
+ root.appendChild(input);
+
+ host.tabIndex = 0;
+}
+
+promise_test(() => {
+ setupShadowDOM(false);
+ return new Promise(
+ function(resolve) {
+ host.focus();
+ assert_equals(window.getComputedStyle(host).display, 'none');
+ assert_equals(document.activeElement, host);
+ assert_equals(root.activeElement, null);
+
+ function onBlur() {
+ assert_equals(window.getComputedStyle(host).display, 'block');
+ assert_equals(document.activeElement, document.body);
+ assert_equals(root.activeElement, null);
+ host.removeEventListener('blur', onBlur);
+ resolve();
+ }
+ host.addEventListener('blur', onBlur);
+ });
+}, 'when shadow host itself is focused, it should match display:none, lose focus then becomes display:block again.');
+
+promise_test(() => {
+ setupShadowDOM(true);
+ return new Promise(
+ function(resolve) {
+ input.focus();
+ assert_equals(window.getComputedStyle(host).display, 'none');
+ assert_equals(document.activeElement, host);
+ assert_equals(root.activeElement, input);
+
+ function onBlur() {
+ assert_equals(window.getComputedStyle(host).display, 'block');
+ assert_equals(document.activeElement, document.body);
+ assert_equals(root.activeElement, null);
+ input.removeEventListener('blur', onBlur);
+ resolve();
+ }
+ input.addEventListener('blur', onBlur);
+ });
+}, 'when shadow host with delegatesFocus=true has focused element inside the shadow, it should also match display:none, then lose focus and become display:block again.');
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html b/testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html
new file mode 100644
index 0000000000..0dffc0157f
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tab-on-shadow-host.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: Use tab to navigate the focus to an element inside shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+ <div id="host"></div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const input = document.createElement("input");
+shadowRoot.appendChild(input);
+
+promise_test(async function() {
+ assert_equals(document.activeElement, document.body);
+ // Press <tab>
+ await navigateFocusForward();
+ assert_equals(document.activeElement, host);
+ assert_equals(shadowRoot.activeElement, input);
+ assert_true(host.matches(':focus'));
+ assert_true(input.matches(':focus'));
+ assert_true(input.matches(':focus-visible'));
+}, ":focus should be applied to the host and :focus-visible should be applied to the child node when the focus is moved by <tab>");
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html
new file mode 100644
index 0000000000..356b0bb329
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and all tabindex=-1 in shadow tree</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+// #shadowRoot (delegatesFocus)
+// <div #aboveSlot tabindex=-1>
+// <slot #slotAbove tabindex=-1>
+// (slotted) <div #slottedAbove tabindex=-1>
+// <slot #slotBelow tabindex=-1>
+// (slotted) <div #slottedBelow tabindex=-1>
+// <div #belowSlot tabindex=-1>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true);
+ setTabIndex(elementsInFlatTreeOrder, -1);
+ setTabIndex([aboveHost, host, belowHost], 0);
+ resetFocus();
+ // Focus should only land on #aboveHost and #belowHost (all others are non-sequentially focusable).
+ return assertFocusOrder([aboveHost, belowHost]);
+}, "Order when all tabindex=-1 is and delegatesFocus = true");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html
new file mode 100644
index 0000000000..ab25ea829b
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-negative.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and negative tabindex in shadow scope</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+// #shadowRoot
+// <div #aboveSlot tabindex=-1>
+// <slot #slotAbove tabindex=-1>
+// (slotted) <div #slottedAbove tabindex=-1>
+// <slot #slotBelow tabindex=-1>
+// (slotted) <div #slottedBelow tabindex=-1>
+// <div #belowSlot tabindex=-1>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, -1);
+ setTabIndex([aboveHost, host, belowHost], 0);
+ resetFocus();
+ return assertFocusOrder([aboveHost, host, belowHost]);
+}, "Order when all elements in shadow tree has negative tabindex");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html
new file mode 100644
index 0000000000..3c9e70867c
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-slot-one.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and all tabindex=0 except for one of the slot</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=1>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ slotBelow.tabIndex = 1;
+ resetFocus();
+ // Focus should move first according to flat tree order to #aboveHost and #host, then into #host's focus scope.
+ // It will then move to #slottedBelow because #slotBelow has tabindex=1 (though we actually won't focus on the slot),
+ // and then back to #host's focus scope again, finally getting out to the document focus scope.
+ return assertFocusOrder([aboveHost, host, slottedBelow, aboveSlot, slottedAbove, belowSlot, belowHost]);
+}, "Order when all tabindex=0, except for one slot that has tabindex=1");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html
new file mode 100644
index 0000000000..67899cff4a
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and tabindex in shadow tree varies</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+// #shadowRoot (delegatesFocus)
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=1>
+// (slotted) <div #slottedAbove tabindex=3>
+// <slot #slotBelow tabindex=2>
+// (slotted) <div #slottedBelow tabindex=1>
+// <div #belowSlot tabindex=-1>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true);
+ setTabIndex(elementsInFlatTreeOrder, -1);
+ setTabIndex([aboveHost, host, aboveSlot, belowHost], 0);
+ setTabIndex([slotAbove, slottedBelow], 1);
+ setTabIndex([slotBelow], 2);
+ setTabIndex([slottedAbove], 3);
+ resetFocus();
+ return assertFocusOrder([aboveHost, slottedAbove, slottedBelow, aboveSlot, belowHost]);
+}, "Order when tabindex varies and delegatesFocus = true");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html
new file mode 100644
index 0000000000..1755aaf442
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-2.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom with varying tabindex values</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+promise_test(async () => {
+ function createButtonInShadowDOM(host) {
+ const root = host.attachShadow({mode: "open"});
+ root.innerHTML = "<input>";
+ document.body.appendChild(host);
+ return root;
+ }
+ const host1 = document.createElement("div");
+ const root1 = createButtonInShadowDOM(host1);
+
+ const forwarder = document.createElement("div");
+ forwarder.setAttribute("tabindex", 0);
+ document.body.appendChild(forwarder);
+
+ const host2 = document.createElement("div");
+ host2.setAttribute("tabindex", -1);
+ const root2 = createButtonInShadowDOM(host2);
+
+ const host3 = document.createElement("div");
+ const root3 = createButtonInShadowDOM(host3);
+
+ root1.querySelector("input").focus();
+
+ let forwarderFocused = false;
+ forwarder.addEventListener("focus", () => {
+ forwarderFocused = true;
+ root2.querySelector("input").focus();
+ });
+
+ // Structure:
+ // <div #host1></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ // <div #forwarder tabindex=0></div>
+ // <div #host2 tabindex=-1></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ // <div #host3></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ assert_equals(document.activeElement, host1);
+ assert_equals(root1.activeElement, root1.querySelector("input"));
+
+ await navigateFocusForward();
+ assert_true(forwarderFocused);
+ assert_equals(document.activeElement, host2);
+ assert_equals(root2.activeElement, root2.querySelector("input"));
+
+ // In buggy Firefox build, the following focus navigation will
+ // move the focus back to #host1's button.
+ await navigateFocusForward();
+ assert_equals(document.activeElement, host3);
+ assert_equals(root3.activeElement, root3.querySelector("input"));
+}, "Order with different tabindex on host")
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html
new file mode 100644
index 0000000000..e0570395ec
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex-3.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with nested shadow dom with varying tabindex values</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+promise_test(async () => {
+ function createButtonInShadowDOM(host, parent) {
+ const root = host.attachShadow({mode: "open"});
+ root.innerHTML = "<input>";
+ parent.appendChild(host);
+ return root;
+ }
+
+ const host1 = document.createElement("div");
+ const root1 = createButtonInShadowDOM(host1, document.body);
+
+ const forwarder = document.createElement("div");
+ forwarder.setAttribute("tabindex", 0);
+ document.body.appendChild(forwarder);
+
+ const host2 = document.createElement("div");
+ host2.setAttribute("tabindex", -1);
+ const root2 = host2.attachShadow({mode: "open"});
+ document.body.appendChild(host2);
+
+ const host2_1 = document.createElement("div");
+ const root2_1 = createButtonInShadowDOM(host2_1, root2);
+
+ const host2_2 = document.createElement("div");
+ host2_2.setAttribute("tabindex", -1);
+ const root2_2 = createButtonInShadowDOM(host2_2, root2);
+
+ const host2_3 = document.createElement("div");
+ const root2_3 = createButtonInShadowDOM(host2_3, root2);
+
+ root1.querySelector("input").focus();
+
+ let forwarderFocused = false;
+ forwarder.addEventListener("focus", () => {
+ forwarderFocused = true;
+ root2_2.querySelector("input").focus();
+ });
+
+ // Structure:
+ // <div #host1></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ // <div #forwarder tabindex=0></div>
+ // <div #host2 tabindex=-1></div>
+ // #ShadowRoot
+ // <div #host2_1></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ // <div #host2_2 tabindex=-1></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ // <div #host2_3></div>
+ // #ShadowRoot
+ // <button>Button</button>
+ assert_equals(document.activeElement, host1);
+ assert_equals(root1.activeElement, root1.querySelector("input"));
+
+ await navigateFocusForward();
+ assert_true(forwarderFocused);
+ assert_equals(document.activeElement, host2);
+ assert_equals(root2_2.activeElement, root2_2.querySelector("input"));
+
+ // In buggy Firefox build, the following focus navigation will
+ // move the focus back to #host1_1's button.
+ await navigateFocusForward();
+ assert_equals(document.activeElement, host2);
+ assert_equals(root2_3.activeElement, root2_3.querySelector("input"));
+}, "Order with different tabindex on host")
+</script>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html
new file mode 100644
index 0000000000..875e5b6814
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-tabindex.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom with varying tabindex values</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=3>
+// <div #host tabindex=3>
+// #shadowRoot
+// <div #aboveSlot tabindex=2>
+// <slot #slotAbove tabindex=1>
+// (slotted) <div #slottedAbove tabindex=4>
+// <slot #slotBelow tabindex=1>
+// (slotted) <div #slottedBelow tabindex=4>
+// <div #belowSlot tabindex=2>
+// <div #belowHost tabindex=3>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex([slotAbove, slotBelow], 1);
+ setTabIndex([aboveSlot, belowSlot], 2);
+ setTabIndex([aboveHost, host, belowHost], 3);
+ setTabIndex([slottedAbove, slottedBelow], 4);
+ resetFocus();
+ return assertFocusOrder([aboveHost, host, slottedAbove, slottedBelow, aboveSlot, belowSlot, belowHost]);
+}, "Order with various tabindex values");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html
new file mode 100644
index 0000000000..5e6ab3a90f
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and all tabindex=0</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+// #shadowRoot (delegatesFocus)
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ resetFocus();
+ // Focus should move in flat tree order since every one of them has tabindex ==0,
+ // but doesn't include slots and #host.
+ return assertFocusOrder([aboveHost, aboveSlot, slottedAbove, slottedBelow, belowSlot, belowHost]);
+}, "Order when all tabindex=0 is and delegatesFocus = true");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html
new file mode 100644
index 0000000000..b491c7d237
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-negative.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and negative host tabindex</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=-1>
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ host.tabIndex = -1;
+ resetFocus();
+ // Focus willl only move within the focus navigation scope of the document (not going to get into #host).
+ return assertFocusOrder([aboveHost, belowHost]);
+}, "Order when all tabindex=0 except for host, which has tabindex=-1");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html
new file mode 100644
index 0000000000..9a12d8b8f4
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set-scrollable.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and scrollable/non-focusable host</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host style="overflow: scroll">
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ removeTabIndex([host]);
+ host.style = "overflow: scroll";
+ resetFocus();
+ // Focus should move in flat tree order since every one of them has tabindex ==0,
+ // but doesn't include #slot since it's not rendered and #host since its tabindex is not set
+ // (but #host is considered as 0 in focus scope navigation, keeping the flat tree order for the shadow root's descendants).
+ return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow && el !== host)));
+}, "Order when all tabindex=0 except scrollable host (tabindex not set)");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html
new file mode 100644
index 0000000000..f257261477
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-not-set.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and non-focusable host</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host>
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ removeTabIndex([host]);
+ resetFocus();
+ // Focus should move in flat tree order since every one of them has tabindex ==0,
+ // but doesn't include #slot since it's not rendered and #host since its tabindex is not set
+ // (but #host is considered as 0 in focus scope navigation, keeping the flat tree order for the shadow root's descendants).
+ return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow && el !== host)));
+}, "Order when all tabindex=0 except host (tabindex not set)");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html
new file mode 100644
index 0000000000..1aa5292997
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-one.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and all tabindex=0 except host (tabindex=1)</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=1>
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ host.tabIndex = 1;
+ resetFocus();
+ // Focus should move first to #host because it has tabindex=1, and then to the contents of its scope
+ // (e.g. the contents of its shadow tree) in flat tree order, and then outside to the document scope
+ // again (aboveHost & belowHost).
+ return assertFocusOrder([host, aboveSlot, slottedAbove, slottedBelow, belowSlot, aboveHost, belowHost]);
+}, "Order when all tabindex=0 except for host, which has tabindex=1");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html
new file mode 100644
index 0000000000..fa8090ca97
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-host-scrollable.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and host is scrollable</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0 style="overflow: scroll">
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ host.style = "overflow: scroll";
+ resetFocus();
+ // Focus should move in flat tree order since every one of them has tabindex==0,
+ // but doesn't include slots.
+ return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow)));
+}, "Order when all tabindex=0 and host is scrollable");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html
new file mode 100644
index 0000000000..d8b12ed8ac
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/focus-tabindex-order-shadow-zero.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom and all tabindex=0</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+// #shadowRoot
+// <div #aboveSlot tabindex=0>
+// <slot #slotAbove tabindex=0>
+// (slotted) <div #slottedAbove tabindex=0>
+// <slot #slotBelow tabindex=0>
+// (slotted) <div #slottedBelow tabindex=0>
+// <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+ let elementsInFlatTreeOrder;
+ let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] =
+ elementsInFlatTreeOrder = prepareDOM(document.body, false);
+ setTabIndex(elementsInFlatTreeOrder, 0);
+ resetFocus();
+ // Focus should move in flat tree order since every one of them has tabindex==0,
+ // but doesn't include slots.
+ return assertFocusOrder(elementsInFlatTreeOrder.filter(el => (el !== slotAbove && el !== slotBelow)));
+}, "Order when all tabindex=0 is and delegatesFocus = false");
+</script>
+</body>
diff --git a/testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js b/testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js
new file mode 100644
index 0000000000..8033ce0169
--- /dev/null
+++ b/testing/web-platform/tests/shadow-dom/focus/resources/shadow-utils.js
@@ -0,0 +1,80 @@
+// Structure:
+// <div #aboveHost>
+// <div #host>
+// #shadowRoot
+// <div #aboveSlot>
+// <slot #slotAbove>
+// (slotted) <div #slottedAbove>
+// <slot #slotBelow>
+// (slotted) <div #slottedBelow>
+// <div #belowSlot>
+// <div #belowHost>
+function prepareDOM(container, delegatesFocus) {
+
+ const aboveHost = document.createElement("div");
+ aboveHost.innerText = "aboveHost";
+ const host = document.createElement("div");
+ host.id = "host";
+ const slottedBelow = document.createElement("div");
+ slottedBelow.innerText = "slotted below";
+ slottedBelow.slot = "below";
+ const slottedAbove = document.createElement("div");
+ slottedAbove.innerText = "slotted above";
+ slottedAbove.slot = "above";
+
+ const belowHost = document.createElement("div");
+ belowHost.innerText = "belowHost";
+ container.appendChild(aboveHost);
+ container.appendChild(host);
+ container.appendChild(belowHost);
+ host.appendChild(slottedBelow);
+ host.appendChild(slottedAbove);
+ const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: delegatesFocus});
+ const aboveSlot = document.createElement("div");
+ aboveSlot.innerText = "aboveSlot";
+
+ const slotAbove = document.createElement("slot");
+ slotAbove.name = "above";
+ const slotBelow = document.createElement("slot");
+ slotBelow.name = "below";
+
+ const belowSlot = document.createElement("div");
+ belowSlot.innerText = "belowSlot";
+ shadowRoot.appendChild(aboveSlot);
+ shadowRoot.appendChild(slotAbove);
+ shadowRoot.appendChild(slotBelow);
+ shadowRoot.appendChild(belowSlot);
+
+ return [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost];
+}
+
+function setTabIndex(elements, value) {
+ for (const el of elements) {
+ el.tabIndex = value;
+ }
+}
+
+function removeTabIndex(elements) {
+ for (const el of elements) {
+ el.removeAttribute("tabindex");
+ }
+}
+
+function resetFocus(root = document) {
+ if (root.activeElement)
+ root.activeElement.blur();
+}
+
+function navigateFocusForward() {
+ // TAB = '\ue004'
+ return test_driver.send_keys(document.body, "\ue004");
+}
+
+async function assertFocusOrder(expectedOrder) {
+ const shadowRoot = document.getElementById("host").shadowRoot;
+ for (const el of expectedOrder) {
+ await navigateFocusForward();
+ const focused = shadowRoot.activeElement ? shadowRoot.activeElement : document.activeElement;
+ assert_equals(focused, el);
+ }
+}