Skip to content
Permalink
Browse files
:focus-within pseudo class doesn't get invalidated when frame loses f…
…ocus

https://bugs.webkit.org/show_bug.cgi?id=242526

Reviewed by Antti Koivisto and Darin Adler.

This patch fixes the bug that :focus-within pseudo fails to start applying or stop applying upon frame
gaining or losing focus, and makes pseudo class invalidation more precise.

A new boolean flag FrameSelection::m_isActive, which mirrors the value of FocusController::isActive()
while the frame associated with FrameSelection is focused, is introduced and used in FrameSelection's
isFocusedAndActive on behalf of FocusController::isActive(). It's used to compute the value of :focus,
:focus-visible, :focus-within and pseudo classes. Most notably, FrameSelection::pageActivationChanged
and FrameSelection::setFocused use it to flip the flag with Style::PseudoClassChangeInvalidation.

Note that the value of FrameSelection::m_isActive could be stale when the frame associated with
FrameSelection is no longer focused. This isn't an issue in practice since it means isFocusedAndActive
would always return false in such cases (since m_focused check fails).

* LayoutTests/fast/shadow-dom/focus-ring-on-shadow-host-focuswithin-expected.html: Added.
* LayoutTests/fast/shadow-dom/focus-ring-on-shadow-host-focuswithin.html: Added.
* LayoutTests/fast/shadow-dom/focus-ring-on-shadow-host-sibling-expected.html: Added.
* LayoutTests/fast/shadow-dom/focus-ring-on-shadow-host-sibling.html: Added.
* LayoutTests/platform/gtk/TestExpectations:
* LayoutTests/platform/win/TestExpectations:
* Source/WebCore/css/SelectorChecker.cpp:
(WebCore::SelectorChecker::checkOne const): Fixed a bug that this was checking hasFocusWithin
without isFrameFocused.
* Source/WebCore/editing/FrameSelection.cpp:
(WebCore::isPageActive): Added.
(WebCore::FrameSelection::FrameSelection): Initialize newly introduced m_isActive.
(WebCore::FrameSelection::focusedOrActiveStateChanged): Moved the logic to invalidate elements to
callers of this function: FrameSelection::pageActivationChanged and FrameSelection::setFocused.
(WebCore::invalidateFocusedElementAndShadowIncludingAncestors): Added.
(WebCore::FrameSelection::pageActivationChanged):
(WebCore::FrameSelection::setFocused):
(WebCore::FrameSelection::isFocusedAndActive const):
* Source/WebCore/editing/FrameSelection.h:

Canonical link: https://commits.webkit.org/252324@main
  • Loading branch information
rniwa committed Jul 10, 2022
1 parent e826a8a commit ac8282feee7f212d45261661d82051c83d344cc2
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 14 deletions.
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<body>
<style>
#host {
width: 100px;
height: 100px;
background: green;
}
</style>
<div id="host"></div>
</body>
</html>
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<body>
<style>
#parent {
width: 100px;
height: 100px;
background: green;
}
#parent:focus-within {
width: 50px;
height: 50px;
}
#host {
width: 50px;
height: 50px;
outline: none;
}
</style>
<div id="parent"><div id="host"></div></div>
<div></div>
<script src="../../resources/ui-helper.js"></script>
<script>

const shadowRoot = host.attachShadow({mode: 'closed', delegatesFocus: true});
shadowRoot.innerHTML = `<div tabindex="0"></div>
<style>
div { width: 50px; height: 50px; }
</style>`;

onload = runTest;

async function runTest() {
if (!window.testRunner)
return;

testRunner.waitUntilDone();

eventSender.mouseMoveTo(host.offsetLeft + 5, host.offsetTop + 5);
eventSender.mouseDown();
eventSender.mouseUp();

await new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 0)));

await UIHelper.resignFirstResponder();

await new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 0)));

testRunner.notifyDone();
}

</script>
</body>
</html>
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<body>
<style>
#host {
width: 100px;
height: 100px;
background: green;
}
</style>
<div id="host"></div>
</body>
</html>
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<body>
<style>
#host:focus {
width: 50px;
height: 50px;
background: green;
overflow: hidden;
}
#host + div {
width: 100px;
height: 50px;
background: green;
}
#host:focus + div {
width: 50px;
height: 50px;
}
</style>
<div id="host"></div>
<div></div>
<script src="../../resources/ui-helper.js"></script>
<script>

const shadowRoot = host.attachShadow({mode: 'closed', delegatesFocus: true});
shadowRoot.innerHTML = `<div tabindex="0"></div>
<style>
div { width: 100px; height: 50px; background: green; }
</style>`;
const foucsableElement = shadowRoot.querySelector('div');

onload = runTest;

async function runTest() {
if (!window.testRunner)
return;

testRunner.waitUntilDone();

eventSender.mouseMoveTo(host.offsetLeft + 5, host.offsetTop + 5);
eventSender.mouseDown();
eventSender.mouseUp();

await new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 0)));

await UIHelper.resignFirstResponder();

await new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 0)));

testRunner.notifyDone();
}

</script>
</body>
</html>
@@ -911,7 +911,9 @@ webkit.org/b/242145 media/mediacapabilities/mediacapabilities-allowed-codecs.htm
webkit.org/b/242145 media/mediacapabilities/mediacapabilities-allowed-containers.html [ Skip ]

# resignFirstResponder is not implemented on GTK+
fast/shadow-dom/focus-ring-on-shadow-host.html
fast/shadow-dom/focus-ring-on-shadow-host.html [ Skip ]
fast/shadow-dom/focus-ring-on-shadow-host-focuswithin.html [ Skip ]
fast/shadow-dom/focus-ring-on-shadow-host-sibling.html [ Skip ]

#////////////////////////////////////////////////////////////////////////////////////////
# End of Expected failures.
@@ -989,7 +989,9 @@ webkit.org/b/77568 fast/text/locale-shaping-complex.html [ ImageOnlyFailure ]
fast/canvas/toDataURL-alpha-permutation.html [ ImageOnlyFailure ]

# resignFirstResponder is not implemented on Windows
fast/shadow-dom/focus-ring-on-shadow-host.html
fast/shadow-dom/focus-ring-on-shadow-host.html [ Skip ]
fast/shadow-dom/focus-ring-on-shadow-host-focuswithin.html [ Skip ]
fast/shadow-dom/focus-ring-on-shadow-host-sibling.html [ Skip ]

################################################################################
########### End Missing Functionality Prevents Testing ##############
@@ -991,7 +991,7 @@ bool SelectorChecker::checkOne(CheckingContext& checkingContext, const LocalCont
case CSSSelector::PseudoClassFocusVisible:
return matchesFocusVisiblePseudoClass(element);
case CSSSelector::PseudoClassFocusWithin:
return element.hasFocusWithin();
return matchesFocusWithinPseudoClass(element);
case CSSSelector::PseudoClassHover:
if (m_strictParsing || element.isLink() || canMatchHoverOrActiveInQuirksMode(context)) {
// See the comment in generateElementIsHovered() in SelectorCompiler.
@@ -57,6 +57,7 @@
#include "LegacyInlineTextBox.h"
#include "Logging.h"
#include "Page.h"
#include "PseudoClassChangeInvalidation.h"
#include "Range.h"
#include "RenderLayer.h"
#include "RenderLayerScrollableArea.h"
@@ -154,6 +155,11 @@ static inline bool shouldAlwaysUseDirectionalSelection(Document* document)
return !document || document->editor().behavior().shouldConsiderSelectionAsDirectional();
}

static inline bool isPageActive(Document* document)
{
return document && document->page() && document->page()->focusController().isActive();
}

FrameSelection::FrameSelection(Document* document)
: m_document(document)
, m_granularity(TextGranularity::CharacterGranularity)
@@ -166,6 +172,7 @@ FrameSelection::FrameSelection(Document* document)
, m_caretPaint(true)
, m_isCaretBlinkingSuspended(false)
, m_focused(document && document->frame() && document->page() && document->page()->focusController().focusedFrame() == document->frame())
, m_isActive(isPageActive(document))
, m_shouldShowBlockCursor(false)
, m_pendingSelectionUpdate(false)
, m_alwaysAlignCursorOnScrollWhenRevealingSelection(false)
@@ -2142,28 +2149,40 @@ void FrameSelection::focusedOrActiveStateChanged()
if (activeAndFocused)
setSelectionFromNone();
setCaretVisibility(activeAndFocused ? Visible : Hidden, ShouldUpdateAppearance::Yes);
#endif
}

// Because Style::Resolver::checkOneSelector() and
// RenderTheme::isFocused() check if the frame is active, we have to
// update style and theme state that depended on those.
for (RefPtr element = m_document->focusedElement(); element; element = element->shadowHost()) {
element->invalidateStyleForSubtree();
if (RenderObject* renderer = element->renderer(); renderer && renderer->style().hasEffectiveAppearance())
renderer->theme().stateChanged(*renderer, ControlStates::States::Focused);
static Vector<Style::PseudoClassChangeInvalidation> invalidateFocusedElementAndShadowIncludingAncestors(Element* focusedElement, bool activeAndFocused)
{
Vector<Style::PseudoClassChangeInvalidation> invalidations;
for (RefPtr element = focusedElement; element; element = element->shadowHost()) {
invalidations.append({ *element, { { CSSSelector::PseudoClassFocus, activeAndFocused }, { CSSSelector::PseudoClassFocusVisible, activeAndFocused } } });
for (auto& lineage : lineageOfType<Element>(*element))
invalidations.append({ lineage, CSSSelector::PseudoClassFocusWithin, activeAndFocused });
}
#endif
return invalidations;
}

void FrameSelection::pageActivationChanged()
{
bool isActive = isPageActive(m_document.get());
RefPtr focusedElement = m_document->focusedElement();
auto invalidations = invalidateFocusedElementAndShadowIncludingAncestors(focusedElement.get(), m_focused && isActive);
m_isActive = isActive;

focusedOrActiveStateChanged();
}

void FrameSelection::setFocused(bool flag)
void FrameSelection::setFocused(bool isFocused)
{
if (m_focused == flag)
if (m_focused == isFocused)
return;
m_focused = flag;

bool isActive = isPageActive(m_document.get());
RefPtr focusedElement = m_document->focusedElement();
auto invalidations = invalidateFocusedElementAndShadowIncludingAncestors(focusedElement.get(), isFocused && isActive);
m_focused = isFocused;
m_isActive = isActive;

focusedOrActiveStateChanged();
}
@@ -349,6 +349,7 @@ class FrameSelection : private CaretBase {
bool m_caretPaint : 1;
bool m_isCaretBlinkingSuspended : 1;
bool m_focused : 1;
bool m_isActive : 1;
bool m_shouldShowBlockCursor : 1;
bool m_pendingSelectionUpdate : 1;
bool m_alwaysAlignCursorOnScrollWhenRevealingSelection : 1;

0 comments on commit ac8282f

Please sign in to comment.