LibWeb: Use invalidation plans for pseudo-class changes

Replace PseudoClassInvalidator's subtree scan with targeted
invalidation for elements whose pseudo-class state changes. This scopes
state changes to affected selectors instead of rechecking a whole
common-ancestor subtree.

Use the ancestor chain that matches each pseudo-class. Hover walks the
shadow-including chain. :focus-within walks the flat-tree chain, so
slotted content invalidates its assigned slot and relevant shadow-tree
descendants. Focus, FocusVisible, and Target invalidate just the state
node.

Route each affected element through Element::invalidate_style with the
pseudo-class property. This uses the same invalidation-plan machinery as
Disabled, Checked, and other pseudo-class state changes.

Interaction state pseudo classes are not tracked in :has() metadata, so
schedule :has() ancestor invalidation explicitly when the state flips.
The callers no longer need cross-scope branching. The chain walk handles
shadow boundaries, and property invalidation already visits every
observer style scope.
This commit is contained in:
Andreas Kling 2026-05-23 19:24:06 +02:00 committed by Andreas Kling
parent c8f9b095ba
commit fa579754bf
Notes: github-actions[bot] 2026-05-23 21:38:41 +00:00
9 changed files with 150 additions and 307 deletions

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2026-present, the Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Node.h>
#include <LibWeb/TraversalDecision.h>
namespace Web::CSS::Invalidation {
enum class AncestorTraversal {
ShadowIncluding,
FlatTree,
};
template<typename Callback>
void for_each_inclusive_ancestor_element(DOM::Node& start, AncestorTraversal traversal, Callback callback)
{
if (auto* element = as_if<DOM::Element>(start)) {
if (callback(*element) == TraversalDecision::Break)
return;
}
auto* ancestor = traversal == AncestorTraversal::FlatTree
? start.flat_tree_parent_element()
: start.parent_or_shadow_host_element();
for (; ancestor; ancestor = traversal == AncestorTraversal::FlatTree ? ancestor->flat_tree_parent_element() : ancestor->parent_or_shadow_host_element()) {
if (callback(*ancestor) == TraversalDecision::Break)
return;
}
}
}

View file

@ -4,253 +4,100 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Function.h>
#include <AK/HashTable.h>
#include <AK/StdLibExtras.h>
#include <AK/TemporaryChange.h>
#include <AK/Vector.h>
#include <LibWeb/CSS/Invalidation/AncestorTraversal.h>
#include <LibWeb/CSS/Invalidation/PseudoClassInvalidator.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/InvalidationSet.h>
#include <LibWeb/CSS/StyleScope.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/DOM/StyleInvalidationReason.h>
namespace Web::CSS::Invalidation {
enum class RequireAffectedByPseudoClassMetadata : u8 {
Yes,
No,
};
enum class RuleOriginFilter : u8 {
All,
User,
};
static bool pseudo_class_state_can_be_observed_across_style_scopes(CSS::PseudoClass pseudo_class)
static bool pseudo_class_propagates_to_ancestors(CSS::PseudoClass pseudo_class)
{
return first_is_one_of(pseudo_class, CSS::PseudoClass::Focus, CSS::PseudoClass::FocusWithin, CSS::PseudoClass::FocusVisible);
return first_is_one_of(pseudo_class, CSS::PseudoClass::Hover, CSS::PseudoClass::FocusWithin);
}
static void schedule_document_user_has_invalidation_for_shadow_pseudo_class_state_change(CSS::PseudoClass pseudo_class, DOM::Node& node)
static AncestorTraversal ancestor_traversal_for_pseudo_class(CSS::PseudoClass pseudo_class)
{
if (!is<DOM::ShadowRoot>(node.root()))
return;
auto& document_style_scope = node.document().style_scope();
if (&node.style_scope() == &document_style_scope)
return;
if (!document_style_scope.may_have_user_has_selectors())
return;
Vector<CSS::InvalidationSet::Property> properties {
{ .type = CSS::InvalidationSet::Property::Type::PseudoClass, .value = pseudo_class },
};
document_style_scope.record_pending_has_invalidation_mutation_features(node, properties);
document_style_scope.schedule_ancestors_style_invalidation_due_to_presence_of_has(node);
}
static CSS::StyleScope& style_scope_for_invalidation_root(DOM::Document& document, DOM::Node& invalidation_root)
{
auto& root = invalidation_root.root();
if (auto* shadow_root = as_if<DOM::ShadowRoot>(root))
return shadow_root->style_scope();
return document.style_scope();
}
static GC::Ptr<DOM::Element const> shadow_host_for_rule_matching(DOM::Element const& element, GC::Ptr<DOM::ShadowRoot const> rule_shadow_root)
{
auto const& root_node = element.root();
auto shadow_root = as_if<DOM::ShadowRoot>(root_node);
auto element_shadow_root = element.shadow_root();
GC::Ptr<DOM::Element const> shadow_host;
if (element_shadow_root)
shadow_host = element;
else if (shadow_root)
shadow_host = shadow_root->host();
if (element.is_shadow_host() && rule_shadow_root != element.shadow_root())
shadow_host = rule_shadow_root ? rule_shadow_root->host() : nullptr;
return shadow_host;
}
template<typename StateSlot, typename NewState>
static void invalidate_style_after_pseudo_class_state_change_in_style_scope(CSS::PseudoClass pseudo_class, DOM::Document& document, StateSlot& state_slot, DOM::Node& invalidation_root, NewState new_state, CSS::StyleScope& style_scope, RequireAffectedByPseudoClassMetadata require_metadata, RuleOriginFilter rule_origin_filter = RuleOriginFilter::All)
{
auto rule_shadow_root = as_if<DOM::ShadowRoot>(style_scope.node());
auto const& rules = style_scope.get_pseudo_class_rule_cache(pseudo_class);
auto& style_computer = document.style_computer();
auto does_rule_match_on_element = [&](DOM::Element const& element, CSS::MatchingRule const& rule) {
auto const& selector = rule.selector;
if (selector.can_use_ancestor_filter() && style_computer.should_reject_with_ancestor_filter(selector))
return false;
SelectorEngine::MatchContext context {
.style_sheet_for_rule = *rule.sheet,
.subject = element,
.rule_shadow_root = rule_shadow_root,
};
auto target_pseudo = selector.target_pseudo_element();
return SelectorEngine::matches(selector, { element, target_pseudo }, shadow_host_for_rule_matching(element, rule_shadow_root), context);
};
auto matches_different_set_of_rules_after_state_change = [&](DOM::Element& element) {
bool result = false;
auto check_matching_rules = [&](auto const& matching_rules) {
for (auto& rule : matching_rules) {
if (rule_origin_filter == RuleOriginFilter::User && rule.cascade_origin != CascadeOrigin::User)
continue;
bool before = does_rule_match_on_element(element, rule);
TemporaryChange change { state_slot, new_state };
bool after = does_rule_match_on_element(element, rule);
if (before != after) {
result = true;
return IterationDecision::Break;
}
}
return IterationDecision::Continue;
};
auto check_abstract_element = [&](DOM::AbstractElement abstract_element) {
rules.for_each_matching_rules(abstract_element, [&](auto const& matching_rules) {
return check_matching_rules(matching_rules);
});
};
check_abstract_element({ element });
for (u8 i = 0; !result && i < to_underlying(CSS::PseudoElement::KnownPseudoElementCount); ++i) {
auto pseudo_element = static_cast<CSS::PseudoElement>(i);
check_abstract_element({ element, pseudo_element });
}
if (!result)
(void)check_matching_rules(rules.slotted_rules);
if (!result)
(void)check_matching_rules(rules.part_rules);
return result;
};
auto should_check_element = [&](DOM::Element const& element) {
if (require_metadata == RequireAffectedByPseudoClassMetadata::No)
return true;
return element.affected_by_pseudo_class(pseudo_class);
};
Function<void(DOM::Node&)> invalidate_affected_elements_recursively = [&](DOM::Node& node) -> void {
if (node.is_element()) {
auto& element = static_cast<DOM::Element&>(node);
style_computer.push_ancestor(element);
if (should_check_element(element) && matches_different_set_of_rules_after_state_change(element))
element.set_needs_style_update(true);
}
node.for_each_child([&](auto& child) {
invalidate_affected_elements_recursively(child);
return IterationDecision::Continue;
});
if (node.is_element())
style_computer.pop_ancestor(static_cast<DOM::Element&>(node));
};
// Seed the ancestor filter with ancestors above the starting node,
// so that ancestor-dependent selectors can still be correctly rejected.
for (auto* ancestor = invalidation_root.parent_or_shadow_host_element(); ancestor; ancestor = ancestor->parent_or_shadow_host_element())
style_computer.push_ancestor(*ancestor);
invalidate_affected_elements_recursively(invalidation_root);
for (auto* ancestor = invalidation_root.parent_or_shadow_host_element(); ancestor; ancestor = ancestor->parent_or_shadow_host_element())
style_computer.pop_ancestor(*ancestor);
}
template<typename StateSlot, typename NewState>
static void invalidate_document_user_style_after_shadow_pseudo_class_state_change(CSS::PseudoClass pseudo_class, DOM::Document& document, StateSlot& state_slot, DOM::Node& invalidation_root, NewState new_state)
{
if (!is<DOM::ShadowRoot>(invalidation_root.root()))
return;
auto& document_style_scope = document.style_scope();
if (!document_style_scope.may_have_user_pseudo_class_selectors(pseudo_class))
return;
invalidate_style_after_pseudo_class_state_change_in_style_scope(pseudo_class, document, state_slot, invalidation_root, new_state, document_style_scope, RequireAffectedByPseudoClassMetadata::Yes, RuleOriginFilter::User);
}
template<typename StateSlot, typename NewState>
static void invalidate_style_after_pseudo_class_state_change_impl(CSS::PseudoClass pseudo_class, DOM::Document& document, StateSlot& state_slot, DOM::Node& invalidation_root, NewState new_state)
{
if (state_slot)
schedule_document_user_has_invalidation_for_shadow_pseudo_class_state_change(pseudo_class, *state_slot);
if (new_state)
schedule_document_user_has_invalidation_for_shadow_pseudo_class_state_change(pseudo_class, *new_state);
auto& root_style_scope = style_scope_for_invalidation_root(document, invalidation_root);
invalidate_style_after_pseudo_class_state_change_in_style_scope(pseudo_class, document, state_slot, invalidation_root, new_state, root_style_scope, RequireAffectedByPseudoClassMetadata::Yes);
invalidate_document_user_style_after_shadow_pseudo_class_state_change(pseudo_class, document, state_slot, invalidation_root, new_state);
if (!pseudo_class_state_can_be_observed_across_style_scopes(pseudo_class))
return;
Vector<CSS::StyleScope*, 4> observer_style_scopes;
auto append_observer_style_scopes = [&](auto node) {
if (!node)
return;
node->for_each_style_scope_which_may_observe_the_node([&](CSS::StyleScope& style_scope) {
if (!observer_style_scopes.contains_slow(&style_scope))
observer_style_scopes.append(&style_scope);
});
};
append_observer_style_scopes(state_slot);
append_observer_style_scopes(new_state);
Vector<DOM::ShadowRoot*, 4> part_invalidation_roots;
auto append_part_invalidation_roots = [&](auto node) {
if (!node)
return;
for (auto shadow_root = node->containing_shadow_root(); shadow_root; shadow_root = shadow_root->containing_shadow_root()) {
if (!part_invalidation_roots.contains_slow(shadow_root.ptr()))
part_invalidation_roots.append(shadow_root.ptr());
}
};
append_part_invalidation_roots(state_slot);
append_part_invalidation_roots(new_state);
for (auto* observer_style_scope : observer_style_scopes) {
if (auto* shadow_root = as_if<DOM::ShadowRoot>(observer_style_scope->node())) {
if (observer_style_scope != &root_style_scope || &invalidation_root != shadow_root)
invalidate_style_after_pseudo_class_state_change_in_style_scope(pseudo_class, document, state_slot, *shadow_root, new_state, *observer_style_scope, RequireAffectedByPseudoClassMetadata::No);
if (auto* host = shadow_root->host())
invalidate_style_after_pseudo_class_state_change_in_style_scope(pseudo_class, document, state_slot, *host, new_state, *observer_style_scope, RequireAffectedByPseudoClassMetadata::No);
} else if (observer_style_scope != &root_style_scope) {
invalidate_style_after_pseudo_class_state_change_in_style_scope(pseudo_class, document, state_slot, observer_style_scope->node(), new_state, *observer_style_scope, RequireAffectedByPseudoClassMetadata::No);
}
if (observer_style_scope->get_pseudo_class_rule_cache(pseudo_class).part_rules.is_empty())
continue;
for (auto* part_invalidation_root : part_invalidation_roots)
invalidate_style_after_pseudo_class_state_change_in_style_scope(pseudo_class, document, state_slot, *part_invalidation_root, new_state, *observer_style_scope, RequireAffectedByPseudoClassMetadata::No);
switch (pseudo_class) {
case CSS::PseudoClass::FocusWithin:
return AncestorTraversal::FlatTree;
default:
return AncestorTraversal::ShadowIncluding;
}
}
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass pseudo_class, DOM::Document& document, GC::Ptr<DOM::Node>& state_slot, DOM::Node& invalidation_root, GC::Ptr<DOM::Node> new_state)
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass pseudo_class, GC::Ptr<DOM::Node> old_state, GC::Ptr<DOM::Node> new_state)
{
invalidate_style_after_pseudo_class_state_change_impl(pseudo_class, document, state_slot, invalidation_root, new_state);
}
if (!old_state && !new_state)
return;
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass pseudo_class, DOM::Document& document, GC::Ptr<DOM::Element>& state_slot, DOM::Node& invalidation_root, GC::Ptr<DOM::Element> new_state)
{
invalidate_style_after_pseudo_class_state_change_impl(pseudo_class, document, state_slot, invalidation_root, new_state);
bool const propagates = pseudo_class_propagates_to_ancestors(pseudo_class);
auto traversal = ancestor_traversal_for_pseudo_class(pseudo_class);
Vector<CSS::InvalidationSet::Property, 1> properties { { CSS::InvalidationSet::Property::Type::PseudoClass, pseudo_class } };
DOM::StyleInvalidationOptions options { .invalidate_self = true };
auto reason = DOM::StyleInvalidationReason::PseudoClassStateChange;
auto invalidate = [&](DOM::Element& element) {
element.invalidate_style(reason, properties, options);
// The interaction-state pseudo classes (Hover/Focus/etc.) aren't tracked in
// pseudo_classes_used_in_has_selectors, so invalidate_node_style_for_properties
// doesn't schedule :has() ancestor invalidation for them. Schedule it directly so
// rules like .a:has(:focus) ... re-evaluate when the state flips.
element.for_each_style_scope_which_may_observe_the_node([&](CSS::StyleScope& scope) {
if (!scope.may_have_has_selectors())
return;
scope.record_pending_has_invalidation_mutation_features(element, properties);
scope.schedule_ancestors_style_invalidation_due_to_presence_of_has(element);
});
};
auto build_chain = [&](GC::Ptr<DOM::Node> start) {
HashTable<DOM::Element const*> chain;
if (!start)
return chain;
if (propagates) {
for_each_inclusive_ancestor_element(*start, traversal, [&](DOM::Element& element) {
chain.set(&element);
return TraversalDecision::Continue;
});
} else if (auto* element = as_if<DOM::Element>(*start)) {
chain.set(element);
}
return chain;
};
auto old_chain = build_chain(old_state);
auto new_chain = build_chain(new_state);
// Walk start's ancestor chain (inclusive) and invalidate each element whose pseudo-class
// state changes. Elements in both chains have unchanged state and are skipped; once we
// reach one, all further ancestors are also in both chains so we stop.
auto walk_and_invalidate = [&](GC::Ptr<DOM::Node> start, HashTable<DOM::Element const*> const& other_chain) {
if (!start)
return;
if (propagates) {
for_each_inclusive_ancestor_element(*start, traversal, [&](DOM::Element& element) {
if (other_chain.contains(&element))
return TraversalDecision::Break;
invalidate(element);
return TraversalDecision::Continue;
});
} else if (auto* element = as_if<DOM::Element>(*start)) {
if (!other_chain.contains(element))
invalidate(*element);
}
};
walk_and_invalidate(old_state, new_chain);
walk_and_invalidate(new_state, old_chain);
}
}

View file

@ -7,19 +7,11 @@
#pragma once
#include <LibGC/Ptr.h>
#include <LibWeb/CSS/Selector.h>
namespace Web::DOM {
class Document;
class Element;
class Node;
}
#include <LibWeb/CSS/PseudoClass.h>
#include <LibWeb/Forward.h>
namespace Web::CSS::Invalidation {
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass, DOM::Document&, GC::Ptr<DOM::Node>& state_slot, DOM::Node& invalidation_root, GC::Ptr<DOM::Node> new_state);
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass, DOM::Document&, GC::Ptr<DOM::Element>& state_slot, DOM::Node& invalidation_root, GC::Ptr<DOM::Element> new_state);
void invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass, GC::Ptr<DOM::Node> old_state, GC::Ptr<DOM::Node> new_state);
}

View file

@ -2656,20 +2656,7 @@ void Document::set_hovered_node(GC::Ptr<Node> node, Optional<HoverEventData> hov
entered_ancestors.append(make_hover_event_target(*target));
}
GC::Ptr<Node> old_hovered_node_root = nullptr;
GC::Ptr<Node> new_hovered_node_root = nullptr;
if (old_hovered_node)
old_hovered_node_root = old_hovered_node->root();
if (node)
new_hovered_node_root = node->root();
if (old_hovered_node_root != new_hovered_node_root) {
if (old_hovered_node_root)
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, *this, m_hovered_node, *old_hovered_node_root, node);
if (new_hovered_node_root)
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, *this, m_hovered_node, *new_hovered_node_root, node);
} else {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, *this, m_hovered_node, *common_ancestor, node);
}
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Hover, old_hovered_node, node);
m_hovered_node = node;
@ -3359,30 +3346,9 @@ void Document::set_focused_area(GC::Ptr<Node> node)
if (auto* old_focused_element = as_if<Element>(old_focused_area.ptr()))
old_focused_element->did_lose_focus();
auto* common_ancestor = find_common_ancestor(old_focused_area, node);
GC::Ptr<Node> old_focused_node_root = nullptr;
GC::Ptr<Node> new_focused_node_root = nullptr;
if (old_focused_area)
old_focused_node_root = old_focused_area->root();
if (node)
new_focused_node_root = node->root();
if (old_focused_node_root != new_focused_node_root) {
if (old_focused_node_root) {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, *this, m_focused_area, *old_focused_node_root, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, *this, m_focused_area, *old_focused_node_root, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, *this, m_focused_area, *old_focused_node_root, node);
}
if (new_focused_node_root) {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, *this, m_focused_area, *new_focused_node_root, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, *this, m_focused_area, *new_focused_node_root, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, *this, m_focused_area, *new_focused_node_root, node);
}
} else {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, *this, m_focused_area, *common_ancestor, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, *this, m_focused_area, *common_ancestor, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, *this, m_focused_area, *common_ancestor, node);
}
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Focus, old_focused_area, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusWithin, old_focused_area, node);
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::FocusVisible, old_focused_area, node);
m_focused_area = node;
@ -3431,24 +3397,7 @@ void Document::set_target_element(GC::Ptr<Element> element)
GC::Ptr<Element> old_target_element = move(m_target_element);
auto* common_ancestor = find_common_ancestor(old_target_element, element);
GC::Ptr<Node> old_target_node_root = nullptr;
GC::Ptr<Node> new_target_node_root = nullptr;
if (old_target_element)
old_target_node_root = old_target_element->root();
if (element)
new_target_node_root = element->root();
if (old_target_node_root != new_target_node_root) {
if (old_target_node_root) {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, *this, m_target_element, *old_target_node_root, element);
}
if (new_target_node_root) {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, *this, m_target_element, *new_target_node_root, element);
}
} else {
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, *this, m_target_element, *common_ancestor, element);
}
CSS::Invalidation::invalidate_style_after_pseudo_class_state_change(CSS::PseudoClass::Target, old_target_element, element);
m_target_element = element;

View file

@ -45,6 +45,7 @@ namespace Web::DOM {
X(NodeRemove) \
X(NodeSetTextContent) \
X(Other) \
X(PseudoClassStateChange) \
X(SetSelectorText) \
X(SettingsChange) \
X(StyleSheetDisabledStateChange) \

View file

@ -11,18 +11,18 @@ focus fragment old before append: rgb(0, 128, 0)
focus fragment new before append: rgb(0, 0, 0)
focus fragment old after append: rgb(0, 0, 0)
focus fragment new after append: rgb(0, 128, 0)
focus pseudo old before append: none
focus pseudo old before append: focus
focus pseudo new before append: none
focus pseudo old after append: none
focus pseudo new after append: focus
focus pseudo new after remove: none
focus pseudo move old before moveBefore: none
focus pseudo move old before moveBefore: focus
focus pseudo move new before moveBefore: none
focus pseudo move old after moveBefore: none
focus pseudo move new after moveBefore: focus
focus pseudo move old after moveBefore back: focus
focus pseudo move new after moveBefore back: none
focus pseudo fragment old before append: none
focus pseudo fragment old before append: focus
focus pseudo fragment new before append: none
focus pseudo fragment old after append: none
focus pseudo fragment new after append: focus
@ -75,18 +75,18 @@ hover pseudo fragment old after append: none
hover pseudo fragment new after append: hover
hover pseudo fragment old after moveBefore back: hover
hover pseudo fragment new after moveBefore back: none
shadow hover pseudo old before append: none
shadow hover pseudo old before append: shadow hover
shadow hover pseudo new before append: none
shadow hover pseudo old after append: none
shadow hover pseudo new after append: shadow hover
shadow hover pseudo new after remove: none
shadow hover pseudo move old before moveBefore: none
shadow hover pseudo move old before moveBefore: shadow hover
shadow hover pseudo move new before moveBefore: none
shadow hover pseudo move old after moveBefore: none
shadow hover pseudo move new after moveBefore: shadow hover
shadow hover pseudo move old after moveBefore back: shadow hover
shadow hover pseudo move new after moveBefore back: none
shadow hover pseudo fragment old before append: none
shadow hover pseudo fragment old before append: shadow hover
shadow hover pseudo fragment new before append: none
shadow hover pseudo fragment old after append: none
shadow hover pseudo fragment new after append: shadow hover

View file

@ -1,11 +1,17 @@
host before light input focus: none
shadow target before light input focus: none
slot before light input focus: none
slotted focus target before light input focus: none
slotted input before light input focus: none
host after light input focus: host
shadow target after light input focus: shadow-target
slot after light input focus: slot
slotted focus target after light input focus: slotted-focus-target
slotted input after light input focus: slotted
host after light input blur: none
shadow target after light input blur: none
slot after light input blur: none
slotted focus target after light input blur: none
slotted input after light input blur: none
host after shadow input focus: host
shadow target after shadow input focus: shadow-target

View file

@ -265,10 +265,10 @@ PASS: featureless-sensitive structural mutation keeps :has walk: child :only-chi
PASS: featureless-sensitive structural mutation keeps :has walk: child :nth-child(2) starts matching | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=2, hasResultCacheHits=0, hasResultCacheMisses=2
PASS: featureless-sensitive structural mutation keeps :has walk: child :nth-last-child(2) starts matching | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=2, hasResultCacheHits=0, hasResultCacheMisses=2
PASS: featureless-sensitive structural mutation keeps :has walk: child :only-of-type stops matching | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=2, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=2, hasResultCacheHits=0, hasResultCacheMisses=2
PASS: unprobeable pseudo structural mutation keeps :has walk: child :focus insertion with unrelated metadata | styleInvalidations=8, fullStyleInvalidations=0, elementStyleRecomputations=7, elementStyleNoopRecomputations=5, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: descendant :focus insertion through wrapper with unrelated metadata | styleInvalidations=9, fullStyleInvalidations=0, elementStyleRecomputations=8, elementStyleNoopRecomputations=6, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :focus-within insertion with unrelated metadata | styleInvalidations=9, fullStyleInvalidations=0, elementStyleRecomputations=8, elementStyleNoopRecomputations=6, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :focus-visible insertion with unrelated metadata | styleInvalidations=8, fullStyleInvalidations=0, elementStyleRecomputations=7, elementStyleNoopRecomputations=5, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :focus insertion with unrelated metadata | styleInvalidations=9, fullStyleInvalidations=0, elementStyleRecomputations=7, elementStyleNoopRecomputations=6, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: descendant :focus insertion through wrapper with unrelated metadata | styleInvalidations=10, fullStyleInvalidations=0, elementStyleRecomputations=8, elementStyleNoopRecomputations=7, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :focus-within insertion with unrelated metadata | styleInvalidations=10, fullStyleInvalidations=0, elementStyleRecomputations=8, elementStyleNoopRecomputations=7, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :focus-visible insertion with unrelated metadata | styleInvalidations=9, fullStyleInvalidations=0, elementStyleRecomputations=7, elementStyleNoopRecomputations=6, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :default checkbox insertion with unrelated metadata | styleInvalidations=1, fullStyleInvalidations=0, elementStyleRecomputations=3, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: descendant :default selected option insertion with unrelated metadata | styleInvalidations=7, fullStyleInvalidations=0, elementStyleRecomputations=9, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :valid insertion with unrelated metadata | styleInvalidations=7, fullStyleInvalidations=0, elementStyleRecomputations=6, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
@ -276,7 +276,7 @@ PASS: unprobeable pseudo structural mutation keeps :has walk: child :invalid ins
PASS: unprobeable pseudo structural mutation keeps :has walk: child :open insertion with unrelated metadata | styleInvalidations=6, fullStyleInvalidations=0, elementStyleRecomputations=6, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :read-only insertion with unrelated metadata | styleInvalidations=7, fullStyleInvalidations=0, elementStyleRecomputations=6, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :read-write insertion with unrelated metadata | styleInvalidations=7, fullStyleInvalidations=0, elementStyleRecomputations=6, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :is(:focus) insertion with unrelated metadata | styleInvalidations=8, fullStyleInvalidations=0, elementStyleRecomputations=7, elementStyleNoopRecomputations=5, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :is(:focus) insertion with unrelated metadata | styleInvalidations=9, fullStyleInvalidations=0, elementStyleRecomputations=7, elementStyleNoopRecomputations=6, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: unprobeable pseudo structural mutation keeps :has walk: child :where(:invalid) insertion with unrelated metadata | styleInvalidations=7, fullStyleInvalidations=0, elementStyleRecomputations=6, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=4, hasResultCacheHits=0, hasResultCacheMisses=4
PASS: case-sensitive name structural mutation keeps :has walk: svg camel-case tag insertion | styleInvalidations=2, fullStyleInvalidations=0, elementStyleRecomputations=3, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=2, hasResultCacheHits=0, hasResultCacheMisses=2
PASS: case-sensitive name structural mutation keeps :has walk: svg camel-case tag nested insertion | styleInvalidations=3, fullStyleInvalidations=0, elementStyleRecomputations=4, elementStyleNoopRecomputations=1, elementInheritedStyleRecomputations=0, elementInheritedStyleNoopRecomputations=0, hasAncestorWalkInvocations=1, hasInvalidationMetadataCandidates=0, hasMatchInvocations=2, hasResultCacheHits=0, hasResultCacheMisses=2

View file

@ -16,28 +16,39 @@
<style>
:host(:focus-within)::before { content: ""; --state: host; }
:host(:focus-within) #shadow-target { --state: shadow-target; }
slot:focus-within { --state: slot; }
slot:focus-within + #slotted-focus-target { --state: slotted-focus-target; }
::slotted(:focus) { --state: slotted; }
</style>
<span id="shadow-target"></span>
<slot></slot>
<span id="slotted-focus-target"></span>
`;
const shadowTarget = shadowRoot.getElementById("shadow-target");
const slot = shadowRoot.querySelector("slot");
const slottedFocusTarget = shadowRoot.getElementById("slotted-focus-target");
println(`host before light input focus: ${state(host, "::before")}`);
println(`shadow target before light input focus: ${state(shadowTarget)}`);
println(`slot before light input focus: ${state(slot)}`);
println(`slotted focus target before light input focus: ${state(slottedFocusTarget)}`);
println(`slotted input before light input focus: ${state(lightInput)}`);
lightInput.focus();
println(`host after light input focus: ${state(host, "::before")}`);
println(`shadow target after light input focus: ${state(shadowTarget)}`);
println(`slot after light input focus: ${state(slot)}`);
println(`slotted focus target after light input focus: ${state(slottedFocusTarget)}`);
println(`slotted input after light input focus: ${state(lightInput)}`);
lightInput.blur();
println(`host after light input blur: ${state(host, "::before")}`);
println(`shadow target after light input blur: ${state(shadowTarget)}`);
println(`slot after light input blur: ${state(slot)}`);
println(`slotted focus target after light input blur: ${state(slottedFocusTarget)}`);
println(`slotted input after light input blur: ${state(lightInput)}`);
const shadowInput = document.createElement("input");