diff --git a/src/App.svelte b/src/App.svelte index 2d454e7..1c78e9a 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -8,8 +8,10 @@ import { createStorage } from './lib/storage.js'; import { rollover, msUntilNextMidnight } from './lib/rollover.js'; import * as taskOps from './lib/tasks.js'; + import { createUndoStack, applyUndo } from './lib/undo.js'; const storage = createStorage(); + const undoStack = createUndoStack(); let tasks = $state([]); let newTask = $state(''); @@ -43,27 +45,55 @@ } function deleteTask(id) { + const index = tasks.findIndex(task => task.id === id); + if (index === -1) return; + undoStack.push({ type: 'delete', task: tasks[index], index }); setTasks(taskOps.deleteTask(tasks, id)); } function clearCompleted() { + // Built in ascending index order, which applyUndo relies on to put each + // task back where it was. + const removed = tasks + .map((task, index) => ({ task, index })) + .filter(({ task }) => task.completed); + + if (removed.length === 0) return; + undoStack.push({ type: 'clearCompleted', removed }); setTasks(taskOps.clearCompleted(tasks)); } + function undo() { + const entry = undoStack.pop(); + if (entry) setTasks(applyUndo(tasks, entry)); + } + function toggleTheme() { darkMode = !darkMode; } - function handleKeydown(event) { + /** Scoped to the input. Previously this also sat on window, so Enter while a + * task was focused would toggle that task *and* add whatever was in the + * input. */ + function handleInputKeydown(event) { if (event.key === 'Enter') addTask(); else if (event.key === 'Escape') newTask = ''; } + function handleGlobalKeydown(event) { + if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'z') return; + // Leave the text field its own native undo. + if (event.target instanceof HTMLInputElement) return; + event.preventDefault(); + undo(); + } + + /** Backspace no longer deletes: it is the key people press meaning "go back", + * and a task destroyed that way used to be unrecoverable. Delete still does, + * and Cmd/Ctrl+Z now reverses it. */ function handleTaskKeydown(event, taskId) { - if (event.key === ' ' || event.key === 'Enter') { + if (event.key === 'Delete') { event.preventDefault(); - toggleTask(taskId); - } else if (event.key === 'Delete' || event.key === 'Backspace') { deleteTask(taskId); } } @@ -170,7 +200,7 @@ }); - +
@@ -235,7 +265,7 @@ type="text" placeholder="+ Add a task" class="task-input" - onkeydown={handleKeydown} + onkeydown={handleInputKeydown} />
@@ -271,9 +301,15 @@

No tasks yet. Add one above to get started!

{:else} + {/if} {/key} diff --git a/src/style.css b/src/style.css index 07c0620..4ee4cb8 100644 --- a/src/style.css +++ b/src/style.css @@ -242,11 +242,18 @@ body { } .task-list { + min-height: 400px; + position: relative; +} + +/* A real list, so screen readers announce item counts and position. */ +.task-items { display: flex; flex-direction: column; gap: 12px; - min-height: 400px; - position: relative; + list-style: none; + margin: 0; + padding: 0; } .task-item { @@ -362,7 +369,11 @@ body { flex-shrink: 0; } -.task-item:hover .delete-btn { +/* focus-within matters as much as hover here: the button is opacity 0 by + default, so a keyboard user tabbing to it previously saw nothing at all — + the focus outline was drawn on an invisible element. */ +.task-item:hover .delete-btn, +.task-item:focus-within .delete-btn { opacity: 1; }