mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 17:46:32 -04:00
dom: implement HTMLDialogElement.{show, showModal, close}
The HTMLDialogElement constructor was exposed with `open` / `returnValue` IDL accessors, but the three instance methods that drive the open/close state were missing. Per HTML §4.11.4 (The dialog element), `show()` sets the `open` attribute if absent; `showModal()` throws `InvalidStateError` when the dialog is already open and otherwise sets `open`; `close()` removes `open`, optionally updates `returnValue`, and fires a non- bubbling `close` event. The non-rendering steps (focus trap, backdrop, top-layer placement) are intentional no-ops here — `[open]` reflecting through to selectors and the `close` event firing are what downstream CDP clients rely on. Closes #2434
This commit is contained in:
@@ -76,3 +76,125 @@
|
||||
testing.expectTrue(dialog instanceof HTMLDialogElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="show_sets_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
testing.expectEqual(false, dialog.open)
|
||||
|
||||
dialog.show()
|
||||
testing.expectEqual(true, dialog.open)
|
||||
testing.expectEqual('', dialog.getAttribute('open'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="show_noop_when_already_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.setAttribute('open', '')
|
||||
testing.expectEqual(true, dialog.open)
|
||||
|
||||
dialog.show()
|
||||
testing.expectEqual(true, dialog.open)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="showModal_sets_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
testing.expectEqual(false, dialog.open)
|
||||
|
||||
dialog.showModal()
|
||||
testing.expectEqual(true, dialog.open)
|
||||
testing.expectEqual('', dialog.getAttribute('open'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="showModal_throws_when_already_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.setAttribute('open', '')
|
||||
|
||||
testing.expectError('InvalidStateError', () => dialog.showModal())
|
||||
testing.expectEqual(true, dialog.open)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="close_removes_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.show()
|
||||
testing.expectEqual(true, dialog.open)
|
||||
|
||||
dialog.close()
|
||||
testing.expectEqual(false, dialog.open)
|
||||
testing.expectEqual(null, dialog.getAttribute('open'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="close_noop_when_not_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
testing.expectEqual(false, dialog.open)
|
||||
|
||||
dialog.close()
|
||||
testing.expectEqual(false, dialog.open)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="close_sets_returnValue_when_arg_given">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.show()
|
||||
|
||||
dialog.close('confirmed')
|
||||
testing.expectEqual('confirmed', dialog.returnValue)
|
||||
testing.expectEqual('confirmed', dialog.getAttribute('returnvalue'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="close_preserves_returnValue_when_no_arg">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
dialog.returnValue = 'preset'
|
||||
dialog.show()
|
||||
|
||||
dialog.close()
|
||||
testing.expectEqual('preset', dialog.returnValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="close_fires_close_event">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
document.body.appendChild(dialog)
|
||||
dialog.show()
|
||||
|
||||
let fired = 0
|
||||
let bubbled = false
|
||||
let cancelable = null
|
||||
dialog.addEventListener('close', (e) => {
|
||||
fired++
|
||||
cancelable = e.cancelable
|
||||
})
|
||||
document.addEventListener('close', () => { bubbled = true })
|
||||
|
||||
dialog.close('done')
|
||||
testing.expectEqual(1, fired)
|
||||
testing.expectEqual(false, bubbled)
|
||||
testing.expectEqual(false, cancelable)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="close_does_not_fire_event_when_not_open">
|
||||
{
|
||||
const dialog = document.createElement('dialog')
|
||||
document.body.appendChild(dialog)
|
||||
|
||||
let fired = 0
|
||||
dialog.addEventListener('close', () => fired++)
|
||||
|
||||
dialog.close()
|
||||
testing.expectEqual(0, fired)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Event = @import("../../Event.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
@@ -39,6 +40,37 @@ pub fn setReturnValue(self: *Dialog, value: []const u8, frame: *Frame) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("returnvalue"), .wrap(value), frame);
|
||||
}
|
||||
|
||||
/// https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-show
|
||||
/// If the open attribute is set, return; otherwise set it to the empty string.
|
||||
/// Focus / inert / top-layer steps are no-ops here — no rendering pipeline.
|
||||
pub fn show(self: *Dialog, frame: *Frame) !void {
|
||||
if (self.getOpen()) return;
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("open"), .wrap(""), frame);
|
||||
}
|
||||
|
||||
/// https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-showmodal
|
||||
/// Throws InvalidStateError if [open] is already set. Sets [open] otherwise.
|
||||
/// Focus trap, backdrop, and top-layer placement are no-ops — Lightpanda has
|
||||
/// no layout/compositor; [open] reflecting through to selectors is what
|
||||
/// downstream consumers rely on.
|
||||
pub fn showModal(self: *Dialog, frame: *Frame) !void {
|
||||
if (self.getOpen()) return error.InvalidStateError;
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("open"), .wrap(""), frame);
|
||||
}
|
||||
|
||||
/// https://html.spec.whatwg.org/multipage/interactive-elements.html#dom-dialog-close
|
||||
/// If [open] is unset, return. Otherwise remove [open], optionally update
|
||||
/// returnValue, and fire a `close` event (non-bubbling, non-cancelable).
|
||||
pub fn close(self: *Dialog, return_value: ?[]const u8, frame: *Frame) !void {
|
||||
if (!self.getOpen()) return;
|
||||
try self.asElement().removeAttribute(comptime .wrap("open"), frame);
|
||||
if (return_value) |v| {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("returnvalue"), .wrap(v), frame);
|
||||
}
|
||||
const event = try Event.init("close", .{ .bubbles = false, .cancelable = false }, frame._page);
|
||||
try frame._event_manager.dispatch(self.asElement().asEventTarget(), event);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Dialog);
|
||||
|
||||
@@ -50,6 +82,10 @@ pub const JsApi = struct {
|
||||
|
||||
pub const open = bridge.accessor(Dialog.getOpen, Dialog.setOpen, .{});
|
||||
pub const returnValue = bridge.accessor(Dialog.getReturnValue, Dialog.setReturnValue, .{});
|
||||
|
||||
pub const show = bridge.function(Dialog.show, .{});
|
||||
pub const showModal = bridge.function(Dialog.showModal, .{ .dom_exception = true });
|
||||
pub const close = bridge.function(Dialog.close, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
|
||||
Reference in New Issue
Block a user