From 5454d6a2131f51bee26c31c1b56c2b06cc877fc1 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Wed, 29 Apr 2026 02:32:42 +0200 Subject: [PATCH 1/2] dom: implement HTMLElement.isContentEditable IDL attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Element.isContentEditable` returned `undefined` because no IDL accessor existed on HTMLElement. Per HTML §7.7.5.2 the IDL attribute returns `true` iff the element's effective content editable state is "true" or "plaintext-only". Walk up to the nearest ancestor (or self) carrying a `contenteditable` attribute; the keyword `false` maps to the false state, every other value (empty string, "true", "plaintext-only", or unrecognized) maps to an editable state. With no ancestor carrying the attribute the document's designMode default ("off") yields `false`. Closes #2309 --- .../tests/element/html/contenteditable.html | 80 +++++++++++++++++++ src/browser/webapi/element/Html.zig | 22 +++++ 2 files changed, 102 insertions(+) create mode 100644 src/browser/tests/element/html/contenteditable.html diff --git a/src/browser/tests/element/html/contenteditable.html b/src/browser/tests/element/html/contenteditable.html new file mode 100644 index 00000000..ad7177e0 --- /dev/null +++ b/src/browser/tests/element/html/contenteditable.html @@ -0,0 +1,80 @@ + + + +
own true
+
own empty
+
own false
+
own plaintext-only
+
own UPPERCASE
+
own FALSEUPPER
+ +
+ child +
grandchild
+
+ +
+ child +
+ re-hosted +
+
+ +
no editing context
+ + + + + + + + + + diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 5853fd14..606f9a83 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -382,6 +382,24 @@ pub fn setTitle(self: *HtmlElement, value: []const u8, frame: *Frame) !void { try self.asElement().setAttributeSafe(comptime .wrap("title"), .wrap(value), frame); } +// HTML §7.7.5.2: returns true iff the element's effective content editable +// state is "true" or "plaintext-only". Walk up to the nearest ancestor (or +// self) with a `contenteditable` attribute; the keyword "false" maps to the +// false state, every other value (including the empty string, "true", and +// "plaintext-only") maps to an editable state. With no ancestor carrying the +// attribute the document's designMode default ("off") yields false. +// +// "contenteditable" is 15 bytes — past the comptime SSO limit — so the +// String wrap runs at runtime, mirroring the pattern in interactive.zig. +pub fn getIsContentEditable(self: *HtmlElement) bool { + var current: ?*Element = self.asElement(); + while (current) |el| : (current = el.parentElement()) { + const raw = el.getAttributeSafe(.wrap("contenteditable")) orelse continue; + return !std.ascii.eqlIgnoreCase(raw, "false"); + } + return false; +} + pub fn getAttributeFunction( self: *HtmlElement, listener_type: GlobalEventHandler, @@ -1220,6 +1238,7 @@ pub const JsApi = struct { pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{}); pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{}); + pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{}); pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{}); pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{}); pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{}); @@ -1357,3 +1376,6 @@ test "WebApi: HTML.event_listeners" { test "WebApi: HTMLElement.props" { try testing.htmlRunner("element/html/htmlelement-props.html", .{}); } +test "WebApi: HTMLElement.contenteditable" { + try testing.htmlRunner("element/html/contenteditable.html", .{}); +} From 2af95af678b36e96b594171dcb77146af456a0f3 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Thu, 30 Apr 2026 07:28:43 +0200 Subject: [PATCH 2/2] dom: return false from isContentEditable, log when spec says true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review (PR #2310), Lightpanda has no caret/keyboard editing pipeline, so honoring the spec-correct value risks routing downstream CDP clients (notably Puppeteer's dispatchKeyEvent path) into an input pipeline that silently no-ops. Switch to always returning false and emit log.info(.not_implemented, "IsContentEditable", .{}) when the spec walk would have produced true, so the unsupported case surfaces in telemetry rather than masquerading as a working state. The HTML §7.7.5.2 walk is preserved (nearest ancestor with `contenteditable` wins, "false" disables) but only used to gate the log emission. The fixture is reduced to assert the always-false return across the same shape of inputs, with a comment pointing back at the rationale. --- .../tests/element/html/contenteditable.html | 52 ++++++------------- src/browser/webapi/element/Html.zig | 21 +++++--- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/browser/tests/element/html/contenteditable.html b/src/browser/tests/element/html/contenteditable.html index ad7177e0..eca1ec2b 100644 --- a/src/browser/tests/element/html/contenteditable.html +++ b/src/browser/tests/element/html/contenteditable.html @@ -1,20 +1,24 @@ + +
own true
own empty
own false
own plaintext-only
-
own UPPERCASE
-
own FALSEUPPER
child -
grandchild
- child
re-hosted
@@ -22,39 +26,18 @@
no editing context
- - - - - - -