diff --git a/datasette/static/datasette-modal.js b/datasette/static/datasette-modal.js index 78012894..08a09e62 100644 --- a/datasette/static/datasette-modal.js +++ b/datasette/static/datasette-modal.js @@ -11,6 +11,8 @@ * - The standard Datasette modal frame: sizing, rounded corners, * backdrop, animations, and optional .modal-header scaffolding * - Close-on-backdrop-click and Escape key handling + * - Declarative cancel buttons: clicking any element inside the modal + * with a `data-modal-cancel` attribute calls requestClose("cancel") * - A `busy` property that blocks user-initiated dismissal while an * operation is in flight * - A `closeGuard` hook for "discard unsaved changes?" style prompts @@ -526,6 +528,14 @@ datasette-modal .btn:disabled { dialog.addEventListener("click", (ev) => { if (ev.target === dialog) { this.requestClose("backdrop"); + return; + } + // Declarative cancel buttons: any element inside the modal + // with a data-modal-cancel attribute requests a close + var cancelTrigger = + ev.target.closest && ev.target.closest("[data-modal-cancel]"); + if (cancelTrigger && dialog.contains(cancelTrigger)) { + this.requestClose("cancel"); } }); diff --git a/datasette/static/edit-tools.js b/datasette/static/edit-tools.js index b656c7f5..b83d6fef 100644 --- a/datasette/static/edit-tools.js +++ b/datasette/static/edit-tools.js @@ -1559,7 +1559,7 @@ function ensureTableCreateDialog(manager) { @@ -1601,10 +1601,6 @@ function ensureTableCreateDialog(manager) { row.querySelector(".table-create-column-name").focus(); }); - tableCreateDialogState.cancelButton.addEventListener("click", function () { - closeTableCreateDialogIfConfirmed(tableCreateDialogState); - }); - tableCreateDialogState.tableName.addEventListener("input", function () { clearTableCreateDialogError(tableCreateDialogState); }); @@ -3640,7 +3636,7 @@ function ensureRowDeleteDialog(manager) {

Delete row ?

`, @@ -3663,10 +3659,6 @@ function ensureRowDeleteDialog(manager) { isBusy: false, }; - rowDeleteDialogState.cancelButton.addEventListener("click", function () { - modal.requestClose("cancel"); - }); - dialog.addEventListener("keydown", function (ev) { if ( ev.key === "Enter" && diff --git a/datasette/static/table.js b/datasette/static/table.js index 8f3804b7..b588fe03 100644 --- a/datasette/static/table.js +++ b/datasette/static/table.js @@ -202,7 +202,7 @@ function ensureSetColumnTypeDialog() {
`, @@ -223,10 +223,6 @@ function ensureSetColumnTypeDialog() { isBusy: false, }; - setColumnTypeDialogState.cancelButton.addEventListener("click", function () { - modal.requestClose("cancel"); - }); - modal.addEventListener("datasette-modal-close", function () { clearSetColumnTypeDialogError(setColumnTypeDialogState); setSetColumnTypeDialogBusy(setColumnTypeDialogState, false); diff --git a/docs/generate-datasette-modal-example.sh b/docs/generate-datasette-modal-example.sh index 4fedf8b3..f774c679 100755 --- a/docs/generate-datasette-modal-example.sh +++ b/docs/generate-datasette-modal-example.sh @@ -73,7 +73,7 @@ document.addEventListener("datasette_init", function (event) {

Hello from a plugin!

`, @@ -81,9 +81,6 @@ document.addEventListener("datasette_init", function (event) { if (!modal) { return; // Browser does not support } - modal.dialog - .querySelector(".my-plugin-cancel") - .addEventListener("click", () => modal.requestClose("cancel")); // Open it later, for example from a button click: // modal.showModal({trigger: button}); }); diff --git a/docs/javascript_plugins.rst b/docs/javascript_plugins.rst index ce285538..357985c8 100644 --- a/docs/javascript_plugins.rst +++ b/docs/javascript_plugins.rst @@ -90,7 +90,7 @@ The simplest way to create a modal from a plugin is ``datasetteManager.createMod

Hello from a plugin!

`, @@ -98,13 +98,12 @@ The simplest way to create a modal from a plugin is ``datasetteManager.createMod if (!modal) { return; // Browser does not support } - modal.dialog - .querySelector(".my-plugin-cancel") - .addEventListener("click", () => modal.requestClose("cancel")); // Open it later, for example from a button click: // modal.showModal({trigger: button}); }); +The ``data-modal-cancel`` attribute on the Cancel button is a declarative shortcut: clicking any element inside the modal that carries this attribute calls ``modal.requestClose("cancel")``, so Cancel buttons need no JavaScript wiring. Like other dismissals it is blocked while the modal is ``busy`` and consults ``closeGuard``, both described below. + Calling ``modal.showModal()`` then displays the dialog: .. Regenerate this screenshot with docs/generate-datasette-modal-example.sh @@ -146,6 +145,7 @@ The element can also be used declaratively in a template - light DOM children be

Dialog content

@@ -171,7 +171,7 @@ Properties, methods and events Closes the modal unconditionally, skipping ``busy`` and ``closeGuard``. Pass ``{restoreFocus: false}`` to leave focus where it is, for example when the page is about to navigate. ``modal.requestClose(reason)`` - Asks the modal to close on the user's behalf, respecting ``busy`` and ``closeGuard``. Returns true if the modal closed. Backdrop clicks and the ``Escape`` key call this internally with reasons ``"backdrop"`` and ``"escape"``. Wire Cancel buttons to this method. + Asks the modal to close on the user's behalf, respecting ``busy`` and ``closeGuard``. Returns true if the modal closed. Backdrop clicks and the ``Escape`` key call this internally with reasons ``"backdrop"`` and ``"escape"``, and clicking an element with a ``data-modal-cancel`` attribute calls it with ``"cancel"``. Cancel buttons can either carry that attribute or call this method directly. ``modal.busy`` - boolean While true, ``Escape``, backdrop clicks and ``requestClose()`` will not close the modal. Set this while an operation is in flight. A ``busy`` attribute is reflected on the element for CSS. diff --git a/tests/test_playwright.py b/tests/test_playwright.py index 4692e018..252003f3 100644 --- a/tests/test_playwright.py +++ b/tests/test_playwright.py @@ -975,6 +975,13 @@ def test_set_column_type_dialog(page, datasette_server): option_names = dialog.locator(".set-column-type-option-name").all_inner_texts() assert "asset" in option_names - # Escape closes the dialog via the shared datasette-modal component + # The declarative data-modal-cancel attribute closes the dialog + dialog.locator("[data-modal-cancel]").click() + dialog.wait_for(state="hidden") + + # Escape also closes the dialog via the shared datasette-modal component + page.locator('th[data-column="title"] svg.dropdown-menu-icon').click() + page.get_by_role("link", name="Set custom type").click() + dialog.wait_for(state="visible") page.keyboard.press("Escape") dialog.wait_for(state="hidden")