diff --git a/src/browser/tests/element/html/contenteditable.html b/src/browser/tests/element/html/contenteditable.html new file mode 100644 index 00000000..eca1ec2b --- /dev/null +++ b/src/browser/tests/element/html/contenteditable.html @@ -0,0 +1,60 @@ + + + + + +
own true
+
own empty
+
own false
+
own plaintext-only
+ +
+ child +
+ +
+
+ re-hosted +
+
+ +
no editing context
+ + + + diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 5853fd14..3ed96f57 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -382,6 +382,31 @@ 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 specifies the IDL attribute as true iff the element's effective +// content editable state is "true" or "plaintext-only". Lightpanda has no +// caret/keyboard editing pipeline, so a true answer cannot be honored +// end-to-end — downstream CDP tools (notably Puppeteer's dispatchKeyEvent +// path) would route into an input pipeline that silently no-ops. Always +// return false, and log .not_implemented when the spec would have said true +// so usage surfaces in telemetry rather than silently depending on an +// unsupported value. Spec walk per HTML §7.7.5.2 still applies — the nearest +// ancestor with `contenteditable` wins; "false" disables. See PR #2310 for +// the routing-vs-fail-loud discussion. +// +// "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; + if (!std.ascii.eqlIgnoreCase(raw, "false")) { + log.info(.not_implemented, "IsContentEditable", .{}); + } + break; + } + return false; +} + pub fn getAttributeFunction( self: *HtmlElement, listener_type: GlobalEventHandler, @@ -1220,6 +1245,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 +1383,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", .{}); +}