feat(redesign): [P5] @dnd-kit drag-and-drop for cards and lists #5
Reference in New Issue
Block a user
Delete Branch "wt/redesign-dnd"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 5 of the ultra-todo Trello-style redesign
Layers
@dnd-kit/core+@dnd-kit/sortable+@dnd-kit/utilitieson top of the static P3+P4 components per the task spec.What landed (commit
7ec1951, on top of61e3169)Production components:
src/components/Card.jsx—useSortablewithcardDndId("card-"+id)prefixed id, opacity 0.4 while dragging,transform+transitionfromCSS.Transform.toString, drag-handle button witharia-label="Drag card: <title>",role=button+aria-roledescription=sortable, optionalonClick(P4 modal hook), optionalonRemove, placeholder mode for drop previews.src/components/List.jsx—useSortableon 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-listSortableContextfor cards is owned by Board; List only owns its ownuseSortablefor column reorder.src/components/Board.jsx— SINGLEDndContextwraps the whole board withPointerSensor(activationConstraint.distance: 5— critical for touch),KeyboardSensor(sortableKeyboardCoordinates), andclosestCornerscollision detection. OptimisticonDragOvermoves cards cross-list IMMEDIATELY (Trello UX) withlastOverRefdedup so onDragOver does not re-render on every pointer move.onDragEnddispatchesapplyMoveCard/applyMoveList.DragOverlayshows the floating card during drag. Prefix IDs disambiguate card vs list (card-vslist-).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 keyultra-todo-v2-state.src/App.css— Trello palette (lists#ebecf0on#0079bfboard 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)
src/components/Card.test.jsxsrc/components/List.test.jsxsrc/components/Board.test.jsxsrc/lib/boardDnd.test.js61e3169) reducer coveragesrc/components/BoardDnD.test.jsx61e3169) fixture integrationsrc/setup.test.js37/37 tests pass.
npm run buildsucceeds (40 modules, 253 kB JS / 2.7 kB CSS, 80 kB gzipped).Pitfalls addressed
PointerSensoractivationConstraint: { distance: 5 }— mobile tap does not fire dragDndContextwraps whole board (not per-list) — cross-list drag worksonDragOverdedup vialastOverRefkey (activeCardId->overListId) — no jankuseSortablereturns spread on the SAME element that receivessetNodeRefuseSortablelisteners — rename still worksarrayMoveused for both same-list reorder and list reorder — no hand-rolled math.boardhasoverflow-x: auto(P5 ships mobile breakpoint, list width 84vw on phones)card-/list-) for type disambiguation in handlersBrowser smoke
Manually booted
npx viteon a local port, verified:Manual mobile smoke procedure (for reviewer)
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 stripmax-width: 500pxfrom.app. P5 appends the Trello palette to the same file. Merge will need conflict resolution; Trello styles should win.src/components/Card.jsxandsrc/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.jsondeps — 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 cleannpm installafterwards.Acceptance
npm test— 37/37 greennpm run build— greenwt/redesign-dndpushed to originProduction 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.Pull request closed