mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge pull request #2294 from navidemad/fix-a24-ua-stylesheet-display-none
css: apply UA stylesheet display:none defaults for unrendered elements
This commit is contained in:
@@ -45,6 +45,7 @@ pub const PointerEventsCache = std.AutoHashMapUnmanaged(*Element, bool);
|
||||
const StyleManager = @This();
|
||||
|
||||
const Tag = Element.Tag;
|
||||
const Input = Element.Html.Input;
|
||||
const RuleList = std.MultiArrayList(VisibilityRule);
|
||||
|
||||
frame: *Frame,
|
||||
@@ -234,14 +235,44 @@ pub fn isHidden(self: *StyleManager, el: *Element, cache: ?*VisibilityCache, opt
|
||||
}
|
||||
|
||||
/// Computed display:none for a single element (own property, no ancestor walk).
|
||||
/// Also honors the HTML `hidden` attribute, matching the UA stylesheet rule
|
||||
/// `[hidden] { display: none }`.
|
||||
/// Honors the UA stylesheet rules per HTML Rendering §15.3.1 "Hidden elements"
|
||||
/// via `isElementHidden`.
|
||||
pub fn hasDisplayNone(self: *StyleManager, el: *Element) bool {
|
||||
self.rebuildIfDirty() catch return false;
|
||||
if (el.hasAttributeSafe(comptime .wrap("hidden"))) return true;
|
||||
return self.isElementHidden(el, .{});
|
||||
}
|
||||
|
||||
/// Centralizes UA-stylesheet display:none truth so `getComputedStyle().display`
|
||||
/// (via `hasDisplayNone`) and `el.checkVisibility()` (via `isHidden`) agree.
|
||||
/// Spec: HTML Rendering §15.3.1 "Hidden elements".
|
||||
fn matchesUaDisplayNoneRule(el: *Element) bool {
|
||||
// Tag check first: O(1) switch, exits for the ~95% of elements with
|
||||
// ordinary tags before we touch the attribute list.
|
||||
const tag = el.getTag();
|
||||
if (tag.isHiddenByUaStylesheet()) return true;
|
||||
|
||||
if (el.hasAttributeSafe(comptime .wrap("hidden"))) return true;
|
||||
|
||||
// input[type="hidden" i] { display: none !important }
|
||||
// _input_type is parsed case-insensitively at attribute-set time.
|
||||
if (tag == .input) {
|
||||
if (el.is(Input)) |input| {
|
||||
if (input._input_type == .hidden) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// details:not([open]) > *:not(summary) { display: none }
|
||||
if (tag != .summary) {
|
||||
if (el.parentElement()) |parent| {
|
||||
if (parent.getTag() == .details and !parent.hasAttributeSafe(comptime .wrap("open"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Computed visibility:hidden for an element, considering only the `visibility`
|
||||
/// chain (walks ancestors since `visibility` inherits by default). Ignores
|
||||
/// display:none: an ancestor with display:none means the element isn't
|
||||
@@ -313,6 +344,16 @@ fn isElementHidden(self: *StyleManager, el: *Element, options: CheckVisibilityOp
|
||||
opacity_priority = INLINE_PRIORITY;
|
||||
}
|
||||
|
||||
// UA stylesheet display:none rules (HTML Rendering §15.3.1 "Hidden elements").
|
||||
// Skipped when an inline `display` is set so author overrides win per cascade.
|
||||
// All UA rules are short-circuited here; the spec marks some as `!important`
|
||||
// (`input[type=hidden]`, `noscript`) and others as normal-origin, but author
|
||||
// CSS overriding `<script>`/`<head>`/closed-`<details>` children is rare
|
||||
// enough that uniform treatment is acceptable.
|
||||
if (options.check_display and display_priority != INLINE_PRIORITY) {
|
||||
if (matchesUaDisplayNoneRule(el)) return true;
|
||||
}
|
||||
|
||||
if (display_priority == INLINE_PRIORITY and visibility_priority == INLINE_PRIORITY and opacity_priority == INLINE_PRIORITY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -191,6 +191,115 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="ua_unrendered_tags_are_display_none">
|
||||
{
|
||||
// Per HTML Rendering §15.3.1 "Hidden elements", these tags get
|
||||
// display:none from the UA stylesheet:
|
||||
// area, base, datalist, head, link, meta, noscript, param, script,
|
||||
// source, style, template, title, track
|
||||
// (basefont/noembed/noframes/rp are obsolete and not represented in
|
||||
// Lightpanda's Element.Tag enum.)
|
||||
const tags = [
|
||||
'area', 'base', 'datalist', 'link', 'meta', 'noscript', 'param',
|
||||
'script', 'source', 'style', 'template', 'title', 'track',
|
||||
];
|
||||
for (const t of tags) {
|
||||
const el = document.createElement(t);
|
||||
document.body.appendChild(el);
|
||||
testing.expectEqual('none', window.getComputedStyle(el).display);
|
||||
testing.expectEqual(false, el.checkVisibility());
|
||||
el.remove();
|
||||
}
|
||||
// <head> itself
|
||||
testing.expectEqual('none', window.getComputedStyle(document.head).display);
|
||||
testing.expectEqual(false, document.head.checkVisibility());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="ua_input_type_hidden_is_display_none">
|
||||
{
|
||||
// input[type="hidden" i] { display: none !important }
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden';
|
||||
document.body.appendChild(inp);
|
||||
testing.expectEqual('none', window.getComputedStyle(inp).display);
|
||||
testing.expectEqual(false, inp.checkVisibility());
|
||||
|
||||
// Other input types are visible
|
||||
inp.type = 'text';
|
||||
testing.expectEqual(true, inp.checkVisibility());
|
||||
|
||||
// Spec uses [type=hidden i] (case-insensitive)
|
||||
inp.type = 'HIDDEN';
|
||||
testing.expectEqual(false, inp.checkVisibility());
|
||||
|
||||
inp.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="ua_closed_details_hides_non_summary_children">
|
||||
{
|
||||
// details:not([open]) > *:not(summary) { display: none }
|
||||
const details = document.createElement('details');
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = 'header';
|
||||
const child = document.createElement('p');
|
||||
child.textContent = 'body';
|
||||
details.appendChild(summary);
|
||||
details.appendChild(child);
|
||||
document.body.appendChild(details);
|
||||
|
||||
// Closed: <summary> visible, body child hidden
|
||||
testing.expectEqual(true, summary.checkVisibility());
|
||||
testing.expectEqual(false, child.checkVisibility());
|
||||
testing.expectEqual('none', window.getComputedStyle(child).display);
|
||||
|
||||
// Open: child visible
|
||||
details.setAttribute('open', '');
|
||||
testing.expectEqual(true, child.checkVisibility());
|
||||
|
||||
// Closed again
|
||||
details.removeAttribute('open');
|
||||
testing.expectEqual(false, child.checkVisibility());
|
||||
|
||||
// Descendants of a closed-details child are hidden via the ancestor walk.
|
||||
const grandchild = document.createElement('span');
|
||||
child.appendChild(grandchild);
|
||||
testing.expectEqual(false, grandchild.checkVisibility());
|
||||
|
||||
details.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="hidden_attribute_propagates_through_check_visibility">
|
||||
{
|
||||
// [hidden] { display: none } applies to the element itself...
|
||||
const parent = document.createElement('div');
|
||||
parent.setAttribute('hidden', '');
|
||||
const child = document.createElement('span');
|
||||
parent.appendChild(child);
|
||||
document.body.appendChild(parent);
|
||||
|
||||
testing.expectEqual(false, parent.checkVisibility());
|
||||
// ...and through ancestor walking, hides descendants too.
|
||||
testing.expectEqual(false, child.checkVisibility());
|
||||
|
||||
parent.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="inline_display_overrides_ua_default">
|
||||
{
|
||||
// Author inline `display: block` on a UA-hidden tag wins per CSS cascade
|
||||
// (inline > UA stylesheet).
|
||||
const s = document.createElement('script');
|
||||
s.style.display = 'block';
|
||||
document.body.appendChild(s);
|
||||
testing.expectEqual(true, s.checkVisibility());
|
||||
s.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="deep_nesting">
|
||||
{
|
||||
const levels = 5;
|
||||
|
||||
@@ -1694,6 +1694,32 @@ pub const Tag = enum {
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
// UA stylesheet display:none defaults per HTML Rendering §15.3.1
|
||||
// "Hidden elements" (https://html.spec.whatwg.org/multipage/rendering.html#hidden-elements).
|
||||
// The spec also lists basefont, noembed, noframes, rp; those tags are
|
||||
// obsolete and not represented in this enum, so they fall through to
|
||||
// `.unknown`/`.custom` and aren't matched here.
|
||||
pub fn isHiddenByUaStylesheet(self: Tag) bool {
|
||||
return switch (self) {
|
||||
.area,
|
||||
.base,
|
||||
.datalist,
|
||||
.head,
|
||||
.link,
|
||||
.meta,
|
||||
.noscript,
|
||||
.param,
|
||||
.script,
|
||||
.source,
|
||||
.style,
|
||||
.template,
|
||||
.title,
|
||||
.track,
|
||||
=> true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -713,13 +713,16 @@ test "cdp.dom: getBoxModel" {
|
||||
});
|
||||
try ctx.expectSentResult(.{ .nodeId = 3 }, .{ .id = 4 });
|
||||
|
||||
// Box model on the <p> nodeId returned above.
|
||||
// Note: nodeId 6 is <head>, which is `display: none` per HTML Rendering
|
||||
// §15.3.1, so its box model is all-zeros — exercise a visible element.
|
||||
try ctx.processMessage(.{
|
||||
.id = 5,
|
||||
.method = "DOM.getBoxModel",
|
||||
.params = .{ .nodeId = 6 },
|
||||
.params = .{ .nodeId = 3 },
|
||||
});
|
||||
try ctx.expectSentResult(.{ .model = BoxModel{
|
||||
.content = Quad{ 10.0, 10.0, 15.0, 10.0, 15.0, 15.0, 10.0, 15.0 },
|
||||
.content = Quad{ 25.0, 25.0, 30.0, 25.0, 30.0, 30.0, 25.0, 30.0 },
|
||||
.padding = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||
.border = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||
.margin = Quad{ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
|
||||
|
||||
Reference in New Issue
Block a user