BottomSheet
showModal()로 여는 네이티브 <dialog> 위의 모달 시트입니다. 브라우저가 시트를 최상위 레이어에 올리고, 뒤의 페이지를 inert로 만들고, 그 페이지로 포커스가 나가지 않게 하고, Escape를 닫기 요청으로 보냅니다. ::backdrop이 스크림입니다. Todae는 스크롤 잠금, 스냅 지점, 드래그를 더합니다. 중간과 전체 높이로 스냅합니다. 그래버에서, 또는 scrollTop === 0일 때 본문에서 드래그합니다. 목록 스크롤은 자유롭게 유지됩니다.
시트는 놓은 자리에서 렌더링되므로 부모의 테마를 따릅니다. 그 대신 부모의 네이티브 동작도 따릅니다. <form> 안에서는 시트의 입력이 그 폼과 함께 제출되고, <fieldset disabled> 안에서는 컨트롤이 비활성화됩니다. 폼, 비활성 fieldset, 닫힌 팝오버나 탭 패널 같은 숨겨진 컨테이너 바깥에 렌더링하세요. 개발 환경에서는 시트가 숨겨진 곳에서 열리면 Todae가 오류를 기록합니다.
열릴 때는 아래에서 스냅 높이로 미끄러져 올라옵니다(높이 전환이 아니라 첫 프레임 자세입니다). 두 오버레이 모두 data-entered를 쓰고, Dialog는 @starting-style도 유지합니다. 닫힐 때는 아래로 미끄러져 나가고 transitionend에서 언마운트됩니다(대체 시간 240ms). 드래그 중에는 transition: none을 유지합니다.
시트는 포털 없이 놓은 자리에서 렌더링되므로, 다른 파트처럼 appearance, density, 테마 오버라이드를 상속합니다. dismissOnEscape={false}는 closedby="none"을 설정해 브라우저가 Escape를 무시하게 합니다. 그렇지 않으면 Escape와 배경 클릭이 onOpenChange(false, reason)을 호출합니다. 제어 방식의 시트는 open을 false로 바꾸면 닫히고, 비제어 방식의 시트는 바로 닫힙니다. 스크립트가 close()를 호출하는 것처럼 Todae 바깥에서 <dialog>를 닫으면 onOpenChange는 native 이유를 받습니다.
사용 예시 스케치
나중에 나올 터치용 Combobox입니다. 버튼이 이 시트를 엽니다. 검색과 옵션은 시트 안에 있습니다. 열린 동안 입력하는 데스크톱 타입어헤드는 Combobox PRD에서 팝오버로 남습니다.
언제 쓰나요
언제: 아래에서 올라오는 모달 시트(터치용 Combobox의 닫힌 컨트롤, 모바일 선택기). 스크림, 모달 포커스, 중간/전체 스냅.
쓰지 않을 때: 데스크톱에서 앵커에 고정된 비모달 → Popover. 가운데 정렬 확인/폼 → Dialog. 호버 팁 → Tooltip.
Props
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| open | boolean | 아니요 | — | 제어 열림 상태. 비제어 시트에서는 생략하세요. |
| defaultOpen | boolean | 아니요 | false | 비제어일 때 마운트 시 시트를 엽니다. 트리거 파트가 없어서 한 번 닫히면 다시 마운트해야 다시 열립니다. |
| onOpenChange | (open: boolean, reason: SheetCloseReason) => void | 아니요 | — | 닫기 요청. 제어 방식에서 열린 상태를 유지하면 시트가 열린 채로 남습니다. <form method="dialog"> 제출이면 reason이 "form"(returnValue를 읽습니다)이고, 그 밖에 Todae 바깥에서 <dialog>를 닫았으면 "native"입니다. 제어 방식에서 open이 true로 남아 있으면 시트가 다시 나타납니다. |
| snap | "mid" | "full" | 아니요 | — | 제어 스냅. 비제어로 쓰려면 생략합니다. |
| defaultSnap | "mid" | "full" | 아니요 | "mid" | 비제어일 때의 초기 스냅. |
| snapPoints | ReadonlyArray<"mid" | "full"> | 아니요 | ["mid", "full"] | 허용되는 스냅. ["full"]이면 mid를 끕니다. |
| dismissOnEscape | boolean | 아니요 | true | Escape가 onOpenChange(false, "escape")를 호출합니다. false이면 closedby="none"을 설정해 브라우저가 닫기 요청을 무시합니다. |
| dismissOnBackdrop | boolean | 아니요 | true | 스크림 클릭이 onOpenChange(false, "backdrop")을 호출합니다. |
| dismissOnDrag | boolean | 아니요 | true | 임계값을 넘겨 끌면 닫힙니다. |
| aria-label | string | 아니요 | — | BottomSheet.Title을 생략했을 때의 이름. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| aria-label | string | 아니요 | "Resize sheet" | 크기 조절 컨트롤의 이름. 기본값은 StringsProvider의 resizeSheet 문자열입니다. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| children | React.ReactNode | 아니요 | — | 제목 텍스트. |
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
| aria-label | string | 아니요 | "Close" when its text, not counting aria-hidden or SVG parts, has fewer than two letters or digits | 접근 가능한 이름. "Cancel"처럼 글자나 숫자가 두 개 이상인 텍스트가 있으면 그 텍스트가 버튼의 이름이 됩니다. 그렇지 않으면 StringsProvider의 close 문자열이 이름이 되며, 영어로는 Close입니다. |
| onClick | React.MouseEventHandler<HTMLButtonElement> | 아니요 | — | 닫기 요청 전에 실행됩니다. 시트를 열어 두려면 event.preventDefault()를 호출하세요. |