Slides: text-edit + adjust drag entry on grouped elements#337
Conversation
Two view-layer paths assumed `slide.elements` is flat: - `enterEditMode` resolved the click target via `Array.prototype.find`, so double-click on text/shape elements nested inside a group silently no-op'd. Slide 22 of the Yorkie deck (text "웹/모바일..." in the right column) reproduced this — `Selection.doubleClick` drilled in correctly but the mount path bailed. - `startAdjustmentDrag` used the same flat `find`, so the yellow adjustment diamond on a grouped shape armed but never wrote. Slide 31's Dogfooding pentagonArrow reproduced this. The render-side mask for the editing element and the live preview for adjustments had the matching flat `slide.elements.map` bug — even if the entry paths were fixed in isolation, the canvas would still ghost-paint the element under the in-place editor / miss the live preview for grouped shapes. Both paths now resolve elements via the existing recursive helpers (`findElement`, `buildElementWorldLookup`) and pass world-frame elements where downstream code expects world coords (overlay text-box mount, world↔local adjustment conversion). The store-side mutation APIs (`requireElement`, `updateElementFrame`, `updateElementData`, `withTextElement`, `withShapeText`) already walk the tree via `findElementPath`, so the writes work transparently. Live-verified on slide 22 (text edit enters at the correct world position, no canvas ghost) and slide 31 (Dogfooding arrow's adjustments commit after drag). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Code review caught a silent miswrite hole in the prior commit: text-edit autofit-grow at commit time writes the editor-reported content height (world / canvas-logical coords) straight into `frame.h` via `store.updateElementFrame`, which expects LOCAL coords. For top-level elements local === world; for groups with rotation 0 + unit scale, w/h/rotation also match. But for a rotated or non-unit-scale group, the world height would be stored as local height, scrambling the element's frame. Detect transformed ancestor by comparing local vs world frame (w/h/rotation) at enter time and skip the autofit-grow write when they diverge. Properly composing the inverse ancestor transform is the right long-term fix, but until then this guard prevents the silent corruption. Scope is unchanged: slide 22's groups have rotation 0 + refSize === frame, so the autofit-grow path still runs there. Also annotate the todo's out-of-scope section with the matching `startConnectorEndpointDrag` flat-find pattern (editor.ts:3135) so future audits don't miss it. PPTX import can produce connectors inside groups; the editor-side group op forbids it, so the user-reachable failure mode is import-only — deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
📝 WalkthroughWalkthroughThis PR fixes a bug where text and shape elements nested inside groups couldn't be edited or dragged because element lookups were non-recursive and frame math used group-local coordinates. The fix adds recursive element traversal, uses world-frame composition for rendering and positioning, and gates stored frame changes when ancestor transforms are present. ChangesGrouped Element Edit Fixes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Verification: verify:selfResult: ✅ PASS in 258.2s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/tasks/README.md (1)
41-41:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate stale “Latest active task” value.
Line 41 still points to
slides font load repaint, but Line 21 addsslides grouped text edit entryas the newest listed active task. Please align this footer line with the current index content.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/tasks/README.md` at line 41, Update the stale footer string "Latest active task: slides font load repaint" so it matches the current newest task entry; replace that value with "slides grouped text edit entry" (the updated latest task referenced earlier in the file) so the footer and the index content are consistent.packages/slides/src/view/editor/editor.ts (2)
3404-3409:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep permanent guides visible during live adjustment preview.
This overlay repaint drops
permanentGuides, so guides disappear on the first adjustmentpointermoveand only come back after mouseup. The other live-paint paths preserve that chrome.Suggested patch
renderOverlay(this.options.overlay, [liveEl], { scale: this.scale(), slideWidth: SLIDE_WIDTH, slideHeight: SLIDE_HEIGHT, allElements: synthetic.elements, + permanentGuides: this.options.store.read().guides, + pendingGuide: this.pendingGuide, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/editor/editor.ts` around lines 3404 - 3409, renderOverlay call during live adjustment is dropping permanentGuides so they disappear on pointermove; update the renderOverlay invocation to preserve and pass the permanent guides into the overlay render. Specifically, when calling renderOverlay(this.options.overlay, [liveEl], {...}), include the permanentGuides (e.g. this.options.overlay.permanentGuides or synthetic.permanentGuides) in the options object or merge them into allElements so renderOverlay receives permanentGuides alongside scale/slideWidth/slideHeight/allElements; ensure renderOverlay receives the same permanentGuides the other live-paint paths do so guides remain visible during pointermove.
2221-2224:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftSync the drill-in scope before selecting a grouped edit target.
The new recursive lookup lets
enterTextEditing()open a nested element from root scope, butthis.selection.set([elementId])leavesSelection.getScope()unchanged. After commit/cancel, the overlay and drag paths in this file still convert frames throughtoWorldFrame(..., this.selection.getScope(), slide), so that grouped element comes back selected with root-scoped geometry and its handles/drags are misplaced.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/view/editor/editor.ts` around lines 2221 - 2224, When selecting a grouped/nested edit target in enterTextEditing(), also update the selection's drill-in scope before calling this.selection.set([elementId]) so Selection.getScope() matches the element’s actual nested scope; compute the element’s scope (e.g. via the same recursive lookup used to locate the element) and call the appropriate selection API to set that scope (for example setScope(...) or the overload that accepts a scope with set([...], scope)) and only then set this.selection and this.editingElementId so subsequent toWorldFrame(..., this.selection.getScope(), slide) conversions use the correct geometry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docs/tasks/README.md`:
- Line 41: Update the stale footer string "Latest active task: slides font load
repaint" so it matches the current newest task entry; replace that value with
"slides grouped text edit entry" (the updated latest task referenced earlier in
the file) so the footer and the index content are consistent.
In `@packages/slides/src/view/editor/editor.ts`:
- Around line 3404-3409: renderOverlay call during live adjustment is dropping
permanentGuides so they disappear on pointermove; update the renderOverlay
invocation to preserve and pass the permanent guides into the overlay render.
Specifically, when calling renderOverlay(this.options.overlay, [liveEl], {...}),
include the permanentGuides (e.g. this.options.overlay.permanentGuides or
synthetic.permanentGuides) in the options object or merge them into allElements
so renderOverlay receives permanentGuides alongside
scale/slideWidth/slideHeight/allElements; ensure renderOverlay receives the same
permanentGuides the other live-paint paths do so guides remain visible during
pointermove.
- Around line 2221-2224: When selecting a grouped/nested edit target in
enterTextEditing(), also update the selection's drill-in scope before calling
this.selection.set([elementId]) so Selection.getScope() matches the element’s
actual nested scope; compute the element’s scope (e.g. via the same recursive
lookup used to locate the element) and call the appropriate selection API to set
that scope (for example setScope(...) or the overload that accepts a scope with
set([...], scope)) and only then set this.selection and this.editingElementId so
subsequent toWorldFrame(..., this.selection.getScope(), slide) conversions use
the correct geometry.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 818cbf1b-9309-4478-a583-89aff86f5d43
📒 Files selected for processing (5)
docs/tasks/README.mddocs/tasks/active/20260603-slides-grouped-text-edit-entry-lessons.mddocs/tasks/active/20260603-slides-grouped-text-edit-entry-todo.mdpackages/slides/src/view/editor/editor.tspackages/slides/test/view/editor/grouped-text-edit-entry.test.ts
Summary
Two view-layer paths in the slides editor assumed
slide.elementsis flat, so any text/shape nested inside a group silently failed to enter the relevant interaction:Selection.doubleClick, butEditor.enterEditModeresolved the click target viaArray.prototype.findand bailed — and the canvas mask for the in-edit element used the same flat pattern, so even an isolated entry fix would have ghost-painted the original underneath.startAdjustmentDraghad the same flatfindAND used the group-local frame for the world↔local conversion.paintLiveAdjustmentshad the matching flat-map bug.Both paths now resolve elements via the existing recursive helpers (
findElement,buildElementWorldLookup) and pass world-frame elements where downstream code expects world coords. Store-side mutation APIs already walk the tree, so writes are transparent.Code-review follow-up commit guards the post-commit autofit-grow write against silent miswrite under rotated / non-unit-scale ancestor groups (height would be stored as local even though it's measured in world). The guard skips the fit when local and world
frame.{w,h,rotation}diverge — for slide 22's groups they don't, so behavior there is unchanged.Test plan
pnpm verify:fast(lint + unit) green.grouped-text-edit-entry.test.tscovers the world-frame mount contract.data.adjustmentsreflects the new value (verified live; the test mutation was undone viastore.undo()).Out of scope (deliberate)
startConnectorEndpointDraghas the same flat-find pattern. PPTX import can produce nested connectors but the editor-side group op forbids them; user-reachable failure mode is import-only and tracked in the todo's out-of-scope section.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests
Documentation