Skip to content

Commit 3275e9e

Browse files
committed
fix(messages-input): fix cursor alignment and auto-resize with overlay
1 parent 5157f0b commit 3275e9e

File tree

1 file changed

+123
-73
lines changed
  • apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input

1 file changed

+123
-73
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx

Lines changed: 123 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
1+
import {
2+
useCallback,
3+
useEffect,
4+
useImperativeHandle,
5+
useLayoutEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from 'react'
210
import { isEqual } from 'lodash'
311
import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react'
412
import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
@@ -382,93 +390,138 @@ export function MessagesInput({
382390
textareaRefs.current[fieldId]?.focus()
383391
}, [])
384392

385-
const autoResizeTextarea = useCallback((fieldId: string) => {
393+
const syncOverlay = useCallback((fieldId: string) => {
386394
const textarea = textareaRefs.current[fieldId]
387-
if (!textarea) return
388395
const overlay = overlayRefs.current[fieldId]
396+
if (!textarea || !overlay) return
389397

390-
// If user has manually resized, respect their chosen height and only sync overlay.
391-
if (userResizedRef.current[fieldId]) {
392-
const currentHeight =
393-
textarea.offsetHeight || Number.parseFloat(textarea.style.height) || MIN_TEXTAREA_HEIGHT_PX
394-
const clampedHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, currentHeight)
395-
textarea.style.height = `${clampedHeight}px`
398+
overlay.style.width = `${textarea.clientWidth}px`
399+
overlay.scrollTop = textarea.scrollTop
400+
overlay.scrollLeft = textarea.scrollLeft
401+
}, [])
402+
403+
const autoResizeTextarea = useCallback(
404+
(fieldId: string) => {
405+
const textarea = textareaRefs.current[fieldId]
406+
const overlay = overlayRefs.current[fieldId]
407+
if (!textarea) return
408+
409+
if (!textarea.value.trim()) {
410+
userResizedRef.current[fieldId] = false
411+
}
412+
413+
if (userResizedRef.current[fieldId]) {
414+
if (overlay) {
415+
overlay.style.height = `${textarea.offsetHeight}px`
416+
}
417+
syncOverlay(fieldId)
418+
return
419+
}
420+
421+
textarea.style.height = 'auto'
422+
const scrollHeight = textarea.scrollHeight
423+
const height = Math.min(
424+
MAX_TEXTAREA_HEIGHT_PX,
425+
Math.max(MIN_TEXTAREA_HEIGHT_PX, scrollHeight)
426+
)
427+
428+
textarea.style.height = `${height}px`
396429
if (overlay) {
397-
overlay.style.height = `${clampedHeight}px`
430+
overlay.style.height = `${height}px`
398431
}
399-
return
400-
}
401432

402-
textarea.style.height = 'auto'
403-
const naturalHeight = textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX
404-
const nextHeight = Math.min(
405-
MAX_TEXTAREA_HEIGHT_PX,
406-
Math.max(MIN_TEXTAREA_HEIGHT_PX, naturalHeight)
407-
)
408-
textarea.style.height = `${nextHeight}px`
433+
syncOverlay(fieldId)
434+
},
435+
[syncOverlay]
436+
)
409437

410-
if (overlay) {
411-
overlay.style.height = `${nextHeight}px`
412-
}
413-
}, [])
438+
const handleResizeStart = useCallback(
439+
(fieldId: string, e: React.MouseEvent<HTMLDivElement>) => {
440+
e.preventDefault()
441+
e.stopPropagation()
414442

415-
const handleResizeStart = useCallback((fieldId: string, e: React.MouseEvent<HTMLDivElement>) => {
416-
e.preventDefault()
417-
e.stopPropagation()
443+
const textarea = textareaRefs.current[fieldId]
444+
if (!textarea) return
418445

419-
const textarea = textareaRefs.current[fieldId]
420-
if (!textarea) return
446+
const startHeight = textarea.offsetHeight || textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX
421447

422-
const startHeight = textarea.offsetHeight || textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX
448+
isResizingRef.current = true
449+
resizeStateRef.current = {
450+
fieldId,
451+
startY: e.clientY,
452+
startHeight,
453+
}
423454

424-
isResizingRef.current = true
425-
resizeStateRef.current = {
426-
fieldId,
427-
startY: e.clientY,
428-
startHeight,
429-
}
455+
const handleMouseMove = (moveEvent: MouseEvent) => {
456+
if (!isResizingRef.current || !resizeStateRef.current) return
430457

431-
const handleMouseMove = (moveEvent: MouseEvent) => {
432-
if (!isResizingRef.current || !resizeStateRef.current) return
458+
const { fieldId: activeFieldId, startY, startHeight } = resizeStateRef.current
459+
const deltaY = moveEvent.clientY - startY
460+
const nextHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, startHeight + deltaY)
433461

434-
const { fieldId: activeFieldId, startY, startHeight } = resizeStateRef.current
435-
const deltaY = moveEvent.clientY - startY
436-
const nextHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, startHeight + deltaY)
462+
const activeTextarea = textareaRefs.current[activeFieldId]
463+
const overlay = overlayRefs.current[activeFieldId]
437464

438-
const activeTextarea = textareaRefs.current[activeFieldId]
439-
if (activeTextarea) {
440-
activeTextarea.style.height = `${nextHeight}px`
441-
}
465+
if (activeTextarea) {
466+
activeTextarea.style.height = `${nextHeight}px`
467+
}
442468

443-
const overlay = overlayRefs.current[activeFieldId]
444-
if (overlay) {
445-
overlay.style.height = `${nextHeight}px`
469+
if (overlay) {
470+
overlay.style.height = `${nextHeight}px`
471+
if (activeTextarea) {
472+
overlay.scrollTop = activeTextarea.scrollTop
473+
overlay.scrollLeft = activeTextarea.scrollLeft
474+
}
475+
}
446476
}
447-
}
448477

449-
const handleMouseUp = () => {
450-
if (resizeStateRef.current) {
451-
const { fieldId: activeFieldId } = resizeStateRef.current
452-
userResizedRef.current[activeFieldId] = true
453-
}
478+
const handleMouseUp = () => {
479+
if (resizeStateRef.current) {
480+
const { fieldId: activeFieldId } = resizeStateRef.current
481+
userResizedRef.current[activeFieldId] = true
482+
syncOverlay(activeFieldId)
483+
}
454484

455-
isResizingRef.current = false
456-
resizeStateRef.current = null
457-
document.removeEventListener('mousemove', handleMouseMove)
458-
document.removeEventListener('mouseup', handleMouseUp)
459-
}
485+
isResizingRef.current = false
486+
resizeStateRef.current = null
487+
document.removeEventListener('mousemove', handleMouseMove)
488+
document.removeEventListener('mouseup', handleMouseUp)
489+
}
460490

461-
document.addEventListener('mousemove', handleMouseMove)
462-
document.addEventListener('mouseup', handleMouseUp)
463-
}, [])
491+
document.addEventListener('mousemove', handleMouseMove)
492+
document.addEventListener('mouseup', handleMouseUp)
493+
},
494+
[syncOverlay]
495+
)
464496

465-
useEffect(() => {
497+
useLayoutEffect(() => {
466498
currentMessages.forEach((_, index) => {
467-
const fieldId = `message-${index}`
468-
autoResizeTextarea(fieldId)
499+
autoResizeTextarea(`message-${index}`)
469500
})
470501
}, [currentMessages, autoResizeTextarea])
471502

503+
useEffect(() => {
504+
const observers: ResizeObserver[] = []
505+
506+
for (let i = 0; i < currentMessages.length; i++) {
507+
const fieldId = `message-${i}`
508+
const textarea = textareaRefs.current[fieldId]
509+
const overlay = overlayRefs.current[fieldId]
510+
511+
if (textarea && overlay) {
512+
const observer = new ResizeObserver(() => {
513+
overlay.style.width = `${textarea.clientWidth}px`
514+
})
515+
observer.observe(textarea)
516+
observers.push(observer)
517+
}
518+
}
519+
520+
return () => {
521+
observers.forEach((observer) => observer.disconnect())
522+
}
523+
}, [currentMessages.length])
524+
472525
return (
473526
<div className='flex w-full flex-col gap-[10px]'>
474527
{currentMessages.map((message, index) => (
@@ -621,19 +674,15 @@ export function MessagesInput({
621674
</div>
622675

623676
{/* Content Input with overlay for variable highlighting */}
624-
<div className='relative w-full'>
677+
<div className='relative w-full overflow-hidden'>
625678
<textarea
626679
ref={(el) => {
627680
textareaRefs.current[fieldId] = el
628681
}}
629-
className='allow-scroll box-border min-h-[80px] w-full resize-none whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[8px] font-[inherit] font-medium text-sm text-transparent leading-[inherit] caret-[var(--text-primary)] outline-none placeholder:text-[var(--text-muted)] focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed'
630-
rows={3}
682+
className='relative z-[2] m-0 box-border h-auto min-h-[80px] w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-none bg-transparent px-[8px] py-[8px] font-medium font-sans text-sm text-transparent leading-[1.5] caret-[var(--text-primary)] outline-none [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed [&::-webkit-scrollbar]:hidden'
631683
placeholder='Enter message content...'
632684
value={message.content}
633-
onChange={(e) => {
634-
fieldHandlers.onChange(e)
635-
autoResizeTextarea(fieldId)
636-
}}
685+
onChange={fieldHandlers.onChange}
637686
onKeyDown={(e) => {
638687
if (e.key === 'Tab' && !isPreview && !disabled) {
639688
e.preventDefault()
@@ -670,12 +719,13 @@ export function MessagesInput({
670719
ref={(el) => {
671720
overlayRefs.current[fieldId] = el
672721
}}
673-
className='scrollbar-none pointer-events-none absolute top-0 left-0 box-border w-full overflow-auto whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[8px] font-[inherit] font-medium text-[var(--text-primary)] text-sm leading-[inherit]'
722+
className='pointer-events-none absolute top-0 left-0 z-[1] m-0 box-border w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-none bg-transparent px-[8px] py-[8px] font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.5] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
674723
>
675724
{formatDisplayText(message.content, {
676725
accessiblePrefixes,
677726
highlightAll: !accessiblePrefixes,
678727
})}
728+
{message.content.endsWith('\n') && '\u200B'}
679729
</div>
680730

681731
{/* Env var dropdown for this message */}
@@ -705,7 +755,7 @@ export function MessagesInput({
705755

706756
{!isPreview && !disabled && (
707757
<div
708-
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
758+
className='absolute right-1 bottom-1 z-[3] flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
709759
onMouseDown={(e) => handleResizeStart(fieldId, e)}
710760
onDragStart={(e) => {
711761
e.preventDefault()

0 commit comments

Comments
 (0)