From d60fbdbb9104dee97644cabe8bdda826d35af939 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Thu, 25 Sep 2025 11:53:18 -0400 Subject: [PATCH] Add auto-scroll to cursor functionality for large paragraphs - Implement scrollToCursor function to calculate cursor position - Auto-scroll when typing to keep cursor visible - Add 20px padding for comfortable viewing - Trigger scroll on text changes with setTimeout for DOM updates - Maintain smooth scrolling behavior - Ensure cursor is always visible when typing at bottom of content --- ui/desktop/src/components/RichChatInput.tsx | 47 ++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/ui/desktop/src/components/RichChatInput.tsx b/ui/desktop/src/components/RichChatInput.tsx index 25550fe23f..2c49c5de5a 100644 --- a/ui/desktop/src/components/RichChatInput.tsx +++ b/ui/desktop/src/components/RichChatInput.tsx @@ -133,6 +133,46 @@ export const RichChatInput = forwardRef(({ displayRef.current.scrollLeft = hiddenTextareaRef.current.scrollLeft; } }, []); + + // Auto-scroll to cursor position when typing + const scrollToCursor = useCallback(() => { + if (hiddenTextareaRef.current) { + const textarea = hiddenTextareaRef.current; + const cursorPos = textarea.selectionStart; + + // Create a temporary element to measure cursor position + const tempDiv = document.createElement('div'); + tempDiv.style.position = 'absolute'; + tempDiv.style.visibility = 'hidden'; + tempDiv.style.whiteSpace = 'pre-wrap'; + tempDiv.style.wordWrap = 'break-word'; + tempDiv.style.fontFamily = textarea.style.fontFamily; + tempDiv.style.fontSize = textarea.style.fontSize; + tempDiv.style.lineHeight = textarea.style.lineHeight; + tempDiv.style.padding = textarea.style.padding; + tempDiv.style.width = textarea.style.width; + + // Add text up to cursor position + tempDiv.textContent = textarea.value.substring(0, cursorPos); + document.body.appendChild(tempDiv); + + const cursorHeight = tempDiv.offsetHeight; + document.body.removeChild(tempDiv); + + // Scroll to ensure cursor is visible + const containerHeight = textarea.clientHeight; + const scrollTop = textarea.scrollTop; + const padding = 20; // Extra padding to ensure visibility + + if (cursorHeight > scrollTop + containerHeight - padding) { + // Cursor is below visible area, scroll down + textarea.scrollTop = cursorHeight - containerHeight + padding; + } else if (cursorHeight < scrollTop + padding) { + // Cursor is above visible area, scroll up + textarea.scrollTop = Math.max(0, cursorHeight - padding); + } + } + }, []); // Spell check tooltip state const [tooltip, setTooltip] = useState<{ @@ -528,7 +568,10 @@ export const RichChatInput = forwardRef(({ console.log('🔄 RichChatInput: onChange', { newValue, newCursorPos }); onChange(newValue, newCursorPos); setCursorPosition(newCursorPos); - }, [onChange]); + + // Auto-scroll to cursor after a short delay to ensure DOM is updated + setTimeout(scrollToCursor, 0); + }, [onChange, scrollToCursor]); const handleTextareaKeyDown = useCallback((e: React.KeyboardEvent) => { // Hide tooltip on any key press @@ -857,6 +900,7 @@ export const RichChatInput = forwardRef(({ style={{ ...style, minHeight: `${rows * 1.5}em`, + maxHeight: style?.maxHeight || 'none', // Respect parent max height constraints zIndex: 3, // Higher z-index, above textarea for misspelled word interactions pointerEvents: 'none', // Don't interfere with text selection by default userSelect: 'none', // Prevent selection on visual layer @@ -868,6 +912,7 @@ export const RichChatInput = forwardRef(({ margin: '0', whiteSpace: 'pre-wrap', // Match textarea wordWrap: 'break-word', + scrollBehavior: 'smooth', // Smooth scrolling }} role="textbox" aria-multiline="true"