tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 72bf441d8ee08eeac5e63b77a2dec65b236637df
parent f91857b4742e59c433d2558f3c2cc3c62c049544
Author: Emilio Cobos Álvarez <emilio@crisal.io>
Date:   Mon, 27 Oct 2025 19:18:33 +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:
Mdom/base/nsFocusManager.cpp | 54++++++++++++++++++++++++++++++++++++++++++++++--------
Atesting/web-platform/tests/css/selectors/focus-within-removal.html | 36++++++++++++++++++++++++++++++++++++
2 files changed, 82 insertions(+), 8 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" @@ -913,8 +914,8 @@ void nsFocusManager::ContentAppended(nsIContent* aFirstNewContent, nsresult nsFocusManager::ContentRemoved(Document* aDocument, nsIContent* aContent, const ContentRemoveInfo& aInfo) { - NS_ENSURE_ARG(aDocument); - NS_ENSURE_ARG(aContent); + MOZ_ASSERT(aDocument); + MOZ_ASSERT(aContent); if (aInfo.mNewParent) { // Handled upon insertion in ContentAppended/Inserted. @@ -926,22 +927,59 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument, return NS_OK; } + 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) { + return NS_OK; + } + + const bool hasFocusWithinInThisDocument = + focusWithinElement->State().HasAtLeastOneOfStates( + ElementState::FOCUS | ElementState::FOCUS_WITHIN); + // 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) { + if (hasFocusWithinInThisDocument) { + // 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; } - if (!nsContentUtils::ContentIsHostIncludingDescendantOf( - previousFocusedElementPtr, aContent)) { + if (previousFocusedElementPtr->State().HasState(ElementState::FOCUS)) { + if (!hasFocusWithinInThisDocument) { + // If the focused element has :focus, that means our ancestor should have + // focus-within. + return NS_OK; + } + } else if (!nsContentUtils::ContentIsFlattenedTreeDescendantOf( + previousFocusedElementPtr, focusWithinElement)) { + // Otherwise, previousFocusedElementPtr could be an <iframe>, we still need + // to clear it in that case. return NS_OK; } - RefPtr<nsPIDOMWindowOuter> window = windowPtr; - RefPtr<Element> previousFocusedElement = previousFocusedElementPtr; - + RefPtr previousFocusedElement = previousFocusedElementPtr; + RefPtr window = windowPtr; RefPtr<Element> newFocusedElement = [&]() -> Element* { if (auto* sr = ShadowRoot::FromNode(aContent)) { if (sr->IsUAWidget() && sr->Host()->IsHTMLElement(nsGkAtoms::input)) { @@ -1014,7 +1052,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>