mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
fix(directionality): inherit CSS through slots
This commit is contained in:
@@ -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/");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user