/* ============================================================================ * 서명전에 — TDS BottomSheet 컴포넌트 * · 화면 하단에서 슬라이드 업으로 나타나는 패널. * · 서브 컴포넌트: * - BottomSheet.Header — 제목 (기본 t4) * - BottomSheet.HeaderDescription — 부제목 (기본 t6) * - BottomSheet.CTA — 단일 주요 버튼 * - BottomSheet.DoubleCTA — 좌우로 나뉜 2버튼 * - BottomSheet.Select — 선택지(라디오) 목록 * * · props (TDS 스펙 기준): * open* — 열림 여부 * onClose — 닫힘 요청 콜백 (ESC, 딤 클릭, 뒤로가기) * header — 제목 영역 (주로 BottomSheet.Header) * headerDescription — 부제목 영역 * cta — 하단 CTA 영역 (주로 BottomSheet.CTA/DoubleCTA) * children — 본문 * className — 시트 영역 class * dimmerClassName — 딤 영역 class * disableDimmer — true 면 딤 제거 * hasTextField — 내부에 키보드 포커스가 있을 때 키보드 위로 올림 * maxHeight — 기본 높이 (px, 숫자) * expandedMaxHeight — 확장 상태 최대 높이 (px, 숫자) * expandBottomSheet — 드래그 확장 허용 * expandBottomSheetWhenScroll — 스크롤로도 확장 * ctaContentGap — CTA 와 본문 사이 여백 (기본 34) * ariaLabelledBy, ariaDescribedBy — 접근성 ID * onEntered, onExited, onExpanded * onDimmerClick * onHandlerTouchStart, onHandlerTouchEnd * portalContainer — UNSAFE_disableFocusLock=true 이 아니면 document.body * UNSAFE_disableFocusLock, UNSAFE_ignoreDimmerClick, UNSAFE_ignoreBackEvent * a11yIncludeHeaderInScroll, disableChildrenDragging * * · 스펙 중 모바일 제스처(드래그로 확장/닫기) 는 스캐폴드 상태. * API(props + 콜백) 는 전부 수신하지만, 현재 포팅 단계에서는 maxHeight/ * expandedMaxHeight 를 기본값으로만 적용하고 그래버 터치 이벤트는 * onHandlerTouchStart/End 로 패스스루. 네이티브 wrapper 도입 시 * 제스처 바인딩을 추가해요. * ========================================================================== */ function BottomSheet({ open, onClose, header, headerDescription, cta, children, className = "", dimmerClassName = "", disableDimmer = false, hasTextField = false, maxHeight, expandedMaxHeight, expandBottomSheet = false, expandBottomSheetWhenScroll = false, ctaContentGap = 34, ariaLabelledBy, ariaDescribedBy, onEntered, onExited, onExpanded, onDimmerClick, onHandlerTouchStart, onHandlerTouchEnd, UNSAFE_disableFocusLock = false, UNSAFE_ignoreDimmerClick = false, UNSAFE_ignoreBackEvent = false, a11yIncludeHeaderInScroll = true, disableChildrenDragging = false, portalContainer, ...rest }) { const [expanded, setExpanded] = useState(false); // ESC 키 닫기 — UNSAFE_ignoreBackEvent 로 억제 가능 (뒤로가기와 동일 취급). useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === "Escape" && !UNSAFE_ignoreBackEvent) onClose?.(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose, UNSAFE_ignoreBackEvent]); // open 이 true → false 바뀔 때 onExited, 반대로 → true 일 때 onEntered. // (transition end 가 아닌 mount 타이밍 기준 — TDS 스펙이 보장하는 최소치) const prevOpen = React.useRef(open); useEffect(() => { if (prevOpen.current !== open) { if (open) onEntered?.(); else onExited?.(); prevOpen.current = open; } }, [open, onEntered, onExited]); // open 이 false 로 바뀌면 expanded 상태도 초기화 useEffect(() => { if (!open && expanded) { setExpanded(false); } }, [open, expanded]); const handleDimmerClick = (e) => { onDimmerClick?.(e); if (!UNSAFE_ignoreDimmerClick) onClose?.(); }; const sheetMaxHeight = expanded && expandedMaxHeight ? `${expandedMaxHeight}px` : maxHeight ? `${maxHeight}px` : undefined; const rootCls = [ "bottom-sheet-root", open ? "is-open" : "", hasTextField ? "bottom-sheet-root--textfield" : "", ] .filter(Boolean) .join(" "); const sheetCls = [ "bottom-sheet", expanded ? "bottom-sheet--expanded" : "", cta ? "bottom-sheet--has-cta" : "", className, ] .filter(Boolean) .join(" "); const dimCls = ["bottom-sheet-root__dimmer", dimmerClassName] .filter(Boolean) .join(" "); // 본문 영역이 CTA 아래로 가려지지 않도록 CTA 가 있을 때 padding-bottom 을 더 준다. const bodyStyle = cta ? { paddingBottom: `${ctaContentGap}px` } : undefined; return (
{children}
; } /* -------------------- BottomSheet.CTA -------------------- */ /* 하단 영역 전체 너비 주요 버튼. TDS Button (color="primary" variant="fill" * size="xlarge" display="block") 위에 .bottom-sheet-cta 클래스로 시트 컨텍스트 * 스타일을 얹는다. loading 등 추가 prop 도 그대로 패스스루. */ function BottomSheetCTA({ children, className = "", onClick, disabled, loading, type = "button", ...rest }) { const cls = ["bottom-sheet-cta", className].filter(Boolean).join(" "); return ( ); } /* -------------------- BottomSheet.DoubleCTA -------------------- */ /* leftButton / rightButton 두 자리. 일반적으로 좌=취소/약한 버튼, 우=주요 버튼. */ function BottomSheetDoubleCTA({ leftButton, rightButton, className = "", ...rest }) { const cls = ["bottom-sheet-double-cta", className] .filter(Boolean) .join(" "); return (