feat(redesign): [P5] @dnd-kit drag-and-drop for cards and lists #5

Closed
kaykayyali wants to merge 2 commits from wt/redesign-dnd into main
Owner

Phase 5 of the ultra-todo Trello-style redesign

Layers @dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities on top of the static P3+P4 components per the task spec.

What landed (commit 7ec1951, on top of 61e3169)

Production components:

  • src/components/Card.jsxuseSortable with cardDndId("card-"+id) prefixed id, opacity 0.4 while dragging, transform+transition from CSS.Transform.toString, drag-handle button with aria-label="Drag card: <title>", role=button + aria-roledescription=sortable, optional onClick (P4 modal hook), optional onRemove, placeholder mode for drop previews.
  • src/components/List.jsxuseSortable on the list BODY only (NOT on the header) so rename keeps working. Listeners/refs spread on body, header stays static. Inline-edit header (click to rename, Enter to commit, Escape to cancel). "+ Add card" button. Per-list SortableContext for cards is owned by Board; List only owns its own useSortable for column reorder.
  • src/components/Board.jsx — SINGLE DndContext wraps the whole board with PointerSensor (activationConstraint.distance: 5 — critical for touch), KeyboardSensor (sortableKeyboardCoordinates), and closestCorners collision detection. Optimistic onDragOver moves cards cross-list IMMEDIATELY (Trello UX) with lastOverRef dedup so onDragOver does not re-render on every pointer move. onDragEnd dispatches applyMoveCard / applyMoveList. DragOverlay shows the floating card during drag. Prefix IDs disambiguate card vs list (card- vs list-).
  • src/defaultBoard.js — 3-list, 5-card demo state for first-run.
  • src/App.jsx — wires <Board> into the app, persists v2 state to localStorage key ultra-todo-v2-state.
  • src/App.css — Trello palette (lists #ebecf0 on #0079bf board background, white cards with subtle shadow, drag handle visible on hover, mobile breakpoint at 640px making lists 84vw).

Tests (37 total, 15 new in this PR)

File Tests Coverage
src/components/Card.test.jsx 4 renders title, forwards onClick, root has aria-roledescription=sortable, drag handle accessible name
src/components/List.test.jsx 5 renders list+cards, header NOT draggable (no aria-roledescription), body IS sortable target, drag within list reorders via arrayMove, + Add card fires onAddCard
src/components/Board.test.jsx 6 renders all lists in order, cards in cardIds order, cross-list drag moves card, same-list drag reorders, list reorder swaps positions, optimistic onDragOver fires cross-list move BEFORE drop
src/lib/boardDnd.test.js 17 (from commit 61e3169) reducer coverage
src/components/BoardDnD.test.jsx 4 (from commit 61e3169) fixture integration
src/setup.test.js 1 (sanity)

37/37 tests pass. npm run build succeeds (40 modules, 253 kB JS / 2.7 kB CSS, 80 kB gzipped).

Pitfalls addressed

  • PointerSensor activationConstraint: { distance: 5 } — mobile tap does not fire drag
  • Single DndContext wraps whole board (not per-list) — cross-list drag works
  • onDragOver dedup via lastOverRef key (activeCardId->overListId) — no jank
  • useSortable returns spread on the SAME element that receives setNodeRef
  • List header does NOT receive useSortable listeners — rename still works
  • arrayMove used for both same-list reorder and list reorder — no hand-rolled math
  • Auto-scroll works because .board has overflow-x: auto (P5 ships mobile breakpoint, list width 84vw on phones)
  • DragOverlay renders a Card copy — smoother visual during drag
  • Prefix IDs (card- / list-) for type disambiguation in handlers

Browser smoke

Manually booted npx vite on a local port, verified:

  • Three columns render (To Do / Doing / Done) with white cards on light gray column backgrounds
  • Cards show drag handles, accessible names confirm via a11y tree
  • Clicking the list title swaps it to a textbox (rename UX works — listeners on body, NOT header)
  • No JS errors in console

Manual mobile smoke procedure (for reviewer)

git fetch origin
git checkout wt/redesign-dnd
npm install
npm run dev   # default port
# 1. Drag a card from To Do over Doing — it snaps in immediately (optimistic onDragOver)
# 2. Drop on a specific card — it lands at that index
# 3. Drag a list header — column reorder
# 4. Click the list name — it becomes an inline input (rename)
# 5. DevTools → device toolbar → iPhone SE
#    - Tap a card briefly (no movement) — click handler fires (P4 modal will consume it)
#    - Long-press + drag — drags (5px activation distance prevents accidental drags)
# 6. Keyboard only: Tab to a card, Space to pick up, Arrow keys to move, Space to drop

Known merge collisions to resolve in P7

  • src/App.jsx — P3, P4, P6 each ship their own App.jsx (legacy / single-board / multi-board). P7 unifies. Mine wires <Board> directly with localStorage persistence (P5 does not introduce a Sidebar — that is P6's job).
  • src/App.css — P3 and P6 both strip max-width: 500px from .app. P5 appends the Trello palette to the same file. Merge will need conflict resolution; Trello styles should win.
  • src/components/Card.jsx and src/components/List.jsx — P4 ships its own <Card> (with detail modal) and <List> (with Add-a-card form). P5 ships a minimal version that knows about DnD. P7 should pick the union: P4's Card/List wired into P5's DnD-aware Board. The DnD-specific bits (useSortable on body, drag affordances) should be preserved; the description / due-date / modal bits should come from P4.
  • jest.config.js / jest.setup.js / babel.config.js — P2, P3, P4, P6 each added slightly different variants. P5 uses jsdom+babel-jest+setupFilesAfterEnv+moduleNameMapper for CSS. P7 picks one canonical config (recommend P5's — also covers dnd-kit-specific DOMRect/ResizeObserver/matchMedia polyfills).
  • package.json deps — each PR adds testing-library or dnd-kit deps at different versions. P7 picks a superset; dnd-kit is P5-only, the rest overlap.
  • package-lock.json — large diffs in every PR; merge will need a clean npm install afterwards.

Acceptance

  • npm test — 37/37 green
  • npm run build — green
  • Browser smoke: board renders, rename works, no JS errors
  • Mobile touch emulation smoke — pending reviewer (5px activation distance is configured; jsdom cannot test touch but real browser does)
  • Branch wt/redesign-dnd pushed to origin
  • PR opened against main
## Phase 5 of the ultra-todo Trello-style redesign Layers `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities` on top of the static P3+P4 components per the task spec. ### What landed (commit 7ec1951, on top of 61e3169) **Production components:** - **`src/components/Card.jsx`** — `useSortable` with `cardDndId("card-"+id)` prefixed id, opacity 0.4 while dragging, `transform`+`transition` from `CSS.Transform.toString`, drag-handle button with `aria-label="Drag card: <title>"`, `role=button` + `aria-roledescription=sortable`, optional `onClick` (P4 modal hook), optional `onRemove`, placeholder mode for drop previews. - **`src/components/List.jsx`** — `useSortable` on the list BODY only (NOT on the header) so rename keeps working. Listeners/refs spread on body, header stays static. Inline-edit header (click to rename, Enter to commit, Escape to cancel). "+ Add card" button. Per-list `SortableContext` for cards is owned by Board; List only owns its own `useSortable` for column reorder. - **`src/components/Board.jsx`** — SINGLE `DndContext` wraps the whole board with `PointerSensor` (`activationConstraint.distance: 5` — critical for touch), `KeyboardSensor` (`sortableKeyboardCoordinates`), and `closestCorners` collision detection. Optimistic `onDragOver` moves cards cross-list IMMEDIATELY (Trello UX) with `lastOverRef` dedup so onDragOver does not re-render on every pointer move. `onDragEnd` dispatches `applyMoveCard` / `applyMoveList`. `DragOverlay` shows the floating card during drag. Prefix IDs disambiguate card vs list (`card-` vs `list-`). - **`src/defaultBoard.js`** — 3-list, 5-card demo state for first-run. - **`src/App.jsx`** — wires `<Board>` into the app, persists v2 state to localStorage key `ultra-todo-v2-state`. - **`src/App.css`** — Trello palette (lists `#ebecf0` on `#0079bf` board background, white cards with subtle shadow, drag handle visible on hover, mobile breakpoint at 640px making lists 84vw). ### Tests (37 total, 15 new in this PR) | File | Tests | Coverage | |---|---|---| | `src/components/Card.test.jsx` | 4 | renders title, forwards onClick, root has aria-roledescription=sortable, drag handle accessible name | | `src/components/List.test.jsx` | 5 | renders list+cards, header NOT draggable (no aria-roledescription), body IS sortable target, drag within list reorders via arrayMove, + Add card fires onAddCard | | `src/components/Board.test.jsx` | 6 | renders all lists in order, cards in cardIds order, cross-list drag moves card, same-list drag reorders, list reorder swaps positions, optimistic onDragOver fires cross-list move BEFORE drop | | `src/lib/boardDnd.test.js` | 17 | (from commit 61e3169) reducer coverage | | `src/components/BoardDnD.test.jsx` | 4 | (from commit 61e3169) fixture integration | | `src/setup.test.js` | 1 | (sanity) | **37/37 tests pass.** `npm run build` succeeds (40 modules, 253 kB JS / 2.7 kB CSS, 80 kB gzipped). ### Pitfalls addressed - `PointerSensor` `activationConstraint: { distance: 5 }` — mobile tap does not fire drag - Single `DndContext` wraps whole board (not per-list) — cross-list drag works - `onDragOver` dedup via `lastOverRef` key (`activeCardId->overListId`) — no jank - `useSortable` returns spread on the SAME element that receives `setNodeRef` - List header does NOT receive `useSortable` listeners — rename still works - `arrayMove` used for both same-list reorder and list reorder — no hand-rolled math - Auto-scroll works because `.board` has `overflow-x: auto` (P5 ships mobile breakpoint, list width 84vw on phones) - DragOverlay renders a Card copy — smoother visual during drag - Prefix IDs (`card-` / `list-`) for type disambiguation in handlers ### Browser smoke Manually booted `npx vite` on a local port, verified: - Three columns render (To Do / Doing / Done) with white cards on light gray column backgrounds - Cards show drag handles, accessible names confirm via a11y tree - Clicking the list title swaps it to a textbox (rename UX works — listeners on body, NOT header) - No JS errors in console ### Manual mobile smoke procedure (for reviewer) ```bash git fetch origin git checkout wt/redesign-dnd npm install npm run dev # default port # 1. Drag a card from To Do over Doing — it snaps in immediately (optimistic onDragOver) # 2. Drop on a specific card — it lands at that index # 3. Drag a list header — column reorder # 4. Click the list name — it becomes an inline input (rename) # 5. DevTools → device toolbar → iPhone SE # - Tap a card briefly (no movement) — click handler fires (P4 modal will consume it) # - Long-press + drag — drags (5px activation distance prevents accidental drags) # 6. Keyboard only: Tab to a card, Space to pick up, Arrow keys to move, Space to drop ``` ### Known merge collisions to resolve in P7 - **`src/App.jsx`** — P3, P4, P6 each ship their own App.jsx (legacy / single-board / multi-board). P7 unifies. Mine wires `<Board>` directly with localStorage persistence (P5 does not introduce a Sidebar — that is P6's job). - **`src/App.css`** — P3 and P6 both strip `max-width: 500px` from `.app`. P5 appends the Trello palette to the same file. Merge will need conflict resolution; Trello styles should win. - **`src/components/Card.jsx` and `src/components/List.jsx`** — P4 ships its own `<Card>` (with detail modal) and `<List>` (with Add-a-card form). P5 ships a minimal version that knows about DnD. **P7 should pick the union: P4's Card/List wired into P5's DnD-aware Board.** The DnD-specific bits (useSortable on body, drag affordances) should be preserved; the description / due-date / modal bits should come from P4. - **`jest.config.js` / `jest.setup.js` / `babel.config.js`** — P2, P3, P4, P6 each added slightly different variants. P5 uses jsdom+babel-jest+setupFilesAfterEnv+moduleNameMapper for CSS. P7 picks one canonical config (recommend P5's — also covers dnd-kit-specific DOMRect/ResizeObserver/matchMedia polyfills). - **`package.json` deps** — each PR adds testing-library or dnd-kit deps at different versions. P7 picks a superset; dnd-kit is P5-only, the rest overlap. - **`package-lock.json`** — large diffs in every PR; merge will need a clean `npm install` afterwards. ### Acceptance - [x] `npm test` — 37/37 green - [x] `npm run build` — green - [x] Browser smoke: board renders, rename works, no JS errors - [ ] Mobile touch emulation smoke — pending reviewer (5px activation distance is configured; jsdom cannot test touch but real browser does) - [x] Branch `wt/redesign-dnd` pushed to origin - [x] PR opened against main
kaykayyali added 2 commits 2026-06-24 05:25:32 +00:00
- @dnd-kit/core ^6.3.1, @dnd-kit/sortable ^10.0.0, @dnd-kit/utilities ^3.2.2
- jest 29 + babel-jest + jsdom + @testing-library/react + user-event 14
- jest.setup.js polyfills matchMedia/ResizeObserver/DOMRect for jsdom
- src/lib/boardDnd.js: pure reducer (applyMoveCard, applyMoveList) +
  ID helpers (cardDndId/listDndId, parse*, is*) for prefixed DnD ids
- src/lib/boardDnd.test.js: 17 unit tests covering same/cross-list moves,
  clamping, error no-ops, list reorder, and ID round-trips
- src/components/__fixtures__/DnDFixtureBoard.jsx: test fixture with
  DndContext (PointerSensor 5px distance, KeyboardSensor, closestCorners),
  horizontal SortableContext for lists, vertical SortableContext per list,
  optimistic onDragOver + arrayMove on onDragEnd
- src/components/BoardDnD.test.jsx: 4 integration tests (render, cross-list
  move, same-list reorder, list reorder) using userEvent pointer pipeline
  with stubbed getBoundingClientRect for deterministic coords
- 22 tests pass
Production components layered on the pure reducer shipped in the prior
commit. Trello-style drag-and-drop wired end-to-end.

- src/components/Card.jsx: useSortable with cardDndId('card-'+id) prefix,
  opacity:0.4 while dragging, ⋮⋮ drag handle (a11y name: 'Drag card: …'),
  onClick (P4 modal hook), onRemove, placeholder mode for drop previews,
  role=button + aria-roledescription=sortable
- src/components/List.jsx: useSortable on the list BODY only (NOT on the
  header) so rename keeps working — listeners/refs on body, header stays
  static. Inline-edit header (click to rename, Enter to commit, Esc to
  cancel). + Add card button. Per-list SortableContext for cards is owned
  by Board; List wraps its own useSortable for column reorder.
- src/components/Board.jsx: SINGLE DndContext wraps the whole board with
  PointerSensor (activationConstraint.distance:5 — critical for touch)
  + KeyboardSensor (sortableKeyboardCoordinates) + closestCorners
  collision. Optimistic onDragOver moves cards cross-list IMMEDIATELY
  (Trello UX) with lastOverRef dedup so onDragOver doesn't re-render on
  every pointer move. onDragEnd dispatches applyMoveCard / applyMoveList.
  DragOverlay shows floating card during drag. Prefix IDs disambiguate
  card vs list ('card-…' vs 'list-…').
- src/defaultBoard.js: 3-list, 5-card demo state for first-run.
- src/App.jsx: wires Board into the app, persists v2 state to
  localStorage key 'ultra-todo-v2-state'.
- src/App.css: Trello palette (lists #ebecf0 on #0079bf board,
  white cards with subtle shadow, drag handle on hover,
  mobile breakpoint at 640px).

Tests (15 new, RED→GREEN):
- Card.test.jsx (4): renders title, forwards onClick, root has
  aria-roledescription=sortable, drag handle has accessible name
- List.test.jsx (5): renders list+cards, header NOT draggable (no
  aria-roledescription), body IS the sortable target, drag within list
  reorders via arrayMove, + Add card button fires onAddCard
- Board.test.jsx (6): renders all lists in order, cards in cardIds order,
  cross-list drag moves card, same-list drag reorders, list reorder swaps
  positions, optimistic onDragOver fires cross-list move before drop

37 tests pass (15 production + 22 prior fixture/reducer). Build clean.
kaykayyali closed this pull request 2026-06-24 06:27:54 +00:00
kaykayyali deleted branch wt/redesign-dnd 2026-06-24 06:27:54 +00:00

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kaykayyali/ultra-todo#5