/* ============================================================================ * 서명전에 — TDS TextArea 컴포넌트 (Task #169) * * · TextField 의 textarea 변형. label / placeholder / help / hasError / disabled * 구조는 TextField 와 동일. prefix / suffix / right 슬롯은 제외. * · variant 는 box (기본) / line / big / hero 가 모두 지원되지만, 디자인 권장은 box. * * · height 모드: * height 숫자(px) 또는 string — 고정 높이. 그 이상은 textarea 내부 스크롤. * minHeight 숫자(px) — 자동 확장 모드. scrollHeight 에 맞춰 height 가 늘어남. * 둘 다 생략 시 — CSS 기본 min-height 84px 만 적용 (보통 5줄 정도). * 두 값이 모두 주어지면 minHeight 가 우선 (자동 확장). * * · ref API: TextField 와 동일 (focus / blur / clear / el) * * · maxLength 가 있으면 우측 하단에 카운터 (현재/최대) 표시. * * · Babel Standalone 환경 호환: __ta 접두사 alias. * ========================================================================== */ const __taUseState = (typeof React !== "undefined" && React.useState) || function (v) { return [v, function () {}]; }; const __taUseRef = (typeof React !== "undefined" && React.useRef) || function () { return { current: null }; }; const __taUseEffect = (typeof React !== "undefined" && React.useEffect) || function () {}; const __taUseLayoutEffect = (typeof React !== "undefined" && React.useLayoutEffect) || function () {}; const __taUseImperativeHandle= (typeof React !== "undefined" && React.useImperativeHandle) || function () {}; const __taForwardRef = (typeof React !== "undefined" && React.forwardRef) || function (fn) { return fn; }; function __taJoin() { let out = ""; for (let i = 0; i < arguments.length; i++) { const v = arguments[i]; if (!v) continue; out = out ? out + " " + v : v; } return out; } function __taEnum(value, allowed, fallback, propPath) { if (allowed.indexOf(value) !== -1) return value; if (value !== undefined && window.console && console.warn) { console.warn("[TextArea] " + propPath + " 값이 잘못됨: " + JSON.stringify(value) + ". 허용: " + allowed.join(", ") + ". 기본 " + fallback + " 사용."); } return fallback; } const TextArea = __taForwardRef(function TextArea(props, ref) { const { label, placeholder = "", help, variant: variantProp, labelOption: labelOptionProp, hasError = false, handleError, disabled = false, required = false, height, minHeight, maxLength, showCounter, value, defaultValue, onChange, onFocus, onBlur, paddingTop, paddingBottom, className = "", style, id, name, autoFocus, autoComplete, inputMode, "aria-label": ariaLabel, ...rest } = props; const variant = __taEnum(variantProp, ["box", "line", "big", "hero"], "box", "variant"); const labelOption = __taEnum(labelOptionProp, ["appear", "sustain"], "sustain", "labelOption"); // ----- controlled / uncontrolled ----- const isControlled = value !== undefined; const [internalValue, setInternalValue] = __taUseState(typeof defaultValue === "string" ? defaultValue : ""); const currentValue = isControlled ? (value == null ? "" : String(value)) : internalValue; // ----- 포커스 ----- const [focused, setFocused] = __taUseState(false); // ----- ref ----- const textareaRef = __taUseRef(null); __taUseImperativeHandle(ref, function () { return { focus: function () { if (textareaRef.current && textareaRef.current.focus) textareaRef.current.focus(); }, blur: function () { if (textareaRef.current && textareaRef.current.blur) textareaRef.current.blur(); }, clear: function () { if (!isControlled) setInternalValue(""); if (typeof onChange === "function") { onChange({ target: { value: "", name: name }, currentTarget: textareaRef.current, type: "change", }); } }, el: function () { return textareaRef.current; }, }; }, [isControlled]); // ----- 자동 확장 (minHeight 모드) ----- // height 가 명시되어 있지 않고 minHeight 가 있으면 scrollHeight 동기화. const isAuto = (height == null) && (minHeight != null); function syncAutoHeight() { if (!isAuto) return; const el = textareaRef.current; if (!el) return; // 한 번 줄였다 다시 측정 — 줄어든 경우도 잡힘 el.style.height = "auto"; const sh = el.scrollHeight; const mh = typeof minHeight === "number" ? minHeight : parseInt(minHeight, 10) || 0; el.style.height = Math.max(sh, mh) + "px"; } __taUseLayoutEffect(function () { syncAutoHeight(); }, [currentValue, isAuto, minHeight]); // ----- 핸들러 ----- const handleChange = function (e) { if (!isControlled) setInternalValue(e.target.value); if (typeof onChange === "function") onChange(e); }; const handleFocus = function (e) { setFocused(true); if (typeof onFocus === "function") onFocus(e); }; const handleBlur = function (e) { setFocused(false); if (typeof onBlur === "function") onBlur(e); }; // ----- label 표시 ----- const hasValueOrFocus = focused || (currentValue && currentValue.length > 0); const labelShown = labelOption === "sustain" || (labelOption === "appear" && hasValueOrFocus); // ----- 클래스 ----- const rootCls = __taJoin( "tds-tf", "tds-tf--textarea", "tds-tf--" + variant, "tds-tf--" + labelOption, labelShown ? "tds-tf--shown" : "", focused ? "tds-tf--focused" : "", hasError ? "tds-tf--error" : "", disabled ? "tds-tf--disabled" : "", className ); // ----- 외곽 style ----- const finalStyle = Object.assign({}, style || {}); if (paddingTop != null) finalStyle.paddingTop = typeof paddingTop === "number" ? paddingTop + "px" : paddingTop; if (paddingBottom != null) finalStyle.paddingBottom = typeof paddingBottom === "number" ? paddingBottom + "px" : paddingBottom; // ----- textarea style ----- const textareaStyle = {}; if (height != null) { textareaStyle.height = typeof height === "number" ? height + "px" : height; textareaStyle.minHeight = textareaStyle.height; textareaStyle.maxHeight = textareaStyle.height; textareaStyle.overflowY = "auto"; } else if (minHeight != null) { textareaStyle.minHeight = typeof minHeight === "number" ? minHeight + "px" : minHeight; textareaStyle.overflowY = "hidden"; // 자동 확장 중엔 스크롤 안 보이게 } // ----- 카운터 표시 여부 ----- const counterVisible = (showCounter !== false) && (typeof maxLength === "number" && maxLength > 0); return (
{label != null && label !== "" ? ( ) : null}