fix(html): apply hidden until-found styles

This commit is contained in:
ldm0
2026-09-23 00:11:50 +08:00
parent 65f3b38d24
commit 844d0a1b4f
10 changed files with 159 additions and 10 deletions
@@ -313,6 +313,7 @@ const LIGHTWEIGHT_STYLE_PROPERTIES: &[&str] = &[
"container-name",
"container-type",
"content",
"content-visibility",
"cursor",
"direction",
"display",
@@ -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,
@@ -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()
}
@@ -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,
@@ -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()
@@ -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(
@@ -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/");
+1
View File
@@ -15,6 +15,7 @@ use crate::{
mod atoms;
mod invalidation;
mod presentation;
mod presentational_hints;
mod query;
mod selector_parse;
mod style_traversal;
+9 -3
View File
@@ -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,
);
}
}
@@ -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<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
|| !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(),
));
}