import React, { useRef, useEffect, CSSProperties, KeyboardEvent, useState, useImperativeHandle } from 'react'; import { useTheme } from '@mui/material/styles'; import './BackstoryTextField.css'; // Define ref interface for exposed methods interface BackstoryTextFieldRef { getValue: () => string; setValue: (value: string) => void; getAndResetValue: () => string; } interface BackstoryTextFieldProps { value?: string; disabled?: boolean; placeholder: string; onEnter?: (value: string) => void; onChange?: (value: string) => void; style?: CSSProperties; } const BackstoryTextField = React.forwardRef((props, ref) => { const { value = '', disabled = false, placeholder, onEnter, onChange, style, } = props; const theme = useTheme(); const textareaRef = useRef(null); const shadowRef = useRef(null); const [editValue, setEditValue] = useState(value); // Sync editValue with prop value if it changes externally useEffect(() => { setEditValue(value || ""); }, [value]); // Adjust textarea height based on content useEffect(() => { if (!textareaRef.current || !shadowRef.current) { return; } const textarea = textareaRef.current; const shadow = shadowRef.current; // Set shadow value to editValue or placeholder if editValue is empty shadow.value = editValue || placeholder; // Ensure shadow textarea has same content-relevant styles const computed = getComputedStyle(textarea); shadow.style.width = computed.width; // Match width for accurate wrapping shadow.style.fontSize = computed.fontSize; shadow.style.lineHeight = computed.lineHeight; shadow.style.fontFamily = computed.fontFamily; shadow.style.letterSpacing = computed.letterSpacing; shadow.style.wordSpacing = computed.wordSpacing; // Use requestAnimationFrame to ensure DOM is settled const raf = requestAnimationFrame(() => { const paddingTop = parseFloat(computed.paddingTop || '0'); const paddingBottom = parseFloat(computed.paddingBottom || '0'); const totalPadding = paddingTop + paddingBottom; // Reset height to auto to allow shrinking textarea.style.height = 'auto'; const newHeight = shadow.scrollHeight + totalPadding; textarea.style.height = `${newHeight}px`; }); // Cleanup RAF to prevent memory leaks return () => cancelAnimationFrame(raf); }, [editValue, placeholder]); // Expose getValue method via ref useImperativeHandle(ref, () => ({ getValue: () => editValue, setValue: (value: string) => setEditValue(value), getAndResetValue: () => { const _ev = editValue; setEditValue(''); return _ev; } })); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); // Prevent newline onEnter && onEnter(editValue); setEditValue(''); // Clear textarea } }; const fullStyle: CSSProperties = { display: 'flex', flexGrow: 1, width: '100%', padding: '16.5px 14px', resize: 'none', overflow: 'hidden', boxSizing: 'border-box', // minHeight: 'calc(1.5rem + 28px)', // lineHeight + padding lineHeight: '1.5', borderRadius: '4px', fontSize: '16px', backgroundColor: 'rgba(0, 0, 0, 0)', fontFamily: theme.typography.fontFamily, ...style, }; return ( <>