Skip to content

Add 'Move to' menu as keyboard alternative to drag-and-drop#39

Merged
alecvdp merged 1 commit intomainfrom
cyrus/irl-25-no-keyboard-non-pointer-alternative-to-drag-and-drop-for
Apr 19, 2026
Merged

Add 'Move to' menu as keyboard alternative to drag-and-drop#39
alecvdp merged 1 commit intomainfrom
cyrus/irl-25-no-keyboard-non-pointer-alternative-to-drag-and-drop-for

Conversation

@alecvdp
Copy link
Copy Markdown
Owner

@alecvdp alecvdp commented Apr 19, 2026

Assignee: @alecvdp (alecvdpoel)

Summary

Adds a keyboard/non-pointer alternative to drag-and-drop for moving tasks between sections in the TasksWidget.

  • Each task row now has a "Move to…" button (ArrowUpDown icon) in the action buttons area that opens a dropdown menu listing all available sections (Today, This Week, Groceries, Recurring, Later) except the current one
  • The button and menu are fully keyboard accessible: reachable via Tab, opened with Enter/Space, menu items navigable via Tab, selectable with Enter, dismissible with Escape or click-outside
  • Task action buttons are now visible on keyboard focus (:focus-within) in addition to hover, ensuring keyboard-only users can discover and use them
  • Uses proper ARIA attributes (aria-haspopup, aria-expanded, role="menu", role="menuitem") for screen reader compatibility

Testing

  • Verified mouse interaction: hover reveals button, click opens menu, selecting an option moves the task to the correct section
  • Verified keyboard interaction: Tab to move button → Enter to open → Tab through menu items → Enter to select → task moves correctly
  • Verified Escape closes the menu, click-outside closes the menu
  • Verified menu correctly excludes the current section from options
  • Build passes, lint clean, all 31 tests pass

Closes IRL-25


Tip: I will respond to comments that @ mention @cyrusagent on this PR. You can also submit a review with all your feedback at once, and I will automatically wake up to address each comment.


Open in Devin Review

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
Copilot AI review requested due to automatic review settings April 19, 2026 04:09
@linear
Copy link
Copy Markdown

linear Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a keyboard-accessible “Move to…” menu to each task row in TasksWidget, providing a non-drag-and-drop way to move tasks between sections.

Changes:

  • Passes currentSection and an onMove callback from TasksWidget into each TaskRow.
  • Reveals task action buttons on keyboard focus (:focus-within) in addition to hover.
  • Introduces a per-row “Move to…” dropdown menu UI with section targets and click-outside/Escape dismissal.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/components/TasksWidget.tsx Wires currentSection + onMove into TaskRow to support keyboard-driven moves.
src/components/TasksWidget.module.css Makes actions visible on focus and adds styling for the new move menu.
src/components/TaskRow.tsx Implements the “Move to…” button + dropdown menu and hooks it up to onMove.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


.taskRow:hover .taskActions {
.taskRow:hover .taskActions,
.taskRow:focus-within .taskActions {
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the move menu is rendered inside .taskActions (which is hidden via opacity: 0 unless the row is hovered/focused), an open menu can become invisible while still being present in the DOM (e.g., if focus moves outside the row without closing the menu). Consider keeping actions/menu visible while the menu is open (e.g., an additional class) and/or closing the menu on focusout/blur so aria-expanded can’t remain true while the UI is visually hidden.

Suggested change
.taskRow:focus-within .taskActions {
.taskRow:focus-within .taskActions,
.taskRow:has(.moveMenu) .taskActions {

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +113
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]);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each open move menu instance attaches global document listeners. Since multiple task rows can potentially have menus open at once, this can lead to multiple global listeners and duplicated work. Consider centralizing the open-menu state (only one menu open at a time) and/or using a shared click-outside handler to keep listener count constant.

Copilot uses AI. Check for mistakes.
Comment on lines +94 to 118
const [moveMenuOpen, setMoveMenuOpen] = useState(false);
const moveMenuRef = useRef<HTMLDivElement>(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 (
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moveMenuOpen state can survive transitions into/out of edit mode because isEditing short-circuits the render but doesn’t reset the state. If the menu was open before editing starts, it will reopen automatically when editing ends. Close the menu when isEditing becomes true (and/or when task.id changes).

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +262
<button
type="button"
className="btn-icon"
onClick={() => setMoveMenuOpen((v) => !v)}
disabled={isBusy}
aria-haspopup="true"
aria-expanded={moveMenuOpen}
aria-label="Move task to another section"
title="Move to…"
>
<ArrowUpDown size={14} />
</button>
{moveMenuOpen && (
<ul className={styles.moveMenu} role="menu">
{moveTargets.map((section) => (
<li key={section} role="none">
<button
type="button"
role="menuitem"
className={styles.moveMenuItem}
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dropdown uses role="menu"/role="menuitem", but focus is not moved into the menu and items remain in the normal Tab order (no roving tabindex / arrow-key handling). This can cause screen readers to announce a menu while keyboard interaction behaves like standard buttons. Either implement the full ARIA menu keyboard pattern (focus management + arrow keys) or remove the menu roles and rely on native button/list semantics; also consider using aria-haspopup="menu" instead of "true" and associating the trigger with the menu via aria-controls/id.

Copilot uses AI. Check for mistakes.
@alecvdp alecvdp merged commit fec9e2e into main Apr 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants