import { type FC, type FocusEvent, type MutableRefObject, type RefObject, useCallback, useEffect, useRef } from "react";
|
import debounce from "lodash/debounce";
|
import { cn } from "../../utils/bem";
|
import { isMacOS } from "../../utils/utilities";
|
|
import "./TextArea.scss";
|
import mergeRefs from "../Utils/mergeRefs";
|
|
export type ActionRefValue = { update?: (text?: string) => void; el?: RefObject<HTMLTextAreaElement> };
|
|
export type TextAreaProps = {
|
value?: string | null;
|
onSubmit?: (value: string) => void | Promise<void>;
|
onChange?: (value: string) => void;
|
onInput?: (value: string) => void;
|
onFocus?: (e: FocusEvent) => void;
|
onBlur?: (e: FocusEvent) => void;
|
ref?: MutableRefObject<HTMLTextAreaElement>;
|
actionRef?: MutableRefObject<ActionRefValue>;
|
rows?: number;
|
maxRows?: number;
|
autoSize?: boolean;
|
className?: string;
|
placeholder?: string;
|
name?: string;
|
id?: string;
|
};
|
|
export const TextArea: FC<TextAreaProps> = ({
|
ref,
|
actionRef,
|
onChange: _onChange,
|
onInput: _onInput,
|
onSubmit,
|
value,
|
autoSize = true,
|
rows = 1,
|
maxRows = 4,
|
className,
|
...props
|
}) => {
|
const inlineAction = !!onSubmit;
|
|
const rootClass = cn("textarea");
|
const classList = [rootClass.mod({ inline: inlineAction, autosize: autoSize }), className].join(" ").trim();
|
|
const autoGrowRef = useRef({
|
rows,
|
maxRows: Math.max(maxRows - 1, 1),
|
lineHeight: 24,
|
maxHeight: Number.POSITIVE_INFINITY,
|
});
|
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
const resizeTextArea = useCallback(
|
debounce(
|
() => {
|
const textarea = textAreaRef.current;
|
|
if (!textarea || !autoGrowRef.current || !textAreaRef.current) return;
|
|
if (autoGrowRef.current.maxHeight === Number.POSITIVE_INFINITY) {
|
textarea.style.height = "auto";
|
const currentValue = textAreaRef.current.value;
|
|
textAreaRef.current.value = "";
|
autoGrowRef.current.lineHeight = textAreaRef.current.scrollHeight / autoGrowRef.current.rows;
|
autoGrowRef.current.maxHeight = autoGrowRef.current.lineHeight * autoGrowRef.current.maxRows;
|
|
textAreaRef.current.value = currentValue;
|
}
|
|
let newHeight: number;
|
|
if (textarea.scrollHeight > autoGrowRef.current.maxHeight) {
|
textarea.style.overflowY = "scroll";
|
newHeight = autoGrowRef.current.maxHeight;
|
} else {
|
textarea.style.overflowY = "hidden";
|
textarea.style.height = "auto";
|
newHeight = textarea.scrollHeight;
|
}
|
const contentLength = textarea.value.length;
|
const cursorPosition = textarea.selectionStart;
|
|
requestAnimationFrame(() => {
|
textarea.style.height = `${newHeight}px`;
|
|
if (contentLength === cursorPosition) {
|
textarea.scrollTop = textarea.scrollHeight;
|
}
|
});
|
},
|
10,
|
{ leading: true },
|
),
|
[],
|
);
|
|
if (actionRef) {
|
actionRef.current = {
|
update: (text = "") => {
|
if (!textAreaRef.current) return;
|
|
textAreaRef.current.value = text;
|
resizeTextArea();
|
},
|
el: textAreaRef,
|
};
|
}
|
|
const onInput = useCallback(
|
(e: any) => {
|
_onInput?.(e.target.value);
|
resizeTextArea();
|
},
|
[_onInput],
|
);
|
|
const onChange = useCallback(
|
(e: any) => {
|
_onChange?.(e.target.value);
|
resizeTextArea();
|
},
|
[_onChange],
|
);
|
|
useEffect(() => {
|
const resize = new ResizeObserver(resizeTextArea);
|
|
resize.observe(textAreaRef.current as any);
|
|
return () => {
|
if (textAreaRef.current) {
|
resize.unobserve(textAreaRef.current as any);
|
}
|
};
|
}, []);
|
|
useEffect(() => {
|
if (textAreaRef.current) {
|
textAreaRef.current.value = value || "";
|
resizeTextArea();
|
}
|
}, [value]);
|
|
useEffect(() => {
|
if (!onSubmit) return;
|
|
const listener = (event: KeyboardEvent) => {
|
if (!textAreaRef.current) return;
|
if (event.key === "Enter" && (event.ctrlKey || (isMacOS() && event.metaKey))) {
|
onSubmit(textAreaRef.current.value);
|
}
|
};
|
|
if (textAreaRef.current) {
|
textAreaRef.current.addEventListener("keydown", listener);
|
}
|
return () => {
|
if (textAreaRef.current) {
|
textAreaRef.current.removeEventListener("keydown", listener);
|
}
|
};
|
}, [onSubmit]);
|
|
return (
|
<textarea
|
ref={mergeRefs(textAreaRef, ref)}
|
className={classList}
|
rows={autoGrowRef.current.rows}
|
onChange={onChange}
|
onInput={onInput}
|
{...props}
|
/>
|
);
|
};
|