diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/shadow-dom/focus-navigation | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/shadow-dom/focus-navigation')
22 files changed, 1878 insertions, 0 deletions
diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html b/testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html new file mode 100644 index 0000000000..dde18128ad --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html @@ -0,0 +1,126 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<!-- Adapted from http://jsbin.com/dexinu/6/edit for layout test --> + +<template id='XMenuTemplate'> + <style> + :host { + display: inline-block; + position: relative; + background-color: #aaa; + } + :host(:focus) { + background-color: #ccc; + } + li { + display: inline-block; + position: relative; + background-color: #eee; + } + li:focus { + background-color: #fff; + } + </style> + <li tabindex='0'>Item One</li> + <li tabindex='0'>Item Two</li> + <li tabindex='0'>Item Three</li> +</template> + +<section> + <x-menu id='XMenu1' tabindex='0'></x-menu> +</section> +<section> + <x-menu id='XMenu2' tabindex='0' delegatesFocus></x-menu> + <x-menu id='XMenu3' tabindex='0' delegatesFocus></x-menu> +</section> +<section> + <x-menu id='XMenu4' tabindex='0' delegatesFocus></x-menu> +</section> + +<script> +'use strict'; + +const template = document.querySelector('#XMenuTemplate'); + +customElements.define('x-menu', class extends HTMLElement { + connectedCallback() { + const delegatesFocus = this.hasAttribute('delegatesFocus'); + this.attachShadow({mode: 'open', delegatesFocus: delegatesFocus}) + .appendChild(document.importNode(template.content, true)); + } +}); + +promise_test(async () => { + let xmenu1 = document.getElementById('XMenu1'); + + xmenu1.focus(); + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu1'); + assert_background_color('XMenu1', 'rgb(204, 204, 204)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu2'); + await assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + await assert_background_color('XMenu2', 'rgb(204, 204, 204)'); + await assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + await assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu3'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(204, 204, 204)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusForward(); + await navigateFocusForward(); + await navigateFocusForward(); + assert_equals(document.activeElement.id, 'XMenu4'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(204, 204, 204)'); + + await navigateFocusBackward(); + await navigateFocusBackward(); + await navigateFocusBackward(); + assert_equals(document.activeElement.id, 'XMenu3'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(204, 204, 204)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusBackward(); + await navigateFocusBackward(); + await navigateFocusBackward(); + assert_equals(document.activeElement.id, 'XMenu2'); + assert_background_color('XMenu1', 'rgb(170, 170, 170)'); + assert_background_color('XMenu2', 'rgb(204, 204, 204)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); + + await navigateFocusBackward(); + await navigateFocusBackward(); + await navigateFocusBackward(); + assert_equals(document.activeElement.id, 'XMenu1'); + assert_background_color('XMenu1', 'rgb(204, 204, 204)'); + assert_background_color('XMenu2', 'rgb(170, 170, 170)'); + assert_background_color('XMenu3', 'rgb(170, 170, 170)'); + assert_background_color('XMenu4', 'rgb(170, 170, 170)'); +}, 'crbug/474687 :focus style should properly be applied to shadow hosts.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html new file mode 100644 index 0000000000..e8e0293e77 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html @@ -0,0 +1,74 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<p>Tests for moving focus by pressing tab key across shadow boundaries.<br> +To manually test, press tab key six times then shift+tab seven times.<br> +It should traverse focusable elements in the increasing numerical order and then in the reverse order.</p> + +<div id='host'> + <div slot='slot5' id='i2' tabindex=4>2. Assigned to slot5 whose tabindex is 2.</div> + <template data-mode='open'> + <slot name='slot1'> + x. The default tabindex for a slot node is set to 0. + <div id='i5' tabindex=0>5. The parent slot node's tabindex is 0. Second.</div> + <div id='i4' tabindex=2>4. The parent slot node's tabindex is 0. First.</div> + </slot> + + <slot name='slot2' id='x1' tabindex=3> + x. The tabindex is 3. The slot node should be ignored. + <div id='i3' tabindex=10>3. The parent slot node's tabindex is 3. The slot node's tabindex matters. This element's tabindex comes after.</div> + </slot> + + <slot name='slot3' id='x2' tabindex=0> + x. The tabindex is 0. The slot node should be ignored. If there is another slot node in same tabindex, the younger child comes first. + <div id='i6' tabindex=1>6. The parent slot node's tabindex is 0. First.</div> + <div id='i7' tabindex=1>7. The parent slot node's tabindex is 0. Second.</div> + </slot> + + <slot name='slot4' id='x3' tabindex=1> + x. The tabindex is 1. The slot node should be ignored. + <div id='i1' tabindex=5>1. The slot node tabindex is 1.</div> + </slot> + + <slot name='slot5' id='x5' tabindex=2> + x. The tabindex is 2. The slot node should be ignored. The host child is assigned to this slot node. + <div id='-' tabindex=1>-. The host child is assigned to the parent slot node. This text shouldn't apeare.</div> + </slot> + + <slot name='slot6' id='x6' tabindex=5> + x. The tabindex is 5. The slot node should be ignored. + <div id='x6' tabindex=-1>x. tabindex is -1. Should be skipped.</div> + </slot> + + <slot name='slot7' id='x7' tabindex=-1> + x. tabindex is -1. Should be skipped. + <div id='x8' tabindex=1>x. The parent slot node is skipped.</div> + </slot> + </template> +</div> +<script> + +promise_test(async () => { + convertTemplatesToShadowRootsWithin(host); + + let elements = [ + 'host/i1', + 'i2', + 'host/i3', + 'host/i4', + 'host/i5', + 'host/i6', + 'host/i7' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Default tabindex for a slot node should be 0.'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html new file mode 100644 index 0000000000..8b29558b50 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html @@ -0,0 +1,71 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id='log'></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j1 -> j2 -> [s1]]<br> + <b>slot #s1: [k1 -> [x-bar] -> k0 -> [s2] -> [s3]]</b><br> + x-bar's shadow tree: [m1 -> m2]<br> + slot #s2: [i1 -> i2]<br> + <b>slot #s3: [l1]<b><br><br> + <b>v1 ideal nav forward: [i0 -> j1 -> j2 -> k1 -> x-bar -> m1 -> m2 -> k0 -> i1 -> i2 -> l1]</b><br> +</p> + +<input id='i0' tabindex=0 value='i0'> +<div id='x-foo'> + <input id='i2' slot='s2' tabindex=2 value='i2'> + <input id='i1' slot='s2' tabindex=1 value='i1'> + <template data-mode='open'> + <input id='j1' tabindex=1 value='j1'> + <slot id='s1' name='s1'> <!-- This slot does not have any assigned elements --> + <input id='k0' tabindex=0 value='k0'> + <input id='k1' tabindex=1 value='k1'> + <slot id='s2' name='s2'> + <input id='should-be-ignored'> + </slot> + <slot id='s3' name='s3'> <!-- This slot does not have any assigned elements --> + <input id='l1' value='l1'> + </slot> + <div id='x-bar' tabindex=2> + <template data-mode='open'> + <input id='m2' value='m2' tabindex=2> + <input id='m1' value='m1' tabindex=1> + </template> + </div> + </slot> + <input id='j2' tabindex=2 value='j2'> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + + let elements = [ + 'i0', + 'x-foo/j1', + 'x-foo/j2', + 'x-foo/k1', + 'x-foo/x-bar', + 'x-foo/x-bar/m1', + 'x-foo/x-bar/m2', + 'x-foo/k0', + 'i1', + 'i2', + 'x-foo/l1' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus should jump to fallback elements when a slot does not have any assigned nodes.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html new file mode 100644 index 0000000000..59f0e4f0cb --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html @@ -0,0 +1,63 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id='i0'> +<div id='outer'> + <template data-mode='open'> + <input id='outer-before'> + <slot></slot> + <input id='outer-after'> + </template> + <div id='dummy1'></div> + <div id='nested1'> + <template data-mode='open'> + <input id='inner-before'> + <button id='button'><slot></slot></button> + <input id='inner-after'> + </template> + <div id='dummy2'></div> + <div id='nested2'> + <template data-mode='open'> + <input id='innermost-before'> + <slot></slot> + <input id='innermost-after'> + </template> + <input id='innermost1'> + <input id='innermost2'> + </div> + <span>button</span> + </div> +</div> +<input id='i1'> + +<script> +promise_test(async () => { + var outer = document.querySelector('#outer'); + convertTemplatesToShadowRootsWithin(outer); + + var elements = [ + 'i0', + 'outer/outer-before', + 'nested1/inner-before', + 'nested1/button', + 'nested2/innermost-before', + 'innermost1', + 'innermost2', + 'nested2/innermost-after', + 'nested1/inner-after', + 'outer/outer-after', + 'i1' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus controller should treat each slot as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html new file mode 100644 index 0000000000..94d8ce8e1f --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html @@ -0,0 +1,51 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<!-- +This test case is based on the crbug.com/618587 reproduction case: +http://jsbin.com/bonudiwagu/1/edit?html,output +--> +<input id='i0'> +<div id='x-foo'> + <template data-mode='open' data-delegatesFocus> + <input id='inner-before'> + <slot></slot> + <input id='inner-after'> + </template> + <div id='nested'> + <template data-mode='open' data-delegatesFocus> + <input id='nested-x'> + <slot></slot> + <input id='nested-y'> + </template> + <input id='light'> + </div> +</div> +<input id='i1'> + +<script> +promise_test(async () => { + var xFoo = document.querySelector('#x-foo'); + convertTemplatesToShadowRootsWithin(xFoo); + + var elements = [ + 'i0', + 'x-foo/inner-before', + 'nested/nested-x', + 'light', + 'nested/nested-y', + 'x-foo/inner-after', + 'i1' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus controller should treat each slot as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html new file mode 100644 index 0000000000..93a6240fe3 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html @@ -0,0 +1,71 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<p>Tests for moving focus by pressing tab key across shadow boundaries.<br> +To manually test, press tab key six times then shift+tab six times.<br> +It should traverse focusable elements in the increasing numerical order and then in the reverse order.</p> +<div id='host'> + <template data-mode='open'> + <slot name='slot1'>Slot shouldn't be focused. + <slot name='slot2'>Slot shouldn't be focused. + <div id='non1' tabindex=0>This text shouldn't appear.</div> + </slot> + <slot name='slot3'>Slot shouldn't be focused. + <div id='second' tabindex=0>2. No host child is assigned to slot3.</div> + </slot> + </slot> + + <div id='third' tabindex=0>3. Inner Shadow Host. + <template data-mode='open'> + <slot name='slot4'>Slot shouldn't be focused. + <slot name='slot5'>Slot shouldn't be focused. + <div id='non2' tabindex=0>This text shouldn't appear.</div> + </slot> + </slot> + </template> + <div id='fourth' slot='slot4' tabindex=0>4. Assigned to slot4.</div> + <div id='non3' slot='slot5' tabindex=0> + This text shouldn't appear. slot5 is in the fallback content of slot4 which has assigned nodes.</div> + </div> + + <div id='fifth' tabindex=0>5. Inner Shadow Host. + <template data-mode='open'> + <slot name='slot6'>Slot shouldn't be focused. + <div id='non4' tabindex=0>This text shouldn't appear.</div> + </slot> + </template> + <slot name='slot7' slot='slot6'>Slot shouldn't be focused. Assigned to slot6. + <div id='non5' tabindex=0>This text shouldn't appear.</div> + </slot> + </div> + </template> + <div id='first' slot='slot2' tabindex=0>1. Assigned to slot2.</div> + <div id='sixth' slot='slot7' tabindex=0>6. Assigned to slot7 which is assigned to slot6.</div> +</div> +<script> +'use strict'; + +promise_test(async () => { + let host = document.getElementById('host'); + convertTemplatesToShadowRootsWithin(host); + + let elements = [ + 'first', + 'host/second', + 'host/third', + 'host/fourth', + 'host/fifth', + 'sixth' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus should cover assigned elements of an assigned slot espacially there are fallback contents.'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html new file mode 100644 index 0000000000..7bfe5dc784 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html @@ -0,0 +1,51 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<!-- +This test case is based on the crbug.com/618587 reproduction case: +http://jsbin.com/bonudiwagu/1/edit?html,output +--> +<input id='i0'> +<div id='x-foo'> + <template data-mode='open'> + <input id='inner-before'> + <slot></slot> + <input id='inner-after'> + </template> + <div id='nested'> + <template data-mode='open'> + <input id='nested-x'> + <slot></slot> + <input id='nested-y'> + </template> + <input id='light'> + </div> +</div> +<input id='i1'> + +<script> +promise_test(async () => { + var xFoo = document.querySelector('#x-foo'); + convertTemplatesToShadowRootsWithin(xFoo); + + var elements = [ + 'i0', + 'x-foo/inner-before', + 'nested/nested-x', + 'light', + 'nested/nested-y', + 'x-foo/inner-after', + 'i1' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus controller should treat each slot as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html new file mode 100644 index 0000000000..7192a42584 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html @@ -0,0 +1,48 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id="i0" tabindex=0 value="i0"> +<div id="fallback"> + <template data-mode="open"> + <slot id="s0" name="s0"> + <div id="x-foo"> + <template data-mode="open"> + <input id="a2" tabindex=3 value="a2"> + <slot id="s1" name="s1" tabindex=2> + <slot id="s2" name="s2"> + <input id="a1" slot="s2" value="a1"> + </slot> + </slot> + <input id="a0" tabindex=1 value="a0"> + </template> + </div> + </slot> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let fallback = document.getElementById('fallback'); + convertTemplatesToShadowRootsWithin(fallback); + + let elements = [ + 'i0', + 'fallback/x-foo/a0', + 'fallback/x-foo/a1', + 'fallback/x-foo/a2' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus should cover assigned elements of an assigned slot, as well as elements that are directly assigned to a slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html new file mode 100644 index 0000000000..4f320574ed --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html @@ -0,0 +1,47 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id="i0" tabindex=0 value="i0"> +<div id="assigned"> + <template data-mode="open"> + <slot id="s0" name="s0"> + <div id="x-foo"> + <input id="a1" slot="s2" value="a1"> + <template data-mode="open"> + <input id="a2" tabindex=3 value="a2"> + <slot id="s1" name="s1" tabindex=2> + <slot id="s2" name="s2"></slot> + </slot> + <input id="a0" tabindex=1 value="a0"> + </template> + </div> + </slot> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let assigned = document.getElementById('assigned'); + convertTemplatesToShadowRootsWithin(assigned); + + let elements = [ + 'i0', + 'assigned/x-foo/a0', + 'assigned/a1', + 'assigned/x-foo/a2' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus should cover assigned elements of an assigned slot, as well as elements that are directly assigned to a slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html new file mode 100644 index 0000000000..880fb83130 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html @@ -0,0 +1,65 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id='log'></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j1 -> [s1] -> [s2] -> j2 ->[x-bar]]<br> + x-bar's shadow tree: [[s3] -> k1]<br> + slot #s1: [i1 -> i2]<br> + slot #s2: [i3]<br> + slot #s3: [l1 -> l2]<br><br> + <b>v1 ideal nav forward: [i0 -> j1 -> i1 -> i2 -> i3 -> j2 -> x-bar -> l1 -> l2 -> k1]</b><br> +</p> + +<input id='i0' tabindex=0 value='i0'> +<div id='x-foo'> + <input id='i2' slot='s1' tabindex=2 value='i2'> + <input id='i1' slot='s1' tabindex=1 value='i1'> + <input id='i3' slot='s2' tabindex=3 value='i3'> + <template data-mode='open'> + <div id='x-bar' tabindex=5> + <input id='l2' slot='s3' tabindex=2 value='l2'> + <input id='l1' slot='s3' tabindex=1 value='l1'> + <template data-mode='open'> + <slot id='s3' name='s3' tabindex=1></slot> + <input id='k1' tabindex=2 value='k1'> + </template> + </div> + <input id='j1' tabindex=1 value='j1'> + <slot id='s2' name='s2' tabindex=3></slot> + <slot id='s1' name='s1' tabindex=2></slot> + <input id='j2' tabindex=4 value='j2'> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + + let elements = [ + 'i0', + 'x-foo/j1', + 'i1', + 'i2', + 'i3', + 'x-foo/j2', + 'x-foo/x-bar', + 'x-foo/l1', + 'x-foo/l2', + 'x-foo/x-bar/k1', + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Slots tabindex should be considred in focus navigation.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html new file mode 100644 index 0000000000..025a4e0f52 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html @@ -0,0 +1,71 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<p>Tests for moving focus by pressing tab key across nodes in slot scope.<br> + +<button id="b1">outside</button> +<div id='host'> + <template data-mode='open'> + <slot></slot> + </template> + <slot> + <button id="1A">single nested slot</button> + <button id="1B">single nested slot</button> + </slot> + <slot> + <button id="1C">single nested slot</button> + </slot> + <slot> + <slot> + <button id="2A">double nested slot</button> + <button id="2B">double nested slot</button> + </slot> + </slot> + <slot> + <button id="3A">single nested slot</button> + <slot> + <button id="3B">double nested slot</button> + <slot> + <button id="3C">Triple nested slot</button> + <button id="3D">Triple nested slot</button> + </slot> + <button id="3E">double nested slot</button> + </slot> + <button id="3F">single nested slot</button> + </slot> +</div> +<button id="b2">outside</button> + +<script> +'use strict'; + +promise_test(async () => { + convertTemplatesToShadowRootsWithin(host); + + let elements = [ + 'b1', + '1A', + '1B', + '1C', + '2A', + '2B', + '3A', + '3B', + '3C', + '3D', + '3E', + '3F', + 'b2', + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus should cover assigned nodes of slot, especially for nested slots in slot scope.'); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html new file mode 100644 index 0000000000..f2de50f6eb --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html @@ -0,0 +1,67 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j1 -> [s1] -> [s2] -> j2 ->[x-bar]]<br> + x-bar's shadow tree: [k1 -> [s3]]<br> + slot #s1: [i1 -> i2]<br> + slot #s2: [i3]<br> + slot #s3: [[s4]]<br> + slot #s4: [i4 -> i5]<br><br> + <b>v1 ideal nav forward: [i0 -> j1 -> i1 -> i2 -> i3 -> j2 -> x-bar -> k1 -> i4 -> i5]</b><br> +</p> + +<input id="i0" tabindex=0 value="i0"> +<div id="x-foo"> + <input id="i2" slot="s1" tabindex=2 value="i2"> + <input id="i1" slot="s1" tabindex=1 value="i1"> + <input id="i3" slot="s2" tabindex=3 value="i3"> + <input id="i4" slot="s4" tabindex=4 value="i4"> + <input id="i5" slot="s4" tabindex=5 value="i5"> + <template data-mode="open"> + <div id="x-bar" tabindex=5> + <slot id="s4" name="s4" slot="s3"></slot> + <template data-mode="open"> + <slot id="s3" name="s3" tabindex=2></slot> + <input id="k1" tabindex=1 value="k1"> + </template> + </div> + <input id="j1" tabindex=1 value="j1"> + <slot id="s2" name="s2" tabindex=3></slot> + <slot id="s1" name="s1" tabindex=2></slot> + <input id="j2" tabindex=4 value="j2"> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + + let elements = [ + 'i0', + 'x-foo/j1', + 'i1', + 'i2', + 'i3', + 'x-foo/j2', + 'x-foo/x-bar', + 'x-foo/x-bar/k1', + 'i4', + 'i5' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus should cover assigned elements of an assigned slot, as well as elements that are directly assigned to a slot.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-input-type-radio.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-input-type-radio.html new file mode 100644 index 0000000000..fe591a3dc4 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-input-type-radio.html @@ -0,0 +1,70 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<template id="custom-radio"> + <input type="radio"> + <slot></slot> +</template> + +<div tabindex="0" id="start">OUT</div> +<form> + <custom-radio name="radio" id="A">A</x-radio> + <custom-radio name="radio" id="B">B</x-radio> +</form> +<form> + <custom-radio name="radio" id="C">C</x-radio> + <custom-radio name="radio" id="D">D</x-radio> +</form> +<div tabindex="0" id="end">OUT</div> + +<script> +const template = document.querySelector('#custom-radio'); + +class CustomRadio extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open', delegatesFocus: true }).appendChild( + template.content.cloneNode(true), + ); + } +} +customElements.define('custom-radio', CustomRadio); + +async function assert_web_component_focus_navigation_forward(elements) { + let start = document.getElementById(elements[0]); + start.focus(); + for (let i = 1; i < elements.length; i++) { + await navigateFocusForward(); + assert_equals(document.activeElement.id, elements[i]); + } +} + +async function assert_web_component_focus_navigation_backward(elements) { + let end = document.getElementById(elements[elements.length - 1]); + end.focus(); + for (let i = elements.length - 2; i >= 0; i--) { + await navigateFocusBackward(); + assert_equals(document.activeElement.id, elements[i]); + } +} + +promise_test(async () => { + let elements = [ + 'start', + 'A', + 'B', + 'C', + 'D', + 'end' + ]; + + await assert_web_component_focus_navigation_forward(elements); + await assert_web_component_focus_navigation_backward(elements); +}, 'Focus for web component input type elements should be bound by <form> inside shadow DOM'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html new file mode 100644 index 0000000000..e097261526 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html @@ -0,0 +1,358 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<p>This tests TAB focus navigation with delegatesFocus flag on shadow hosts</p> +<pre id="console"></pre> +<div id="sandbox"></div> +<script> + +function prepareDOMTree(parent, mode, tabindex, delegatesFocus) { + parent.innerHTML = ` + <div id="testform"> + <input id="input-before"> + <div id="host-div"> + <input id="inner-input"> + </div> + <input id="input-after"> + </div> + `; + const hostDiv = document.getElementById('host-div'); + const shadowRoot = hostDiv.attachShadow({ mode, delegatesFocus }); + + const inputBefore = document.getElementById('input-before'); + const innerInput = document.getElementById('inner-input'); + const inputAfter = document.getElementById('input-after'); + shadowRoot.appendChild(innerInput); + + if (tabindex !== null) + hostDiv.tabIndex = tabindex; + + return { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + }; + +} + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', null, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, no tabindex and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', null, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, no tabindex and delegatesFocus=true.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 0, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + 'input-before', + 'host-div', + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, tabindex=0 and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 0, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + 'input-before', + // 'host-div', // should skip host when delegatesFocus=true + 'host-div/inner-input', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, tabindex=0 and delegatesFocus=true.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', -1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, tabindex=-1 and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', -1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + 'input-before', + // 'host-div/inner-input', // The whole shadow tree should be skipped + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, tabindex=-1 and delegatesFocus=true.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + 'host-div', + 'host-div/inner-input', + 'input-before', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, tabindex=1 and delegatesFocus=false.'); + +promise_test(async () => { + const { shadowRoot, hostDiv } = prepareDOMTree(sandbox, 'open', 1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + // 'host-div', // should skip host when delegatesFocus=true + 'host-div/inner-input', + 'input-before', + 'input-after', + ]; + + await assert_focus_navigation_forward(elements, false); + elements.reverse(); + await assert_focus_navigation_backward(elements, false); +}, 'Testing tab navigation order with mode open, tabindex=1 and delegatesFocus=true.'); + + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', null, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, no tabindex and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', null, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, no tabindex and delegatesFocus=true.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 0, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + [inputBefore], + [hostDiv], + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=0 and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 0, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 0); + + const elements = [ + [inputBefore], + // [hostDiv], // should skip host when delegatesFocus=true + [innerInput, shadowRoot], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=0 and delegatesFocus=true.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', -1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=-1 and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', -1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, -1); + + const elements = [ + [inputBefore], + // [innerInput, shadowRoot], // The whole shadow tree should be skipped + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=-1 and delegatesFocus=true.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 1, false); + assert_false(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + [hostDiv], + [innerInput, shadowRoot], + [inputBefore], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=1 and delegatesFocus=false.'); + +promise_test(async () => { + const { + hostDiv, + shadowRoot, + inputBefore, + innerInput, + inputAfter, + } = prepareDOMTree(sandbox, 'closed', 1, true); + assert_true(shadowRoot.delegatesFocus); + assert_equals(hostDiv.tabIndex, 1); + + const elements = [ + // [hostDiv], // should skip host when delegatesFocus=true + [innerInput, shadowRoot], + [inputBefore], + [inputAfter], + ]; + + await assert_focus_navigation_forward_with_shadow_root(elements, false); + elements.reverse(); + await assert_focus_navigation_backward_with_shadow_root(elements, false); +}, 'Testing tab navigation order with mode closed, tabindex=1 and delegatesFocus=true.'); + + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html new file mode 100644 index 0000000000..fabcf88170 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html @@ -0,0 +1,73 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j5 -> [x-bar] -> j6]<br> + x-bar's shadow tree: [k1 -> k0 -> [s2]]<br> + slot #s2: [j1 -> j2 -> j3 -> j4 -> [s1] -> j0]<br><br> + slot #s1: [i1 -> i2]<br> + <b>v1 ideal nav forward: [i0 -> j5 -> xbar -> k1 -> k0 -> j1 -> j2 -> j3 -> j4 -> i1 -> i2 -> j0 -> j6]</b><br> +</p> + +<input id="i0" tabindex=0 value="i0"> +<div id="x-foo"> + <input id="i2" slot="s1" tabindex=2 value="i2"> + <input id="i1" slot="s1" tabindex=1 value="i1"> + <template data-mode="open"> + <div id="x-bar" tabindex=4> + <input id="j1" slot="s2" tabindex=1 value="j1"> + <slot id="s1" name="s1" slot="s2"></slot> + <input id="j0" slot="s2" tabindex=0 value="j0"> + <input id="j3" slot="s2" tabindex=2 value="j3"> + <div id="j4" slot="s2" tabindex=3> + <input id="j2" tabindex=1 value="j2"> + </div> + <template data-mode="open"> + <input id="k0" tabindex=0 value="k0"> + <slot id="s2" name="s2"></slot> + <input id="k1" tabindex=1 value="k1"> + </template> + </div> + <input id="j6" tabindex=4 value="j6"> + <input id="j5" tabindex=3 value="j5"> + </template> +</div> + +<script> +'use strict'; + +promise_test(async () => { + let xfoo = document.getElementById('x-foo'); + convertTemplatesToShadowRootsWithin(xfoo); + let sr = xfoo.shadowRoot; + let xbar = sr.querySelector('div'); + convertTemplatesToShadowRootsWithin(xbar); + + let elements = [ + 'i0', + 'x-foo/j5', + 'x-foo/x-bar', + 'x-foo/x-bar/k1', + 'x-foo/x-bar/k0', + 'x-foo/j1', + 'x-foo/j2', + 'x-foo/j3', + 'x-foo/j4', + 'i1', + 'i2', + 'x-foo/j0', + 'x-foo/j6' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus controller should treat slots as a focus scope.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html new file mode 100644 index 0000000000..747a8a1262 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1209217"> +<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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<button id=button1>one</button> +<span> + <template shadowroot=open> + <slot name=myslot></slot> + </template> + <slot slot=myslot> + <button id=button2>two</button> + <button id=button3>three</button> + <button id=button4>four</button> + </slot> +</span> +<button id=button5>five</button> + +<script> + +promise_test(async () => { + convertDeclarativeTemplatesToShadowRootsWithin(document); + button1.focus(); + assert_equals(document.activeElement, button1); + + await navigateFocusForward(); + assert_equals(document.activeElement, button2); + await navigateFocusForward(); + assert_equals(document.activeElement, button3); + await navigateFocusForward(); + assert_equals(document.activeElement, button4); + await navigateFocusForward(); + assert_equals(document.activeElement, button5); + await navigateFocusBackward(); + assert_equals(document.activeElement, button4); + await navigateFocusBackward(); + assert_equals(document.activeElement, button3); + await navigateFocusBackward(); + assert_equals(document.activeElement, button2); + await navigateFocusBackward(); + assert_equals(document.activeElement, button1); +}, `Verifies that focus order goes in flat tree order with buttons inside nested slots which have a mixture of assigned and unassigned states.`); + +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html new file mode 100644 index 0000000000..cacf94f412 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1014868"> +<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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<div> + <template shadowroot=open> + <slot /> + </template> + <slot> + <input id=input1> + </slot> + <slot> + <input id=input2> + </slot> +</div> + +<script> +promise_test(async () => { + convertDeclarativeTemplatesToShadowRootsWithin(document); + input2.focus(); + assert_equals(document.activeElement, input2); + + await navigateFocusBackward(); + assert_equals(document.activeElement, input1); +}, `Verifies that focusing backwards from an input inside a slot which has no shadow root goes to the previous focusable element in light DOM.`); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html new file mode 100644 index 0000000000..e19ea44288 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html @@ -0,0 +1,39 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id="log"></div> + +<input id=i0 value=i0> +<div id=outer> + <template data-mode=open> + <div id=inner> + <template data-mode=open> + <div> + <slot name=inside></slot> + </div> + </template> + <slot name=inside slot=inside> + <input id=i1 value=i1> + </slot> + </div> + </template> +</div> +<input id=i2 value=i2> + +<script> +promise_test(async () => { + convertTemplatesToShadowRootsWithin(document.getElementById('outer')); + + const elements = [ + 'i2', + 'outer/i1', + 'i0' + ]; + await assert_focus_navigation_backward(elements); +}, `Verifies that focusing backwards from a button inside a slot which has no assigned nodes goes to the previous focusable element.`); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html new file mode 100644 index 0000000000..135569d94c --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Joey Arhar" href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1209217"> +<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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> + +<slot> + <input id=input1> +</slot> +<slot> + <input id=input2> +</slot> + +<script> +promise_test(async () => { + input1.focus(); + assert_equals(document.activeElement, input1); + + await navigateFocusForward(); + assert_equals(document.activeElement, input2); +}, `Verifies that focusing forwards from an input inside a slot which has no shadow root goes to the next focusable element in light DOM.`); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html new file mode 100644 index 0000000000..95dc98fc02 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html @@ -0,0 +1,92 @@ +<!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/testdriver-actions.js"></script> +<script src="resources/shadow-dom.js"></script> +<script src="resources/focus-utils.js"></script> +<div id='log'></div> +<p> + document tree: [i0 -> [x-foo]]<br> + x-foo's shadow tree: [j5 -> [x-bar] -> j6]<br> + x-bar's shadow tree: [k1 -> k0 -> [s2]]<br> + slot #s2: [j1 -> j2 -> j3 -> j4 -> [s1] -> j0]<br><br> + slot #s1: [i1 -> i2]<br> + <b>v1 ideal nav forward: [i0 -> j5 -> xbar -> k1 -> k0 -> j6]</b><br> +</p> + + <input id='i0' tabindex=0 value='i0'> + <div id='x-foo'> + <input id='i2' slot='s1' tabindex=2 value='i2'> + <input id='i1' slot='s1' tabindex=1 value='i1'> + <template data-mode='open'> + <div id='x-bar' tabindex=4> + <input id='j1' slot='s2' tabindex=1 value='j1'> + <slot id='s1' name='s1' slot='s2'></slot> + <input id='j0' slot='s2' tabindex=0 value='j0'> + <input id='j3' slot='s2' tabindex=2 value='j3'> + <div id='j4' slot='s2' tabindex=3> + <input id='j2' tabindex=1 value='j2'> + </div> + <template data-mode='open'> + <input id='k0' tabindex=0 value='k0'> + <slot id='s2' name='s2' tabindex=-1></slot> + <input id='k1' tabindex=1 value='k1'> + </template> + </div> + <div id='to-be-ignored-host' tabindex=-1> + <template data-mode='open'> + <input id='ignored-input-in-shadow-host1' tabindex=1 value='ignored'> + <input id='ignored-input-in-shadow-host2' tabindex=2 value='ignored'> + </template> + </div> + <input id='j6' tabindex=4 value='j6'> + <input id='j5' tabindex=3 value='j5'> + </template> + </div> +</div> + +<script> +'use strict'; + +let xfoo = document.getElementById('x-foo'); +convertTemplatesToShadowRootsWithin(xfoo); +let sr = xfoo.shadowRoot; + +promise_test(async () => { + let elements = [ + 'i0', + 'x-foo/j5', + 'x-foo/x-bar', + 'x-foo/x-bar/k1', + 'x-foo/x-bar/k0', + 'x-foo/j6' + ]; + + await assert_focus_navigation_forward(elements); + elements.reverse(); + await assert_focus_navigation_backward(elements); +}, 'Focus controller should treat slots as a focus scope.'); + +promise_test(async () => { + let ignoredHost = sr.getElementById('to-be-ignored-host'); + let ignoredInput1 = ignoredHost.shadowRoot.querySelector('input'); + let ignoredInput2 = ignoredInput1.nextElementSibling; + + let elements = [ + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host1', + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host2', + 'x-foo/j6' + ]; + + await assert_focus_navigation_forward(elements); + + let elementsBackward = [ + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host2', + 'x-foo/to-be-ignored-host/ignored-input-in-shadow-host1', + 'x-foo/x-bar/k0' + ]; + await assert_focus_navigation_backward(elementsBackward); +}, 'This is a regression test: After focusing negative tabindex-ed elements, focus moves in tree order.'); +</script> diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js new file mode 100644 index 0000000000..c23581a15e --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js @@ -0,0 +1,153 @@ +'use strict'; + +async function navigateFocusForward() { + return new test_driver.Actions() + .keyDown('\uE004') + .keyUp('\uE004') + .send(); +} + +async function navigateFocusBackward() { + return new test_driver.Actions() + .keyDown('\uE050') + .keyDown('\uE004') + .keyUp('\uE004') + .keyUp('\uE050') + .send(); +} + +// If shadow root is open, can find element using element path +// If shadow root is open, can find the shadowRoot from the element + +function innermostActiveElement(element) { + element = element || document.activeElement; + if (isIFrameElement(element)) { + if (element.contentDocument.activeElement) + return innermostActiveElement(element.contentDocument.activeElement); + return element; + } + if (isShadowHost(element)) { + let shadowRoot = element.shadowRoot; + if (shadowRoot) { + if (shadowRoot.activeElement) + return innermostActiveElement(shadowRoot.activeElement); + } + } + return element; +} + +function isInnermostActiveElement(path) { + const element = getNodeInComposedTree(path); + if (!element) + return false; + return element === innermostActiveElement(); +} + +async function shouldNavigateFocus(fromElement, direction) { + if (!fromElement) + return false; + + fromElement.focus(); + if (fromElement !== innermostActiveElement()) + return false; + + if (direction == 'forward') + await navigateFocusForward(); + else + await navigateFocusBackward(); + + return true; +} + +async function assert_focus_navigation_element(fromPath, toPath, direction) { + const fromElement = getNodeInComposedTree(fromPath); + const result = await shouldNavigateFocus(fromElement, direction); + assert_true(result, 'Failed to focus ' + fromPath); + + const message = + `Focus should move ${direction} from ${fromPath} to ${toPath}`; + const toElement = getNodeInComposedTree(toPath); + assert_equals(innermostActiveElement(), toElement, message); +} + +async function assert_focus_navigation_elements(elements, direction) { + assert_true( + elements.length >= 2, + 'length of elements should be greater than or equal to 2.'); + for (var i = 0; i + 1 < elements.length; ++i) + await assert_focus_navigation_element(elements[i], elements[i + 1], direction); + +} + +async function assert_focus_navigation_forward(elements) { + return assert_focus_navigation_elements(elements, 'forward'); +} + +async function assert_focus_navigation_backward(elements) { + return assert_focus_navigation_elements(elements, 'backward'); +} + + +// If shadow root is closed, need to pass shadowRoot and element to find +// innermost active element + +function isShadowHostOfRoot(shadowRoot, node) { + return shadowRoot && shadowRoot.host.isEqualNode(node); +} + +function innermostActiveElementWithShadowRoot(shadowRoot, element) { + element = element || document.activeElement; + if (isIFrameElement(element)) { + if (element.contentDocument.activeElement) + return innermostActiveElementWithShadowRoot(shadowRoot, element.contentDocument.activeElement); + return element; + } + if (isShadowHostOfRoot(shadowRoot, element)) { + if (shadowRoot.activeElement) + return innermostActiveElementWithShadowRoot(shadowRoot, shadowRoot.activeElement); + } + return element; +} + +async function shouldNavigateFocusWithShadowRoot(from, direction) { + const [fromElement, shadowRoot] = from; + if (!fromElement) + return false; + + fromElement.focus(); + if (fromElement !== innermostActiveElementWithShadowRoot(shadowRoot)) + return false; + + if (direction == 'forward') + await navigateFocusForward(); + else + await navigateFocusBackward(); + + return true; +} + +async function assert_focus_navigation_element_with_shadow_root(from, to, direction) { + const result = await shouldNavigateFocusWithShadowRoot(from, direction); + const [fromElement] = from; + const [toElement, toShadowRoot] = to; + assert_true(result, 'Failed to focus ' + fromElement.id); + const message = + `Focus should move ${direction} from ${fromElement.id} to ${toElement.id}`; + assert_equals(innermostActiveElementWithShadowRoot(toShadowRoot), toElement, message); +} + +async function assert_focus_navigation_elements_with_shadow_root(elements, direction) { + assert_true( + elements.length >= 2, + 'length of elements should be greater than or equal to 2.'); + for (var i = 0; i + 1 < elements.length; ++i) + await assert_focus_navigation_element_with_shadow_root(elements[i], elements[i + 1], direction); +} + +async function assert_focus_navigation_forward_with_shadow_root(elements) { + return assert_focus_navigation_elements_with_shadow_root(elements, 'forward'); +} + +async function assert_focus_navigation_backward_with_shadow_root(elements) { + return assert_focus_navigation_elements_with_shadow_root(elements, 'backward'); +}
\ No newline at end of file diff --git a/testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js new file mode 100644 index 0000000000..83dfc0d643 --- /dev/null +++ b/testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js @@ -0,0 +1,175 @@ +function removeWhiteSpaceOnlyTextNodes(node) { + for (var i = 0; i < node.childNodes.length; i++) { + var child = node.childNodes[i]; + if (child.nodeType === Node.TEXT_NODE && + child.nodeValue.trim().length == 0) { + node.removeChild(child); + i--; + } else if ( + child.nodeType === Node.ELEMENT_NODE || + child.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + removeWhiteSpaceOnlyTextNodes(child); + } + } + if (node.shadowRoot) { + removeWhiteSpaceOnlyTextNodes(node.shadowRoot); + } +} + +function convertTemplatesToShadowRootsWithin(node) { + var nodes = node.querySelectorAll('template'); + for (var i = 0; i < nodes.length; ++i) { + var template = nodes[i]; + var mode = template.getAttribute('data-mode'); + var delegatesFocus = template.hasAttribute('data-delegatesFocus'); + var parent = template.parentNode; + parent.removeChild(template); + var shadowRoot; + if (!mode) { + shadowRoot = parent.attachShadow({ mode: 'open' }); + } else { + shadowRoot = + parent.attachShadow({ 'mode': mode, 'delegatesFocus': delegatesFocus }); + } + var expose = template.getAttribute('data-expose-as'); + if (expose) + window[expose] = shadowRoot; + if (template.id) + shadowRoot.id = template.id; + var fragments = document.importNode(template.content, true); + shadowRoot.appendChild(fragments); + + convertTemplatesToShadowRootsWithin(shadowRoot); + } +} + +function convertDeclarativeTemplatesToShadowRootsWithin(root) { + root.querySelectorAll("template[shadowroot]").forEach(template => { + const mode = template.getAttribute("shadowroot"); + const shadowRoot = template.parentNode.attachShadow({ mode }); + shadowRoot.appendChild(template.content); + template.remove(); + convertDeclarativeTemplatesToShadowRootsWithin(shadowRoot); + }); +} + +function isShadowHost(node) { + return node && node.nodeType == Node.ELEMENT_NODE && node.shadowRoot; +} + +function isIFrameElement(element) { + return element && element.nodeName == 'IFRAME'; +} + +// Returns node from shadow/iframe tree "path". +function getNodeInComposedTree(path) { + var ids = path.split('/'); + var node = document.getElementById(ids[0]); + for (var i = 1; node != null && i < ids.length; ++i) { + if (isIFrameElement(node)) + node = node.contentDocument.getElementById(ids[i]); + else if (isShadowHost(node)) + node = node.shadowRoot.getElementById(ids[i]); + else + return null; + } + return node; +} + +function createTestTree(node) { + let ids = {}; + + function attachShadowFromTemplate(template) { + let parent = template.parentNode; + parent.removeChild(template); + let shadowRoot; + if (template.getAttribute('data-slot-assignment') === 'manual') { + shadowRoot = + parent.attachShadow({ + mode: template.getAttribute('data-mode'), + slotAssignment: 'manual' + }); + } else { + shadowRoot = + parent.attachShadow({ mode: template.getAttribute('data-mode') }); + } + let id = template.id; + if (id) { + shadowRoot.id = id; + ids[id] = shadowRoot; + } + shadowRoot.appendChild(document.importNode(template.content, true)); + return shadowRoot; + } + + function walk(root) { + if (root.id) { + ids[root.id] = root; + } + for (let e of Array.from(root.querySelectorAll('[id]'))) { + ids[e.id] = e; + } + for (let e of Array.from(root.querySelectorAll('template'))) { + walk(attachShadowFromTemplate(e)); + } + } + + walk(node.cloneNode(true)); + return ids; +} + +function dispatchEventWithLog(nodes, target, event) { + function labelFor(e) { + return e.id || e.tagName; + } + + let log = []; + let attachedNodes = []; + for (let label in nodes) { + let startingNode = nodes[label]; + for (let node = startingNode; node; node = node.parentNode) { + if (attachedNodes.indexOf(node) >= 0) + continue; + let id = node.id; + if (!id) + continue; + attachedNodes.push(node); + node.addEventListener(event.type, (e) => { + // Record [currentTarget, target, relatedTarget, composedPath()] + log.push([ + id, labelFor(e.target), + e.relatedTarget ? labelFor(e.relatedTarget) : null, + e.composedPath().map((n) => { + return labelFor(n); + }) + ]); + }); + } + } + target.dispatchEvent(event); + return log; +} + +// This function assumes that testharness.js is available. +function assert_event_path_equals(actual, expected) { + assert_equals(actual.length, expected.length); + for (let i = 0; i < actual.length; ++i) { + assert_equals( + actual[i][0], expected[i][0], + 'currentTarget at ' + i + ' should be same'); + assert_equals( + actual[i][1], expected[i][1], 'target at ' + i + ' should be same'); + assert_equals( + actual[i][2], expected[i][2], + 'relatedTarget at ' + i + ' should be same'); + assert_array_equals( + actual[i][3], expected[i][3], + 'composedPath at ' + i + ' should be same'); + } +} + +function assert_background_color(path, color) { + assert_equals( + window.getComputedStyle(getNodeInComposedTree(path)).backgroundColor, + color, 'backgroundColor for ' + path + ' should be ' + color); +} |