Clue 401 drag tile to workspace toolbar button to delete#2785
Clue 401 drag tile to workspace toolbar button to delete#2785
Conversation
Two-phase design for click-to-pick tile movement as an alternative to click-and-drag. Phase 1: mouse-based click-pick-click workflow. Phase 2: full keyboard navigation with visible drop zones and ARIA. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11-task plan covering Phase 1 (mouse click-to-pick) and Phase 2 (keyboard navigation with visible drop zones and ARIA). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now drag a tile onto the Delete button in the vertical toolbar to delete it. The button highlights on drag-over and shows a confirmation modal on drop. Only the dragged tile is deleted (not all selected tiles). Handles container tiles (e.g. question tiles) by falling back to kDragTiles data when kDragTileId is not set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When clicking the drag handle without dragging, the tile is "picked up" into a click-to-pick state tracked in the UI store. Clicking the handle again cancels the pick-up. Starting a real HTML5 drag also cancels any active pick-up state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ghost image follows the cursor when a tile is picked up via click-to-pick. Body gets `cursor: grabbing` class. Pressing Escape cancels pick-up. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clicking anywhere other than a drag handle or delete button while a tile is picked up cancels the pick-up. Later, the document-content placement handler will intercept clicks on drop zones before this cancel fires. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a tile is picked up, moving the mouse over the document content shows drop zone highlights (same as native drag). Clicking a drop zone places the tile. Clicking inside the document without a valid drop zone cancels pick-up. The pick-up click stops propagation to prevent the document-content handler from immediately treating it as a placement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When placing a picked-up tile, find the source document using documents.findDocumentOfTile. If the source and target documents differ (e.g. picking from the resources pane), copy the tile using handleDragCopyTiles instead of moving it with userMoveTiles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a tile is picked up via click-to-pick, clicking the Delete button stores the picked-up tile ID, clears the pick-up state (removing the ghost), then shows the single-tile delete confirmation dialog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updated to document what was actually built vs what was planned: - Cross-document copy support (resources → workspace) - stopPropagation fix for pick-up click bubbling - Ghost component architecture (portal, capture-phase listeners) - getDropRowInfoFromPoint refactor (not extracted to utility) - Files modified list with test files - Phase 1 verification checklist marked complete Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DragTileButton now has tabIndex={0}, role="button", and onKeyDown
handler for Enter/Space. aria-label changes from "Move tile" to
"Cancel move" when the tile is picked up.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
During click-to-pick, all valid drop zones are visible at 10% opacity so users can see where tiles can be placed. The actively hovered zone remains at full 25% opacity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Arrow keys navigate between drop zones, Enter places the tile, Tab focuses the Delete button. ARIA attributes on drop zones and aria-live announcement provide screen reader support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ick-up When a tile is picked up via keyboard (Tab + Enter), the ghost image now appears near the drag handle instead of at (0,0). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All four arrow keys now navigate the flat list of drop zones so every target is reachable. Down/Right advance, Up/Left go back. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getDropZoneList() was using the global allRows index as rowInsertIndex, but the drop system expects the local index within the containing RowList. Now uses getDropInfoForGlobalRowIndex() to match the mouse- based code, fixing left/right side-by-side placement via keyboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Active zones: increased opacity from 0.25 to 0.4 and added 2px solid border using the focus-ring-color for clear non-color contrast. Dimmed zones: increased opacity from 0.10 to 0.15 and added 1px dashed border so all possible drop positions are visible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Renders the tile type's registered Icon SVG centered on the ghost. Positions the ghost below-right of the cursor so the grabbing hand does not obscure the icon. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Position the ghost so its top-right corner aligns with the cursor, extending leftward over the tile and the grab handle area. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2785 +/- ##
===========================================
- Coverage 86.16% 66.59% -19.58%
===========================================
Files 849 845 -4
Lines 46449 46704 +255
Branches 12072 12152 +80
===========================================
- Hits 40024 31102 -8922
- Misses 6018 14477 +8459
- Partials 407 1125 +718
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds an alternative, accessible “click-to-pick” tile move flow (with ghost + keyboard navigation) and enables drag-to-delete by dropping tiles onto the toolbar Delete button (CLUE-401).
Changes:
- Extend the UI store with “picked up tile” state and focused drop-zone state to support click-to-pick + keyboard placement.
- Add ghost overlay + drop-zone highlighting/ARIA wiring for click-to-pick placement and keyboard navigation.
- Add drag/drop (and picked-up click) behavior to the Delete toolbar button for single-tile deletion with confirmation.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/models/stores/ui.ts | Adds picked-up tile + focused drop-zone state and actions/views. |
| src/models/stores/ui.test.ts | Adds unit tests for picked-up tile state/actions. |
| src/components/toolbar.tsx | Wires a single-tile delete handler into the Delete button. |
| src/components/tiles/tile-component.tsx | Makes drag handle clickable/focusable to toggle pick-up; cancels pick-up on real drag. |
| src/components/picked-up-tile-ghost.tsx | New global ghost overlay with cursor tracking + Escape/click-outside cancel + aria-live. |
| src/components/document/tile-row.tsx | Shows/dims all drop zones during pick-up and adds ARIA props. |
| src/components/document/tile-row.scss | Updates drop-zone visuals for active vs dimmed states. |
| src/components/document/document-content.tsx | Adds pick-up mousemove highlighting, keyboard navigation, and click-to-place logic. |
| src/components/delete-button.tsx | Adds drag/drop-to-delete + picked-up click-to-delete confirmation flow. |
| src/components/delete-button.test.tsx | Updates tests to provide stores context and new prop. |
| src/components/app.tsx | Renders the global PickedUpTileGhost. |
| src/components/app.scss | Adds global “grabbing” cursor styling while a tile is picked up. |
| docs/plans/2026-03-04-drag-to-delete-design.md | Documents drag-to-delete design decisions. |
| docs/plans/2026-03-04-click-to-pick-tiles-implementation.md | Detailed implementation plan for click-to-pick (phased). |
| docs/plans/2026-03-04-click-to-pick-tiles-design.md | Documents click-to-pick design, a11y goals, and verification checklist. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| case "Enter": { | ||
| e.preventDefault(); | ||
| if (currentIndex !== undefined && currentIndex < zones.length) { | ||
| this.handlePickUpPlace(zones[currentIndex].dropRowInfo); | ||
| } | ||
| break; | ||
| } | ||
| case "Tab": { | ||
| // Move focus to the delete button | ||
| e.preventDefault(); | ||
| const deleteButton = document.querySelector<HTMLElement>(".delete-button"); | ||
| if (deleteButton) { | ||
| deleteButton.focus(); | ||
| } | ||
| break; | ||
| } |
There was a problem hiding this comment.
handlePickUpKeyDown calls preventDefault() for Enter unconditionally while a tile is picked up. After the Tab case moves focus to the delete button, pressing Enter is likely to be intercepted here (preventing the button’s default keyboard activation), and could also interfere with Enter in other focused controls. Consider ignoring key events when focus is inside .delete-button (or other form controls), or removing the global keydown listener when focus leaves the document content.
src/components/document/tile-row.tsx
Outdated
| ? { role: "option" as const, | ||
| "aria-label": `${location} ${rowLabel}`, | ||
| "aria-selected": isHighlighted } |
There was a problem hiding this comment.
The drop zone elements set role="option"/aria-selected, but they aren’t in a role="listbox" container and are not focusable (and have pointer-events: none). This is invalid/ineffective ARIA semantics and may confuse assistive tech. Consider using a simpler role (or none) with aria-label, or implement a proper listbox pattern (listbox + active descendant/focus management).
| ? { role: "option" as const, | |
| "aria-label": `${location} ${rowLabel}`, | |
| "aria-selected": isHighlighted } | |
| ? { | |
| "aria-label": `${location} ${rowLabel}` | |
| } |
| const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { | ||
| // If a tile is picked up, treat click as delete-via-pick-up | ||
| if (ui.pickedUpTileId) { | ||
| dragTileIdRef.current = ui.pickedUpTileId; | ||
| ui.clearPickedUpTile(); | ||
| showDragDeleteAlert(); | ||
| return; | ||
| } | ||
| !isDisabled && onClick(e, toolButton); | ||
| }; |
There was a problem hiding this comment.
New behaviors (delete a picked-up tile via click, and drag/drop delete with DataTransfer parsing) were added here, but the existing tests only cover the basic confirmation flow. Consider adding unit tests that (1) set ui.pickedUpTileId and verify clicking the button triggers the single-tile confirmation and calls onDeleteTile on confirm, and (2) simulate a drop with kDragTileId/kDragTiles to verify the drag-delete path.
| @@ -0,0 +1,827 @@ | |||
| # Click-to-Pick Tiles Implementation Plan | |||
|
|
|||
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | |||
There was a problem hiding this comment.
This implementation plan includes an instruction directed at a specific AI tool ("For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans"). If this doc is intended for humans, this line is confusing/out of place and will age poorly in the repo. Consider removing it or rewriting it as tool-agnostic guidance.
| > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. | |
| > **Note:** This implementation plan is meant to be followed and executed task-by-task. |
src/components/toolbar.tsx
Outdated
| const { ui } = stores; | ||
| ui.removeTileIdFromSelection(tileId); | ||
| document?.deleteTile(tileId); |
There was a problem hiding this comment.
handleDeleteTile() deletes from the toolbar’s current document, but pickedUpTileId/drag-drop can originate from a different document (e.g. resources pane). In that case the confirmation will succeed but document?.deleteTile(tileId) will be a no-op. Consider deleting via stores.documents.findDocumentOfTile(tileId) (or ui.pickedUpDocId) so the correct source document is targeted.
| const { ui } = stores; | |
| ui.removeTileIdFromSelection(tileId); | |
| document?.deleteTile(tileId); | |
| const { ui, documents } = stores; | |
| ui.removeTileIdFromSelection(tileId); | |
| const sourceDocument: DocumentModelType | undefined = | |
| documents && typeof (documents as any).findDocumentOfTile === "function" | |
| ? (documents as any).findDocumentOfTile(tileId) | |
| : document; | |
| sourceDocument?.deleteTile(tileId); |
| this.pickUpReactionDisposer = reaction( | ||
| () => this.stores.ui.pickedUpTileId, | ||
| (pickedUpTileId) => { | ||
| if (pickedUpTileId && !this.props.readOnly) { | ||
| this.domElement?.addEventListener("mousemove", this.handlePickUpMouseMove); | ||
| this.domElement?.addEventListener("mouseleave", this.handlePickUpMouseLeave); | ||
| document.addEventListener("keydown", this.handlePickUpKeyDown); | ||
| } else { | ||
| this.domElement?.removeEventListener("mousemove", this.handlePickUpMouseMove); | ||
| this.domElement?.removeEventListener("mouseleave", this.handlePickUpMouseLeave); | ||
| document.removeEventListener("keydown", this.handlePickUpKeyDown); | ||
| this.clearDropRowInfo(); |
There was a problem hiding this comment.
The reaction() adds a global document.addEventListener("keydown", ...) per DocumentContentComponent instance. In multi-pane views (e.g. 2-up/4-up, problem+workspace) this will register multiple handlers and each keypress will be processed multiple times. Consider centralizing pick-up keyboard handling in a single global component (e.g. PickedUpTileGhost) or gating registration so only one active/target document registers the keydown listener.
Curriculum/resources tiles live in problem.sections, not the documents store, so findDocumentOfTile() returned null for them. Store tile type in UI state at pick-up time for the ghost icon, and add findContentOfTile() that searches problem/teacherGuide sections as a fallback for placement. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
CLUE-401-drag-tile-to-workspace-toolbar-button-to-delete
|
| Run status |
|
| Run duration | 03m 14s |
| Commit |
|
| Committer | lbondaryk |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
…eview fixes - Tiles shrink to scale(0.95) during drag/pick-up so drop zones show around them - Empty documents show drop target for click-to-place and keyboard placement - Left/right drop zones inset 8px to prevent corner overlap - Fix cross-document delete for resources pane tiles - Guard keydown handler to not intercept interactive elements (delete button) - Fix ARIA: remove invalid role="option"/aria-selected, keep aria-label - Add delete-button tests for pick-up-delete and drag-drop-delete - Update design doc with Phase 3 details Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This expanded a bit into an experiment in an accessible and touchpad friendly click-to-pick design for CLUE tiles that can be dropped on drop zones and of course on the trashcan in the toolbar.