From 60bbcbca245ee6eeee7f31ac805600eee5de4b28 Mon Sep 17 00:00:00 2001 From: jwxbond Date: Wed, 28 Jan 2026 21:49:10 +0800 Subject: [PATCH 01/16] fix(css-selector): order exported styles by cascade priority, add `has-basic.ts` tests and snapshots --- .../css/check_pseudo_has_argument_context.h | 235 ++++++++- .../css/check_pseudo_has_traversal_iterator.h | 86 +++- bridge/core/css/resolver/style_cascade.cc | 15 +- bridge/core/dom/element.cc | 46 +- docs/CSS_SELECTORS_L4_HAS_PLAN.md | 117 +++++ docs/TAILWINDCSS_SUPPORT_PLAN.md | 6 +- .../css-selectors/has-basic.ts.0939efdd1.png | Bin 0 -> 2412 bytes .../css-selectors/has-basic.ts.18ff8aed1.png | Bin 0 -> 2415 bytes .../css-selectors/has-basic.ts.2f79ef791.png | Bin 0 -> 2421 bytes .../css-selectors/has-basic.ts.30aca6431.png | Bin 0 -> 2408 bytes .../css-selectors/has-basic.ts.3eb0b88a1.png | Bin 0 -> 2406 bytes .../css-selectors/has-basic.ts.408ddde91.png | Bin 0 -> 2418 bytes .../css-selectors/has-basic.ts.5b0607b81.png | Bin 0 -> 2402 bytes .../css-selectors/has-basic.ts.5ce78b901.png | Bin 0 -> 2403 bytes .../css-selectors/has-basic.ts.6ed3146e1.png | Bin 0 -> 2418 bytes .../css-selectors/has-basic.ts.79d56e171.png | Bin 0 -> 4298 bytes .../css-selectors/has-basic.ts.85a326141.png | Bin 0 -> 2415 bytes .../css-selectors/has-basic.ts.91127d8c1.png | Bin 0 -> 2428 bytes .../css-selectors/has-basic.ts.95c226b01.png | Bin 0 -> 2412 bytes .../css-selectors/has-basic.ts.c713df221.png | Bin 0 -> 2761 bytes .../css-selectors/has-basic.ts.d395e40c1.png | Bin 0 -> 3399 bytes .../css-selectors/has-basic.ts.e06d14041.png | Bin 0 -> 2407 bytes .../css-selectors/has-basic.ts.f2c499201.png | Bin 0 -> 2406 bytes .../css-selectors/has-basic.ts.f70261ee1.png | Bin 0 -> 2422 bytes .../css-selectors/has-basic.ts.f89e13111.png | Bin 0 -> 2402 bytes .../css-selectors/has-basic.ts.f93e75831.png | Bin 0 -> 2411 bytes .../css-selectors/has-basic.ts.fe50857e1.png | Bin 0 -> 2424 bytes .../specs/css/css-selectors/has-basic.ts | 463 ++++++++++++++++++ 28 files changed, 933 insertions(+), 35 deletions(-) create mode 100644 docs/CSS_SELECTORS_L4_HAS_PLAN.md create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.0939efdd1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.18ff8aed1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.2f79ef791.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.30aca6431.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.3eb0b88a1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.408ddde91.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.5b0607b81.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.5ce78b901.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.6ed3146e1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.79d56e171.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.85a326141.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.91127d8c1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.95c226b01.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.c713df221.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.d395e40c1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.e06d14041.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f2c499201.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f70261ee1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f89e13111.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f93e75831.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.fe50857e1.png create mode 100644 integration_tests/specs/css/css-selectors/has-basic.ts diff --git a/bridge/core/css/check_pseudo_has_argument_context.h b/bridge/core/css/check_pseudo_has_argument_context.h index 242e0f2ac7..f14571e1ba 100644 --- a/bridge/core/css/check_pseudo_has_argument_context.h +++ b/bridge/core/css/check_pseudo_has_argument_context.h @@ -6,6 +6,8 @@ #define WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_ #include "core/css/css_selector.h" +#include "core/css/css_selector_list.h" +#include "core/dom/has_invalidation_flags.h" #include "foundation/macros.h" #include @@ -19,28 +21,229 @@ using Vector = std::vector; class CheckPseudoHasArgumentContext { WEBF_STACK_ALLOCATED(); public: - explicit CheckPseudoHasArgumentContext(const CSSSelector* selector) - : selector_(selector) {} - - CSSSelector::RelationType LeftmostRelation() const { - // Return a default relation type - return CSSSelector::kRelativeDescendant; + explicit CheckPseudoHasArgumentContext(const CSSSelector* selector) + : selector_(selector) { + BuildContext(); } - int AdjacentDistanceLimit() const { return 0; } - bool AdjacentDistanceFixed() const { return false; } - int DepthLimit() const { return 0; } - bool DepthFixed() const { return false; } - bool AllowSiblingsAffectedByHas() const { return false; } - unsigned GetSiblingsAffectedByHasFlags() const { return 0; } - bool SiblingCombinatorAtRightmost() const { return false; } - bool SiblingCombinatorBetweenChildOrDescendantCombinator() const { return false; } - Vector GetPseudoHasArgumentHashes() const { return Vector(); } + CSSSelector::RelationType LeftmostRelation() const { return leftmost_relation_; } + + int AdjacentDistanceLimit() const { return adjacent_distance_limit_; } + bool AdjacentDistanceFixed() const { return adjacent_distance_fixed_; } + int DepthLimit() const { return depth_limit_; } + bool DepthFixed() const { return depth_fixed_; } + bool AllowSiblingsAffectedByHas() const { return allow_siblings_affected_by_has_; } + unsigned GetSiblingsAffectedByHasFlags() const { return siblings_affected_by_has_flags_; } + bool SiblingCombinatorAtRightmost() const { return sibling_combinator_at_rightmost_; } + bool SiblingCombinatorBetweenChildOrDescendantCombinator() const { + return sibling_combinator_between_child_or_descendant_; + } + Vector GetPseudoHasArgumentHashes() const { return pseudo_has_argument_hashes_; } private: + static inline bool IsRelativeRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kRelativeDescendant || relation == CSSSelector::kRelativeChild || + relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsAdjacentRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kDirectAdjacent || relation == CSSSelector::kIndirectAdjacent || + relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsIndirectAdjacent(CSSSelector::RelationType relation) { + return relation == CSSSelector::kIndirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsRelativeAdjacent(CSSSelector::RelationType relation) { + return relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsDescendantRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kDescendant || relation == CSSSelector::kRelativeDescendant; + } + + static inline bool IsChildRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kChild || relation == CSSSelector::kRelativeChild; + } + + static inline bool IsChildOrDescendantRelation(CSSSelector::RelationType relation) { + return IsDescendantRelation(relation) || IsChildRelation(relation); + } + + void BuildContext() { + leftmost_relation_ = CSSSelector::kRelativeDescendant; + adjacent_distance_limit_ = 0; + adjacent_distance_fixed_ = true; + depth_limit_ = 0; + depth_fixed_ = true; + allow_siblings_affected_by_has_ = false; + siblings_affected_by_has_flags_ = kNoSiblingsAffectedByHasFlags; + sibling_combinator_at_rightmost_ = false; + sibling_combinator_between_child_or_descendant_ = false; + pseudo_has_argument_hashes_.clear(); + + if (!selector_) { + return; + } + + // Collect relations from rightmost to leftmost (towards the relative anchor). + Vector relations; + bool found_relative_relation = false; + for (const CSSSelector* current = selector_; current; current = current->NextSimpleSelector()) { + AddHashesForSimpleSelector(*current); + + CSSSelector::RelationType relation = current->Relation(); + if (relation == CSSSelector::kSubSelector || relation == CSSSelector::kScopeActivation) { + continue; + } + relations.push_back(relation); + if (IsRelativeRelation(relation)) { + found_relative_relation = true; + break; + } + } + + if (relations.empty()) { + return; + } + + leftmost_relation_ = relations.back(); + if (!found_relative_relation) { + // Be defensive: if the parser didn't convert the leftmost combinator + // to a relative relation, map it here so :has() matching can proceed. + leftmost_relation_ = ConvertRelationToRelative(leftmost_relation_); + } + sibling_combinator_at_rightmost_ = IsAdjacentRelation(relations.front()); + + // Determine if an adjacent combinator appears between child/descendant combinators. + bool seen_child_or_descendant_to_right = false; + for (const auto& relation : relations) { + if (IsChildOrDescendantRelation(relation)) { + seen_child_or_descendant_to_right = true; + continue; + } + if (IsAdjacentRelation(relation) && seen_child_or_descendant_to_right) { + sibling_combinator_between_child_or_descendant_ = true; + } + } + + // Compute adjacency and depth constraints from leftmost (anchor side) to rightmost. + bool in_sibling_phase = IsRelativeAdjacent(leftmost_relation_); + for (auto it = relations.rbegin(); it != relations.rend(); ++it) { + CSSSelector::RelationType relation = *it; + if (IsChildOrDescendantRelation(relation)) { + depth_limit_++; + if (IsDescendantRelation(relation)) { + depth_fixed_ = false; + } + in_sibling_phase = false; + continue; + } + + if (IsAdjacentRelation(relation) && in_sibling_phase) { + if (IsIndirectAdjacent(relation)) { + // Indirect adjacency is unbounded. + adjacent_distance_fixed_ = false; + adjacent_distance_limit_ = 0; + } else if (adjacent_distance_fixed_) { + adjacent_distance_limit_++; + } + } + } + + allow_siblings_affected_by_has_ = IsRelativeAdjacent(leftmost_relation_); + if (allow_siblings_affected_by_has_) { + siblings_affected_by_has_flags_ = + depth_limit_ > 0 ? kFlagForSiblingDescendantRelationship : kFlagForSiblingRelationship; + } + } + + void AddHash(unsigned hash) { + if (hash) { + pseudo_has_argument_hashes_.push_back(hash); + } + } + + void AddHashesForSelectorList(const CSSSelector* selector_list_first) { + if (!selector_list_first) { + return; + } + for (const CSSSelector* complex = selector_list_first; complex; complex = CSSSelectorList::Next(*complex)) { + for (const CSSSelector* simple = complex; simple; simple = simple->NextSimpleSelector()) { + AddHashesForSimpleSelector(*simple); + } + } + } + + void AddHashesForSimpleSelector(const CSSSelector& selector) { + switch (selector.Match()) { + case CSSSelector::kTag: + if (selector.TagQName().LocalName() != CSSSelector::UniversalSelectorAtom()) { + AddHash(selector.TagQName().LocalName().Hash() * kTagSalt); + } + break; + case CSSSelector::kId: + AddHash(selector.Value().Hash() * kIdSalt); + break; + case CSSSelector::kClass: + AddHash(selector.Value().Hash() * kClassSalt); + break; + case CSSSelector::kAttributeExact: + case CSSSelector::kAttributeSet: + case CSSSelector::kAttributeList: + case CSSSelector::kAttributeHyphen: + case CSSSelector::kAttributeContain: + case CSSSelector::kAttributeBegin: + case CSSSelector::kAttributeEnd: + AddHash(selector.Attribute().LocalName().Hash() * kAttributeSalt); + break; + case CSSSelector::kPseudoClass: { + CSSSelector::PseudoType pseudo_type = selector.GetPseudoType(); + switch (pseudo_type) { + case CSSSelector::kPseudoNot: + case CSSSelector::kPseudoIs: + case CSSSelector::kPseudoWhere: + case CSSSelector::kPseudoParent: + AddHashesForSelectorList(selector.SelectorListOrParent()); + break; + case CSSSelector::kPseudoVisited: + case CSSSelector::kPseudoRelativeAnchor: + break; + default: + AddHash(static_cast(pseudo_type) * kPseudoSalt); + if (const CSSSelectorList* list = selector.SelectorList()) { + AddHashesForSelectorList(list->First()); + } + break; + } + break; + } + default: + break; + } + } + + static constexpr unsigned kClassSalt = 13; + static constexpr unsigned kIdSalt = 29; + static constexpr unsigned kTagSalt = 7; + static constexpr unsigned kAttributeSalt = 19; + static constexpr unsigned kPseudoSalt = 23; + const CSSSelector* selector_; + + CSSSelector::RelationType leftmost_relation_; + int adjacent_distance_limit_; + bool adjacent_distance_fixed_; + int depth_limit_; + bool depth_fixed_; + bool allow_siblings_affected_by_has_; + unsigned siblings_affected_by_has_flags_; + bool sibling_combinator_at_rightmost_; + bool sibling_combinator_between_child_or_descendant_; + Vector pseudo_has_argument_hashes_; }; } // namespace webf -#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_ \ No newline at end of file +#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_ diff --git a/bridge/core/css/check_pseudo_has_traversal_iterator.h b/bridge/core/css/check_pseudo_has_traversal_iterator.h index efe0e40fc1..9fe5d544f8 100644 --- a/bridge/core/css/check_pseudo_has_traversal_iterator.h +++ b/bridge/core/css/check_pseudo_has_traversal_iterator.h @@ -6,8 +6,10 @@ #define WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_ #include "core/dom/element.h" +#include "core/dom/element_traversal.h" #include "core/css/check_pseudo_has_argument_context.h" #include "foundation/macros.h" +#include #include namespace webf { @@ -22,16 +24,90 @@ class CheckPseudoHasArgumentTraversalIterator { public: CheckPseudoHasArgumentTraversalIterator(Element& element, const CheckPseudoHasArgumentContext& context) - : current_(&element), depth_(0) {} + : context_(context) { + Initialize(element); + } bool AtEnd() const { return current_ == nullptr; } - void operator++() { current_ = nullptr; } // Stub - stop after first element + void operator++() { Advance(); } Element* CurrentElement() const { return current_; } int CurrentDepth() const { return depth_; } private: - Element* current_; - int depth_; + struct TraversalEntry { + Element* element; + int depth; + }; + + void Initialize(Element& anchor) { + stack_.clear(); + + const CSSSelector::RelationType leftmost_relation = context_.LeftmostRelation(); + if (leftmost_relation == CSSSelector::kRelativeDirectAdjacent || + leftmost_relation == CSSSelector::kRelativeIndirectAdjacent) { + int distance = 0; + for (Element* sibling = ElementTraversal::NextSibling(anchor); sibling; + sibling = ElementTraversal::NextSibling(*sibling)) { + distance++; + if (context_.AdjacentDistanceFixed() && context_.AdjacentDistanceLimit() > 0 && + distance > context_.AdjacentDistanceLimit()) { + break; + } + stack_.push_back({sibling, 0}); + if (context_.AdjacentDistanceFixed() && context_.AdjacentDistanceLimit() > 0 && + distance == context_.AdjacentDistanceLimit()) { + break; + } + } + } else { + for (Element* child = ElementTraversal::FirstChild(anchor); child; + child = ElementTraversal::NextSibling(*child)) { + stack_.push_back({child, 1}); + } + } + + std::reverse(stack_.begin(), stack_.end()); + Advance(); + } + + bool ShouldDescendFrom(int depth) const { + if (context_.DepthLimit() == 0) { + return false; + } + if (context_.DepthFixed() && depth >= context_.DepthLimit()) { + return false; + } + return true; + } + + void PushChildren(Element& element, int depth) { + if (!ShouldDescendFrom(depth)) { + return; + } + for (Element* child = ElementTraversal::LastChild(element); child; + child = ElementTraversal::PreviousSibling(*child)) { + stack_.push_back({child, depth + 1}); + } + } + + void Advance() { + if (stack_.empty()) { + current_ = nullptr; + return; + } + TraversalEntry entry = stack_.back(); + stack_.pop_back(); + current_ = entry.element; + depth_ = entry.depth; + if (current_) { + PushChildren(*current_, depth_); + } + } + + const CheckPseudoHasArgumentContext& context_; + Vector stack_; + Element* current_{nullptr}; + int depth_{0}; }; // Stub implementation of CheckPseudoHasFastRejectFilter @@ -50,4 +126,4 @@ class CheckPseudoHasFastRejectFilter { } // namespace webf -#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_ \ No newline at end of file +#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_ diff --git a/bridge/core/css/resolver/style_cascade.cc b/bridge/core/css/resolver/style_cascade.cc index 085d1d2a27..5c70ac05b1 100644 --- a/bridge/core/css/resolver/style_cascade.cc +++ b/bridge/core/css/resolver/style_cascade.cc @@ -401,7 +401,7 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe CSSPropertyName name; std::shared_ptr value; bool important; - uint32_t position; + CascadePriority priority; }; std::vector exported_properties; @@ -461,17 +461,16 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe // so the UICommand receives the original logical names unchanged. CSSPropertyName declared_name = prop_ref.Name(); exported_properties.push_back( - ExportedProperty{id, declared_name, to_set, important, pos}); + ExportedProperty{id, declared_name, to_set, important, *prio}); } - // Sort by encoded position so that later declarations (including shorthands) - // are emitted after earlier ones, matching cascade/source order. This is - // important for consumers like the Dart style engine that replay SetStyle - // commands sequentially. + // Sort by cascade priority so higher-priority declarations are emitted last. + // This keeps shorthand/longhand interactions correct when Dart replays + // SetStyle commands sequentially without full cascade context. std::sort(exported_properties.begin(), exported_properties.end(), [](const ExportedProperty& a, const ExportedProperty& b) { - if (a.position != b.position) { - return a.position < b.position; + if (a.priority != b.priority) { + return a.priority < b.priority; } // Tie-breaker: keep deterministic order by property id. return static_cast(a.id) < static_cast(b.id); diff --git a/bridge/core/dom/element.cc b/bridge/core/dom/element.cc index e5843f263d..c2dfd79338 100644 --- a/bridge/core/dom/element.cc +++ b/bridge/core/dom/element.cc @@ -18,6 +18,7 @@ #include "child_list_mutation_scope.h" #include "comment.h" #include "core/css/css_identifier_value.h" +#include "core/css/css_selector_list.h" #include "core/css/css_property_value_set.h" #include "core/css/css_selector_list.h" #include "core/css/css_style_sheet.h" @@ -27,12 +28,14 @@ #include "core/css/parser/css_parser.h" #include "core/css/parser/css_parser_context.h" #include "core/css/selector_checker.h" +#include "core/css/parser/css_parser_context.h" #include "core/css/style_recalc_change.h" #include "core/css/style_recalc_context.h" #include "core/css/style_scope_frame.h" #include "core/css/style_scope_data.h" #include "core/css/style_engine.h" #include "core/css/style_sheet_contents.h" +#include "core/css/selector_checker.h" #include "core/css/white_space.h" #include "core/dom/document_fragment.h" #include "core/dom/element_rare_data_vector.h" @@ -158,6 +161,43 @@ bool IsPotentiallyDisableableFormControl(const Element& element) { tag_name == "option"; } +std::shared_ptr ParseSelectorListOrThrow(const AtomicString& selectors, + ExceptionState& exception_state, + JSContext* ctx) { + auto parser_context = std::make_shared(kHTMLStandardMode); + auto sheet = std::make_shared(parser_context); + + std::vector arena; + tcb::span vector = + CSSParser::ParseSelector(parser_context, CSSNestingType::kNone, /*parent_rule_for_nesting=*/nullptr, sheet, + selectors.GetString(), arena); + + auto selector_list = CSSSelectorList::AdoptSelectorVector(vector); + if (!selector_list->IsValid()) { + exception_state.ThrowException(ctx, ErrorType::SyntaxError, + "'" + selectors.ToUTF8String() + "' is not a valid selector."); + return nullptr; + } + return selector_list; +} + +bool MatchesAnySelectorInList(Element& element, + const CSSSelectorList& selector_list, + const ContainerNode* scope) { + SelectorChecker checker(SelectorChecker::kQueryingRules); + SelectorChecker::SelectorCheckingContext context(&element); + context.scope = scope; + + for (const CSSSelector* selector = selector_list.First(); selector; selector = CSSSelectorList::Next(*selector)) { + context.selector = selector; + SelectorChecker::MatchResult result; + if (checker.Match(context, result)) { + return true; + } + } + return false; +} + } // namespace AttributeCollection Element::Attributes() const { @@ -570,7 +610,7 @@ bool Element::matches(const AtomicString& selectors, ExceptionState& exception_s if (!selector_list) { return false; } - return MatchesAnySelectorInList(*this, *selector_list, *this); + return MatchesAnySelectorInList(*this, *selector_list, this); } NativeValue arguments[] = {NativeValueConverter::ToNativeValue(ctx(), selectors)}; @@ -588,9 +628,9 @@ Element* Element::closest(const AtomicString& selectors, ExceptionState& excepti if (!selector_list) { return nullptr; } - + ContainerNode* scope = this; for (Element* current = this; current; current = current->parentElement()) { - if (MatchesAnySelectorInList(*current, *selector_list, *this)) { + if (MatchesAnySelectorInList(*current, *selector_list, scope)) { return current; } } diff --git a/docs/CSS_SELECTORS_L4_HAS_PLAN.md b/docs/CSS_SELECTORS_L4_HAS_PLAN.md new file mode 100644 index 0000000000..2b1a9d943b --- /dev/null +++ b/docs/CSS_SELECTORS_L4_HAS_PLAN.md @@ -0,0 +1,117 @@ +# CSS Selectors Level 4 — :has() Support Plan (C++ Blink/bridge) + +Goal: implement correct `:has()` matching + invalidation in the Blink/bridge C++ selector engine. + +Status (Jan 28, 2026): +- Parsing + selector wiring exists (`CSSSelector::kPseudoHas`, `SelectorChecker::CheckPseudoHas`). +- Traversal, argument context, cache, and fast‑reject are **stubbed**: + - `bridge/core/css/check_pseudo_has_argument_context.h` + - `bridge/core/css/check_pseudo_has_traversal_iterator.h` + - `bridge/core/css/check_pseudo_has_cache_scope.h` +- Element invalidation flags are declared but **no-op** in `bridge/core/dom/element.h`. +- Style invalidation hooks for `:has()` are **not implemented** in `StyleEngine`. + +--- + +## Phase 1 — Argument context (correctness baseline) +Implement `CheckPseudoHasArgumentContext` to compute: +- Leftmost relation (`kRelativeDescendant`, `kRelativeChild`, `kRelativeDirectAdjacent`, `kRelativeIndirectAdjacent`). +- Fixed vs unbounded depth and adjacency limits. +- Whether siblings may be affected and which flags apply: + - `AllowSiblingsAffectedByHas()` + - `GetSiblingsAffectedByHasFlags()` +- Hashes for fast‑reject: class/id/tag/attr/pseudo in the argument list. + +Files: +- `bridge/core/css/check_pseudo_has_argument_context.h` (and a new .cc if needed) +- `bridge/core/css/css_selector.h/.cc` (if new helper APIs are required) + +--- + +## Phase 2 — Argument traversal iterator +Replace stub traversal with real iteration: +- Descendant/child traversal with depth tracking. +- Sibling / adjacent traversal (`+`, `~`) with optional subtree descent. +- Ensure traversal respects “relative selector” semantics (leftmost combinator). +- Avoid duplicated traversal when multiple anchors exist (use the “reversed DOM order” guidance in `has_invalidation_flags.h`). + +Files: +- `bridge/core/css/check_pseudo_has_traversal_iterator.h` (consider splitting into .cc) +- `bridge/core/dom/element_traversal.h` or existing traversal helpers if needed + +--- + +## Phase 3 — Cache + fast reject (perf‑safe) +Implement `CheckPseudoHasCacheScope` to avoid repeated checks: +- Per‑document cache keyed by (anchor element, selector hash). +- Track checked/matched to short‑circuit repeated argument checks. +Implement `CheckPseudoHasFastRejectFilter`: +- Conservative bloom filter (or “always false” for FastReject as a correctness fallback). + +Files: +- `bridge/core/css/check_pseudo_has_cache_scope.h` +- `bridge/core/css/check_pseudo_has_traversal_iterator.h` + +--- + +## Phase 4 — Element invalidation flags +Wire `HasInvalidationFlags` storage into `Element`: +- Add a struct field in `ElementRareDataVector` or `NodeRareData`. +- Implement non‑no‑op setters/getters in `bridge/core/dom/element.h/.cc`: + - `SetAffectedBySubjectHas()` + - `SetAffectedByNonSubjectHas()` + - `SetAffectedByPseudoInHas()` + - `SetAffectedByLogicalCombinationsInHas()` + - `SetAncestorsOrAncestorSiblingsAffectedByHas()` + - `SetSiblingsAffectedByHasFlags()` + - `AffectedByMultipleHas()` / `SetAffectedByMultipleHas()` + +Files: +- `bridge/core/dom/element.h/.cc` +- `bridge/core/dom/element_rare_data_vector.h` or `bridge/core/dom/node_rare_data.h` + +--- + +## Phase 5 — Style invalidation wiring +Add `:has()` invalidation entry points to `StyleEngine` and DOM mutation paths. + +New `StyleEngine` APIs: +- `InvalidateElementAffectedByHas(Element& changed)` +- `InvalidateAncestorsOrSiblingsAffectedByHas(Element& changed)` + +Call sites: +- `StyleEngine::IdChangedForElement` +- `StyleEngine::ClassAttributeChangedForElement` +- `StyleEngine::AttributeChangedForElement` +- Insert/remove paths in `bridge/core/dom/container_node.cc` + +Rules: +- Use `RuleFeatureSet::NeedsHasInvalidationFor*` to short‑circuit. +- For subject `:has()`: mark anchor element `SetNeedsStyleRecalc(kLocalStyleChange, kAffectedByHas)`. +- For non‑subject `:has()`: schedule descendant/sibling invalidation sets on the anchor element. + +Files: +- `bridge/core/css/style_engine.h/.cc` +- `bridge/core/dom/container_node.cc` + +--- + +## Phase 6 — Tests +Unit tests: +- Selector parsing & matching in `bridge/core/css/css_selector_test.cc`: + - Descendant/child/sibling forms (`:has(.b)`, `:has(> .b)`, `:has(+ .b)`, `:has(~ .b)`). + - Nested logical combos: `:has(:is(.a .b))`, `:has(:where(.a .b))`. + +Integration tests: +- Mutation invalidation scenarios: + - class add/remove on descendants/siblings + - sibling insertion/removal + - multiple anchors with overlapping scopes + +--- + +## Build/Test checklist (C++) +- Build: `npm run build:bridge:macos:arm64` +- Unit: `node scripts/run_bridge_unit_test.js` +- Integration: `cd integration_tests && npm run integration -- specs/css/css-selectors/has-*.ts` + diff --git a/docs/TAILWINDCSS_SUPPORT_PLAN.md b/docs/TAILWINDCSS_SUPPORT_PLAN.md index e7d72b0096..c4586a934a 100644 --- a/docs/TAILWINDCSS_SUPPORT_PLAN.md +++ b/docs/TAILWINDCSS_SUPPORT_PLAN.md @@ -26,7 +26,7 @@ Target: **Tailwind CSS v3.4.x** (core preflight + core utilities + core variants | `@supports` | `supports-*` variants | ❌ | `webf/lib/src/css/parser/parser.dart` returns `null` for `DIRECTIVE_SUPPORTS` | Implement `@supports` parsing + evaluation (at least allow/deny blocks, and “declare support” checks used by Tailwind). | | Media queries (MQ) | Responsive (`sm/md/...`), `dark`, `motion-*`, `orientation-*`, `print`, `forced-colors`, `prefers-contrast` | ⚠️ | `webf/lib/src/css/css_rule.dart` only evaluates `min/max-width`, `min/max-aspect-ratio`, `prefers-color-scheme`; rejects `print` type | Expand media query parsing/evaluation for Tailwind variants: `prefers-reduced-motion`, `orientation`, `forced-colors`, `prefers-contrast`, `hover`, `pointer`, `print`. | | Selector pseudo `:where()` | Preflight selectors (`abbr:where([title])`, `[hidden]:where(:not(...))`), direction variants (`rtl:`/`ltr:`) | ⚠️ | Blink/bridge (C++) supports `:where()` parsing/matching and 0-specificity (`bridge/core/css/css_selector.cc` handles `CSSSelector::kPseudoWhere`), but the legacy Dart selector engine still lacks selector-list parsing/matching (`webf/lib/src/css/parser/parser.dart`, `webf/lib/src/css/query_selector.dart`). | Implement Selectors L4 `:where()`/`:is()` selector-list parsing + matching + specificity in the Dart engine; keep coverage via `integration_tests/specs/css/css-selectors/is-where-*.ts`. | -| Selector pseudo `:has()` | `has-*`, `group-has-*`, `peer-has-*` variants | ❌ | No `:has()` parsing/matching in Dart selector engine | Implement `:has()` parsing/matching + invalidation strategy (at least a correct baseline, then optimize). | +| Selector pseudo `:has()` | `has-*`, `group-has-*`, `peer-has-*` variants | ⚠️ | Blink/bridge parses `:has()` and has selector-checking/invalidation hooks, but traversal/caching are stubbed (`bridge/core/css/check_pseudo_has_*`); Dart selector engine still lacks `:has()`. | Implement full :has traversal/caching/invalidation in C++; add Dart parsing/matching; cover with integration tests. | | Pseudo-classes (state) | `hover:`, `focus:`, `focus-visible:`, `focus-within:`, `active:`, `enabled:`, `disabled:` | ❌ | `webf/lib/src/css/query_selector.dart` only implements a small set (e.g., `:root`, `:empty`, `:first-child`, `:nth-*`); does not implement hover/focus/active/etc | Add element state model + selector matching for Tailwind pseudo-class variants; hook to Flutter pointer/focus events. | | Pseudo-classes (forms) | `checked:`, `indeterminate:`, `placeholder-shown:`, `required:`, `valid:`/`invalid:`… | ❌ | Not implemented in selector evaluator | Implement form state pseudos based on element type/attributes/value validity model (as supported by WebF’s form elements). | | Pseudo-elements used by Tailwind variants | `::before/::after`, `::placeholder`, `::selection`, `::marker`, `::file-selector-button`, `::backdrop`, `::first-letter/line` | ⚠️ | WebF has real `::before/::after` elements and first-line/first-letter plumbing, but selector matcher treats only a “legacy” subset as matchable; others return false | Extend pseudo-element matching + rendering support where meaningful; at minimum `::placeholder` for preflight + utilities. | @@ -49,7 +49,7 @@ Tailwind v3.4.18 core variants are exposed by `variantPlugins` (15 groups). | `orientationVariants` | `@media (orientation: portrait/landscape)` | ❌ | Media feature not evaluated today. | | `printVariant` | `@media print` | ❌ | WebF currently rejects non-`screen` media types. | | `supportsVariants` | `@supports (...)` | ❌ | `@supports` not parsed/evaluated today. | -| `hasVariants` | `:has(...)` | ❌ | Not parsed/evaluated today. | +| `hasVariants` | `:has(...)` | ⚠️ | Blink/bridge has parser + checker wiring, but :has traversal/caching are stubbed; Dart selector engine lacks `:has()`. | | `ariaVariants` | Attribute selectors like `&[aria-checked=\"true\"]` | ✅ | Attribute selectors are supported; needs Tailwind fixture tests. | | `dataVariants` | Attribute selectors like `&[data-state=\"open\"]` | ✅ | Attribute selectors are supported; needs Tailwind fixture tests. | | `childVariant` | `& > *` | ✅ | Combinators supported. | @@ -66,7 +66,7 @@ Tailwind v3.4.18 core utilities are exposed by `corePlugins` (179 keys). Below i **P0 blockers (must fix first)** - Selector parsing/matching: `:where()` (Tailwind preflight + direction variants). -- `:has()` correctness + performance + invalidation strategy. +- `:has()` correctness + performance + invalidation strategy (C++ has stubbed traversal; Dart missing). - Selector matching: interactive pseudo-classes (hover/focus/active/disabled/checked/…). - https://github.com/openwebf/webf/issues/659 - Media query evaluation: `orientation`, `prefers-contrast`. diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.0939efdd1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.0939efdd1.png new file mode 100644 index 0000000000000000000000000000000000000000..9d2b2359f11be5e33ce0bb54426521506826264b GIT binary patch literal 2412 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+sPZ!6KiaBrZTJs$;5O8qpDJrnoxw7fVdY?~CX{$8yVrI*BwkGx3xW3_u^&fFHfese-QQ-P6MMuSEpTZa(&)QZH_<_?oNr4+I%|8LCu z{%`i$dvY8HJZCOoT*M-!;6(wI^Odc`$dTFQ<||%-)Fz4?3G(BntI`UmSa=1#ZoK+C zvnN)6lO;dvgA|_&41r9%0;&{H`^$xHJ!CJ?5H@j`;=t^}IZBNN!DuQN%?P7q!Dw+f gS|iZEHoU?dcS_~!)VG(Sfh{HmPgg&ebxsLQ0AROKpa1{> literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.18ff8aed1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.18ff8aed1.png new file mode 100644 index 0000000000000000000000000000000000000000..bb8d2d1f1f72952d5b8bf3ca2049c0dc77baafb4 GIT binary patch literal 2415 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+^PZ!6KiaBrZTJkkJ2rxLVKYIA%>FbO_jr{_bBSJadw*+RJ%`$vG z$DE<)UAjC&!})1{b9>%y%$r|q``UxOFpxp-7z?j}>IH^CCh{rMoUd#dMvlxby6f$a zCEtGc*F1V}tUyCINQl$KVTuF!)Xi7C0;x@G9hsXDnb`#3H5OMFF+%y~>uu{0~CZG8!}***b(qsnH-9O$DPFVYDn5Ee=O( f1p3#8E7YS7seIj5FLNH)Xkzem^>bP0l+XkKHwfp= literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.2f79ef791.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.2f79ef791.png new file mode 100644 index 0000000000000000000000000000000000000000..67de1fc16e75f8e152b0092a1ae794728f9468e8 GIT binary patch literal 2421 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n-fPZ!6KiaBrZZsa=TAmZv6{^{Ybr`d{H61s=y9q>^Ni! i^-$2TpiTz5!dzY>^S|}1VI{EX#Ng@b=d#Wzp$Py7cT*Dp literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.30aca6431.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.30aca6431.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b1dd361d9b31e27a0e146c92797f3757026667 GIT binary patch literal 2408 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+cPZ!6KiaBrZTJjxo5Mgy>`}ttsPXP%J9>rN^tGptV+}A#T(Qo;D z595U5_`eJcHQV;??=7r(bNu|y^;JvcKQ3a3>0#v+P`$tq$V5J68uOJcW77i0McXdh zpMQO?{xCOFL736>9~K#JKhabDEIr< zzhFmtM^YAq6vn`)c1qLmXb6smAo(TWXzCeFJ)^0If+hs@G7x{Eqs;$k2P;)zYl*?r L)z4*}Q$iB};8_OA literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.3eb0b88a1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.3eb0b88a1.png new file mode 100644 index 0000000000000000000000000000000000000000..264d262b4ece5af5f412641980ab61de9c7b6a85 GIT binary patch literal 2406 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+6PZ!6KiaBrZ8VWTzh_E_d`}E*WO>fTuf${~roENLkta7VAWBHuT z;av7tW(EuMZMD|17mEM?PhMvt_s#ae=g12TflRytsuWO%L-QNdxTO?c)&5BBQRUy+ zW6yEGmkQ<{pWTx8gHa^{=(KfJ%q~AvhX>n|>kr!k+e!?cu6{1- HoD!MVJUX< z4B-HR8jh3>1_n+UPZ!6KiaBrZ8uB$ch%f|RFDS728I|B85?$bNFI(Y4j_J?nzlP7} zm@^c;`)|j@bU*9Op9&u&dAt7wyFvWq{g_C^h%7&}b3r@1|3T%C| zuO|2VJ{xhC2a9|zFa$F33aC;*{hr%W_JdJn#sbDgIbYd2j2tO)CCHOEU-1g0HnDZw z^1q<}cw_GOd9P<4x12DWvA|F*qd}vQtwV@>YK7tQ)LI5t$_=8>9i!SQjmgmv91TJ8 q%Yo6VJUX< z4B-HR8jh3>1_n+ZPZ!6KiaBrZD)KQo3b-DWJhZoQvN;!X#HN1VsV-+{P49nYYrL7^ z#7>#N3=I2jZ@#u>>$QFIf2Z7^HMw5Cz)sA>VTuE@3n%&1m9neS3r;q%b?n;re}nw* z8Porqxbw%6;ooFdUIEn$41rAKQ%_f!J4^u@1EZ;A+}FaZzmJyYHzY?=<~)#d)>SdP zP{9}q0*S&v8P!f{Djf~M(GVoRj2lfoqp4>!^-!n&^S>vh`g&EhR{*e`#Ng@b=d#Wz Gp$PzK90$4p literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.5ce78b901.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.5ce78b901.png new file mode 100644 index 0000000000000000000000000000000000000000..3917faa14cb2ffb0c99c7b088752948e70a63c3c GIT binary patch literal 2403 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n-EPZ!6KiaBrZ8VWTzh_D`vJ>C5HFa$F33aC;*T@KA}P~(bsuV{!w7CKh?z6A(YXe(MUd3bX8gbWDJa^l5thLUe!L|Wqx3z4rR^*IVXHC<02{; zLqQ->7$~FKDNUuLAvhX>VJUX< z4B-HR8jh3>1_n+UPZ!6KiaBrZ8uA@75MeziQTU;tC|H3@XUD8pU-Ohy|mx*@{X7PnckglV(SpfXwYaRpORWv#cUFJfg$kxVf|#C z$`hG23Jl_@j?6BcCJs{^$fsoUg?2q;pO64FNmlGjMeVw(^R-8d{T_5P?9kwrQt(>9 zxQK;(D(lS440(r%th@rHJ6`>Ly371Plo)011$lS%J%&Ik7(+p*Q5fi>+9^%8qaipN qg5(!^qp4>!^^B$-s?>j1qY=5&*WF7(8A5T-G@yGywoOu1!S% literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.79d56e171.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.79d56e171.png new file mode 100644 index 0000000000000000000000000000000000000000..3bd7eae89bf28cb5ed8292a81f8f98634ef62f79 GIT binary patch literal 4298 zcmeI0Sx{418h|gmfROHH8We#zdIgmw&`}^wOmJ(tpg}EV5i}qQI4l8TLxKsQfI_>_ zbrB^XLG8h%p$Ur!5fa>xxnc+{Dndd6&26Fr37tR?0%WMEshXFm>VBB6dGWlQI{$h2 ztN!}_@2_(T_fmFQS=d_u0AK|L?AQ+g2Jrx3n6u1uDN=9$%k8DjfWCj%HlViK;r-IV zn7$1nV)>LI2?)*($VeS4#8Nai4!1id+#=GXLEw&wv zvAp4S_oC%@E1I0w++6d_vCduJZ)0QcEc=NWFZ+5h9y`~vXc-&b#}-n+FU@I-aM z4cDX-ylkn;QS6}du9)cU9xK`1eT~kY=mu3fG$TvF3{ks=TWt=(Y2*eq{ol4=fSilw zoBTJNHeKJO@t0SkBT(|-qXvxI7JVP+ql^;hIczr`CdGKbucsPHE(;Knk#n&0E7c$u z6+w&aa(5YwehJk^Feb`C)!?I_SuAa;oTPUEFn+Bw^An>WEm-}&YMoOF+Bp+_p2|hX z!P^dXF~+`YelnR8g0w(&2PlTr-`Wh|y6c_@eLavFr^_)WzBg-3=GahjP#OelY-L-L z7v1Jqa5FgjmPp1UJ0VJiqS=MXhBd|4T06Mp5|maf^%P^t?SBCRv=_E!$8xu1nI%M) zqe@U5sGc&Ph_I8jlaXw3|4eK$Mg4%_M}a4Azbu zAQfzCGfc^gu$+SM>1{Ty5~93N;J)4ml9S9dz)ZJ8K|+9(f%Z7`38fRkw?#{T+cf1}G^#OLfcwAveZ-5~JaIIZge zs*L~lmrV_2HBQRhs7F8Qt)njD|C9Eh$($p!m}S;ASc#s_S6~Cx3coPb8;};1rpa@s_Mfk?^O z9v$kOVh1u0!R--;hKe;ZO7~t&eo3LG*LvnO;Bc(fuJT$LdfVlt>p4~F{pq+?su%2xeT_GM?{KP z743Os1zHA@H@AC|$Rp2u+}$+8pigprWot2U19A|`azj+2mgy>q{uJ=)Z9>7w9gs&! zi-uj{`FEzSQqO%^sjxhpv9Lj-_zomDcVu35p3QZ)e)kbnRbF>S!Xe>WWLGh3+B*Br zQeAi}Ettb?+O^Oniu$Z$ve8hAF1vT!BS1Y~IBw@1HY~Tj5-}yxe8iQH$Fl`-Npyel z;Q~6$njp%WM49JF4Tu*h8++d%r6tK1GgpEaD4V?*G`96`g{#^!KG<%@jEz;aA-ZzM zo?Oly0qI2p>%whPL6U)k$11IAERHeqr6yW(lULd)@d;BY-Pg3d&NLe^x9n_ucYk{w zEyw71ugxnexAQ7akTpjX#=V~orm(P$h}Jto73E8J;zeXGIG&4=17^>aA{pZ(Of5f= zdcK>3`J76?4zNcq9He@yvh~iAh)C5_Fqq3r%wCNII}>8bSp_@})tXj;*@&a4Z-V5@ zXtN@Y?~PPzW3a%vf!f4W`-PW#&blqe-x3N=-gD+@%yR#ZezNW3)XK-c^yo*s zXm#oh8ip$Ioe3l(@*K%U<%6KsUn*ENG@KX;6O)t8rt`s2(F!%r-TfoqDnuCLR76qN z7HW9Te46q)TaxFL!*1>?95csHO}+yAaPEw0myB@4r6jEBy7ct zp)jl~$qxyFBk$iE(i9WdARU_dk1I`?KNHDGXMK*8pk>U7$6zSPKbh;eG;EUG?4v~7 z@@F!y?O&`Ktk6wy8d=BTh`9dVQA_78CFeb(XrR-Njo;m9^lUCA^^#=R!bMyuawtZ< zblH)>F5+4XpekJz#c4|*`5O=Q^y5wUn8)-y^n7aYUlji;x%UC^H6iy8;P)pCgx^K} zQ_eqU5`QZE9}(^+`aaS3NsRw`V15$D|5q5V^oIM=)2NsFXueDT5dgH4vZHoe)R}(* Dq6wiV literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.85a326141.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.85a326141.png new file mode 100644 index 0000000000000000000000000000000000000000..bb8d2d1f1f72952d5b8bf3ca2049c0dc77baafb4 GIT binary patch literal 2415 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+^PZ!6KiaBrZTJkkJ2rxLVKYIA%>FbO_jr{_bBSJadw*+RJ%`$vG z$DE<)UAjC&!})1{b9>%y%$r|q``UxOFpxp-7z?j}>IH^CCh{rMoUd#dMvlxby6f$a zCEtGc*F1V}tUyCINQl$KVTuF!)Xi7C0;x@G9hsXDnb`#3H5OMFF+%y~>uu{0~CZG8!}***b(qsnH-9O$DPFVYDn5Ee=O( f1p3#8E7YS7seIj5FLNH)Xkzem^>bP0l+XkKHwfp= literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.91127d8c1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.91127d8c1.png new file mode 100644 index 0000000000000000000000000000000000000000..3f5766e686df7130a94d32eb61a11d395510fc4a GIT binary patch literal 2428 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+wPZ!6KiaBrZ8u}e|;Bh!8QTUb8yV0Pgo zpSrThG(R9Oh5ZrrePf`f*e{ zrKw~z1V=-V{4!=V^^B&T(bPjhB~6_SbVYrcoI=_3Bj&q+Ehq+0S3j3^P6VJUX< z4B-HR8jh3>1_n+sPZ!6KiaBrZTJs$;5O8qpDJrnoxw7fVdY?~CX{$8yVrI*BwkGx3xW3_u^&fFHfese-QQ-P6MMuSEpTZa(&)QZH_<_?oNr4+I%|8LCu z{%`i$dvY8HJZCOoT*M-!;6(wI^Odc`$dTFQ<||%-)Fz4?3G(BntI`UmSa=1#ZoK+C zvnN)6lO;dvgA|_&41r9%0;&{H`^$xHJ!CJ?5H@j`;=t^}IZBNN!DuQN%?P7q!Dw+f gS|iZEHoU?dcS_~!)VG(Sfh{HmPgg&ebxsLQ0AROKpa1{> literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.c713df221.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.c713df221.png new file mode 100644 index 0000000000000000000000000000000000000000..17e5858c4228b71b7a5e356c2e04b3a86b225216 GIT binary patch literal 2761 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_rKTPZ!6KiaBrZ8fJ%vGaUFhS*WR-L$8tNt`EC#`f|Uti4BXr%1vTyjPw1??c`Gw}m^O%kB8^yrAO! zo~<7*&)_vmv#*jpAbvmQ3k$;!t46jCp^OF%2({wt%I^Dr|NhZ^! zJU^4!wng$m{X-qTh7~6rfIi&tfq9xC(Dk!;zMNjG&dvJm$ALPAb9qJlzusOI|HsSv zpy@_4axj5HD{6MH;qDin1&7YOX4to(30-=YP50xU*S0_Yxb^kpg5>0N2iMEU_Oidf z*R%I~{W)vL@F9W z)A*$nycRGnVj+sk`ed?O+ua>x<(+%GjlVOmlaW1JtA9Xic8sHa#rL#@2jt{$@E^Fj z_Uy44v0qdy5}%ba#AET?_qB`-YTEF00ZksG6fC2RhQw${jOGqR#W9*6M)SjHei*X( cVa55p{nI9~R?U+FHb)seUHx3vIVCg!0HvkKE&u=k literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.d395e40c1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.d395e40c1.png new file mode 100644 index 0000000000000000000000000000000000000000..9d99792abcb442b37f1182d81f733402a0067dfc GIT binary patch literal 3399 zcmeH~>rYd67{?EnAt*#0nvKf9co}Ras6%90py&{W;9bK+1Q87oJuOgd!9!c*64WuO zi9m!xnHv|K1GR7pr57c}4v|(T2>L6O%Vt456k2=Z&~rxi+WiINdGpEhynOO}zu!Dp z4(^ZgSV~z6L6ArE_YsF7$RQ1a9P?dW7AE&8V~q=NU>=Uz4K=I0Cl?xL=I-dDt_zjt zdgd|&xz9#N>^hnwz=lpVHRtY9&T7;4pY^G`eczqx_H}GuagOKe#2;4mU#p@2D!J~z zbIWnBBYm+>thMe16YD5<-#;mEFSzCJf6vvir!MyCt;sspy{c~}w{=#N75%5x?J2Kn z?LlF_gCXZaODwtCw$V-kC&ScS{a|6|c0wyL=PMGe2TiTO?hLhWz2tKF)3kj5kD+OK z=VYjFc2xY+Lbhf)71ErJd=4FQ^jNX+;{Yh_wRssJrdYI<`ODi2!WA6Q!oYuDa~`i0 zVW9_zZQ{Oau`CNHhNk@-XB^^IO!y9&ufoDMOuQM!Uy86w;e0w#*`hJO+r*8{uh=9Q zYe#^XexSRgNWlc!kWX|D?VRQ<&uOg~Tb2FDGdCO9L825ZgVn%X1?v^})JZU|f`ysM z!e9no0$a`_Q(om-5jMLojqFWR_#T>)9z^GRlOgi-ib=XTUc8y+pp3a<498hHK53_5pX9=8Fn``y9+zDJq zK48`>16BRL3|SD(CEss!siQroa0W;1f1nCkE8zj;lL>EGFHDClxT>5o(6}x!JD?XC z>5{kX7zorNkv)7mULl&l1k;$w+R7yB+;Ctp@vvi629Tcud@vh$Z_jnDUS z?^xQFVJUX< z4B-HR8jh3>1_n++PZ!6KiaBrZ8uA@75Mgzce)^!GD0nH?l!@k-d|Fq;tc_b`e?2Oj zVZ!HIe;F9+tk21RHa;$qyypA4=XXxDGkq`+GjW*W!0f_FK6Rz!s`P@BO>7-jdjHSt ze5$zpVfR~QgWC-L7SU0V<>dRsCG)@aWn)+Ly+RaU^EepCZf?qL_uvzc?NpL-RLOu VU-`(5!@$N8gQu&X%Q~loCIAZwc)$Pv literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.f2c499201.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.f2c499201.png new file mode 100644 index 0000000000000000000000000000000000000000..05b2a151b5c7a26092020737919a492bf5e6591f GIT binary patch literal 2406 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+6PZ!6KiaBrZ8uA@75MeziQTXA`oe+@|0d8@NW==Yl8P@-*-}1Q) z!->i_bqoymdmr1_iG8v8Zu`vZ@v{4@cc!!Q3aDOS2xKCkGL8Alma%C8%k0jcL QY%4K%y85}Sb4q9e02SX1od5s; literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.f70261ee1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.f70261ee1.png new file mode 100644 index 0000000000000000000000000000000000000000..8ba9db8bb2374b48804550a6ba1d1888e3748afa GIT binary patch literal 2422 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+APZ!6KiaBrZTJs$;5O8qpv9OqPM?_GB<fuce2<$L=1k#~Qt(>9 zxQK;(Dr^7b3w-tsEW#!ZSL&MTHr|y^&;+ulI54|#l22WEKewgq2cr%ZJWgS_jcTVf y<&B2mXb6&DZjGj%(bO}VdMK#)sgr@Kn6mCD{JJw$)fL!wV(@hJb6Mw<&;$VQKlG;n literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.f89e13111.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.f89e13111.png new file mode 100644 index 0000000000000000000000000000000000000000..93395959c91813525cfaa39c8c6de868d18a0dcf GIT binary patch literal 2402 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+ZPZ!6KiaBrZ8uB$8h_D{Cc=Rv4Y?Vsu!k#@Y#-fWBMb)k{e>Uec zqr z+X3I53m6x%NGW(xKwa~7lCERm1i5haJ%&KaT?#X1#n+~~&F^?OOy#D`fk6Mgs+xSE zP5wX_6&y!dEKnGUquMFW2BRT38iM2(qNAy2H1&+89tv6^&VJUX< z4B-HR8jh3>1_n-1PZ!6KiaBrZ8uA@75MgzcF8okXbUDyLLwH)ovL!}mUoHJryml)a zgXQ*kdxnPE;+*7i$MtXj9GA(Ad-I>+db%UC3#W<06bJGt-|)SRvve{VR{Z|W|Jm^P zj!U8W@*D@|Z(6{(h($`livlXqs8H9jX?j}@GHyYgDQomH+*IVHkcSZ MUHx3vIVCg!0IpvZ`2YX_ literal 0 HcmV?d00001 diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.fe50857e1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.fe50857e1.png new file mode 100644 index 0000000000000000000000000000000000000000..2b23744683d470e1be5e565e762f624586f253d9 GIT binary patch literal 2424 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_n+gPZ!6KiaBrZ8uA@75MXfZ`BYG`Q**)sjwuJFO@dulu8W$xTDtgL zC8NUgT~!PWcDLi=lXYaD6v%NMIC*0M<02L*1uqJyd*+;X-!OY@bYynP-EIHB?C$N; z3>KQwCJs{^m|Zx@r>^`{TPR(}z-gAzu;MP8?Yr}5rDkU>4QnXN-8qd}vQ zd`ha=>K1!I3ecciv0v*XA1`l~Ip@r1AxQ;?f1llw_k&S~3XY`EkE7ZtO(mltI2wZF qmocNMXEgPUrXC6^Y3gL4Du&$SGXHO|@#h0uPYj-}elF{r5}E+MgY$_1 literal 0 HcmV?d00001 diff --git a/integration_tests/specs/css/css-selectors/has-basic.ts b/integration_tests/specs/css/css-selectors/has-basic.ts new file mode 100644 index 0000000000..9a11009b69 --- /dev/null +++ b/integration_tests/specs/css/css-selectors/has-basic.ts @@ -0,0 +1,463 @@ +/** + * CSS Selectors: Basic :has() matching behavior + * Based on WPT: css/selectors/has-basic.html + * + * Key behaviors: + * - :has() matches elements that have descendants/siblings matching the argument + * - Works with descendant, child, and sibling combinators + * - Works with querySelectorAll, querySelector, closest, matches APIs + */ +describe('CSS Selectors: :has() basic matching', () => { + const green = 'rgb(0, 128, 0)'; + const red = 'rgb(255, 0, 0)'; + + function appendStyle(cssText: string) { + const style = document.createElement('style'); + style.textContent = cssText; + document.head.appendChild(style); + return style; + } + + function createTestDOM() { + const main = document.createElement('main'); + main.id = 'main'; + main.innerHTML = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `; + document.body.appendChild(main); + return main; + } + + function formatElements(elements: Element[]) { + return elements.map(e => e.id).sort().join(','); + } + + // ========== Basic :has() with descendant selector ========== + + it('A1 :has(#a) matches nothing (no element has #a as descendant)', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: green; width: 50px; height: 20px; margin: 2px; } + :has(#a) { background-color: red; } + `); + + const actual = Array.from(main.querySelectorAll(':has(#a)')); + expect(formatElements(actual)).toBe(''); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('A2 :has(.ancestor) matches element a', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(.ancestor) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(.ancestor)')); + expect(formatElements(actual)).toBe('a'); + expect(getComputedStyle(main.querySelector('#a')!).backgroundColor).toBe(green); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('A3 :has(.target) matches ancestors of .target elements', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(.target) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(.target)')); + expect(formatElements(actual)).toBe('a,b,f,h'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('A4 :has(.descendant) matches all ancestors of .descendant', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(.descendant) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(.descendant)')); + expect(formatElements(actual)).toBe('a,b,c,f,h,j'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() with class filter ========== + + it('A5 .parent:has(.target) filters by class', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .parent:has(.target) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll('.parent:has(.target)')); + expect(formatElements(actual)).toBe('b,f,h'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() with sibling combinator ========== + + it('A6 :has(.sibling ~ .target) matches elements with sibling-then-target pattern', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(.sibling ~ .target) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(.sibling ~ .target)')); + expect(formatElements(actual)).toBe('a,b'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('A7 .parent:has(.sibling ~ .target) with class filter', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .parent:has(.sibling ~ .target) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll('.parent:has(.sibling ~ .target)')); + expect(formatElements(actual)).toBe('b'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() with :is() nested ========== + + it('A8 :has(:is(.target ~ .sibling .descendant)) complex nesting', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(:is(.target ~ .sibling .descendant)) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(:is(.target ~ .sibling .descendant))')); + expect(formatElements(actual)).toBe('a,h,j'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('A9 .parent:has(:is(.target ~ .sibling .descendant)) with class filter', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .parent:has(:is(.target ~ .sibling .descendant)) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll('.parent:has(:is(.target ~ .sibling .descendant))')); + expect(formatElements(actual)).toBe('h'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() in sibling selector context ========== + + it('A10 .sibling:has(.descendant) ~ .target matches target after sibling with descendant', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .sibling:has(.descendant) ~ .target { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll('.sibling:has(.descendant) ~ .target')); + expect(formatElements(actual)).toBe('e'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() with child combinator ========== + + it('B1 :has(> .parent) matches only direct parent of .parent', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(> .parent) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(> .parent)')); + expect(formatElements(actual)).toBe('a'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('B2 :has(> .target) matches direct parents of .target', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(> .target) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(> .target)')); + expect(formatElements(actual)).toBe('b,f,h'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('B3 :has(> .parent, > .target) matches with OR logic', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(> .parent, > .target) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(> .parent, > .target)')); + expect(formatElements(actual)).toBe('a,b,f,h'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() with adjacent sibling ========== + + it('B4 :has(+ #h) matches element immediately before #h', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(+ #h) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll(':has(+ #h)')); + expect(formatElements(actual)).toBe('f'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== :has() with general sibling ========== + + it('B5 .parent:has(~ #h) matches .parent elements before #h', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .parent:has(~ #h) { background-color: green; } + `); + + const actual = Array.from(main.querySelectorAll('.parent:has(~ #h)')); + expect(formatElements(actual)).toBe('b,f'); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== querySelector API ========== + + it('C1 querySelector returns first matching element', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .sibling:has(.descendant) { background-color: green; } + `); + + const result = main.querySelector('.sibling:has(.descendant)'); + expect(result).toBe(main.querySelector('#c')); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== closest API ========== + + it('C2 closest finds nearest ancestor matching :has()', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + .ancestor:has(.descendant) { background-color: green; } + `); + + const k = main.querySelector('#k')!; + const result = k.closest('.ancestor:has(.descendant)'); + expect(result).toBe(main.querySelector('#h')); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + // ========== matches API ========== + + it('C3 matches returns true for matching :has()', async () => { + const main = createTestDOM(); + const style = appendStyle(` + main div { background-color: red; width: 50px; height: 20px; margin: 2px; } + :has(.target ~ .sibling .descendant) { background-color: green; } + `); + + const h = main.querySelector('#h')!; + expect(h.matches(':has(.target ~ .sibling .descendant)')).toBe(true); + + await snapshot(); + + style.remove(); + main.remove(); + }); + + it('C4 matches returns false for non-matching :has()', async () => { + const main = createTestDOM(); + + const b = main.querySelector('#b')!; + // b has .target but not .target ~ .sibling .descendant pattern + expect(b.matches(':has(.target ~ .sibling .descendant)')).toBe(false); + + main.remove(); + }); +}); + +/** + * CSS Selectors: :has() visual styling tests + * Tests that :has() correctly applies styles visually + */ +describe('CSS Selectors: :has() visual styling', () => { + const green = 'rgb(0, 128, 0)'; + + function appendStyle(cssText: string) { + const style = document.createElement('style'); + style.textContent = cssText; + document.head.appendChild(style); + return style; + } + + it('D1 Parent styled green when child exists', async () => { + const container = document.createElement('div'); + container.innerHTML = ` +
+ Child +
+
+ No child span +
+ `; + document.body.appendChild(container); + + const style = appendStyle(` + .parent:has(.child) { background-color: green !important; } + `); + + const parents = container.querySelectorAll('.parent'); + expect(getComputedStyle(parents[0]).backgroundColor).toBe(green); + // Second parent has no .child, should remain red + expect(getComputedStyle(parents[1]).backgroundColor).toBe('rgb(255, 0, 0)'); + + await snapshot(); + + style.remove(); + container.remove(); + }); + + it('D2 Multiple levels of :has() nesting', async () => { + const container = document.createElement('div'); + container.innerHTML = ` +
+
+ Nested +
+
+ `; + document.body.appendChild(container); + + const style = appendStyle(` + .grandparent:has(.child) { background-color: green !important; } + `); + + const grandparent = container.querySelector('.grandparent')!; + expect(getComputedStyle(grandparent).backgroundColor).toBe(green); + + await snapshot(); + + style.remove(); + container.remove(); + }); + + it('D3 :has() with attribute selector', async () => { + const container = document.createElement('div'); + container.innerHTML = ` +
+ +
+
+ +
+ `; + document.body.appendChild(container); + + const style = appendStyle(` + .item:has(input[checked]) { background-color: green !important; } + `); + + const items = container.querySelectorAll('.item'); + expect(getComputedStyle(items[0]).backgroundColor).toBe(green); + + await snapshot(); + + style.remove(); + container.remove(); + }); +}); From 074f2d04dc78fa05e81076294d0ca1f9c33136e0 Mon Sep 17 00:00:00 2001 From: jwxbond Date: Wed, 28 Jan 2026 22:13:44 +0800 Subject: [PATCH 02/16] fix(css-selector): support kPseudoChecked, add `has-specificity` tests and snapshots --- bridge/core/css/selector_checker.cc | 25 +- .../has-specificity.ts.08664f2e1.png | Bin 0 -> 2972 bytes .../has-specificity.ts.0f44a1611.png | Bin 0 -> 3210 bytes .../has-specificity.ts.1cd508271.png | Bin 0 -> 2369 bytes .../has-specificity.ts.2a31f0e21.png | Bin 0 -> 2369 bytes .../has-specificity.ts.349ee1d41.png | Bin 0 -> 2374 bytes .../has-specificity.ts.4a91931a1.png | Bin 0 -> 3097 bytes .../has-specificity.ts.4c1451771.png | Bin 0 -> 2369 bytes .../has-specificity.ts.4eb2a4ac1.png | Bin 0 -> 2972 bytes .../has-specificity.ts.52bee3611.png | Bin 0 -> 2369 bytes .../has-specificity.ts.71df37f01.png | Bin 0 -> 2408 bytes .../has-specificity.ts.7b83fd0e1.png | Bin 0 -> 3210 bytes .../has-specificity.ts.9fe4eb3c1.png | Bin 0 -> 2408 bytes .../has-specificity.ts.a610a60b1.png | Bin 0 -> 2369 bytes .../has-specificity.ts.d4a25c9a1.png | Bin 0 -> 2374 bytes .../has-specificity.ts.d52b56311.png | Bin 0 -> 2369 bytes .../has-specificity.ts.e60faaa81.png | Bin 0 -> 3203 bytes .../has-specificity.ts.f2b936581.png | Bin 0 -> 2633 bytes .../css/css-selectors/has-specificity.ts | 454 ++++++++++++++++++ 19 files changed, 469 insertions(+), 10 deletions(-) create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.08664f2e1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.0f44a1611.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.1cd508271.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.2a31f0e21.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.349ee1d41.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.4a91931a1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.4c1451771.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.4eb2a4ac1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.52bee3611.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.71df37f01.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.7b83fd0e1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.9fe4eb3c1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.a610a60b1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.d4a25c9a1.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.d52b56311.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.e60faaa81.png create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.f2b936581.png create mode 100644 integration_tests/specs/css/css-selectors/has-specificity.ts diff --git a/bridge/core/css/selector_checker.cc b/bridge/core/css/selector_checker.cc index 7a204fee96..cfc409773d 100644 --- a/bridge/core/css/selector_checker.cc +++ b/bridge/core/css/selector_checker.cc @@ -1805,16 +1805,21 @@ bool SelectorChecker::CheckPseudoClass(const SelectorCheckingContext& context, M case CSSSelector::kPseudoInvalid: return element.MatchesValidityPseudoClasses() && !element.IsValidElement(); case CSSSelector::kPseudoChecked: { - // TODO: Implement form control checked state - // if (auto* input_element = DynamicTo(element)) { - // if (input_element->ShouldAppearChecked() && !input_element->ShouldAppearIndeterminate()) { - // return true; - // } - // } else if (auto* option_element = DynamicTo(element)) { - // if (option_element->Selected()) { - // return true; - // } - // } + // Minimal :checked support for WebF's DOM model. + // Match checkable inputs with [checked] and