Bug 1741936 - Implement focus delegate algorithm r=emilio

Spec: https://html.spec.whatwg.org/multipage/interaction.html#focus-delegate

Differential Revision: https://phabricator.services.mozilla.com/D153689
This commit is contained in:
Sean Feng 2022-10-14 01:20:08 +00:00
Родитель 64a41edf66
Коммит c6d5b35934
10 изменённых файлов: 85 добавлений и 107 удалений

Просмотреть файл

@ -1027,6 +1027,68 @@ bool nsIContent::IsFocusable(int32_t* aTabIndex, bool aWithMouse) {
return false;
}
Element* nsIContent::GetFocusDelegate(bool aWithMouse) const {
const nsIContent* whereToLook = this;
if (ShadowRoot* root = GetShadowRoot()) {
if (!root->DelegatesFocus()) {
// 1. If focusTarget is a shadow host and its shadow root 's delegates
// focus is false, then return null.
return nullptr;
}
whereToLook = root;
}
auto IsFocusable = [&](Element* aElement) {
nsIFrame* frame = aElement->GetPrimaryFrame();
return frame && frame->IsFocusable(aWithMouse);
};
Element* potentialFocus = nullptr;
for (nsINode* node = whereToLook->GetFirstChild(); node;
node = node->GetNextNode(whereToLook)) {
auto* el = Element::FromNode(*node);
if (!el) {
continue;
}
const bool autofocus = el->GetBoolAttr(nsGkAtoms::autofocus);
if (autofocus) {
if (IsFocusable(el)) {
// Found an autofocus candidate.
return el;
}
} else if (!potentialFocus && IsFocusable(el)) {
// This element could be the one if we can't find an
// autofocus candidate which has the precedence.
potentialFocus = el;
}
if (!autofocus && potentialFocus) {
// Nothing else to do, we are not looking for more focusable elements
// here.
continue;
}
if (auto* shadow = el->GetShadowRoot()) {
if (shadow->DelegatesFocus()) {
if (Element* delegatedFocus = shadow->GetFocusDelegate(aWithMouse)) {
if (autofocus) {
// This element has autofocus and we found an focus delegates
// in its descendants, so use the focus delegates
return delegatedFocus;
}
if (!potentialFocus) {
potentialFocus = delegatedFocus;
}
}
}
}
}
return potentialFocus;
}
bool nsIContent::IsFocusableInternal(int32_t* aTabIndex, bool aWithMouse) {
if (aTabIndex) {
*aTabIndex = -1; // Default, not tabbable

Просмотреть файл

@ -807,37 +807,6 @@ void ShadowRoot::MaybeUnslotHostChild(nsIContent& aChild) {
}
}
Element* ShadowRoot::GetFirstFocusable(bool aWithMouse) const {
MOZ_ASSERT(DelegatesFocus(), "Why are we here?");
Element* potentialFocus = nullptr;
for (nsINode* node = GetFirstChild(); node; node = node->GetNextNode(this)) {
auto* el = Element::FromNode(*node);
if (!el) {
continue;
}
nsIFrame* frame = el->GetPrimaryFrame();
if (frame && frame->IsFocusable(aWithMouse)) {
if (el->GetBoolAttr(nsGkAtoms::autofocus)) {
return el;
}
if (!potentialFocus) {
potentialFocus = el;
}
}
if (!potentialFocus) {
ShadowRoot* shadow = el->GetShadowRoot();
if (shadow && shadow->DelegatesFocus()) {
if (Element* nested = shadow->GetFirstFocusable(aWithMouse)) {
potentialFocus = nested;
}
}
}
}
return potentialFocus;
}
void ShadowRoot::MaybeSlotHostChild(nsIContent& aChild) {
MOZ_ASSERT(aChild.GetParent() == GetHost());
// Check to ensure that the child not an anonymous subtree root because even

Просмотреть файл

@ -67,9 +67,6 @@ class ShadowRoot final : public DocumentFragment,
// child from the currently-assigned slot, if any.
void MaybeUnslotHostChild(nsIContent&);
// Find the first focusable element in this tree.
Element* GetFirstFocusable(bool aWithMouse) const;
// Shadow DOM v1
Element* Host() const {
MOZ_ASSERT(GetHost(),

Просмотреть файл

@ -2138,7 +2138,7 @@ Element* nsFocusManager::FlushAndCheckIfFocusable(Element* aElement,
}
if (Element* firstFocusable =
root->GetFirstFocusable(aFlags & FLAG_BYMOUSE)) {
root->GetFocusDelegate(aFlags & FLAG_BYMOUSE)) {
return firstFocusable;
}
}

Просмотреть файл

@ -296,6 +296,9 @@ class nsIContent : public nsINode {
bool IsFocusable(int32_t* aTabIndex = nullptr, bool aWithMouse = false);
virtual bool IsFocusableInternal(int32_t* aTabIndex, bool aWithMouse);
// https://html.spec.whatwg.org/multipage/interaction.html#focus-delegate
mozilla::dom::Element* GetFocusDelegate(bool aWithMouse) const;
/*
* Get desired IME state for the content.
*

Просмотреть файл

@ -3491,7 +3491,7 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext,
if (ShadowRoot* root = newFocus->GetShadowRoot()) {
if (root->DelegatesFocus()) {
if (Element* firstFocusable =
root->GetFirstFocusable(/* aWithMouse */ true)) {
root->GetFocusDelegate(/* aWithMouse */ true)) {
newFocus = firstFocusable;
break;
}

Просмотреть файл

@ -143,28 +143,7 @@ void HTMLDialogElement::FocusDialog() {
doc->FlushPendingNotifications(FlushType::Frames);
}
Element* controlCandidate = nullptr;
for (auto* child = GetFirstChild(); child; child = child->GetNextNode(this)) {
auto* element = Element::FromNode(child);
if (!element) {
continue;
}
nsIFrame* frame = element->GetPrimaryFrame();
if (!frame || !frame->IsFocusable()) {
continue;
}
if (element->HasAttr(nsGkAtoms::autofocus)) {
// Find the first descendant of element of subject that this not inert and
// has autofocus attribute.
controlCandidate = element;
break;
}
if (!controlCandidate) {
// If there isn't one, then let control be the first non-inert descendant
// element of subject, in tree order.
controlCandidate = element;
}
}
Element* controlCandidate = GetFocusDelegate(false /* aWithMouse */);
// If there isn't one of those either, then let control be subject.
if (!controlCandidate) {

Просмотреть файл

@ -1,8 +0,0 @@
[dialog-focus-shadow-double-nested.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[show()]
expected: FAIL
[showModal()]
expected: FAIL

Просмотреть файл

@ -1,38 +0,0 @@
[dialog-focus-shadow.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[show: No autofocus, yes delegatesFocus, no siblings]
expected: FAIL
[showModal: No autofocus, yes delegatesFocus, no siblings]
expected: FAIL
[show: No autofocus, yes delegatesFocus, sibling after]
expected: FAIL
[showModal: No autofocus, yes delegatesFocus, sibling after]
expected: FAIL
[show: Autofocus on shadow host, yes delegatesFocus, no siblings]
expected: FAIL
[showModal: Autofocus on shadow host, yes delegatesFocus, no siblings]
expected: FAIL
[show: Autofocus on shadow host, yes delegatesFocus, sibling before]
expected: FAIL
[showModal: Autofocus on shadow host, yes delegatesFocus, sibling before]
expected: FAIL
[show: Autofocus on shadow host, yes delegatesFocus, sibling after]
expected: FAIL
[showModal: Autofocus on shadow host, yes delegatesFocus, sibling after]
expected: FAIL
[show: Autofocus inside shadow tree, yes delegatesFocus, no siblings]
expected: FAIL
[showModal: Autofocus inside shadow tree, yes delegatesFocus, no siblings]
expected: FAIL

Просмотреть файл

@ -171,10 +171,10 @@
<dialog data-description="Autofocus inside shadow tree, yes delegatesFocus, sibling after">
<template class="turn-into-shadow-tree delegates-focus">
<button tabindex="-1">Focusable</button>
<button tabindex="-1" autofocus>Focusable</button>
<button tabindex="-1" autofocus class="focus-me">Focusable</button>
<button disabled>Non-focusable</button>
</template>
<button tabindex="-1" class="focus-me">Focusable</button>
<button tabindex="-1">Focusable</button>
</dialog>
<dialog data-description="Autofocus inside shadow tree, no delegatesFocus, no siblings">
@ -203,11 +203,25 @@
<button tabindex="-1" class="focus-me">Focusable</button>
</dialog>
<dialog data-description="Two shadow trees, both delegatesFocus, first tree doesn't have autofocus element, second does">
<template class="turn-into-shadow-tree delegates-focus">
<button disabled>Non-focusable</button>
<button tabindex="-1" class="focus-me">Focusable</button>
<button disabled>Non-focusable</button>
</template>
<template class="turn-into-shadow-tree delegates-focus">
<button tabindex="-1" autofocus>Focusable</button>
</template>
</dialog>
<script>
for (const template of document.querySelectorAll(".turn-into-shadow-tree")) {
const div = document.createElement("div");
div.attachShadow({ mode: "open", delegatesFocus: template.classList.contains("delegates-focus") });
div.autofocus = template.classList.contains("autofocus");
if (template.classList.contains("autofocus")) {
div.setAttribute("autofocus", true);
}
div.shadowRoot.append(template.content);
template.replaceWith(div);
}