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.
This commit is contained in:
Patrick Buckley
2026-04-08 17:18:59 -07:00
committed by Patrick Buckley
parent b038dbdd5b
commit e9c141aba5
3 changed files with 65 additions and 5 deletions
+23 -1
View File
@@ -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();
+1 -1
View File
@@ -60,7 +60,7 @@
<div id="ws-delete-bar" class="ws-delete-bar">
<span class="ws-delete-count-label" id="ws-delete-bar-count" role="status" aria-live="polite" aria-atomic="true">0 selected</span>
<button class="ws-delete-cancel-btn" onclick="cancelWsDeleteMode()">Cancel</button>
<button class="ws-delete-cancel-btn" id="ws-delete-bar-select-all" onclick="toggleSelectAll()">Select All</button>
<button class="ws-delete-selectall-btn" id="ws-delete-bar-select-all" onclick="toggleSelectAll()">Select All</button>
<button class="ws-delete-bar-btn" id="ws-delete-bar-delete" onclick="confirmWsDeleteSelection()" disabled>Delete Selected</button>
</div>
</section>
+41 -3
View File
@@ -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; }