From 9352ce54feeb3bc4913e99ec3ac3880ec2477387 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 15 Jul 2026 18:24:25 +0800 Subject: [PATCH] fix(directionality): inherit CSS through slots --- .../wpt-cross-current/failed-cases.txt | 1 - .../wpt-cross-current/passed-cases.txt | 1 + .../context_host/host_environment.rs | 10 ++++ .../element/styles/declaration/values.rs | 5 +- .../script_vm/tests/dom_xhr/computed_style.rs | 49 +++++++++++++++++ moli-selector/src/lib.rs | 44 +++++++++++++++ moli-selector/src/stylo/presentation.rs | 12 +++- .../src/stylo/presentational_hints.rs | 55 +++++++++++++++++++ moli-selector/src/stylo/query/pseudo.rs | 44 ++++++++++++++- moli-selector/src/stylo/style_traversal.rs | 32 ++++++++++- 10 files changed, 245 insertions(+), 8 deletions(-) diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 025bc0697f..344068c0d6 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2925,7 +2925,6 @@ html/dom/documents/dom-tree-accessors/nameditem-06.html html/dom/documents/dom-tree-accessors/nameditem-07.html html/dom/documents/dom-tree-accessors/nameditem-08.html html/dom/documents/dom-tree-accessors/nameditem-names.html -html/dom/elements/global-attributes/dir-slots-directionality.html html/dom/elements/the-innertext-and-outertext-properties/dynamic-getter.html html/dom/elements/the-innertext-and-outertext-properties/getter.html html/dom/elements/the-innertext-and-outertext-properties/innertext-setter.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index de577ee385..93e551856f 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -5424,6 +5424,7 @@ html/dom/elements/global-attributes/dataset-set.html html/dom/elements/global-attributes/dataset.html html/dom/elements/global-attributes/dir-auto-div-append-child.html html/dom/elements/global-attributes/dir-bdi-script.html +html/dom/elements/global-attributes/dir-slots-directionality.html html/dom/elements/global-attributes/document-dir.html html/dom/elements/global-attributes/id-attribute.html html/dom/elements/global-attributes/id-name-specialcase.html diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs index 070c5855b1..c2fcd762b2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_environment.rs @@ -2136,6 +2136,16 @@ impl JsContextHost { | StyloElementState::VALIDITY_STATES, old_state, ); + if self + .dom_host() + .get_attribute(handle, "dir") + .is_some_and(|value| value.eq_ignore_ascii_case("auto")) + { + // The resolved direction is also synthesized into the CSS cascade as + // a presentation hint, so changing the value must rebuild the input's + // style even when no selector depends on :dir(). + self.note_style_subtree_context_change(handle); + } for (container, old_state) in container_old_states { self.note_element_state_style_activity_with_old_state( *container, diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs index 97ccf51a83..3fbb771356 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/values.rs @@ -5875,8 +5875,9 @@ fn computed_direction_with_resolution( } } let direction = resolution.raw_property(runtime, handle, "direction"); - if direction.eq_ignore_ascii_case("rtl") { - return "rtl".to_owned(); + let direction = direction.to_ascii_lowercase(); + if matches!(direction.as_str(), "ltr" | "rtl") { + return direction; } html_directionality(runtime.dom_host(), handle) .as_str() diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs index aa8116a498..e474adea0c 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs @@ -10088,6 +10088,55 @@ fn shadow_dir_pseudo_styles_slotted_nodes_from_document_direction() { assert_eq!(result, "rgb(0, 128, 0)"); } +#[test] +fn slotted_nodes_inherit_css_direction_from_slot_without_changing_html_directionality() { + let mut vm = new_storage_test_vm("https://slotted-direction-inheritance.test/"); + + let result = vm + .eval( + r#" +(() => { + if (!document.documentElement) { + document.appendChild(document.createElement('html')); + } + if (!document.body) { + document.documentElement.appendChild(document.createElement('body')); + } + + const host = document.createElement('div'); + const slotted = document.createElement('span'); + host.appendChild(slotted); + const shadow = host.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = 'slot { color: rgb(1, 2, 3); }'; + const slot = document.createElement('slot'); + slot.dir = 'rtl'; + shadow.append(style, slot); + document.body.appendChild(host); + + const inherited = `${slotted.matches(':dir(ltr)')}:${getComputedStyle(slotted).direction}:${getComputedStyle(slotted).color}`; + + const overriddenHost = document.createElement('div'); + const overriddenSlotted = document.createElement('span'); + overriddenHost.appendChild(overriddenSlotted); + const overriddenShadow = overriddenHost.attachShadow({ mode: 'open' }); + const overriddenStyle = document.createElement('style'); + overriddenStyle.textContent = 'slot { direction: ltr; }'; + const overriddenSlot = document.createElement('slot'); + overriddenSlot.dir = 'rtl'; + overriddenShadow.append(overriddenStyle, overriddenSlot); + document.body.appendChild(overriddenHost); + + const authorOverride = `${overriddenSlot.matches(':dir(rtl)')}:${getComputedStyle(overriddenSlot).direction}:${getComputedStyle(overriddenSlotted).direction}`; + return `${inherited}|${authorOverride}`; +})() +"#, + ) + .expect("slotted direction inheritance should evaluate"); + + assert_eq!(result, "true:rtl:rgb(1, 2, 3)|true:ltr:ltr"); +} + #[test] fn computed_direction_tracks_input_html_directionality() { let mut vm = new_storage_test_vm("https://input-direction-computed-style.test/"); diff --git a/moli-selector/src/lib.rs b/moli-selector/src/lib.rs index 0a830153de..7d2dd1e6eb 100644 --- a/moli-selector/src/lib.rs +++ b/moli-selector/src/lib.rs @@ -1573,6 +1573,50 @@ mod tests { assert!(engine.matches_host(&host, auto, ":dir(rtl)").unwrap()); } + #[test] + fn dom_api_selectors_dir_auto_resolves_slot_directionality_boundaries() { + let url = url::Url::parse("https://example.test/").unwrap(); + let mut host = DomHost::from_dom(NativeDom::new_html(url)); + host.reset_html_document_shell(); + let body = host.document_body_handle().unwrap(); + let engine = QueryEngine; + + let assigned_host = host.create_element("div"); + assert!(host.append_child(body, assigned_host)); + let assigned_text = host.create_text_node("\u{0627}\u{062e}\u{062a}\u{0628}\u{0631}"); + assert!(host.append_child(assigned_host, assigned_text)); + let assigned_shadow = host.attach_shadow_root(assigned_host, "open").unwrap(); + let auto_slot = host.create_element("slot"); + assert!(host.set_attribute(auto_slot, "dir", "auto")); + assert!(host.append_child(assigned_shadow, auto_slot)); + + assert!(engine.matches_host(&host, auto_slot, ":dir(rtl)").unwrap()); + assert!(!engine.matches_host(&host, auto_slot, ":dir(ltr)").unwrap()); + + let inherited_host = host.create_element("div"); + assert!(host.set_attribute(inherited_host, "dir", "rtl")); + assert!(host.append_child(body, inherited_host)); + let inherited_shadow = host.attach_shadow_root(inherited_host, "open").unwrap(); + let auto_container = host.create_element("div"); + assert!(host.set_attribute(auto_container, "dir", "auto")); + let inherited_slot = host.create_element("slot"); + let later_ltr_text = host.create_text_node("A"); + assert!(host.append_child(auto_container, inherited_slot)); + assert!(host.append_child(auto_container, later_ltr_text)); + assert!(host.append_child(inherited_shadow, auto_container)); + + assert!( + engine + .matches_host(&host, auto_container, ":dir(rtl)") + .unwrap() + ); + assert!( + !engine + .matches_host(&host, auto_container, ":dir(ltr)") + .unwrap() + ); + } + #[test] fn dom_api_selectors_invalid_form_walks_deep_dom_iteratively() { let url = url::Url::parse("https://example.test/").unwrap(); diff --git a/moli-selector/src/stylo/presentation.rs b/moli-selector/src/stylo/presentation.rs index 2932177a64..1b0dfa8816 100644 --- a/moli-selector/src/stylo/presentation.rs +++ b/moli-selector/src/stylo/presentation.rs @@ -27,7 +27,11 @@ use style_traits::{ParsingMode, ToCss}; use crate::dom::native::Element; use super::{ - presentational_hints::synthesize_hidden_until_found_presentational_hint, query::QueryElement, + presentational_hints::{ + synthesize_directionality_presentational_hint, + synthesize_hidden_until_found_presentational_hint, + }, + query::QueryElement, }; const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; @@ -176,6 +180,12 @@ impl QueryElement<'_> { self.shared_lock(), hints, ); + synthesize_directionality_presentational_hint( + self.host(), + self.handle(), + self.shared_lock(), + hints, + ); } } diff --git a/moli-selector/src/stylo/presentational_hints.rs b/moli-selector/src/stylo/presentational_hints.rs index 3a13ad9828..4d7ef0b2cf 100644 --- a/moli-selector/src/stylo/presentational_hints.rs +++ b/moli-selector/src/stylo/presentational_hints.rs @@ -4,6 +4,7 @@ use style::{ properties::{ Importance, PropertyDeclaration, PropertyDeclarationBlock, longhands::content_visibility::SpecifiedValue as ContentVisibility, + longhands::direction::SpecifiedValue as Direction, }, rule_tree::{CascadeLevel, CascadeOrigin}, servo_arc::Arc, @@ -16,6 +17,8 @@ use crate::dom::{ native::{DomHost, Node}, }; +use super::query::html_directionality; + const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; pub(super) fn synthesize_hidden_until_found_presentational_hint( @@ -47,3 +50,55 @@ pub(super) fn synthesize_hidden_until_found_presentational_hint( LayerOrder::root(), )); } + +pub(super) fn synthesize_directionality_presentational_hint( + host: &DomHost, + handle: NodeId, + shared_lock: &SharedRwLock, + hints: &mut V, +) where + V: Push, +{ + let Some(element) = host.node(handle).and_then(Node::as_element) else { + return; + }; + if element.namespace() != HTML_NAMESPACE { + return; + } + + let dir = element.attribute("dir"); + let direction = match dir { + Some(value) if value.eq_ignore_ascii_case("ltr") => Direction::Ltr, + Some(value) if value.eq_ignore_ascii_case("rtl") => Direction::Rtl, + Some(value) if value.eq_ignore_ascii_case("auto") => { + direction_from_html_directionality(host, handle) + } + value + if element.is_html_element("bdi") + && !value.is_some_and(|value| { + matches!(value.to_ascii_lowercase().as_str(), "ltr" | "rtl" | "auto") + }) => + { + direction_from_html_directionality(host, handle) + } + Some(_) if element.is_html_element("body") => Direction::Ltr, + _ => return, + }; + + let declarations = PropertyDeclarationBlock::with_one( + PropertyDeclaration::Direction(direction), + Importance::Normal, + ); + hints.push(ApplicableDeclarationBlock::from_declarations( + Arc::new(shared_lock.wrap(declarations)), + CascadeLevel::new(CascadeOrigin::PresHints), + LayerOrder::root(), + )); +} + +fn direction_from_html_directionality(host: &DomHost, handle: NodeId) -> Direction { + match html_directionality(host, handle) { + crate::CssDirection::Ltr => Direction::Ltr, + crate::CssDirection::Rtl => Direction::Rtl, + } +} diff --git a/moli-selector/src/stylo/query/pseudo.rs b/moli-selector/src/stylo/query/pseudo.rs index 7f4d82c857..090988b250 100644 --- a/moli-selector/src/stylo/query/pseudo.rs +++ b/moli-selector/src/stylo/query/pseudo.rs @@ -715,6 +715,37 @@ fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option Option { let mut stack = host.child_handles(root).collect::>(); stack.reverse(); while let Some(handle) = stack.pop() { @@ -733,6 +764,13 @@ fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option>(); children.reverse(); stack.extend(children); @@ -741,7 +779,11 @@ fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option bool { - if element.is_html_element("bdi") { + if matches!( + element.local_name(), + "bdi" | "script" | "style" | "textarea" + ) && element.namespace() == "http://www.w3.org/1999/xhtml" + { return true; } element.attribute("dir").is_some_and(|value| { diff --git a/moli-selector/src/stylo/style_traversal.rs b/moli-selector/src/stylo/style_traversal.rs index 7034c86103..607f657050 100644 --- a/moli-selector/src/stylo/style_traversal.rs +++ b/moli-selector/src/stylo/style_traversal.rs @@ -1821,6 +1821,13 @@ impl<'a> TNode for StyleNode<'a> { } fn traversal_parent(&self) -> Option { + if let Some(slot) = self.host().assigned_slot_for_node(self.handle()) { + return StyleElement::from_handle_in_document( + self.style_state(), + self.document(), + slot, + ); + } let parent = self.parent_node()?; if let Some(element) = parent.as_element() { return Some(element); @@ -1892,9 +1899,24 @@ impl<'a> TElement for StyleElement<'a> { } fn traversal_children(&self) -> LayoutIterator { - let children = self - .host() - .child_handles(self.handle()) + let mut child_parent = self.handle(); + let children = if self.element().is_html_element("slot") { + let assigned = self + .host() + .assigned_nodes_for_slot_with_options(self.handle(), false); + if assigned.is_empty() { + self.host().child_handles(child_parent).collect() + } else { + assigned + } + } else { + if let Some(shadow_root) = self.host().shadow_root_handle(self.handle()) { + child_parent = shadow_root; + } + self.host().child_handles(child_parent).collect() + }; + let children = children + .into_iter() .map(|handle| { StyleNode( self.style_state().node_data(handle, self.document()), @@ -1905,6 +1927,10 @@ impl<'a> TElement for StyleElement<'a> { LayoutIterator(children.into_iter()) } + fn inheritance_parent(&self) -> Option { + self.as_node().traversal_parent() + } + fn is_html_element(&self) -> bool { self.element().namespace() == "http://www.w3.org/1999/xhtml" }