diff --git a/moli-renderer-v8/src/detached_css_style.rs b/moli-renderer-v8/src/detached_css_style.rs index 7a25108f3a..543458b22b 100644 --- a/moli-renderer-v8/src/detached_css_style.rs +++ b/moli-renderer-v8/src/detached_css_style.rs @@ -313,6 +313,7 @@ const LIGHTWEIGHT_STYLE_PROPERTIES: &[&str] = &[ "container-name", "container-type", "content", + "content-visibility", "cursor", "direction", "display", diff --git a/moli-renderer-v8/src/native_bridge/element/geometry.rs b/moli-renderer-v8/src/native_bridge/element/geometry.rs index f4b825ba8e..28afbf1025 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry.rs @@ -25,6 +25,7 @@ pub(in crate::native_bridge) use metrics::{ node_scroll_top_getter_function, node_scroll_top_setter_function, node_scroll_width_getter_function, }; +pub(super) use mock::element_has_hidden_attribute; pub(crate) use mock::{ compute_mock_client_rect, compute_mock_intersection_client_rect, compute_mock_intersection_scrollport_client_rect, diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/mock/layout.rs b/moli-renderer-v8/src/native_bridge/element/geometry/mock/layout.rs index bf2f976732..27f1f50ab9 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/mock/layout.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/mock/layout.rs @@ -160,7 +160,10 @@ fn element_attribute(runtime: &JsContextHost, handle: DomHandle, name: &str) -> runtime.dom_host().get_attribute(handle, name) } -fn element_has_hidden_attribute(runtime: &JsContextHost, handle: DomHandle) -> bool { +pub(in crate::native_bridge::element) fn element_has_hidden_attribute( + runtime: &JsContextHost, + handle: DomHandle, +) -> bool { element_attribute(runtime, handle, "hidden").is_some() } diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/mock/mod.rs b/moli-renderer-v8/src/native_bridge/element/geometry/mock/mod.rs index 7cd054d004..85f1d80945 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/mock/mod.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/mock/mod.rs @@ -1,6 +1,7 @@ mod layout; mod queries; +pub(in crate::native_bridge::element) use layout::element_has_hidden_attribute; pub(crate) use layout::{ compute_mock_client_rect, compute_mock_intersection_client_rect, compute_mock_intersection_scrollport_client_rect, compute_mock_scroll_adjusted_client_rect, diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/parse.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/parse.rs index 321151a382..2f483ac40d 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/parse.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/parse.rs @@ -1802,6 +1802,12 @@ fn style_entries_affecting_property(entries: &[StyleEntry], property: &str) -> V fn cssom_style_property_write_uses_pdb(name: &str, value: &str) -> bool { let name = canonical_style_property_name(name); + // This Stylo build parses `content-visibility` but does not expose it + // through the declaration-block mutation/query surface. Keep the value in + // the validated side-entry path so CSSOM writes remain observable. + if name == "content-visibility" { + return false; + } if moli_css_parse::is_cssom_custom_property_name(&name) { return !value.is_empty() && stylo_pdb_entries_for_property(&name, value, false).is_some(); } @@ -2809,6 +2815,21 @@ pub(crate) fn parse_style_property_entries_with_base( }); } + if name == "content-visibility" { + let value = css_wide_keyword(value).or_else(|| { + let value = value.trim().to_ascii_lowercase(); + matches!(value.as_str(), "visible" | "auto" | "hidden").then_some(value) + })?; + return Some(ParsedStylePropertyEntries { + entries: vec![StyleEntry { + name: name.clone(), + value, + priority, + }], + affected_names: vec![name], + }); + } + if name == "width" && value .trim_start() 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 8d50fd4b02..ba5188bca5 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 @@ -12,8 +12,8 @@ use crate::{ }, document_runtime::DomHandle, native_bridge::element::geometry::{ - ClientRect, observable_bounding_client_rect, observable_bounding_client_rects, - observable_used_grid_tracks, + ClientRect, element_has_hidden_attribute, observable_bounding_client_rect, + observable_bounding_client_rects, observable_used_grid_tracks, }, style_engine::{ ComputedDisplayKind, ComputedRenderedStyleFacts, FullStyleWorldSnapshot, StyleViewport, @@ -1383,6 +1383,10 @@ pub(super) fn computed_style_default_value( "line-height" => "normal".to_owned(), "link-parameters" => "none".to_owned(), "content" => "normal".to_owned(), + "content-visibility" => match element_hidden_attribute_state(runtime, handle) { + HiddenAttributeState::UntilFound => "hidden".to_owned(), + HiddenAttributeState::Missing | HiddenAttributeState::Hidden => "visible".to_owned(), + }, "background-color" => "rgba(0, 0, 0, 0)".to_owned(), "background-attachment" => "scroll".to_owned(), "background-blend-mode" | "mix-blend-mode" => "normal".to_owned(), @@ -2329,7 +2333,9 @@ fn computed_style_property_value_after_style_update( if !computed_style_applies(runtime, handle) { return String::new(); } - if property == "display" && element_has_hidden_attribute(runtime, handle) { + if property == "display" + && element_hidden_attribute_state(runtime, handle) == HiddenAttributeState::Hidden + { return "none".to_owned(); } let inputs = prepared_inputs; @@ -2565,6 +2571,13 @@ fn computed_style_property_value_from_moli( "link-parameters" => Some(computed_non_inherited_css_keyword_property_value( runtime, handle, property, "none", )), + "content-visibility" => { + inline_style_entry_for_inline_style(runtime, handle, property).map(|_| { + computed_non_inherited_css_keyword_property_value( + runtime, handle, property, "visible", + ) + }) + } "text-size-adjust" => Some(computed_text_size_adjust_value(runtime, handle)), "transition-property" | "transition-behavior" => { inline_style_entry_for_inline_style(runtime, handle, property).map(|entry| entry.value) @@ -5108,12 +5121,29 @@ fn css_px_values_equal(left: f64, right: f64) -> bool { (left - right).abs() < 0.001 } -fn element_has_hidden_attribute(runtime: &JsContextHost, handle: DomHandle) -> bool { - runtime +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum HiddenAttributeState { + Missing, + Hidden, + UntilFound, +} + +fn element_hidden_attribute_state( + runtime: &JsContextHost, + handle: DomHandle, +) -> HiddenAttributeState { + let value = runtime .dom_host() .node(handle) .and_then(Node::as_element) - .is_some_and(|element| element.has_attribute("hidden")) + .and_then(|element| element.attribute("hidden")); + match value { + None => HiddenAttributeState::Missing, + Some(value) if value.eq_ignore_ascii_case("until-found") => { + HiddenAttributeState::UntilFound + } + Some(_) => HiddenAttributeState::Hidden, + } } fn resolve_moli_computed_style_value( 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 53df860ce1..7ca5e506c5 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 @@ -6910,6 +6910,42 @@ fn computed_display_treats_hidden_attribute_as_none() { assert_eq!(result, "true|none|inline|0|0"); } +#[test] +fn computed_style_distinguishes_hidden_and_until_found_states() { + let mut vm = new_storage_test_vm("https://hidden-until-found-computed-style.test/"); + + let result = vm + .eval( + r#" +(() => { + const target = document.createElement('div'); + (document.body || document.documentElement || document).appendChild(target); + const read = () => [ + getComputedStyle(target).display, + getComputedStyle(target).contentVisibility + ].join(':'); + const values = [read()]; + for (const value of ['', 'asdf', 'until-found', 'UNTIL-FOUND', 'UnTiL-FoUnD', '0']) { + target.setAttribute('hidden', value); + values.push(read()); + } + target.removeAttribute('hidden'); + target.style.contentVisibility = 'hidden'; + values.push(`${target.style.contentVisibility}/${read()}`); + target.style.contentVisibility = 'bogus'; + values.push(`${target.style.contentVisibility}/${read()}`); + return values.join('|'); +})() +"#, + ) + .expect("hidden presentation states should affect computed style"); + + assert_eq!( + result, + "block:visible|none:visible|none:visible|block:hidden|block:hidden|block:hidden|none:visible|hidden/block:hidden|hidden/block:hidden" + ); +} + #[test] fn detached_nested_iframe_window_get_computed_style_uses_iframe_width() { let mut vm = new_storage_test_vm("https://nested-computed-width.test/"); diff --git a/moli-selector/src/stylo.rs b/moli-selector/src/stylo.rs index 4b681dec41..f22ed1e4f0 100644 --- a/moli-selector/src/stylo.rs +++ b/moli-selector/src/stylo.rs @@ -15,6 +15,7 @@ use crate::{ mod atoms; mod invalidation; mod presentation; +mod presentational_hints; mod query; mod selector_parse; mod style_traversal; diff --git a/moli-selector/src/stylo/presentation.rs b/moli-selector/src/stylo/presentation.rs index d91a15a86c..f8e9e2d3b1 100644 --- a/moli-selector/src/stylo/presentation.rs +++ b/moli-selector/src/stylo/presentation.rs @@ -26,7 +26,9 @@ use style_traits::{ParsingMode, ToCss}; use crate::dom::native::Element; -use super::query::QueryElement; +use super::{ + presentational_hints::synthesize_hidden_until_found_presentational_hint, query::QueryElement, +}; const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; @@ -162,8 +164,6 @@ impl QueryElement<'_> { } } else if element.namespace() == HTML_NAMESPACE { append_html_table_presentation_declarations(*self, &mut block); - } else { - return; } if !block.is_empty() { @@ -173,6 +173,12 @@ impl QueryElement<'_> { LayerOrder::root(), )); } + synthesize_hidden_until_found_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 new file mode 100644 index 0000000000..3a13ad9828 --- /dev/null +++ b/moli-selector/src/stylo/presentational_hints.rs @@ -0,0 +1,49 @@ +use selectors::sink::Push; +use style::{ + applicable_declarations::ApplicableDeclarationBlock, + properties::{ + Importance, PropertyDeclaration, PropertyDeclarationBlock, + longhands::content_visibility::SpecifiedValue as ContentVisibility, + }, + rule_tree::{CascadeLevel, CascadeOrigin}, + servo_arc::Arc, + shared_lock::SharedRwLock, + stylesheets::layer_rule::LayerOrder, +}; + +use crate::dom::{ + NodeId, + native::{DomHost, Node}, +}; + +const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; + +pub(super) fn synthesize_hidden_until_found_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 + || !element + .attribute("hidden") + .is_some_and(|value| value.eq_ignore_ascii_case("until-found")) + { + return; + } + + let declarations = PropertyDeclarationBlock::with_one( + PropertyDeclaration::ContentVisibility(ContentVisibility::Hidden), + Importance::Normal, + ); + hints.push(ApplicableDeclarationBlock::from_declarations( + Arc::new(shared_lock.wrap(declarations)), + CascadeLevel::new(CascadeOrigin::PresHints), + LayerOrder::root(), + )); +}