@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
106 lines (100 loc) • 3.48 kB
JavaScript
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import themed from '@dr.pogodin/react-themes';
import defaultTheme from "./style.scss";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const TextArea = ({
disabled,
error,
label,
onBlur,
onChange,
onKeyDown,
placeholder,
testId,
theme,
value
}) => {
const hiddenAreaRef = useRef(null);
const [height, setHeight] = useState();
const textAreaRef = useRef(null);
const [localValue, setLocalValue] = useState(value ?? '');
if (value !== undefined && localValue !== value) setLocalValue(value);
// This resizes text area's height when its width is changed for any reason.
useEffect(() => {
const el = hiddenAreaRef.current;
if (!el) return undefined;
const cb = () => {
setHeight(el.scrollHeight);
};
const observer = new ResizeObserver(cb);
observer.observe(el);
return () => {
observer.disconnect();
};
}, []);
// Resizes the text area when its content is modified.
//
// NOTE: useLayoutEffect() instead of useEffect() makes difference here,
// as it helps to avoid visible "content/height" jumps (i.e. with just
// useEffect() it becomes visible how the content is modified first,
// and then input height is incremented, if necessary).
// See: https://github.com/birdofpreyru/react-utils/issues/313
useLayoutEffect(() => {
const el = hiddenAreaRef.current;
if (el) setHeight(el.scrollHeight);
}, [localValue]);
let containerClassName = theme.container;
if (error) containerClassName += ` ${theme.error}`;
return /*#__PURE__*/_jsxs("div", {
className: containerClassName,
onFocus: () => {
textAreaRef.current?.focus();
},
children: [label === undefined ? null : /*#__PURE__*/_jsx("div", {
className: theme.label,
children: label
}), /*#__PURE__*/_jsx("textarea", {
className: `${theme.textarea} ${theme.hidden}`
// This text area is hidden underneath the primary one below,
// and it is used for text measurements, to implement auto-scaling
// of the primary textarea's height.
,
readOnly: true,
ref: hiddenAreaRef
// The "-1" value of "tabIndex" removes this hidden text area from
// the tab-focus-chain.
,
tabIndex: -1
// NOTE: With empty string value ("") the scrolling height of this text
// area is zero, thus collapsing <TextArea> height below the single line
// input height. To avoid it we fallback to whitespace (" ") character
// here.
,
value: localValue || ' '
}), /*#__PURE__*/_jsx("textarea", {
className: theme.textarea,
"data-testid": process.env.NODE_ENV === 'production' ? undefined : testId,
disabled: disabled,
onBlur: onBlur
// When value is "undefined" the text area is not-managed, and we should
// manage it internally for the measurement / resizing functionality
// to work.
,
onChange: value === undefined ? e => {
setLocalValue(e.target.value);
} : onChange,
onKeyDown: onKeyDown,
placeholder: placeholder,
ref: textAreaRef,
style: {
height
},
value: localValue
}), error && error !== true ? /*#__PURE__*/_jsx("div", {
className: theme.errorMessage,
children: error
}) : null]
});
};
export default themed(TextArea, 'TextArea', defaultTheme);
//# sourceMappingURL=index.js.map