mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-28 12:10:28 +00:00
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:
parent
c8f9b095ba
commit
fa579754bf
Notes:
github-actions[bot]
2026-05-23 21:38:41 +00:00
Author: https://github.com/awesomekling
Commit: fa579754bf
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/9585
9 changed files with 150 additions and 307 deletions
37
Libraries/LibWeb/CSS/Invalidation/AncestorTraversal.h
Normal file
37
Libraries/LibWeb/CSS/Invalidation/AncestorTraversal.h
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ namespace Web::DOM {
|
|||
X(NodeRemove) \
|
||||
X(NodeSetTextContent) \
|
||||
X(Other) \
|
||||
X(PseudoClassStateChange) \
|
||||
X(SetSelectorText) \
|
||||
X(SettingsChange) \
|
||||
X(StyleSheetDisabledStateChange) \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue