Add the support for ShadowRoot.delegateFocus
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 12 Oct 2019 06:09:04 +0000 (06:09 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 12 Oct 2019 06:09:04 +0000 (06:09 +0000)
https://bugs.webkit.org/show_bug.cgi?id=166484
<rdar://problem/29816058>

Reviewed by Antti Koivisto.

LayoutTests/imported/w3c:

Import W3C tests from https://github.com/web-platform-tests/wpt/pull/18035/commits/a8a89f224f2170723170a452cb18b46cafb723b6.

* web-platform-tests/resources/testdriver-vendor.js:
* web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html: Added.
* web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html: Added.
* web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html: Added.
* web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html: Added.
* web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html: Added.
* web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html: Added.
* web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus-expected.txt: Added.
* web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html: Added.

Source/WebCore:

Implement delegatesFocus as specified in https://github.com/whatwg/html/pull/4796

When the shadow root of an element has delegates focus flag set, focusing on the shadow host would automatically
"delegates" focus to the first focusable element in the shadow tree instead.

The first focusable element is determined as the first element that is programatically focusable or mouse focusable
in the flat tree (composed tree in WebKit's terminology) in the case of the element getting focused via DOM API,
Element.prototype.focus, by via mouse down. In the case of sequential focus navigation (via tab key), it's the
first keyboard focusable element in the tabIndex order.

Tests: imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html
       imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html
       imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html
       imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html
       imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html
       imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html
       imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html

* dom/Element.cpp:
(WebCore::Element::isKeyboardFocusable const): A shadow host with the delegates focus flag is not considered as
keyboard focusable. The rest is taken care of by the existing logic in FocusController.
(WebCore::isProgramaticallyFocusable): Extracted from Element::focus.
(WebCore::findFirstProgramaticallyFocusableElementInComposedTree): Added.
(WebCore::Element::focus): Added the support for delegatesFocus.
* dom/Element.h:
(WebCore::ShadowRootInit::delegatesFocus): Added.
* dom/Element.idl: Ditto.
* dom/ShadowRoot.cpp:
(WebCore::ShadowRoot::ShadowRoot): Added delegatesFocus to the constructor.
* dom/ShadowRoot.h:
* page/EventHandler.cpp:
(WebCore::findFirstMouseFocusableElementInComposedTree): Added.
(WebCore::EventHandler::dispatchMouseEvent): Added the support for delegatesFocus. Uses the first mouse focusable
element in the flat tree (composed tree) order.
* page/FocusController.cpp:
(WebCore::FocusController::findFirstFocusableElementInShadowRoot):
* page/FocusController.h:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@251043 268f45cc-cd09-0410-ab3c-d52691b4dbfc

23 files changed:
LayoutTests/imported/w3c/ChangeLog
LayoutTests/imported/w3c/web-platform-tests/resources/testdriver-vendor.js
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus-expected.txt [new file with mode: 0644]
LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/Element.cpp
Source/WebCore/dom/Element.h
Source/WebCore/dom/Element.idl
Source/WebCore/dom/ShadowRoot.cpp
Source/WebCore/dom/ShadowRoot.h
Source/WebCore/page/EventHandler.cpp

index 017b47f..285bf06 100644 (file)
@@ -1,3 +1,29 @@
+2019-10-11  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add the support for ShadowRoot.delegateFocus
+        https://bugs.webkit.org/show_bug.cgi?id=166484
+        <rdar://problem/29816058>
+
+        Reviewed by Antti Koivisto.
+
+        Import W3C tests from https://github.com/web-platform-tests/wpt/pull/18035/commits/a8a89f224f2170723170a452cb18b46cafb723b6.
+
+        * web-platform-tests/resources/testdriver-vendor.js:
+        * web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html: Added.
+        * web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html: Added.
+        * web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html: Added.
+        * web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html: Added.
+        * web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html: Added.
+        * web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html: Added.
+        * web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus-expected.txt: Added.
+        * web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html: Added.
+
 2019-10-10  Carlos Alberto Lopez Perez  <clopez@igalia.com>
 
         Import css/css-images WPT tests
index dc5bb1a..c930ecf 100644 (file)
@@ -178,6 +178,27 @@ window.test_driver_internal.send_keys = function(element, keys)
     return Promise.resolve();
 }
 
+window.test_driver_internal.click = function (element, coords)
+{
+    if (!window.eventSender)
+        return Promise.reject(new Error("window.eventSender is undefined."));
+
+    if (testRunner.isIOSFamily && testRunner.isWebKit2) {
+        return new Promise((resolve) => {
+            testRunner.runUIScript(`
+                uiController.singleTapAtPoint(${coords.x}, ${coords.y}, function() {
+                    uiController.uiScriptComplete();
+                });`, resolve);
+        });
+    }
+
+    eventSender.mouseMoveTo(coords.x, coords.y);
+    eventSender.mouseDown();
+    eventSender.mouseUp();
+
+    return Promise.resolve();
+}
+
 window.test_driver_internal.action_sequence = function(sources)
 {
     // https://w3c.github.io/webdriver/#processing-actions    
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method-expected.txt
new file mode 100644 (file)
index 0000000..2f852cd
--- /dev/null
@@ -0,0 +1,6 @@
+slotted
+outside
+
+PASS call click() on host with delegatesFocus, all tabindex=0 
+PASS call click() on slotted element in delegatesFocus shadow tree, all tabindex=0 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html
new file mode 100644 (file)
index 0000000..92212cd
--- /dev/null
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+  <div id="slotted">slotted</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slotted = document.getElementById("slotted");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlot = document.createElement("div");
+aboveSlot.innerText = "aboveSlot";
+const slot = document.createElement("slot");
+shadowRoot.appendChild(aboveSlot);
+shadowRoot.appendChild(slot);
+
+const elementsInFlatTreeOrder = [host, aboveSlot, slot, slotted, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+//    #shadowRoot
+//      <div #aboveSlot>
+//      <slot #slot>
+//        (slotted) <div #slotted>
+// <div #outside>
+
+function setAllTabIndex(value) {
+  setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+  removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+  removeAllTabIndex();
+  resetFocus(document);
+  resetFocus(shadowRoot);
+}
+
+test(() => {
+  resetTabIndexAndFocus();
+  setAllTabIndex(0);
+  host.click();
+  assert_equals(shadowRoot.activeElement, null);
+  assert_equals(document.activeElement, document.body);
+}, "call click() on host with delegatesFocus, all tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  setAllTabIndex(0);
+  slotted.click();
+  assert_equals(shadowRoot.activeElement, null);
+  assert_equals(document.activeElement, document.body);
+}, "call click() on slotted element in delegatesFocus shadow tree, all tabindex=0");
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies-expected.txt
new file mode 100644 (file)
index 0000000..a550148
--- /dev/null
@@ -0,0 +1,5 @@
+slotted
+outside
+
+PASS click on host with delegatesFocus, #aboveSlot tabindex = 2, #slot and #slotted tabindex = 1 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html
new file mode 100644 (file)
index 0000000..4051db1
--- /dev/null
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+  <div id="slotted">slotted</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slotted = document.getElementById("slotted");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlot = document.createElement("div");
+aboveSlot.innerText = "aboveSlot";
+const slot = document.createElement("slot");
+// Add an unfocusable spacer, because test_driver.click will click on the
+// center point of #host, and we don't want the click to land on #aboveSlot
+// or #slot.
+const spacer = document.createElement("div");
+spacer.style = "height: 1000px;";
+shadowRoot.appendChild(spacer);
+shadowRoot.appendChild(aboveSlot);
+shadowRoot.appendChild(slot);
+
+const elementsInFlatTreeOrder = [host, aboveSlot, spacer, slot, slotted, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+//    #shadowRoot
+//      <div #spacer>
+//      <div #aboveSlot>
+//      <slot #slot>
+//        (slotted) <div #slotted>
+// <div #outside>
+
+function setAllTabIndex(value) {
+  setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+  removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+  removeAllTabIndex();
+  resetFocus(document);
+  resetFocus(shadowRoot);
+}
+
+promise_test(async () => {
+  resetTabIndexAndFocus();
+  setTabIndex([aboveSlot], 2);
+  setTabIndex([slot, slotted], 1);
+  await test_driver.click(host);
+  assert_equals(shadowRoot.activeElement, aboveSlot);
+  assert_equals(document.activeElement, host);
+}, "click on host with delegatesFocus, #aboveSlot tabindex = 2, #slot and #slotted tabindex = 1");
+
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero-expected.txt
new file mode 100644 (file)
index 0000000..a5913c9
--- /dev/null
@@ -0,0 +1,5 @@
+slotted
+outside
+
+PASS click on host with delegatesFocus, all tabindex=0 except spacer 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html
new file mode 100644 (file)
index 0000000..5f7914f
--- /dev/null
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: click on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+  <div id="slotted">slotted</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slotted = document.getElementById("slotted");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlot = document.createElement("div");
+aboveSlot.innerText = "aboveSlot";
+const slot = document.createElement("slot");
+// Add an unfocusable spacer, because test_driver.click will click on the
+// center point of #host, and we don't want the click to land on #aboveSlot
+// or #slot.
+const spacer = document.createElement("div");
+spacer.style = "height: 1000px;";
+shadowRoot.appendChild(spacer);
+shadowRoot.appendChild(aboveSlot);
+shadowRoot.appendChild(slot);
+
+const elementsInFlatTreeOrder = [host, aboveSlot, spacer, slot, slotted, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+//    #shadowRoot
+//      <div #spacer>
+//      <div #aboveSlot>
+//      <slot #slot>
+//        (slotted) <div #slotted>
+// <div #outside>
+
+function setAllTabIndex(value) {
+  setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+  removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+  removeAllTabIndex();
+  resetFocus(document);
+  resetFocus(shadowRoot);
+}
+
+promise_test(async () => {
+  resetTabIndexAndFocus();
+  setAllTabIndex(0);
+  removeTabIndex([spacer]);
+  await test_driver.click(host);
+  assert_equals(shadowRoot.activeElement, aboveSlot);
+  assert_equals(document.activeElement, host);
+}, "click on host with delegatesFocus, all tabindex=0 except spacer");
+
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus-expected.txt
new file mode 100644 (file)
index 0000000..ac1770d
--- /dev/null
@@ -0,0 +1,16 @@
+slottedToSecondSlot
+slottedToFirstSlot
+outside
+
+PASS focus() on host with delegatesFocus, all tabindex=0 
+PASS focus() on host with delegatesFocus & tabindex =-1, all other tabindex=0 
+PASS focus() on host with delegatesFocus & no tabindex, all other tabindex=0 
+PASS focus() on host with delegatesFocus & tabindex = 0, all other tabindex=-1 
+PASS focus() on host with delegatesFocus, all without tabindex 
+PASS focus() on host with delegatesFocus, all tabindex=-1 
+PASS focus() on host with delegatesFocus & tabindex=0, #belowSlots with tabindex=0 
+PASS focus() on host with delegatesFocus & tabindex=0, #outside with tabindex=0 
+PASS focus() on host with delegatesFocus & tabindex=0, #aboveSlots and #belowSlots with tabindex=0 
+PASS focus() on host with delegatesFocus & tabindex=0, #aboveSlots with tabindex=0 and #belowSlots with tabindex=1 
+PASS focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots  with tabindex=0 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html
new file mode 100644 (file)
index 0000000..368604b
--- /dev/null
@@ -0,0 +1,231 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus() on shadow host with delegatesFocus</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+
+<body>
+<div id="host">
+  <div id="slottedToSecondSlot" slot="secondSlot">slottedToSecondSlot</div>
+  <div id="slottedToFirstSlot" slot="firstSlot">slottedToFirstSlot</div>
+</div>
+<div id="outside">outside</div>
+</body>
+
+<script>
+const host = document.getElementById("host");
+const slottedToSecondSlot = document.getElementById("slottedToSecondSlot");
+const slottedToFirstSlot = document.getElementById("slottedToFirstSlot");
+const outside = document.getElementById("outside");
+
+const shadowRoot = host.attachShadow({ mode: "open", delegatesFocus: true });
+const aboveSlots = document.createElement("div");
+aboveSlots.innerText = "aboveSlots";
+const firstSlot = document.createElement("slot");
+firstSlot.name = "firstSlot";
+const secondSlot = document.createElement("slot");
+secondSlot.name = "secondSlot";
+const belowSlots = document.createElement("div");
+belowSlots.innerText = "belowSlots";
+shadowRoot.appendChild(aboveSlots);
+shadowRoot.appendChild(firstSlot);
+shadowRoot.appendChild(secondSlot);
+shadowRoot.appendChild(belowSlots);
+
+const elementsInFlatTreeOrder = [host, aboveSlots, firstSlot,
+  slottedToFirstSlot, secondSlot, slottedToSecondSlot, belowSlots, outside];
+
+// Final structure:
+// <div #host> (delegatesFocus=true)
+//    #shadowRoot
+//      <div #aboveSlots>
+//      <slot #firstSlot>
+//        (slotted) <div #slottedToFirstSlot>
+//      <slot #secondSlot>
+//        (slotted) <div #slottedToSecondSlot>
+//      <div #belowSlots>
+// <div #outside>
+
+
+function setAllTabIndex(value) {
+  setTabIndex(elementsInFlatTreeOrder, value);
+}
+
+function removeAllTabIndex() {
+  removeTabIndex(elementsInFlatTreeOrder);
+}
+
+function resetTabIndexAndFocus() {
+  removeAllTabIndex();
+  resetFocus(document);
+  resetFocus(shadowRoot);
+}
+
+test(() => {
+  resetTabIndexAndFocus();
+  setAllTabIndex(0);
+  // Structure:
+  // <div #host> (delegatesFocus=true) tabindex=0
+  //    #shadowRoot
+  //      <div #aboveSlots> tabindex=0
+  //      <slot #firstSlot> tabindex=0
+  //        (slotted) <div #slottedToFirstSlot> tabindex=0
+  //      <slot #secondSlot> tabindex=0
+  //        (slotted) <div #slottedToSecondSlot> tabindex=0
+  //      <div #belowSlots> tabindex=0
+  // <div #outside> tabindex=0
+  // First focusable = #aboveSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus, all tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  setAllTabIndex(0);
+  setTabIndex([host], -1);
+  // First focusable = #aboveSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex =-1, all other tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  setTabIndex([aboveSlots, slottedToFirstSlot, slottedToSecondSlot, belowSlots], 0);
+  // First focusable = #aboveSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & no tabindex, all other tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  setAllTabIndex(-1);
+  setTabIndex([host], 0);
+  // First focusable = #aboveSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex = 0, all other tabindex=-1");
+
+test(() => {
+  resetTabIndexAndFocus();
+  removeAllTabIndex();
+  // No focusable element under #host in the flat tree.
+  host.focus();
+  assert_equals(shadowRoot.activeElement, null);
+  assert_equals(document.activeElement, document.body);
+}, "focus() on host with delegatesFocus, all without tabindex");
+
+test(() => {
+  resetTabIndexAndFocus();
+  // First focusable = #aboveSlots
+  setAllTabIndex(-1);
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus, all tabindex=-1");
+
+test(() => {
+  resetTabIndexAndFocus();
+  removeAllTabIndex();
+  setTabIndex([host, belowSlots], 0);
+  // Structure:
+  // <div #host> (delegatesFocus=true) tabindex=0
+  //    #shadowRoot
+  //      <div #aboveSlots>
+  //      <slot #firstSlot>
+  //        (slotted) <div #slottedToFirstSlot>
+  //      <slot #secondSlot>
+  //        (slotted) <div #slottedToSecondSlot>
+  //      <div #belowSlots> tabindex=0
+  // <div #outside>
+  // First focusable = #belowSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, belowSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #belowSlots with tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  removeAllTabIndex();
+  setTabIndex([host, outside], 0);
+  // Structure:
+  // <div #host> (delegatesFocus=true) tabindex=0
+  //    #shadowRoot
+  //      <div #aboveSlots>
+  //      <slot #firstSlot>
+  //        (slotted) <div #slottedToFirstSlot>
+  //      <slot #secondSlot>
+  //        (slotted) <div #slottedToSecondSlot>
+  //      <div #belowSlots>
+  // <div #outside> tabindex=0
+  // No focusable element under #host in the flat tree.
+  host.focus();
+  assert_equals(shadowRoot.activeElement, null);
+  assert_equals(document.activeElement, document.body);
+}, "focus() on host with delegatesFocus & tabindex=0, #outside with tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  setTabIndex([host, aboveSlots, belowSlots], 0);
+  // Structure:
+  // <div #host> (delegatesFocus=true) tabindex=0
+  //    #shadowRoot
+  //      <div #aboveSlots> tabindex=0
+  //      <slot #firstSlot>
+  //        (slotted) <div #slottedToFirstSlot>
+  //      <slot #secondSlot>
+  //        (slotted) <div #slottedToSecondSlot>
+  //      <div #belowSlots> tabindex=0
+  // <div #outside>
+  // First focusable = #aboveSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #aboveSlots and #belowSlots with tabindex=0");
+
+test(() => {
+  resetTabIndexAndFocus();
+  setTabIndex([host, aboveSlots], 0);
+  setTabIndex([belowSlots], 1);
+  // Structure:
+  // <div #host> (delegatesFocus=true) tabindex=0
+  //    #shadowRoot
+  //      <div #aboveSlots> tabindex=0
+  //      <slot #firstSlot>
+  //        (slotted) <div #slottedToFirstSlot>
+  //      <slot #secondSlot>
+  //        (slotted) <div #slottedToSecondSlot>
+  //      <div #belowSlots> tabindex=1
+  // <div #outside>
+  // First focusable = #aboveSlots
+  host.focus();
+  assert_equals(shadowRoot.activeElement, aboveSlots);
+  assert_equals(document.activeElement, host);
+}, "focus() on host with delegatesFocus & tabindex=0, #aboveSlots with tabindex=0 and #belowSlots with tabindex=1");
+  
+test(() => {
+  resetTabIndexAndFocus();
+  setTabIndex([host, slottedToFirstSlot, slottedToSecondSlot, belowSlots], 0);
+  // Structure:
+  // <div #host> (delegatesFocus=true) tabindex=0
+  //    #shadowRoot
+  //      <div #aboveSlots>
+  //      <slot #firstSlot>
+  //        (slotted) <div #slottedToFirstSlot> tabindex=0
+  //      <slot #secondSlot>
+  //        (slotted) <div #slottedToSecondSlot> tabindex=0
+  //      <div #belowSlots> tabindex=0
+  // <div #outside>
+  // First focusable = #slottedToFirstSlot
+  host.focus();
+  assert_equals(shadowRoot.activeElement, null);
+  assert_equals(document.activeElement, slottedToFirstSlot);
+}, "focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots  with tabindex=0");
+
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus-expected.txt
new file mode 100644 (file)
index 0000000..6ffebe0
--- /dev/null
@@ -0,0 +1,7 @@
+aboveHost
+slotted below
+slotted above
+belowHost
+
+PASS Order when all tabindex=-1 is and delegatesFocus = true 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html
new file mode 100644 (file)
index 0000000..356b0bb
--- /dev/null
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and all tabindex=-1 in shadow tree</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+//    #shadowRoot (delegatesFocus)
+//      <div #aboveSlot tabindex=-1>
+//      <slot #slotAbove tabindex=-1>
+//        (slotted) <div #slottedAbove tabindex=-1>
+//      <slot #slotBelow tabindex=-1>
+//        (slotted) <div #slottedBelow tabindex=-1>
+//      <div #belowSlot tabindex=-1>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+  let elementsInFlatTreeOrder;
+  let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true);
+  setTabIndex(elementsInFlatTreeOrder, -1);
+  setTabIndex([aboveHost, host, belowHost], 0);
+  resetFocus();
+  // Focus should only land on #aboveHost and #belowHost (all others are non-sequentially focusable).
+  return assertFocusOrder([aboveHost, belowHost]);
+}, "Order when all tabindex=-1 is and delegatesFocus = true");
+</script>
+</body>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus-expected.txt
new file mode 100644 (file)
index 0000000..14543f8
--- /dev/null
@@ -0,0 +1,7 @@
+aboveHost
+slotted below
+slotted above
+belowHost
+
+PASS Order when tabindex varies and delegatesFocus = true 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html
new file mode 100644 (file)
index 0000000..67899cf
--- /dev/null
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and tabindex in shadow tree varies</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+//    #shadowRoot (delegatesFocus)
+//      <div #aboveSlot tabindex=0>
+//      <slot #slotAbove tabindex=1>
+//        (slotted) <div #slottedAbove tabindex=3>
+//      <slot #slotBelow tabindex=2>
+//        (slotted) <div #slottedBelow tabindex=1>
+//      <div #belowSlot tabindex=-1>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+  let elementsInFlatTreeOrder;
+  let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true);
+  setTabIndex(elementsInFlatTreeOrder, -1);
+  setTabIndex([aboveHost, host, aboveSlot, belowHost], 0);
+  setTabIndex([slotAbove, slottedBelow], 1);
+  setTabIndex([slotBelow], 2);
+  setTabIndex([slottedAbove], 3);
+  resetFocus();
+  return assertFocusOrder([aboveHost, slottedAbove, slottedBelow, aboveSlot, belowHost]);
+}, "Order when tabindex varies and delegatesFocus = true");
+</script>
+</body>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus-expected.txt
new file mode 100644 (file)
index 0000000..25868b6
--- /dev/null
@@ -0,0 +1,7 @@
+aboveHost
+slotted below
+slotted above
+belowHost
+
+PASS Order when all tabindex=0 is and delegatesFocus = true 
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html b/LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html
new file mode 100644 (file)
index 0000000..5e6ab3a
--- /dev/null
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focus - the sequential focus navigation order with shadow dom that delegates focus and all tabindex=0</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="resources/shadow-utils.js"></script>
+<body>
+<script>
+// Structure:
+// <div #aboveHost tabindex=0>
+// <div #host tabindex=0>
+//    #shadowRoot (delegatesFocus)
+//      <div #aboveSlot tabindex=0>
+//      <slot #slotAbove tabindex=0>
+//        (slotted) <div #slottedAbove tabindex=0>
+//      <slot #slotBelow tabindex=0>
+//        (slotted) <div #slottedBelow tabindex=0>
+//      <div #belowSlot tabindex=0>
+// <div #belowHost tabindex=0>
+
+promise_test(() => {
+  let elementsInFlatTreeOrder;
+  let [aboveHost, host, aboveSlot, slotAbove, slottedAbove, slotBelow, slottedBelow, belowSlot, belowHost] = elementsInFlatTreeOrder = prepareDOM(document.body, true);
+  setTabIndex(elementsInFlatTreeOrder, 0);
+  resetFocus();
+  // Focus should move in flat tree order since every one of them has tabindex ==0,
+  // but doesn't include slots and #host.
+  return assertFocusOrder([aboveHost, aboveSlot, slottedAbove, slottedBelow, belowSlot, belowHost]);
+}, "Order when all tabindex=0 is and delegatesFocus = true");
+</script>
+</body>
index 9476017..74f72ae 100644 (file)
@@ -1,3 +1,49 @@
+2019-10-11  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Add the support for ShadowRoot.delegateFocus
+        https://bugs.webkit.org/show_bug.cgi?id=166484
+        <rdar://problem/29816058>
+
+        Reviewed by Antti Koivisto.
+
+        Implement delegatesFocus as specified in https://github.com/whatwg/html/pull/4796
+
+        When the shadow root of an element has delegates focus flag set, focusing on the shadow host would automatically
+        "delegates" focus to the first focusable element in the shadow tree instead.
+
+        The first focusable element is determined as the first element that is programatically focusable or mouse focusable
+        in the flat tree (composed tree in WebKit's terminology) in the case of the element getting focused via DOM API,
+        Element.prototype.focus, by via mouse down. In the case of sequential focus navigation (via tab key), it's the
+        first keyboard focusable element in the tabIndex order.
+
+        Tests: imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-click-method.html
+               imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-varies.html
+               imported/w3c/web-platform-tests/shadow-dom/focus/click-focus-delegatesFocus-tabindex-zero.html
+               imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus.html
+               imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-negative-delegatesFocus.html
+               imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-varying-delegatesFocus.html
+               imported/w3c/web-platform-tests/shadow-dom/focus/focus-tabindex-order-shadow-zero-delegatesFocus.html
+
+        * dom/Element.cpp:
+        (WebCore::Element::isKeyboardFocusable const): A shadow host with the delegates focus flag is not considered as
+        keyboard focusable. The rest is taken care of by the existing logic in FocusController.
+        (WebCore::isProgramaticallyFocusable): Extracted from Element::focus.
+        (WebCore::findFirstProgramaticallyFocusableElementInComposedTree): Added.
+        (WebCore::Element::focus): Added the support for delegatesFocus.
+        * dom/Element.h:
+        (WebCore::ShadowRootInit::delegatesFocus): Added.
+        * dom/Element.idl: Ditto.
+        * dom/ShadowRoot.cpp:
+        (WebCore::ShadowRoot::ShadowRoot): Added delegatesFocus to the constructor.
+        * dom/ShadowRoot.h:
+        * page/EventHandler.cpp:
+        (WebCore::findFirstMouseFocusableElementInComposedTree): Added.
+        (WebCore::EventHandler::dispatchMouseEvent): Added the support for delegatesFocus. Uses the first mouse focusable
+        element in the flat tree (composed tree) order.
+        * page/FocusController.cpp:
+        (WebCore::FocusController::findFirstFocusableElementInShadowRoot):
+        * page/FocusController.h:
+
 2019-10-11  Rob Buis  <rbuis@igalia.com>
 
         Cleanup RuntimeEnabledFeatures includes
index c769a92..965c40e 100644 (file)
@@ -35,6 +35,7 @@
 #include "ChromeClient.h"
 #include "ClassChangeInvalidation.h"
 #include "ComposedTreeAncestorIterator.h"
+#include "ComposedTreeIterator.h"
 #include "ContainerNodeAlgorithms.h"
 #include "CustomElementReactionQueue.h"
 #include "CustomElementRegistry.h"
@@ -278,7 +279,13 @@ void Element::setTabIndexForBindings(int value)
 
 bool Element::isKeyboardFocusable(KeyboardEvent*) const
 {
-    return isFocusable() && !shouldBeIgnoredInSequentialFocusNavigation() && tabIndexSetExplicitly().valueOr(0) >= 0;
+    if (!(isFocusable() && !shouldBeIgnoredInSequentialFocusNavigation() && tabIndexSetExplicitly().valueOr(0) >= 0))
+        return false;
+    if (auto* root = shadowRoot()) {
+        if (root->delegatesFocus())
+            return false;
+    }
+    return true;
 }
 
 bool Element::isMouseFocusable() const
@@ -2333,7 +2340,7 @@ ExceptionOr<ShadowRoot&> Element::attachShadow(const ShadowRootInit& init)
         return Exception { NotSupportedError };
     if (init.mode == ShadowRootMode::UserAgent)
         return Exception { TypeError };
-    auto shadow = ShadowRoot::create(document(), init.mode);
+    auto shadow = ShadowRoot::create(document(), init.mode, init.delegatesFocus ? ShadowRoot::DelegatesFocus::Yes : ShadowRoot::DelegatesFocus::No);
     auto& result = shadow.get();
     addShadowRoot(WTFMove(shadow));
     return result;
@@ -2881,41 +2888,76 @@ bool Element::hasAttributeNS(const AtomString& namespaceURI, const AtomString& l
     return elementData()->findAttributeByName(qName);
 }
 
+static bool isProgramaticallyFocusable(Element& element)
+{
+    ScriptDisallowedScope::InMainThread scriptDisallowedScope;
+    // If the stylesheets have already been loaded we can reliably check isFocusable.
+    // If not, we continue and set the focused node on the focus controller below so that it can be updated soon after attach.
+    if (element.document().haveStylesheetsLoaded()) {
+        if (!element.isFocusable())
+            return false;
+    }
+    return element.supportsFocus();
+}
+
+static RefPtr<Element> findFirstProgramaticallyFocusableElementInComposedTree(Element& host)
+{
+    ASSERT(host.shadowRoot());
+    for (auto& node : composedTreeDescendants(host)) {
+        if (!is<Element>(node))
+            continue;
+        auto& element = downcast<Element>(node);
+        if (isProgramaticallyFocusable(element))
+            return &element;
+    }
+    return nullptr;
+}
+
 void Element::focus(bool restorePreviousSelection, FocusDirection direction)
 {
     if (!isConnected())
         return;
 
-    if (document().focusedElement() == this) {
-        if (document().page())
-            document().page()->chrome().client().elementDidRefocus(*this);
+    auto document = makeRef(this->document());
+    if (document->focusedElement() == this) {
+        if (document->page())
+            document->page()->chrome().client().elementDidRefocus(*this);
+        return;
+    }
+
+    RefPtr<Element> newTarget = this;
+    if (document->haveStylesheetsLoaded())
+        document->updateStyleIfNeeded();
 
+    if (&newTarget->document() != document.ptr())
         return;
+
+    if (auto root = makeRefPtr(shadowRoot())) {
+        if (root->delegatesFocus()) {
+            newTarget = findFirstProgramaticallyFocusableElementInComposedTree(*this);            
+            if (!newTarget)
+                return;
+        }
     }
 
-    // If the stylesheets have already been loaded we can reliably check isFocusable.
-    // If not, we continue and set the focused node on the focus controller below so
-    // that it can be updated soon after attach. 
-    if (document().haveStylesheetsLoaded()) {
-        document().updateStyleIfNeeded();
-        if (!isFocusable())
-            return;
+    if (document->focusedElement() == newTarget) {
+        if (document->page())
+            document->page()->chrome().client().elementDidRefocus(*newTarget);
+        return;
     }
 
-    if (!supportsFocus())
+    if (!isProgramaticallyFocusable(*newTarget))
         return;
 
-    RefPtr<Node> protect;
-    if (Page* page = document().page()) {
-        auto& frame = *document().frame();
-        if (!frame.hasHadUserInteraction() && !frame.isMainFrame() && !document().topDocument().securityOrigin().canAccess(document().securityOrigin()))
+    if (Page* page = document->page()) {
+        auto& frame = *document->frame();
+        if (!frame.hasHadUserInteraction() && !frame.isMainFrame() && !document->topDocument().securityOrigin().canAccess(document->securityOrigin()))
             return;
 
         // Focus and change event handlers can cause us to lose our last ref.
         // If a focus event handler changes the focus to a different node it
         // does not make sense to continue and update appearence.
-        protect = this;
-        if (!page->focusController().setFocusedElement(this, *document().frame(), direction))
+        if (!page->focusController().setFocusedElement(newTarget.get(), *document->frame(), direction))
             return;
     }
 
@@ -2924,7 +2966,7 @@ void Element::focus(bool restorePreviousSelection, FocusDirection direction)
     // Focusing a form element triggers animation in UIKit to scroll to the right position.
     // Calling updateFocusAppearance() would generate an unnecessary call to ScrollView::setScrollPosition(),
     // which would jump us around during this animation. See <rdar://problem/6699741>.
-    bool isFormControl = is<HTMLFormControlElement>(*this);
+    bool isFormControl = is<HTMLFormControlElement>(newTarget);
     if (isFormControl)
         revealMode = SelectionRevealMode::RevealUpToMainFrame;
 #endif
@@ -2936,6 +2978,7 @@ void Element::focus(bool restorePreviousSelection, FocusDirection direction)
     target->updateFocusAppearance(restorePreviousSelection ? SelectionRestorationMode::Restore : SelectionRestorationMode::SetDefault, revealMode);
 }
 
+// https://html.spec.whatwg.org/#focus-processing-model
 RefPtr<Element> Element::focusAppearanceUpdateTarget()
 {
     return this;
index c34b57e..408ce34 100644 (file)
@@ -290,6 +290,7 @@ public:
 
     struct ShadowRootInit {
         ShadowRootMode mode;
+        bool delegatesFocus { false };
     };
     ExceptionOr<ShadowRoot&> attachShadow(const ShadowRootInit&);
 
index 4c51d0a..fae40b9 100644 (file)
 
 dictionary ShadowRootInit {
     required ShadowRootMode mode;
+    boolean delegatesFocus = false;
 };
 
 Element implements AccessibilityRole;
index 2a3267c..edea322 100644 (file)
@@ -56,9 +56,10 @@ struct SameSizeAsShadowRoot : public DocumentFragment, public TreeScope {
 
 COMPILE_ASSERT(sizeof(ShadowRoot) == sizeof(SameSizeAsShadowRoot), shadowroot_should_stay_small);
 
-ShadowRoot::ShadowRoot(Document& document, ShadowRootMode type)
+ShadowRoot::ShadowRoot(Document& document, ShadowRootMode type, DelegatesFocus delegatesFocus)
     : DocumentFragment(document, CreateShadowRoot)
     , TreeScope(*this, document)
+    , m_delegatesFocus(delegatesFocus == DelegatesFocus::Yes)
     , m_type(type)
     , m_styleScope(makeUnique<Style::Scope>(*this))
 {
index 7657858..6615e05 100644 (file)
@@ -41,9 +41,12 @@ class StyleSheetList;
 class ShadowRoot final : public DocumentFragment, public TreeScope {
     WTF_MAKE_ISO_ALLOCATED(ShadowRoot);
 public:
-    static Ref<ShadowRoot> create(Document& document, ShadowRootMode type)
+
+    enum class DelegatesFocus : uint8_t { Yes, No };
+
+    static Ref<ShadowRoot> create(Document& document, ShadowRootMode type, DelegatesFocus delegatesFocus = DelegatesFocus::No)
     {
-        return adoptRef(*new ShadowRoot(document, type));
+        return adoptRef(*new ShadowRoot(document, type, delegatesFocus));
     }
 
     static Ref<ShadowRoot> create(Document& document, std::unique_ptr<SlotAssignment>&& assignment)
@@ -61,6 +64,7 @@ public:
     bool resetStyleInheritance() const { return m_resetStyleInheritance; }
     void setResetStyleInheritance(bool);
 
+    bool delegatesFocus() const { return m_delegatesFocus; }
     bool containsFocusedElement() const { return m_containsFocusedElement; }
     void setContainsFocusedElement(bool flag) { m_containsFocusedElement = flag; }
 
@@ -100,12 +104,10 @@ public:
     const PartMappings& partMappings() const;
     void invalidatePartMappings();
 
-protected:
-    ShadowRoot(Document&, ShadowRootMode);
-
+private:
+    ShadowRoot(Document&, ShadowRootMode, DelegatesFocus);
     ShadowRoot(Document&, std::unique_ptr<SlotAssignment>&&);
 
-private:
     bool childTypeAllowed(NodeType) const override;
 
     Ref<Node> cloneNodeInternal(Document&, CloningOperation) override;
@@ -117,6 +119,7 @@ private:
 
     bool m_resetStyleInheritance { false };
     bool m_hasBegunDeletingDetachedChildren { false };
+    bool m_delegatesFocus { false };
     bool m_containsFocusedElement { false };
     ShadowRootMode m_type { ShadowRootMode::UserAgent };
 
index b9ef9ee..7ce5b3d 100644 (file)
@@ -34,6 +34,7 @@
 #include "Chrome.h"
 #include "ChromeClient.h"
 #include "ComposedTreeAncestorIterator.h"
+#include "ComposedTreeIterator.h"
 #include "CursorList.h"
 #include "DocumentMarkerController.h"
 #include "DragController.h"
@@ -2619,6 +2620,19 @@ void EventHandler::updateMouseEventTargetNode(Node* targetNode, const PlatformMo
     }
 }
 
+static RefPtr<Element> findFirstMouseFocusableElementInComposedTree(Element& host)
+{
+    ASSERT(host.shadowRoot());
+    for (auto& node : composedTreeDescendants(host)) {
+        if (!is<Element>(node))
+            continue;
+        auto& element = downcast<Element>(node);
+        if (element.isMouseFocusable())
+            return &element;
+    }
+    return nullptr;
+}
+
 bool EventHandler::dispatchMouseEvent(const AtomString& eventType, Node* targetNode, bool /*cancelable*/, int clickCount, const PlatformMouseEvent& platformMouseEvent, bool setUnder)
 {
     Ref<Frame> protectedFrame(m_frame);
@@ -2650,6 +2664,12 @@ bool EventHandler::dispatchMouseEvent(const AtomString& eventType, Node* targetN
     // Walk up the DOM tree to search for an element to focus.
     RefPtr<Element> element;
     for (element = m_elementUnderMouse.get(); element; element = element->parentElementInComposedTree()) {
+        if (auto* shadowRoot = element->shadowRoot()) {
+            if (shadowRoot->delegatesFocus()) {
+                element = findFirstMouseFocusableElementInComposedTree(*element);
+                break;
+            }
+        }
         if (element->isMouseFocusable())
             break;
     }