Skip to content

Clue 401 drag tile to workspace toolbar button to delete#2785

Open
lbondaryk wants to merge 25 commits intomasterfrom
CLUE-401-drag-tile-to-workspace-toolbar-button-to-delete
Open

Clue 401 drag tile to workspace toolbar button to delete#2785
lbondaryk wants to merge 25 commits intomasterfrom
CLUE-401-drag-tile-to-workspace-toolbar-button-to-delete

Conversation

@lbondaryk
Copy link
Contributor

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.

lbondaryk and others added 23 commits March 4, 2026 10:58
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>
@lbondaryk lbondaryk requested a review from Copilot March 4, 2026 18:52
@lbondaryk lbondaryk self-assigned this Mar 4, 2026
@codecov
Copy link

codecov bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 34.11371% with 197 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.59%. Comparing base (ea92ae1) to head (6f2ce75).

Files with missing lines Patch % Lines
src/components/document/document-content.tsx 11.18% 115 Missing and 12 partials ⚠️
src/components/picked-up-tile-ghost.tsx 28.57% 25 Missing and 5 partials ⚠️
src/components/tiles/tile-component.tsx 25.00% 19 Missing and 2 partials ⚠️
src/components/delete-button.tsx 75.51% 10 Missing and 2 partials ⚠️
src/components/toolbar.tsx 20.00% 4 Missing ⚠️
src/models/stores/ui.ts 84.61% 2 Missing ⚠️
src/components/document/tile-row.tsx 94.44% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (ea92ae1) and HEAD (6f2ce75). Click for more details.

HEAD has 19 uploads less than BASE
Flag BASE (ea92ae1) HEAD (6f2ce75)
cypress-regression 13 0
cypress 6 0
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     
Flag Coverage Δ
cypress ?
cypress-regression ?
cypress-smoke 42.93% <22.80%> (-0.41%) ⬇️
jest 50.36% <32.10%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

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 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.

Comment on lines +588 to +603
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;
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +231 to +233
? { role: "option" as const,
"aria-label": `${location} ${rowLabel}`,
"aria-selected": isHighlighted }
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
? { role: "option" as const,
"aria-label": `${location} ${rowLabel}`,
"aria-selected": isHighlighted }
? {
"aria-label": `${location} ${rowLabel}`
}

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 37
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);
};
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
@@ -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.
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
> **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.

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +229
const { ui } = stores;
ui.removeTileIdFromSelection(tileId);
document?.deleteTile(tileId);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);

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

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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>
@cypress
Copy link

cypress bot commented Mar 4, 2026

collaborative-learning    Run #17987

Run Properties:  status check passed Passed #17987  •  git commit 6f2ce75c1e: feat: Phase 3 polish - tile shrink animation, empty doc support, PR review fixes
Project collaborative-learning
Branch Review CLUE-401-drag-tile-to-workspace-toolbar-button-to-delete
Run status status check passed Passed #17987
Run duration 03m 14s
Commit git commit 6f2ce75c1e: feat: Phase 3 polish - tile shrink animation, empty doc support, PR review fixes
Committer lbondaryk
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 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>
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