webapi: implement the HTMLElement.draggable IDL attribute

Reading el.draggable returned undefined for every element, and assigning
it created a plain JS own property with no reflection onto the content
attribute. Implement the reflected property per the HTML spec: the
"true"/"false" keywords are matched ASCII case-insensitively, the auto
state defaults to true for <img> and for <a> with an href (matching
Chrome), and the setter writes the literal "true"/"false" onto the
content attribute.

Closes #3255
This commit is contained in:
Navid EMAD committed 2026-08-24 15:37:01 +02:00
1 parent 910395c764
commit 763ffcee41
2 files changed
+66

No files matched your search

@@ -82,3 +82,44 @@
testing.expectEqual(-1, d3.tabIndex);
}
</script>
<script id="draggable">
{
// Explicit keywords win, matched ASCII case-insensitively.
const d = document.createElement('div');
d.setAttribute('draggable', 'true');
testing.expectEqual(true, d.draggable);
d.setAttribute('draggable', 'TRUE');
testing.expectEqual(true, d.draggable);
d.setAttribute('draggable', 'false');
testing.expectEqual(false, d.draggable);
// Missing or invalid values are the auto state: false for most elements.
d.removeAttribute('draggable');
testing.expectEqual(false, d.draggable);
d.setAttribute('draggable', 'auto');
testing.expectEqual(false, d.draggable);
d.setAttribute('draggable', '');
testing.expectEqual(false, d.draggable);
// Auto defaults to true for <img> and <a> with an href.
const img = document.createElement('img');
testing.expectEqual(true, img.draggable);
img.setAttribute('draggable', 'false');
testing.expectEqual(false, img.draggable);
const a = document.createElement('a');
testing.expectEqual(false, a.draggable);
a.setAttribute('href', '/x');
testing.expectEqual(true, a.draggable);
a.setAttribute('draggable', 'false');
testing.expectEqual(false, a.draggable);
// The setter reflects to the content attribute as "true"/"false".
const s = document.createElement('div');
s.draggable = true;
testing.expectEqual('true', s.getAttribute('draggable'));
s.draggable = false;
testing.expectEqual('false', s.getAttribute('draggable'));
}
</script>
+25
View File
@@ -471,6 +471,30 @@ pub fn setTranslate(self: *HtmlElement, translate: bool, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("translate"), .wrap(if (translate) "yes" else "no"), frame);
}
// The draggable IDL attribute reflects the enumerated content attribute:
// "true" => true, "false" => false, anything else (or no attribute) is the
// auto state, which defaults to true only for <img> and <a> with an href.
// https://html.spec.whatwg.org/multipage/dnd.html#the-draggable-attribute
pub fn getDraggable(self: *HtmlElement) bool {
if (self.asElement().getAttributeSafe(comptime .wrap("draggable"))) |value| {
if (std.ascii.eqlIgnoreCase(value, "true")) {
return true;
}
if (std.ascii.eqlIgnoreCase(value, "false")) {
return false;
}
}
return switch (self._type) {
.img => true,
.anchor => self.asElement().getAttributeSafe(comptime .wrap("href")) != null,
else => false,
};
}
pub fn setDraggable(self: *HtmlElement, draggable: bool, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("draggable"), .wrap(if (draggable) "true" else "false"), frame);
}
// accessKeyLabel: the UA-assigned shortcut for a valid (single character)
// accesskey, or the empty string. We report an Alt+ chord like Chromium.
pub fn getAccessKeyLabel(self: *HtmlElement, frame: *Frame) ![]const u8 {
@@ -1808,6 +1832,7 @@ pub const JsApi = struct {
pub const accessKey = bridge.accessor(HtmlElement.getAccessKey, HtmlElement.setAccessKey, .{ .ce_reactions = true });
pub const autofocus = bridge.accessor(HtmlElement.getAutofocus, HtmlElement.setAutofocus, .{ .ce_reactions = true });
pub const dir = reflect.enumerated("dir", &.{ "ltr", "rtl", "auto" }, .{});
pub const draggable = bridge.accessor(HtmlElement.getDraggable, HtmlElement.setDraggable, .{ .ce_reactions = true });
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{ .ce_reactions = true });
pub const translate = bridge.accessor(HtmlElement.getTranslate, HtmlElement.setTranslate, .{ .ce_reactions = true });
pub const accessKeyLabel = bridge.accessor(HtmlElement.getAccessKeyLabel, null, .{});