summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/shadow-dom/focus-navigation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/shadow-dom/focus-navigation
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/delegatesFocus-highlight-sibling.html126
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html74
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-fallback.html71
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-2levels.html63
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-delegatesFocus.html51
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested-fallback.html71
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-nested.html51
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-fallback.html48
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-shadow-in-slot.html47
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slot-with-tabindex.html65
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots-in-slot.html71
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-slots.html67
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-web-component-input-type-radio.html70
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation-with-delegatesFocus.html358
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-navigation.html73
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-nested-slots.html51
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassignable-slot.html34
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-reverse-unassigned-slot.html39
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-unassignable-slot.html28
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/focus-with-negative-index.html92
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/resources/focus-utils.js153
-rw-r--r--testing/web-platform/tests/shadow-dom/focus-navigation/resources/shadow-dom.js175
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);
+}