fix(directionality): inherit CSS through slots

This commit is contained in:
ldm0
2026-09-14 13:34:54 +08:00
parent 5d0dabf917
commit 9352ce54fe
10 changed files with 245 additions and 8 deletions
@@ -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
@@ -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
@@ -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,
@@ -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()
@@ -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/");
+44
View File
@@ -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();
+11 -1
View File
@@ -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,
);
}
}
@@ -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<V>(
@@ -47,3 +50,55 @@ pub(super) fn synthesize_hidden_until_found_presentational_hint<V>(
LayerOrder::root(),
));
}
pub(super) fn synthesize_directionality_presentational_hint<V>(
host: &DomHost,
handle: NodeId,
shared_lock: &SharedRwLock,
hints: &mut V,
) where
V: Push<ApplicableDeclarationBlock>,
{
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,
}
}
+43 -1
View File
@@ -715,6 +715,37 @@ fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option<CssDirecti
return input_auto_direction(element);
}
if host.is_html_element_named(root, "slot") {
let assigned = host.assigned_nodes_for_slot_with_options(root, false);
if !assigned.is_empty() {
for handle in assigned {
let Some(node) = host.node(handle) else {
continue;
};
if let Some(text) = node.as_text() {
if let Some(direction) = first_strong_text_direction(text.data()) {
return Some(direction);
}
continue;
}
let Some(element) = node.as_element() else {
continue;
};
if descendant_is_directionally_isolated_for_auto(element) {
continue;
}
if let Some(direction) = contained_text_auto_directionality(host, handle) {
return Some(direction);
}
}
return None;
}
}
contained_text_auto_directionality(host, root)
}
fn contained_text_auto_directionality(host: &DomHost, root: NodeId) -> Option<CssDirection> {
let mut stack = host.child_handles(root).collect::<Vec<_>>();
stack.reverse();
while let Some(handle) = stack.pop() {
@@ -733,6 +764,13 @@ fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option<CssDirecti
if descendant_is_directionally_isolated_for_auto(element) {
continue;
}
if element.is_html_element("slot")
&& let Some(shadow_host) = host
.containing_shadow_root(handle)
.and_then(|root| host.shadow_root_host(root))
{
return Some(html_directionality(host, shadow_host));
}
let mut children = host.child_handles(handle).collect::<Vec<_>>();
children.reverse();
stack.extend(children);
@@ -741,7 +779,11 @@ fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option<CssDirecti
}
fn descendant_is_directionally_isolated_for_auto(element: &Element) -> 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| {
+29 -3
View File
@@ -1821,6 +1821,13 @@ impl<'a> TNode for StyleNode<'a> {
}
fn traversal_parent(&self) -> Option<Self::ConcreteElement> {
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<Self::TraversalChildrenIterator> {
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> {
self.as_node().traversal_parent()
}
fn is_html_element(&self) -> bool {
self.element().namespace() == "http://www.w3.org/1999/xhtml"
}