/* ============================================================================ * 서명전에 — TDS SearchField 컴포넌트 * · 검색 입력창 — 좌측 돋보기 아이콘 + 가운데 input + 우측 X 삭제 버튼. * · 텍스트가 비어 있으면 X 버튼 자동 숨김. * * · controlled / uncontrolled 모두 지원: * controlled — value + onChange 전달 * uncontrolled — defaultValue (옵션) + 내부 useState 로 관리 * * · SearchFieldProps: * fixed boolean (기본 false) — 상단에 position: fixed 로 고정 * takeSpace boolean (기본 true) — fixed=true 일 때 같은 높이의 * 빈 div (placeholder) 를 원래 * 자리에 삽입해 콘텐츠가 위로 * 점프하지 않게 함 * onDeleteClick () => void — X 삭제 버튼 클릭 시 추가 콜백 * (input 비우기는 자동, 그 외 액션) * value string — controlled value * defaultValue string — uncontrolled 초기값 * onChange (e) => void — 값 변경 콜백 (controlled/uncontrolled 모두) * placeholder string — input placeholder * disabled boolean * aria-label 기본 "검색어 입력" * 그 외 모든 input props (name, autoFocus, inputMode, maxLength, …) 패스스루 * className, style, id → 외곽 wrapper 에 적용 * * · 접근성: * · input 에 type="search" + role 자동 * · X 버튼은 aria-label="검색어 지우기" + type="button" (form submit 방지) * · 돋보기 SVG 는 aria-hidden="true" * ========================================================================== */ // ----- 좌측 돋보기 SVG (24px) ----- function __SearchFieldMagnifierSvg() { return ( ); } // ----- 우측 X SVG (14px, 회색 원 안에) ----- function __SearchFieldClearSvg() { return ( ); } function SearchField({ fixed = false, takeSpace = true, onDeleteClick, value, defaultValue, onChange, placeholder, disabled = false, className = "", style, id, "aria-label": ariaLabel, ...rest }) { // controlled vs uncontrolled 판별 const isControlled = value !== undefined; const [internalValue, setInternalValue] = React.useState( typeof defaultValue === "string" ? defaultValue : "" ); const currentValue = isControlled ? (value || "") : internalValue; // input ref — clear 후 포커스 복원에 사용 const inputRef = React.useRef(null); const finalAriaLabel = ariaLabel != null ? ariaLabel : "검색어 입력"; // ----- 입력 변화 핸들러 ----- const handleChange = function (e) { if (!isControlled) { setInternalValue(e.target.value); } if (typeof onChange === "function") onChange(e); }; // ----- X 버튼 클릭 핸들러 ----- const handleClear = function () { if (disabled) return; // 내부 value 비우기 (uncontrolled) if (!isControlled) { setInternalValue(""); } // onChange 가 있으면 동기적으로 빈 값으로 호출 — 가짜 이벤트 객체 // (controlled 사용처가 외부 상태를 갱신할 수 있도록) if (typeof onChange === "function" && inputRef.current) { const synthetic = { target: inputRef.current, currentTarget: inputRef.current, type: "change", // input 의 value 는 한 프레임 뒤에 비울 거라 즉시 "" 보장 nativeEvent: null, }; // input 의 실제 값도 "" 로 — controlled 일 때는 외부에서 다음 렌더에 반영 try { inputRef.current.value = ""; } catch (_e) { // some input types disallow direct .value assign — ignore } // target.value 가 "" 인 새 객체로 던져 줌 synthetic.target = { ...inputRef.current, value: "" }; onChange(synthetic); } // 사용자 콜백 if (typeof onDeleteClick === "function") onDeleteClick(); // input 으로 포커스 복원 — 사용자가 곧바로 다시 입력 가능 if (inputRef.current && typeof inputRef.current.focus === "function") { inputRef.current.focus(); } }; // ----- 클래스 조립 ----- const wrapCls = [ "tds-searchfield", fixed ? "tds-searchfield--fixed" : "", className, ] .filter(Boolean) .join(" "); const showClear = !disabled && currentValue !== ""; // ----- spacer (fixed + takeSpace 일 때만) ----- const spacer = fixed && takeSpace ? (
) : null; // takeSpace 가 fixed=false 인데 명시적으로 true 면 무의미함을 한 번 알려줌 if (takeSpace === false && fixed === false) { // takeSpace 기본이 true 라 명시 false 일 때만 정보성 — 경고는 스킵 } if (takeSpace && !fixed) { // 사용자가 takeSpace 만 true 로 줬다면 fixed=false 라 효과 없음. 한 번 알림. if (window.console && console.info) { // 너무 시끄럽지 않게 info 레벨로 // (warn 은 fixed=false 인 기본 사용 시 모두 떠서 노이즈) } } return (