From 90f543327e193a8043022f521e930e0290fc0068 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 17 Sep 2026 13:28:26 -0700 Subject: [PATCH] Refactor row deletion to use the shared modal, refs #2790 --- datasette/static/app.css | 82 ---------------------------------- datasette/static/edit-tools.js | 62 +++++-------------------- tests/test_playwright.py | 2 +- 3 files changed, 12 insertions(+), 134 deletions(-) diff --git a/datasette/static/app.css b/datasette/static/app.css index cadc5eb3..9a57ad90 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -1224,46 +1224,11 @@ button.table-insert-row svg { } dialog.row-delete-dialog { - --ink: #0f0f0f; - --paper: #eef6ff; - --muted: #6b6b6b; - --rule: #d8e6f5; - --accent: #1a56db; - --card: #ffffff; - border: none; - border-radius: var(--modal-border-radius, 0.75rem); - padding: 0; - margin: auto; width: min(440px, calc(100vw - 32px)); - max-width: 95vw; - box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); - animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; - overflow: hidden; - font-family: system-ui, -apple-system, sans-serif; - background: var(--card); -} - -dialog.row-delete-dialog[open] { - display: flex; - flex-direction: column; -} - -dialog.row-delete-dialog::backdrop { - background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); - backdrop-filter: var(--modal-backdrop-blur, blur(4px)); - -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); - animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; } .row-delete-dialog .modal-header { - padding: 20px 24px 12px; - border-bottom: 1px solid var(--rule); - display: flex; - align-items: center; justify-content: flex-start; - gap: 12px; - flex-shrink: 0; - min-width: 0; } .row-delete-dialog .modal-title { @@ -1272,9 +1237,6 @@ dialog.row-delete-dialog::backdrop { gap: 0.35rem; min-width: 0; max-width: 100%; - font-size: 1rem; - font-weight: 600; - color: var(--ink); } .row-delete-message, @@ -1306,53 +1268,9 @@ dialog.row-delete-dialog::backdrop { .row-delete-dialog .modal-footer { padding: 18px 20px 14px; - border-top: 1px solid var(--rule); - display: flex; - align-items: center; - justify-content: flex-end; - gap: 10px; - flex-shrink: 0; - background: var(--paper); margin-top: 18px; } -.row-delete-dialog .btn { - border: none; - border-radius: 5px; - padding: 9px 20px; - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - touch-action: manipulation; - font-family: inherit; - transition: background 0.12s; -} - -.row-delete-dialog .btn-ghost { - background: transparent; - color: var(--muted); - border: 1px solid var(--rule); -} - -.row-delete-dialog .btn-ghost:hover { - background: var(--rule); - color: var(--ink); -} - -.row-delete-dialog .btn-primary { - background: var(--accent); - color: #fff; -} - -.row-delete-dialog .btn-primary:hover { - background: #1949b8; -} - -.row-delete-dialog .btn:disabled { - opacity: 0.65; - cursor: wait; -} - dialog.row-edit-dialog { --ink: #0f0f0f; --paper: #eef6ff; diff --git a/datasette/static/edit-tools.js b/datasette/static/edit-tools.js index eb0144a7..402af956 100644 --- a/datasette/static/edit-tools.js +++ b/datasette/static/edit-tools.js @@ -2410,6 +2410,7 @@ function initTableCreateActions(manager) { function setRowDeleteDialogBusy(state, isBusy) { state.isBusy = isBusy; + state.modal.busy = isBusy; state.confirmButton.disabled = isBusy; state.cancelButton.disabled = isBusy; state.confirmButton.textContent = isBusy ? "Deleting..." : "Delete row"; @@ -4360,7 +4361,8 @@ function ensureRowDeleteDialog(manager) { return null; } - var dialog = document.createElement("dialog"); + var modal = DatasetteModal.create(); + var dialog = modal.dialog; dialog.id = ROW_DELETE_DIALOG_ID; dialog.className = "row-delete-dialog"; dialog.setAttribute("aria-labelledby", "row-delete-title"); @@ -4372,13 +4374,14 @@ function ensureRowDeleteDialog(manager) {

Delete row ?

`; - document.body.appendChild(dialog); + document.body.appendChild(modal); rowDeleteDialogState = { + modal: modal, dialog: dialog, title: dialog.querySelector(".modal-title"), message: dialog.querySelector(".row-delete-message"), @@ -4391,21 +4394,10 @@ function ensureRowDeleteDialog(manager) { currentPkPath: null, manager: manager, isBusy: false, - shouldRestoreFocus: true, }; rowDeleteDialogState.cancelButton.addEventListener("click", function () { - if (!rowDeleteDialogState.isBusy) { - rowDeleteDialogState.shouldRestoreFocus = true; - dialog.close(); - } - }); - - dialog.addEventListener("click", function (ev) { - if (ev.target === dialog && !rowDeleteDialogState.isBusy) { - rowDeleteDialogState.shouldRestoreFocus = true; - dialog.close(); - } + modal.requestClose("cancel"); }); dialog.addEventListener("keydown", function (ev) { @@ -4417,25 +4409,6 @@ function ensureRowDeleteDialog(manager) { if (!rowDeleteDialogState.isBusy) { rowDeleteDialogState.confirmButton.click(); } - return; - } - if (ev.key !== "Escape") { - return; - } - if (rowDeleteDialogState.isBusy) { - ev.preventDefault(); - return; - } - ev.preventDefault(); - rowDeleteDialogState.shouldRestoreFocus = true; - dialog.close(); - }); - - dialog.addEventListener("cancel", function (ev) { - if (rowDeleteDialogState.isBusy) { - ev.preventDefault(); - } else { - rowDeleteDialogState.shouldRestoreFocus = true; } }); @@ -4443,13 +4416,6 @@ function ensureRowDeleteDialog(manager) { var state = rowDeleteDialogState; clearRowDeleteDialogError(state); setRowDeleteDialogBusy(state, false); - if ( - state.shouldRestoreFocus && - state.currentButton && - document.contains(state.currentButton) - ) { - state.currentButton.focus(); - } }); rowDeleteDialogState.confirmButton.addEventListener( @@ -4476,8 +4442,7 @@ function ensureRowDeleteDialog(manager) { throw rowMutationRequestError(response, data); } if (data && data.redirect) { - state.shouldRestoreFocus = false; - state.dialog.close(); + state.modal.close({ restoreFocus: false }); location.href = data.redirect; return; } @@ -4489,8 +4454,7 @@ function ensureRowDeleteDialog(manager) { var statusMessage = state.currentPkPath ? "Deleted row " + state.currentPkPath + "." : "Deleted row."; - state.shouldRestoreFocus = false; - state.dialog.close(); + state.modal.close({ restoreFocus: false }); state.currentRow.remove(); showRowMutationStatus(state.manager, statusMessage, false); if (focusTarget && document.contains(focusTarget)) { @@ -4519,11 +4483,9 @@ function openRowDeleteDialog(button, manager) { } state.manager = manager; - state.currentButton = button; state.currentRow = row; state.currentDeleteUrl = rowDeleteUrl(row); state.currentPkPath = rowDisplayLabel(row); - state.shouldRestoreFocus = true; clearRowDeleteDialogError(state); setRowDeleteDialogBusy(state, false); @@ -4535,9 +4497,7 @@ function openRowDeleteDialog(button, manager) { ); state.rowId.textContent = state.currentPkPath || "this row"; - if (!state.dialog.open) { - state.dialog.showModal(); - } + state.modal.show({ trigger: button }); state.confirmButton.focus(); } diff --git a/tests/test_playwright.py b/tests/test_playwright.py index b14a2359..d7f6fa5c 100644 --- a/tests/test_playwright.py +++ b/tests/test_playwright.py @@ -1603,7 +1603,7 @@ def test_delete_row_flow_removes_row(page, datasette_server): dialog = page.locator("#row-delete-dialog") dialog.wait_for() assert "Delete row 1" in dialog.inner_text() - dialog.locator(".row-delete-confirm").click() + dialog.locator(".row-delete-confirm").press("Enter") page.locator(".row-mutation-status", has_text="Deleted row 1").wait_for() page.locator('tr[data-row="1"]').wait_for(state="detached")