Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
629270d
docs: add approved inline text editing design spec
DavidBabinec Jun 10, 2026
f975e9a
feat(module-engine): add inlineTextEdit contract, declare on text/but…
DavidBabinec Jun 10, 2026
a0a7f24
feat(editor-store): add inline text edit session slice with coalesced…
DavidBabinec Jun 10, 2026
6bfcbf6
feat(editor-store): force-close inline edit sessions on deletion and …
DavidBabinec Jun 11, 2026
20edb1d
feat(canvas): add inline-edit typography mirroring helpers
DavidBabinec Jun 11, 2026
6bad4b7
feat(canvas): add InlineTextEditOverlay, mounted per breakpoint frame
DavidBabinec Jun 11, 2026
ee59e9f
feat(canvas): wire double-click inline editing and hide doubled text
DavidBabinec Jun 11, 2026
41b2575
docs: document canvas inline text editing (parent-doc overlay)
DavidBabinec Jun 11, 2026
a617084
docs: fix remaining stale slice counts in editor.md (11 -> 12)
DavidBabinec Jun 11, 2026
83bf384
docs: fix stale slice count in adminUi header comment (11 -> 12)
DavidBabinec Jun 11, 2026
7447335
Merge origin/main into feat/inline-text-editing
DavidBabinec Jun 11, 2026
6cfeb1a
fix(canvas): render canvas + inline-edit field in the site's fonts, n…
DavidBabinec Jun 11, 2026
61b3457
fix(canvas): stop inline-edit field scroll-shifting multi-line text
DavidBabinec Jun 11, 2026
b7b265a
fix(canvas): remove inline-edit open flash by positioning in a layout…
DavidBabinec Jun 11, 2026
a23853d
feat(canvas): rewrite inline text editing to edit the real element in…
DavidBabinec Jun 11, 2026
84b59ea
fix(canvas): stop React clobbering the inline-edit caret every keystroke
DavidBabinec Jun 11, 2026
4fe6f20
fix(canvas): let the spacebar type a space during inline editing
DavidBabinec Jun 11, 2026
3486ff0
fix(canvas): finalize inline editing — keyboard-forward guard, select…
DavidBabinec Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(canvas): stop inline-edit field scroll-shifting multi-line text
A <textarea> scrolls its own content to keep the caret visible whenever the
text is taller than the box — which happens for any node with line-height < 1
(negative leading makes glyphs overflow the line boxes, exactly how the block
node renders them with overflow:visible). Switching lines / wrapping made the
whole heading jump.

Grow the field to its full content height each RAF tick (measured after
typography is mirrored) so there is nothing to scroll, and pin any residual
offset. The selection-affordance ring moves to its own element bound to the
node's box (matching the normal selection ring) so it no longer grows with
the field — the field itself is now invisible chrome.

Verified in a real browser: caret on line 2 keeps scrollTop 0 (overflowsBy 0),
ring matches the node box exactly, text stays put across line switches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  • Loading branch information
DavidBabinec and claude committed Jun 11, 2026
commit 61b34574246b7307a74ce2564d2c6802f9750e9e
13 changes: 13 additions & 0 deletions src/admin/pages/site/canvas/InlineTextEditOverlay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,18 @@
overflow: hidden;
resize: none;
pointer-events: auto;
}

/* The editing affordance ring. A separate element so it can bound the node's
own box (matching the normal selection ring) while the field itself grows
to its full content height to stop the textarea scroll-shifting multi-line
text (negative line-heights make text taller than the box). */
.ring {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
border-radius: 1px;
pointer-events: none;
box-shadow: var(--canvas-selection-ring);
}
23 changes: 23 additions & 0 deletions src/admin/pages/site/canvas/InlineTextEditOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function InlineTextEditOverlay({ breakpointId, iframeElement }: InlineTex
const endInlineEdit = useEditorStore((s) => s.endInlineEdit)
const cancelInlineEdit = useEditorStore((s) => s.cancelInlineEdit)
const fieldRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null)
const ringRef = useRef<HTMLDivElement | null>(null)
const viewportActions = use(CanvasViewportActionsContext)

// Stable per-session identity. The session OBJECT is replaced when
Expand Down Expand Up @@ -81,6 +82,27 @@ export function InlineTextEditOverlay({ breakpointId, iframeElement }: InlineTex
field.style.width = `${rect.width}px`
field.style.height = `${rect.height}px`
mirrorInlineEditTypography(field, target, iframeElement)
// The affordance ring tracks the NODE's own box (like the normal selection
// ring), independent of the field growing below for the no-scroll fix.
const ring = ringRef.current
if (ring) {
ring.style.transform = `translate(${rect.x}px, ${rect.y}px)`
ring.style.width = `${rect.width}px`
ring.style.height = `${rect.height}px`
}
// A <textarea> scrolls its own content to keep the caret visible whenever
// the text is taller than the box — which happens for any node with
// line-height < 1 (negative leading makes glyphs overflow the line boxes,
// exactly how the block node renders them with overflow:visible). Grow the
// field to its full content height (measured AFTER typography is mirrored)
// so there is nothing to scroll, then pin any residual offset. Without this
// the text shifts as the caret moves between lines. `scrollHeight` is the
// greater of content and box height, so single-line / line-height ≥ 1 nodes
// leave the height at the node's rect.
const contentHeight = field.scrollHeight
if (contentHeight > rect.height) field.style.height = `${contentHeight}px`
if (field.scrollTop !== 0) field.scrollTop = 0
if (field.scrollLeft !== 0) field.scrollLeft = 0
})

// Position + typography RAF loop, armed only while this frame owns the
Expand Down Expand Up @@ -175,6 +197,7 @@ export function InlineTextEditOverlay({ breakpointId, iframeElement }: InlineTex

return createPortal(
<div className={styles.layer} data-canvas-inline-edit-mode={positionMode}>
<div className={styles.ring} ref={ringRef} aria-hidden="true" />
{session.multiline ? (
<textarea
key={sessionKey}
Expand Down