Skip to content

Commit

Permalink
Update delegatesFocus to match recent spec changes
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=232564

Reviewed by Darin Adler.

Implemented the new behavior of focusDelegates and added the support for autofocus focus delegation.
Also fixed a bug that an element with autofocus content attribute inside a shadow tree gets focus.

See whatwg/html#7079

* LayoutTests/fast/shadow-dom/autofocus-in-shadow-tree-autofocus-host-expected.txt: Added.
* LayoutTests/fast/shadow-dom/autofocus-in-shadow-tree-autofocus-host.html: Added.
* LayoutTests/fast/shadow-dom/autofocus-in-shadow-tree-expected.txt: Added.
* LayoutTests/fast/shadow-dom/autofocus-in-shadow-tree.html: Added.
* LayoutTests/imported/w3c/web-platform-tests/css/selectors/focus-visible-020.html: Merge the test fix
posted in web-platform-tests/wpt#35257.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-autofocus-expected.txt: Rebaselined.
* LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-method-delegatesFocus-expected.txt: Ditto.

* Source/WebCore/dom/Element.cpp:
(WebCore::shouldAutofocus): Fixed the bug that this wasn't ignoring elements inside shadow trees.
Autofocus attribute yields autofocus candidates if the relevant element is **inserted** into a document,
not connected to it. See https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute

(WebCore::autoFocusDelegate): Added. This implements autofocus delegation mechanism for shadow root/host.

(WebCore::focusDelegateFromShadowHost): Renamed from findFirstProgramaticallyFocusableElementInComposedTree
and made it recursively traverse shadow trees instead of the composed/flat tree.

(WebCore::Element::focus):

Canonical link: https://commits.webkit.org/252901@main
  • Loading branch information
rniwa committed Jul 28, 2022
1 parent 50d2549 commit c1607d1
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

PASS an element with autofocus content attribute in a shadow tree whereby shadow host also has autofocus should automatically get focus

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<body>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id="host" tabindex="0" autofocus></div>
<script>

const shadowRoot = host.attachShadow({mode: 'closed', delegatesFocus: true});
shadowRoot.innerHTML = `<input type="text" id="target"><input type="text" id="target" autofocus>`;

promise_test(async () => {
await new Promise((resolve) => { requestAnimationFrame(() => setTimeout(resolve, 0)); });
assert_equals(document.activeElement, host);
assert_equals(shadowRoot.activeElement, shadowRoot.lastChild);
}, 'an element with autofocus content attribute in a shadow tree whereby shadow host also has autofocus should automatically get focus');

</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

PASS an element with autofocus content attribute in a shadow tree should not automatically get focus

20 changes: 20 additions & 0 deletions LayoutTests/fast/shadow-dom/autofocus-in-shadow-tree.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<body>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id="host" tabindex="0"></div>
<script>

const shadowRoot = host.attachShadow({mode: 'closed', delegatesFocus: true});
shadowRoot.innerHTML = `<input type="text" id="target" autofocus>`;

promise_test(async () => {
await new Promise(requestAnimationFrame);
assert_equals(document.activeElement, document.body);
assert_equals(shadowRoot.activeElement, null);
}, 'an element with autofocus content attribute in a shadow tree should not automatically get focus');

</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
test_valid_selector(':focus-visible');

async_test((t) => {
host.focus();
window.requestAnimationFrame(t.step_func_done(() => {
assert_not_equals(getComputedStyle(host).backgroundColor, "rgb(255, 0, 0)", `backgroundColor for ${host.tagName}#${host.id} should NOT be red`);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@

FAIL The second input should be focused since it has autofocus assert_equals: expected Element node <div tabindex="0" autofocus="true"></div> but got Element node <div tabindex="0"></div>
FAIL Focus should not be delegated to the autofocus element because the inner host doesn't have delegates focus assert_equals: expected Element node <body>
<script>
function createShadowDOMTree() {
... but got Element node <div id="host"></div>
FAIL Focus should be delegated to the autofocus element when the inner host has delegates focus assert_equals: expected Element node <div tabindex="0" autofocus="true"></div> but got Element node <div tabindex="0"></div>
FAIL Focus should not be delegated to the slotted elements assert_equals: expected Element node <div id="host"><div tabindex="0" autofocus="true"></div><... but got Element node <div tabindex="0" autofocus="true"></div>
FAIL Focus should be delegated to the nested div which has autofocus based on the tree order assert_equals: expected Element node <div tabindex="0" autofocus="true"></div> but got Element node <div tabindex="0"><div tabindex="0"><div tabindex="0" aut...
PASS The second input should be focused since it has autofocus
PASS Focus should not be delegated to the autofocus element because the inner host doesn't have delegates focus
PASS Focus should be delegated to the autofocus element when the inner host has delegates focus
PASS Focus should not be delegated to the slotted elements
PASS Focus should be delegated to the nested div which has autofocus based on the tree order

Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ PASS focus() on host with delegatesFocus & tabindex=0, #belowSlots with tabindex
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
FAIL focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots with tabindex=0 assert_equals: expected Element node <div tabindex="0">belowSlots</div> but got null
PASS focus() on host with delegatesFocus & tabindex=0, #slottedToFirstSlot, #slottedToSecondSlot, #belowSlots with tabindex=0
PASS focus() on host with delegatesFocus and already-focused non-first shadow descendant
FAIL focus() on host with delegatesFocus with another host with no delegatesFocus and a focusable child assert_equals: expected Element node <input></input> but got Element node <span></span>
PASS focus() on host with delegatesFocus with another host with no delegatesFocus and a focusable child
PASS focus() on host with delegatesFocus with another host with delegatesFocus and a focusable child
FAIL focus() on host with delegatesFocus and slotted focusable children assert_equals: expected Element node <div><div><input></div></div> but got Element node <input></input>
PASS focus() on host with delegatesFocus and slotted focusable children

34 changes: 27 additions & 7 deletions Source/WebCore/dom/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ static bool shouldAutofocus(const Element& element)
return false;

auto& document = element.document();
if (!element.isConnected() || !document.hasBrowsingContext())
if (!element.isInDocumentTree() || !document.hasBrowsingContext())
return false;

if (document.isSandboxed(SandboxAutomaticFeatures)) {
// FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists.
document.addConsoleMessage(MessageSource::Security, MessageLevel::Error, "Blocked autofocusing on a form control because the form's frame is sandboxed and the 'allow-scripts' permission is not set."_s);
Expand Down Expand Up @@ -3209,13 +3210,32 @@ static bool isProgramaticallyFocusable(Element& element)
return element.supportsFocus();
}

static RefPtr<Element> findFirstProgramaticallyFocusableElementInComposedTree(Element& host)
// https://html.spec.whatwg.org/multipage/interaction.html#autofocus-delegate
static RefPtr<Element> autoFocusDelegate(ShadowRoot& target)
{
ASSERT(host.shadowRoot());
for (auto& node : composedTreeDescendants(host)) {
if (!is<Element>(node))
for (auto& element : descendantsOfType<Element>(target)) {
if (!element.hasAttributeWithoutSynchronization(HTMLNames::autofocusAttr))
continue;
auto& element = downcast<Element>(node);
if (auto root = shadowRootWithDelegatesFocus(element)) {
if (auto target = autoFocusDelegate(*root))
return target;
}
if (isProgramaticallyFocusable(element))
return &element;
}
return nullptr;
}

// https://html.spec.whatwg.org/multipage/interaction.html#focus-delegate
static RefPtr<Element> focusDelegateFromShadowHost(ShadowRoot& target)
{
if (auto element = autoFocusDelegate(target))
return element;
for (auto& element : descendantsOfType<Element>(target)) {
if (auto root = shadowRootWithDelegatesFocus(element)) {
if (auto target = focusDelegateFromShadowHost(*root))
return target;
}
if (isProgramaticallyFocusable(element))
return &element;
}
Expand Down Expand Up @@ -3252,7 +3272,7 @@ void Element::focus(const FocusOptions& options)
return;
}

newTarget = findFirstProgramaticallyFocusableElementInComposedTree(*this);
newTarget = focusDelegateFromShadowHost(*root);
if (!newTarget)
return;
} else if (!isProgramaticallyFocusable(*newTarget))
Expand Down

0 comments on commit c1607d1

Please sign in to comment.