commit 99b320901a54318b2fb25abaad74a79678d09fac
parent 7638a4348e53857fdf135192073a6254dee3b840
Author: Emilio Cobos Álvarez <emilio@crisal.io>
Date: Mon, 27 Oct 2025 15:23:52 +0000
Bug 1996182 - Clear stale focus-within state from ContentRemoved. r=smaug
If the element we're in the process of focusing gets removed in between
the blur and the focus (in focusout in the test-case), clear the
:focus-within state properly.
Differential Revision: https://phabricator.services.mozilla.com/D270141
Diffstat:
2 files changed, 65 insertions(+), 10 deletions(-)
diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp
@@ -8,6 +8,7 @@
#include <algorithm>
+#include "AncestorIterator.h"
#include "BrowserChild.h"
#include "ChildIterator.h"
#include "ContentParent.h"
@@ -926,22 +927,40 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument,
return NS_OK;
}
- // if the content is currently focused in the window, or is an
- // shadow-including inclusive ancestor of the currently focused element,
- // reset the focus within that window.
- Element* previousFocusedElementPtr = windowPtr->GetFocusedElement();
- if (!previousFocusedElementPtr) {
+ const Element* focusWithinElement = [&]() -> Element* {
+ if (auto* el = Element::FromNode(aContent)) {
+ return el;
+ }
+ if (auto* shadow = ShadowRoot::FromNode(aContent)) {
+ // Note that we only get here with ShadowRoots for shadow roots of form
+ // controls that we can un-attach. So if there's a focused element it must
+ // be inside our shadow tree already.
+ return shadow->Host();
+ }
+ // Removing text / comments / etc can't affect the focus state.
+ return nullptr;
+ }();
+ if (!focusWithinElement ||
+ !focusWithinElement->State().HasAtLeastOneOfStates(
+ ElementState::FOCUS | ElementState::FOCUS_WITHIN)) {
return NS_OK;
}
- if (!nsContentUtils::ContentIsHostIncludingDescendantOf(
- previousFocusedElementPtr, aContent)) {
+ // if the content is currently focused in the window, or is an
+ // shadow-including inclusive ancestor of the currently focused element,
+ // reset the focus within that window.
+ RefPtr previousFocusedElement = windowPtr->GetFocusedElement();
+ if (!previousFocusedElement) {
+ // If we're in-between a blur and an incoming focus, we might have stale
+ // :focus-within in our ancestor chain. Fix it up now.
+ for (auto* el :
+ focusWithinElement->InclusiveFlatTreeAncestorsOfType<Element>()) {
+ el->RemoveStates(ElementState::FOCUS_WITHIN, true);
+ }
return NS_OK;
}
RefPtr<nsPIDOMWindowOuter> window = windowPtr;
- RefPtr<Element> previousFocusedElement = previousFocusedElementPtr;
-
RefPtr<Element> newFocusedElement = [&]() -> Element* {
if (auto* sr = ShadowRoot::FromNode(aContent)) {
if (sr->IsUAWidget() && sr->Host()->IsHTMLElement(nsGkAtoms::input)) {
@@ -1014,7 +1033,7 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument,
}
if (!newFocusedElement) {
- NotifyFocusStateChange(previousFocusedElement, newFocusedElement, 0,
+ NotifyFocusStateChange(previousFocusedElement, nullptr, 0,
/* aGettingFocus = */ false, false);
} else {
// We should already have the right state, which is managed by the <input>
diff --git a/testing/web-platform/tests/css/selectors/focus-within-removal.html b/testing/web-platform/tests/css/selectors/focus-within-removal.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Removing an element from its own focus() call doesn't leave stale :focus-within state</title>
+<link rel="help" href="https://drafts.csswg.org/selectors-4/#the-focus-within-pseudo">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1996182">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.com" title="Mozilla">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="root">
+ <div id="container" tabindex="-1">
+ <input type="text">
+ </div>
+</div>
+<script>
+onload = function() {
+ test(function() {
+ let root = document.getElementById("root");
+ let container = document.getElementById("container");
+ let input = document.querySelector("input");
+
+ input.focus();
+ assert_equals(document.activeElement, input, "activeElement after focus");
+ assert_true(container.matches(":focus-within"), "container matches :focus-within");
+ assert_true(root.matches(":focus-within"), "root also matches :focus-within");
+ // This fires from within the next focus() call
+ input.addEventListener("focusout", () => { root.innerHTML = "" });
+ // container is focusable, but gets removed before we get a chance at focusing it.
+ container.focus();
+ assert_equals(document.activeElement, document.body, "activeElement after trying to focus sibling");
+ assert_equals(container.parentNode, null, "container should get removed");
+ assert_false(container.matches(":focus-within"), "container no longer matches :focus-within");
+ assert_false(root.matches(":focus-within"), "root no longer matches :focus-within");
+ });
+}
+</script>