diff options
Diffstat (limited to 'testing/web-platform/tests/shadow-dom/focus')
35 files changed, 2395 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); + } +} diff --git a/testing/web-platform/tests/shadow-dom/focus/text-selection-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus/text-selection-with-delegatesFocus.html new file mode 100644 index 0000000000..7c92d35394 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus/text-selection-with-delegatesFocus.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<body> + <x-shadow id="withoutFocus"></x-shadow> + <x-shadow id="withFocus"></x-shadow> +</body> + +<script> +'use strict'; + +/** + * build shadow-root with delegates focus, a focusable element, and an (ideally) selectable text + */ +function buildShadowRootWithSelectableText( element, shouldDelegateFocus ) { + element.attachShadow({ mode: 'open', delegatesFocus: shouldDelegateFocus }); + const span = document.createElement('span'); + span.textContent = 'Example Text to Select '; + const button = document.createElement('button'); + button.textContent = 'Button'; + element.shadowRoot.append(span, button); +} + +/** + * command to select text in shadow-root + */ +function selectText(element, start, end) { + getSelection().empty(); + const actions = new test_driver.Actions(); + actions.pointerMove(start, 0, {origin: element}); + actions.pointerDown(); + actions.pointerMove(end, 0, {origin: element}); + actions.pointerUp(); + return actions.send(); +} + +promise_test(async () => { + const xShadow = document.getElementById('withoutFocus'); + buildShadowRootWithSelectableText(xShadow, false); + + // starting selection from the center of the element, and going right. + // The important part here is that we start the selection in the center + // (where mouse-down events may be delegated) + await selectText(xShadow, 0, 50) + const s = getSelection(); + + // because browsers may handle rendering differently, we can get different amounts of + // text selected, even when using the same start-end values. We opt in this case to + // verify just if any text is selected, since all we care about is if some text is + // selected. + assert_greater_than(s.toString().length, 0); +}, 'shadow root has selectable text when focus is not delegated'); + +promise_test(async () => { + const xShadow = document.getElementById('withFocus'); + buildShadowRootWithSelectableText(xShadow, true); + + await selectText(xShadow, 0, 50) + const s = getSelection(); + + assert_greater_than(s.toString().length, 0); +}, 'shadow root has selectable text when focus is delegated'); + +</script> |