148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
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<BackstoryTextFieldRef, BackstoryTextFieldProps>((props, ref) => {
|
|
const {
|
|
value = '',
|
|
disabled = false,
|
|
placeholder,
|
|
onEnter,
|
|
onChange,
|
|
style,
|
|
} = props;
|
|
const theme = useTheme();
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
const shadowRef = useRef<HTMLTextAreaElement>(null);
|
|
const [editValue, setEditValue] = useState<string>(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<HTMLTextAreaElement>) => {
|
|
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 (
|
|
<>
|
|
<textarea
|
|
className="BackstoryTextField"
|
|
ref={textareaRef}
|
|
value={editValue}
|
|
disabled={disabled}
|
|
placeholder={placeholder}
|
|
onChange={(e) => { setEditValue(e.target.value); onChange && onChange(e.target.value); }}
|
|
onKeyDown={handleKeyDown}
|
|
style={fullStyle}
|
|
/>
|
|
<textarea
|
|
className="BackgroundTextField"
|
|
ref={shadowRef}
|
|
aria-hidden="true"
|
|
style={{
|
|
...fullStyle,
|
|
position: 'absolute',
|
|
top: '-9999px',
|
|
left: '-9999px',
|
|
visibility: 'hidden',
|
|
padding: '0px', // No padding to match content height
|
|
margin: '0px',
|
|
border: '0px', // Remove border to avoid extra height
|
|
height: 'auto', // Allow natural height
|
|
minHeight: '0px',
|
|
}}
|
|
readOnly
|
|
tabIndex={-1}
|
|
/>
|
|
</>
|
|
);
|
|
});
|
|
|
|
export type {
|
|
BackstoryTextFieldRef
|
|
};
|
|
|
|
export { BackstoryTextField }; |