Skip to content

Commit aa84c75

Browse files
aadamgoughAdam Gough
andauthored
fix(cursor-and-input): fixes cursor and input canvas error (#1168)
* fixed long input * lint * fix gray canvas * fixed auto-pan * remove duplicate useEffect * fix auto-pan for wide mode * removed any --------- Co-authored-by: Adam Gough <[email protected]>
1 parent ebb8cf8 commit aa84c75

File tree

3 files changed

+309
-49
lines changed

3 files changed

+309
-49
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export function LongInput({
8888
const [cursorPosition, setCursorPosition] = useState(0)
8989
const textareaRef = useRef<HTMLTextAreaElement>(null)
9090
const overlayRef = useRef<HTMLDivElement>(null)
91+
const overlayInnerRef = useRef<HTMLDivElement>(null)
9192
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
9293
const containerRef = useRef<HTMLDivElement>(null)
9394

@@ -140,6 +141,35 @@ export function LongInput({
140141
}
141142
}, [rows])
142143

144+
// Set overlay width to match textarea clientWidth
145+
useLayoutEffect(() => {
146+
if (!textareaRef.current || !overlayRef.current) return
147+
const textarea = textareaRef.current
148+
const overlay = overlayRef.current
149+
150+
const applyWidth = () => {
151+
// Match overlay content width to the inner content area of the textarea
152+
overlay.style.width = `${textarea.clientWidth}px`
153+
}
154+
155+
applyWidth()
156+
157+
const resizeObserver = new ResizeObserver(() => {
158+
applyWidth()
159+
})
160+
resizeObserver.observe(textarea)
161+
162+
return () => {
163+
resizeObserver.disconnect()
164+
}
165+
}, [])
166+
167+
// Initialize overlay transform to current scroll
168+
useLayoutEffect(() => {
169+
// Initialize overlay transform to current scroll
170+
syncScrollPositions()
171+
}, [])
172+
143173
// Handle input changes
144174
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
145175
// Don't allow changes if disabled or streaming
@@ -172,19 +202,21 @@ export function LongInput({
172202

173203
// Sync scroll position between textarea and overlay
174204
const handleScroll = (e: React.UIEvent<HTMLTextAreaElement>) => {
175-
if (overlayRef.current) {
176-
overlayRef.current.scrollTop = e.currentTarget.scrollTop
177-
overlayRef.current.scrollLeft = e.currentTarget.scrollLeft
178-
}
205+
if (!overlayInnerRef.current) return
206+
const { scrollTop, scrollLeft } = e.currentTarget
207+
overlayInnerRef.current.style.transform = `translate(${-scrollLeft}px, ${-scrollTop}px)`
208+
}
209+
210+
// Force synchronize scroll positions
211+
const syncScrollPositions = () => {
212+
if (!textareaRef.current || !overlayInnerRef.current) return
213+
const { scrollTop, scrollLeft } = textareaRef.current
214+
overlayInnerRef.current.style.transform = `translate(${-scrollLeft}px, ${-scrollTop}px)`
179215
}
180216

181217
// Ensure overlay updates when content changes
182218
useEffect(() => {
183-
if (textareaRef.current && overlayRef.current) {
184-
// Ensure scrolling is synchronized
185-
overlayRef.current.scrollTop = textareaRef.current.scrollTop
186-
overlayRef.current.scrollLeft = textareaRef.current.scrollLeft
187-
}
219+
syncScrollPositions()
188220
}, [value])
189221

190222
// Handle resize functionality
@@ -208,6 +240,8 @@ export function LongInput({
208240
if (containerRef.current) {
209241
containerRef.current.style.height = `${newHeight}px`
210242
}
243+
// Keep overlay aligned with textarea scroll during live resize
244+
syncScrollPositions()
211245
}
212246
}
213247

@@ -220,6 +254,8 @@ export function LongInput({
220254
isResizing.current = false
221255
document.removeEventListener('mousemove', handleMouseMove)
222256
document.removeEventListener('mouseup', handleMouseUp)
257+
// After resizing completes, re-sync to ensure caret at end remains visually aligned
258+
syncScrollPositions()
223259
}
224260

225261
document.addEventListener('mousemove', handleMouseMove)
@@ -335,9 +371,7 @@ export function LongInput({
335371
}
336372

337373
// For regular scrolling (without Ctrl/Cmd), let the default behavior happen
338-
if (overlayRef.current) {
339-
overlayRef.current.scrollTop = e.currentTarget.scrollTop
340-
}
374+
// No overlay scroll; overlay position is synced via transform on scroll handler
341375
}
342376

343377
return (
@@ -365,6 +399,7 @@ export function LongInput({
365399
ref={textareaRef}
366400
className={cn(
367401
'allow-scroll min-h-full w-full resize-none text-transparent caret-foreground placeholder:text-muted-foreground/50',
402+
'!text-[14px]', // Force override any responsive text sizes from Textarea component
368403
isConnecting &&
369404
config?.connectionDroppable !== false &&
370405
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500',
@@ -391,25 +426,69 @@ export function LongInput({
391426
}}
392427
disabled={isPreview || disabled}
393428
style={{
394-
fontFamily: 'inherit',
395-
lineHeight: 'inherit',
429+
// Explicit font properties for perfect alignment
430+
fontFamily:
431+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
432+
fontSize: '14px',
433+
fontWeight: '400',
434+
// Match the fixed pixel line-height used on the textarea
435+
lineHeight: '21px',
436+
letterSpacing: 'normal',
396437
height: `${height}px`,
438+
// Text wrapping properties
397439
wordBreak: 'break-word',
398440
whiteSpace: 'pre-wrap',
441+
overflowWrap: 'break-word',
442+
// Box sizing to ensure padding is calculated correctly
443+
boxSizing: 'border-box',
444+
// Remove text rendering optimizations that can affect layout
445+
textRendering: 'auto',
399446
}}
400447
/>
401448
<div
402449
ref={overlayRef}
403-
className='pointer-events-none absolute inset-0 whitespace-pre-wrap break-words bg-transparent px-3 py-2 text-sm'
450+
className='pointer-events-none absolute bg-transparent'
404451
style={{
405-
fontFamily: 'inherit',
406-
lineHeight: 'inherit',
407-
width: '100%',
408-
height: `${height}px`,
452+
// Position exactly over the textarea content area
453+
top: '0',
454+
left: '0',
455+
// width is set dynamically to match textarea clientWidth to ensure identical wrapping
456+
// right is disabled to avoid conflicts with explicit width
457+
right: 'auto',
458+
// Padding: py-2 px-3 = top/bottom: 8px, left/right: 12px
459+
paddingTop: '8px',
460+
paddingBottom: '8px',
461+
paddingLeft: '12px',
462+
paddingRight: '12px',
463+
// No border; border would shrink content width under border-box and break wrapping parity
464+
// Exact same font properties as textarea
465+
fontFamily:
466+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
467+
fontSize: '14px',
468+
fontWeight: '400',
469+
lineHeight: '21px', // Use fixed pixel line-height to prevent subpixel rounding drift with overlay
470+
letterSpacing: 'normal',
471+
// Text wrapping properties - must match textarea exactly
472+
wordBreak: 'break-word',
473+
whiteSpace: 'pre-wrap',
474+
overflowWrap: 'break-word',
475+
// Hide overlay scrolling to avoid dual scroll offsets
409476
overflow: 'hidden',
477+
// Box sizing to ensure padding is calculated correctly
478+
boxSizing: 'border-box',
479+
// Match text rendering
480+
textRendering: 'auto',
410481
}}
411482
>
412-
{formatDisplayText(value?.toString() ?? '', true)}
483+
<div
484+
ref={overlayInnerRef}
485+
style={{
486+
willChange: 'transform',
487+
lineHeight: '21px',
488+
}}
489+
>
490+
{formatDisplayText(value?.toString() ?? '', true)}
491+
</div>
413492
</div>
414493

415494
{/* Wand Button */}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,12 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
383383
if (height !== blockHeight) {
384384
updateBlockHeight(id, height)
385385
updateNodeInternals(id)
386+
try {
387+
const evt = new CustomEvent('workflow-node-resized', {
388+
detail: { id, height },
389+
})
390+
window.dispatchEvent(evt)
391+
} catch {}
386392
}
387393
}, 100)
388394

@@ -925,6 +931,12 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
925931
variant='ghost'
926932
size='sm'
927933
onClick={() => {
934+
try {
935+
const evt = new CustomEvent('workflow-layout-change', {
936+
detail: { reason: 'wide-toggle', blockId: id },
937+
})
938+
window.dispatchEvent(evt)
939+
} catch {}
928940
if (currentWorkflow.isDiffMode) {
929941
setDiffIsWide((prev) => !prev)
930942
} else if (userPermissions.canEdit) {

0 commit comments

Comments
 (0)