From 4302606ed73ad0fffc976af75d168492dd6a02da Mon Sep 17 00:00:00 2001 From: Alec Date: Sat, 18 Apr 2026 21:09:33 -0700 Subject: [PATCH] Add "Move to" menu as keyboard alternative to drag-and-drop Each task row now has a move button (ArrowUpDown icon) that opens a dropdown menu listing all sections except the current one. The menu is fully keyboard accessible: Tab to the button, Enter/Space to open, Tab through menu items, Enter to select, Escape to dismiss. Also adds :focus-within to taskActions so buttons are visible when any child receives keyboard focus (not just on hover). Closes IRL-25 --- src/components/TaskRow.tsx | 64 +++++++++++++++++++++++++++ src/components/TasksWidget.module.css | 43 +++++++++++++++++- src/components/TasksWidget.tsx | 2 + 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/components/TaskRow.tsx b/src/components/TaskRow.tsx index f039ce6..251810b 100644 --- a/src/components/TaskRow.tsx +++ b/src/components/TaskRow.tsx @@ -1,6 +1,8 @@ "use client"; +import { useEffect, useRef, useState } from "react"; import { + ArrowUpDown, CalendarDays, Check, Flag, @@ -23,10 +25,13 @@ import { taskPriorityOrder, taskRecurrenceMeta, taskRecurrenceOrder, + taskSectionMeta, + taskSectionOrder, type TaskAssignee, type TaskBucket, type TaskPriority, type TaskRecurrence, + type TaskSection, } from "@/lib/tasks"; import type { Task } from "./TasksWidget"; @@ -51,6 +56,7 @@ type TaskRowProps = { task: Task; isBusy: boolean; isEditing: boolean; + currentSection: TaskSection; editState: { title: string; assignee: TaskAssignee; @@ -66,12 +72,14 @@ type TaskRowProps = { onSaveEdit: (taskId: string) => void; onDelete: (taskId: string) => void; onEditChange: (field: string, value: string | boolean) => void; + onMove: (task: Task, section: TaskSection) => void; }; export default function TaskRow({ task, isBusy, isEditing, + currentSection, editState, onToggle, onStartEdit, @@ -79,9 +87,32 @@ export default function TaskRow({ onSaveEdit, onDelete, onEditChange, + onMove, }: TaskRowProps) { const dueInfo = getDueLabel(task.dueDate); const resetLabel = task.completed ? formatResetLabel(task.nextResetAt) : null; + const [moveMenuOpen, setMoveMenuOpen] = useState(false); + const moveMenuRef = useRef(null); + + useEffect(() => { + if (!moveMenuOpen) return; + function handleClickOutside(e: MouseEvent) { + if (moveMenuRef.current && !moveMenuRef.current.contains(e.target as Node)) { + setMoveMenuOpen(false); + } + } + function handleEscape(e: KeyboardEvent) { + if (e.key === "Escape") setMoveMenuOpen(false); + } + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; + }, [moveMenuOpen]); + + const moveTargets = taskSectionOrder.filter((s) => s !== currentSection); if (isEditing) { return ( @@ -208,6 +239,39 @@ export default function TaskRow({
+
+ + {moveMenuOpen && ( +
    + {moveTargets.map((section) => ( +
  • + +
  • + ))} +
+ )} +
diff --git a/src/components/TasksWidget.module.css b/src/components/TasksWidget.module.css index 84459fc..3c3be91 100644 --- a/src/components/TasksWidget.module.css +++ b/src/components/TasksWidget.module.css @@ -247,10 +247,51 @@ flex-shrink: 0; } -.taskRow:hover .taskActions { +.taskRow:hover .taskActions, +.taskRow:focus-within .taskActions { opacity: 1; } +/* ── Move menu ── */ + +.moveMenuWrapper { + position: relative; +} + +.moveMenu { + position: absolute; + right: 0; + top: 100%; + z-index: 20; + list-style: none; + margin: 4px 0 0; + padding: 4px 0; + min-width: 140px; + background: var(--surface-elevated, #2a2d35); + border: 1px solid var(--surface-border); + border-radius: var(--radius-sm); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); +} + +.moveMenuItem { + display: block; + width: 100%; + background: none; + border: none; + color: var(--text-secondary); + font-size: 0.82rem; + padding: 7px 14px; + text-align: left; + cursor: pointer; + transition: all 0.12s ease; +} + +.moveMenuItem:hover, +.moveMenuItem:focus-visible { + color: var(--text-primary); + background: rgba(255, 255, 255, 0.06); +} + /* ── Edit card ── */ .editCard { diff --git a/src/components/TasksWidget.tsx b/src/components/TasksWidget.tsx index 9e9fbdb..001c7d5 100644 --- a/src/components/TasksWidget.tsx +++ b/src/components/TasksWidget.tsx @@ -233,6 +233,7 @@ export default function TasksWidget({ initialTasks }: { initialTasks: Task[] }) task={task} isBusy={loadingIds.has(task.id)} isEditing={editingTaskId === task.id} + currentSection={section} editState={{ title: editingTitle, assignee: editingAssignee, @@ -248,6 +249,7 @@ export default function TasksWidget({ initialTasks }: { initialTasks: Task[] }) onSaveEdit={handleUpdate} onDelete={setDeleteTarget} onEditChange={handleEditChange} + onMove={handleMove} /> ))}