From e9c141aba583f55e2df5d07efa125faf8aa6164e Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Wed, 8 Apr 2026 17:18:59 -0700 Subject: [PATCH] fix(ui): improve delete workstream UX and accessibility (#339) * fix(ui): improve delete workstream UX and accessibility Card body click no longer deselects (prevents confusing red border loss); checkbox pulse hint guides users to deselect affordance. Adds keyboard navigation, aria-labels, hover feedback, animations, and neutral Close button styling after deletion. * fix(ui): remove duplicate a11y checkbox from delete-mode cards Hide the visual checkbox from the a11y tree and tab order so the card (role=checkbox) is the sole keyboard/screen-reader target. Addresses Copilot review feedback about nested interactive elements. --- turnstone/ui/static/app.js | 24 ++++++++++++++++++- turnstone/ui/static/index.html | 2 +- turnstone/ui/static/style.css | 44 +++++++++++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/turnstone/ui/static/app.js b/turnstone/ui/static/app.js index 0e7c0d62..6ee6f06c 100644 --- a/turnstone/ui/static/app.js +++ b/turnstone/ui/static/app.js @@ -3368,19 +3368,39 @@ function renderSavedWorkstreams(items) { chk.type = "checkbox"; chk.className = "ws-card-check"; chk.checked = !!_wsDeleteSelected[sess.ws_id]; + chk.setAttribute("tabindex", "-1"); + chk.setAttribute("aria-hidden", "true"); chk.onclick = function (e) { e.stopPropagation(); if (chk.checked) _wsDeleteSelected[sess.ws_id] = true; else delete _wsDeleteSelected[sess.ws_id]; card.classList.toggle("ws-selected", chk.checked); + card.setAttribute("aria-checked", chk.checked ? "true" : "false"); updateWsDeleteBar(); }; card.appendChild(chk); + card.setAttribute("tabindex", "0"); + card.setAttribute("role", "checkbox"); + card.setAttribute("aria-checked", chk.checked ? "true" : "false"); card.onclick = function (e) { if (e.target === chk) return; - chk.checked = !chk.checked; + if (chk.checked) { + chk.classList.add("ws-check-hint"); + setTimeout(function () { + chk.classList.remove("ws-check-hint"); + }, 600); + return; + } + chk.checked = true; chk.onclick(e); }; + card.onkeydown = function (e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + chk.checked = !chk.checked; + chk.onclick(e); + } + }; } else { card.setAttribute("role", "button"); card.setAttribute("tabindex", "0"); @@ -3505,6 +3525,7 @@ function confirmWsDeleteSelection() { if (delBtn) { delBtn.textContent = "Delete"; delBtn.disabled = false; + delBtn.classList.remove("ws-delete-close"); delBtn.onclick = confirmWsDelete; } var cancelBtn = document.getElementById("ws-delete-cancel-btn"); @@ -3636,6 +3657,7 @@ function confirmWsDelete() { if (delBtn) { delBtn.disabled = false; delBtn.textContent = "Close"; + delBtn.classList.add("ws-delete-close"); delBtn.onclick = function () { cancelWsDelete(); cancelWsDeleteMode(); diff --git a/turnstone/ui/static/index.html b/turnstone/ui/static/index.html index b3535392..67bfc499 100644 --- a/turnstone/ui/static/index.html +++ b/turnstone/ui/static/index.html @@ -60,7 +60,7 @@
0 selected - +
diff --git a/turnstone/ui/static/style.css b/turnstone/ui/static/style.css index ea2d2218..649370db 100644 --- a/turnstone/ui/static/style.css +++ b/turnstone/ui/static/style.css @@ -1504,8 +1504,12 @@ audio.media-player { .dashboard-card .card-meta { font-size: 11px; color: var(--fg-dim); } /* Delete mode */ -.dashboard-card.ws-delete-mode { cursor: default; } -.dashboard-card.ws-delete-mode:hover { border-color: var(--border); background: var(--bg-surface); } +.dashboard-card.ws-delete-mode { cursor: pointer; } +.dashboard-card.ws-delete-mode:hover { border-color: var(--red); background: rgba(248, 113, 113, 0.04); } +.dashboard-card.ws-delete-mode.ws-selected { cursor: default; } +.dashboard-card.ws-delete-mode.ws-selected:hover { border-color: var(--red); background: rgba(248, 113, 113, 0.08); } +[data-theme="light"] .dashboard-card.ws-delete-mode:hover { background: rgba(220, 38, 38, 0.04); } +[data-theme="light"] .dashboard-card.ws-delete-mode.ws-selected:hover { background: rgba(220, 38, 38, 0.08); } .ws-card-check { position: absolute; top: 8px; @@ -1515,7 +1519,16 @@ audio.media-player { accent-color: var(--red); cursor: pointer; z-index: 1; + opacity: 0; + animation: ws-check-fadein 0.2s ease-out forwards; } +.ws-card-check.ws-check-hint { + outline: 2px solid var(--red); + outline-offset: 2px; + animation: ws-check-pulse 0.6s ease-out; +} +@keyframes ws-check-fadein { to { opacity: 1; } } +@keyframes ws-check-pulse { 0% { transform: scale(1.3); } 100% { transform: scale(1); } } .dashboard-card.ws-selected { border-color: var(--red); background: rgba(248, 113, 113, 0.08); @@ -1533,7 +1546,13 @@ audio.media-player { border: 1px solid var(--border); border-radius: var(--radius); } -.ws-delete-bar.visible { display: flex; } +.ws-delete-bar.visible { display: flex; animation: ws-bar-slide 0.2s ease-out; } +@keyframes ws-bar-slide { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } +@media (prefers-reduced-motion: reduce) { + .ws-card-check { animation: none; opacity: 1; } + .ws-card-check.ws-check-hint { animation: none; } + .ws-delete-bar.visible { animation: none; } +} .ws-delete-bar .ws-delete-count-label { font-size: 12px; color: var(--fg-dim); } .ws-delete-bar .ws-delete-bar-btn { margin-left: auto; @@ -1563,6 +1582,20 @@ audio.media-player { border-color: var(--border-strong); background: var(--bg-highlight); } +.ws-delete-bar .ws-delete-selectall-btn { + background: transparent; + color: var(--fg-bright); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 6px 12px; + font-size: 12px; + font-weight: 500; + cursor: pointer; +} +.ws-delete-bar .ws-delete-selectall-btn:hover { + border-color: var(--border-strong); + background: var(--bg-highlight); +} /* Delete modal */ #ws-delete-overlay { @@ -1608,6 +1641,11 @@ audio.media-player { color: #fff; border-color: var(--red); } +#ws-delete-buttons button.ws-delete-close { + background: transparent; + color: var(--fg-bright); + border-color: var(--border); +} /* Server dashboard row — clickable */ .dash-row { cursor: pointer; }