import { CSSProperties, forwardRef, TextareaHTMLAttributes, ForwardedRef, useEffect, useRef, useImperativeHandle, useCallback, } from 'react'; import styled, { css } from 'styled-components'; import { spacing } from '../../spacing'; type TextAreaVariant = 'code' | 'text'; type Props = TextareaHTMLAttributes & { variant?: TextAreaVariant; width?: CSSProperties['width']; height?: CSSProperties['height']; /** * Automatically adjust height to fit content * When enabled, the textarea will grow/shrink to show all content */ autoGrow?: boolean; }; type RefType = HTMLTextAreaElement | null; const TextAreaContainer = styled.textarea<{ variant: TextAreaVariant; width?: CSSProperties['width']; height?: CSSProperties['height']; autoGrow?: boolean; }>` padding: ${spacing.r12} ${spacing.r8}; border-radius: 4px; resize: ${(props) => (props.autoGrow ? 'none' : 'vertical')}; font-family: ${(props) => props.variant === 'code' ? 'Courier New' : 'Lato'}; font-size: ${spacing.f14}; ${(props) => props.disabled && css` opacity: 0.5; cursor: not-allowed; `} ${(props) => props.width && css` width: ${props.width}; `} ${(props) => props.height && css` height: ${props.height}; `} ${(props) => props.autoGrow && css` overflow: hidden; box-sizing: border-box; `} &:placeholder-shown { font-style: italic; } &::placeholder { color: ${(props) => props.theme.textSecondary}; opacity: 0.5; } &:hover { ${(props) => !props.disabled && `border: ${spacing.r1} solid ${props.theme.infoPrimary};`} } &:focus { border: ${spacing.r1} solid ${(props) => props.theme.infoPrimary}; } ${(props) => { const { border, textSecondary, backgroundLevel1, selectedActive } = props.theme; return css` border-color: ${border}; color: ${textSecondary}; background: ${backgroundLevel1}; &:focus { border-color: ${selectedActive}; outline: none; } `; }} `; function TextAreaElement( { rows = 3, cols = 20, width, height, variant = 'code', autoGrow = false, value, defaultValue, onChange, ...rest }: Props, ref: ForwardedRef, ) { const internalRef = useRef(null); // Expose the textarea element to parent components via forwarded ref useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement); // Adjust height on mount and when value changes (for controlled components) const adjustHeight = useCallback(() => { const textarea = internalRef.current; if (!textarea || !autoGrow) return; // Reset height to auto to get the correct scrollHeight textarea.style.height = '0px'; // Set the height to match the content const newHeight = textarea.scrollHeight; textarea.style.height = `${newHeight}px`; }, [autoGrow]); useEffect(() => { adjustHeight(); }, [adjustHeight, value]); // Handle onChange to support both controlled and uncontrolled components const handleChange = useCallback( (event: React.ChangeEvent) => { if (autoGrow) { adjustHeight(); } if (onChange) { onChange(event); } }, [autoGrow, adjustHeight, onChange], ); if (width || height) { return ( ); } return ( ); } export const TextArea = forwardRef(TextAreaElement);