From 701e361dd5ac794f0944f9590eb75cefe798994b Mon Sep 17 00:00:00 2001
From: Karl Seguin
Date: Tue, 18 Aug 2026 16:17:16 +0800
Subject: [PATCH 1/2] webapi: Re-parse element.style when style attribute
changes
If we have a materialized element.style (stored in Frame._element_styles) and
the style attribute changes (e.g. via setAttribute('style', '....'), then the
materialized CSSStyleDeclaration has to be updated (cleared and the style
re-parsed)
---
src/browser/Frame.zig | 11 ++++
.../tests/element/css_style_properties.html | 29 ++++++++++
.../webapi/css/CSSStyleDeclaration.zig | 57 ++++++++++++-------
3 files changed, 78 insertions(+), 19 deletions(-)
diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig
index 45555932e..1ff57df9d 100644
--- a/src/browser/Frame.zig
+++ b/src/browser/Frame.zig
@@ -2964,6 +2964,8 @@ pub fn attributeChange(self: *Frame, element: *Element, name: String, value: Str
} else if (name.eql(comptime .wrap("popover"))) {
const old = if (old_value) |o| o.str() else null;
popover.attributeChanged(element, old, value.str(), self);
+ } else if (name.eql(comptime .wrap("style"))) {
+ self.styleAttributeChanged(element, value.str());
}
}
@@ -2985,9 +2987,18 @@ pub fn attributeRemove(self: *Frame, element: *Element, name: String, old_value:
}
} else if (name.eql(comptime .wrap("popover"))) {
popover.attributeChanged(element, old_value.str(), null, self);
+ } else if (name.eql(comptime .wrap("style"))) {
+ self.styleAttributeChanged(element, null);
}
}
+fn styleAttributeChanged(self: *Frame, element: *Element, value: ?[]const u8) void {
+ const style = self._element_styles.get(element) orelse return;
+ style.asCSSStyleDeclaration().styleAttributeChanged(value, self) catch |err| {
+ log.err(.frame, "style attribute reparse", .{ .err = err, .type = self._type, .url = self.url });
+ };
+}
+
pub fn signalSlotChange(self: *Frame, slot: *Element.Html.Slot) void {
self._slots_pending_slotchange.put(self.arena, slot, {}) catch |err| {
log.err(.frame, "signalSlotChange.put", .{ .err = err, .type = self._type, .url = self.url });
diff --git a/src/browser/tests/element/css_style_properties.html b/src/browser/tests/element/css_style_properties.html
index 3592df67e..61cb8e945 100644
--- a/src/browser/tests/element/css_style_properties.html
+++ b/src/browser/tests/element/css_style_properties.html
@@ -191,3 +191,32 @@
testing.expectEqual('0.5em 0.25em', div.style.margin);
}
+
+
diff --git a/src/browser/webapi/css/CSSStyleDeclaration.zig b/src/browser/webapi/css/CSSStyleDeclaration.zig
index 8a9ca1258..a78bd3b5d 100644
--- a/src/browser/webapi/css/CSSStyleDeclaration.zig
+++ b/src/browser/webapi/css/CSSStyleDeclaration.zig
@@ -34,6 +34,7 @@ const CSSStyleDeclaration = @This();
_element: ?*Element = null,
_properties: std.DoublyLinkedList = .{},
_is_computed: bool = false,
+_syncing: bool = false,
// Parse the element's existing style attribute into _properties so that
// subsequent JS reads and writes see all CSS properties, not just newly
@@ -43,11 +44,14 @@ pub fn parseInlineStyle(self: *CSSStyleDeclaration, frame: *Frame) !void {
return;
}
const el = self._element orelse return;
- if (el.getAttributeSafe(comptime .wrap("style"))) |attr_value| {
- var it = CssParser.parseDeclarationsList(attr_value);
- while (it.next()) |declaration| {
- try self.applyParsedDeclaration(declaration, frame);
- }
+ const attr_value = el.getAttributeSafe(comptime .wrap("style")) orelse return;
+ try self.applyDeclarations(attr_value, frame);
+}
+
+fn applyDeclarations(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !void {
+ var it = CssParser.parseDeclarationsList(text);
+ while (it.next()) |declaration| {
+ try self.applyParsedDeclaration(declaration, frame);
}
}
@@ -213,9 +217,36 @@ fn removePropertyImpl(self: *CSSStyleDeclaration, property_name: []const u8, fra
fn syncStyleAttribute(self: *CSSStyleDeclaration, frame: *Frame) !void {
const element = self._element orelse return;
const css_text = try self.getCssText(frame);
+ self._syncing = true;
+ defer self._syncing = false;
try element.setAttributeSafe(comptime .wrap("style"), .wrap(css_text), frame);
}
+// The element's style attribute changed (null: removed)
+pub fn styleAttributeChanged(self: *CSSStyleDeclaration, text: ?[]const u8, frame: *Frame) !void {
+ if (self._syncing) {
+ // this was us making the change internally, automatically in-sync
+ return;
+ }
+ // ok, this change was from the outside, e.g. via setAttribute('style', ....)
+ // we need to get this declaration back in sync
+ self.clearProperties(frame);
+ if (text) |t| {
+ try self.applyDeclarations(t, frame);
+ }
+}
+
+fn clearProperties(self: *CSSStyleDeclaration, frame: *Frame) void {
+ var node = self._properties.first;
+ while (node) |n| {
+ const next = n.next;
+ const prop = Property.fromNodeLink(n);
+ self._properties.remove(n);
+ frame._factory.destroy(prop);
+ node = next;
+ }
+}
+
pub fn getFloat(self: *const CSSStyleDeclaration, frame: *Frame) []const u8 {
return self.getPropertyValue("float", frame);
}
@@ -232,21 +263,9 @@ pub fn getCssText(self: *const CSSStyleDeclaration, frame: *Frame) ![]const u8 {
}
pub fn setCssText(self: *CSSStyleDeclaration, text: []const u8, frame: *Frame) !void {
- // Clear existing properties
- var node = self._properties.first;
- while (node) |n| {
- const next = n.next;
- const prop = Property.fromNodeLink(n);
- self._properties.remove(n);
- frame._factory.destroy(prop);
- node = next;
- }
+ self.clearProperties(frame);
- // Parse and set new properties
- var it = CssParser.parseDeclarationsList(text);
- while (it.next()) |declaration| {
- try self.applyParsedDeclaration(declaration, frame);
- }
+ try self.applyDeclarations(text, frame);
try self.syncStyleAttribute(frame);
}
From 4b06c248e70a7e1c1bdea65302d3c929afef09a4 Mon Sep 17 00:00:00 2001
From: Karl Seguin
Date: Wed, 19 Aug 2026 07:33:38 +0800
Subject: [PATCH 2/2] use element.ownerFrame to access StyleManager
---
src/browser/StyleManager.zig | 2 +
.../tests/frames/cross_realm_attributes.html | 43 +++++++++++++++++++
.../support/cross_realm_attributes.html | 4 ++
src/browser/webapi/Element.zig | 19 +++++---
.../webapi/css/CSSStyleDeclaration.zig | 7 +--
.../webapi/element/svg/TextContent.zig | 3 +-
src/browser/webapi/svg/Length.zig | 7 +--
7 files changed, 72 insertions(+), 13 deletions(-)
diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig
index b054de4f3..6dc83ead9 100644
--- a/src/browser/StyleManager.zig
+++ b/src/browser/StyleManager.zig
@@ -1191,6 +1191,8 @@ const CheckVisibilityOptions = struct {
// its field max, so a real rule can never pack to all-ones.
const INLINE_PRIORITY: u64 = std.math.maxInt(u64);
+// `frame` is the StyleManager's frame, which callers guarantee is el's owner
+// frame (el.ownerFrame) — the map where the materialized style lives.
fn getInlineStyleProperty(el: *Element, property_name: String, frame: *Frame) ?*CSSStyleProperty {
const style = frame._element_styles.get(el) orelse blk: {
// No JS-set style object and no style attribute -> nothing inline to read.
diff --git a/src/browser/tests/frames/cross_realm_attributes.html b/src/browser/tests/frames/cross_realm_attributes.html
index d06e5d45f..ac38acf20 100644
--- a/src/browser/tests/frames/cross_realm_attributes.html
+++ b/src/browser/tests/frames/cross_realm_attributes.html
@@ -80,4 +80,47 @@ reads of iframe data-* returned null (WPT encoding/*).
});
}
+
+
x
diff --git a/src/browser/tests/frames/support/cross_realm_attributes.html b/src/browser/tests/frames/support/cross_realm_attributes.html
index 756e5c0d4..0629b8766 100644
--- a/src/browser/tests/frames/support/cross_realm_attributes.html
+++ b/src/browser/tests/frames/support/cross_realm_attributes.html
@@ -1,4 +1,5 @@
+