Skip to content

Commit 576c2e6

Browse files
te6-inclaude
andauthored
feat(dialog, drawer): add onOpenChange details with reason and event (#1198)
* refactor: internalize useControllableState * feat(use-controllable-state): add meta parameter support Add optional meta parameter to useControllableState for passing additional context through onChange callbacks. This enables use cases like Dialog's onOpenChange(open, { reason }) to indicate why the state changed (trigger, closeButton, escapeKeyDown, interactOutside). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: meta → details * feat(headless/dialog): add onOpenChange details with reason and event - Add DialogReasonToEventMap for type-safe event mapping - Add DialogChangeDetails discriminated union type - Pass reason and event to onOpenChange callback - Update DismissableLayer handlers to include event details - Switch from radix useControllableState to internal implementation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(react/dialog): use DialogPrimitive.CloseButton for DialogAction Simplify DialogAction by delegating to primitive CloseButton which now handles the onOpenChange details properly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(dialog): update onOpenChange documentation - Rename meta to details in documentation - Add details.event documentation - Add open-change-reason and prevent-close examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add missing await * feat(headless/drawer): add onOpenChange details with reason and event Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(bottom-sheet): add onOpenChange details documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(headless/drawer): remove unsupported trigger reason from type Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: fix deps * docs * refactor: make less diff * docs * fix: fix build * docs: changeset * docs * refactor * docs * refactor: give native events to `onOpenChange` * refactor: make reason optional * docs --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4767a10 commit 576c2e6

File tree

22 files changed

+892
-37
lines changed

22 files changed

+892
-37
lines changed

.changeset/busy-goats-count.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@seed-design/react-use-controllable-state": major
3+
"@seed-design/react-dialog": patch
4+
"@seed-design/react-drawer": patch
5+
"@seed-design/react": patch
6+
---
7+
8+
`AlertDialogRoot`, `MenuSheetRoot``BottomSheetRoot``onOpenChange` 두 번째 인자로 `details`를 제공합니다. `details.reason``details.event`를 사용할 수 있습니다.
9+
10+
`DialogAction``DialogPrimitive.CloseButton`으로 교체합니다. `AlertDialogAction` `onClick` 핸들러에서 `event.preventDefault()`를 호출하여 닫기 동작을 방지할 수 있습니다. [(예제)](https://seed-design.io/react/components/alert-dialog#prevent-close)

bun.lock

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/react/components/alert-dialog.mdx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,45 @@ Trigger 외의 방식으로 AlertDialog를 열고 닫을 수 있습니다. 이
149149
```
150150
</ComponentExample>
151151

152+
### Prevent Close
153+
154+
`AlertDialogAction``onClick`에서 `e.preventDefault()`를 호출하면 다이얼로그가 닫히지 않습니다.
155+
156+
<ComponentExample name="react/alert-dialog/prevent-close">
157+
```json doc-gen:file
158+
{
159+
"file": "examples/react/alert-dialog/prevent-close.tsx",
160+
"codeblock": true
161+
}
162+
```
163+
</ComponentExample>
164+
165+
### `onOpenChange` Details
166+
167+
`onOpenChange` 두 번째 인자로 `details`가 제공됩니다.
168+
169+
#### `reason`
170+
171+
**열릴 때** (`open: true`)
172+
173+
- `"trigger"`: `AlertDialogTrigger` (`Dialog.Trigger`)로 열림
174+
175+
**닫힐 때** (`open: false`)
176+
177+
- `"closeButton"`: `AlertDialogAction`으로 닫힘
178+
- `"escapeKeyDown"`: <kbd>ESC</kbd> 키 사용
179+
- `"interactOutside"`: 외부 영역 클릭
180+
- `AlertDialogRoot`는 기본적으로 `closeOnInteractOutside={false}`입니다. `interactOutside`는 이 옵션을 `true`로 설정한 경우에만 발생할 수 있습니다.
181+
182+
<ComponentExample name="react/alert-dialog/open-change-reason">
183+
```json doc-gen:file
184+
{
185+
"file": "examples/react/alert-dialog/open-change-reason.tsx",
186+
"codeblock": true
187+
}
188+
```
189+
</ComponentExample>
190+
152191
### Portalled
153192

154193
Portal은 기본적으로 `document.body`에 렌더링됩니다.

docs/content/react/components/bottom-sheet.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,29 @@ Trigger 외의 방식으로 BottomSheet를 열고 닫을 수 있습니다. 이
9393
```
9494
</ComponentExample>
9595

96+
### `onOpenChange` Details
97+
98+
`onOpenChange` 두 번째 인자로 `details`가 제공됩니다.
99+
100+
#### `reason`
101+
102+
**닫힐 때** (`open: false`)
103+
104+
- `"closeButton"`: `BottomSheet.CloseButton`으로 닫힘
105+
- `"escapeKeyDown"`: <kbd>ESC</kbd> 키 사용
106+
- `"interactOutside"`: 외부 영역 클릭
107+
- `"drag"`: 드래그로 닫힘
108+
- `"handleClickOnLastSnapPoint"`: 마지막 스냅 포인트에서 핸들 클릭으로 닫힘
109+
110+
<ComponentExample name="react/bottom-sheet/open-change-reason">
111+
```json doc-gen:file
112+
{
113+
"file": "examples/react/bottom-sheet/open-change-reason.tsx",
114+
"codeblock": true
115+
}
116+
```
117+
</ComponentExample>
118+
96119
### Header Align
97120

98121
`<BottomSheetRoot>``headerAlign` prop을 설정하여 title과 description의 정렬을 설정할 수 있습니다.

docs/content/react/components/menu-sheet.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,31 @@ npx @seed-design/cli@latest add ui:menu-sheet
134134
```
135135
</ComponentExample>
136136

137+
### `onOpenChange` Details
138+
139+
`onOpenChange` 두 번째 인자로 `details`가 제공됩니다.
140+
141+
#### `reason`
142+
143+
**열릴 때** (`open: true`)
144+
145+
- `"trigger"`: `MenuSheetTrigger` (`MenuSheet.Trigger`)로 열림
146+
147+
**닫힐 때** (`open: false`)
148+
149+
- `"closeButton"`: `MenuSheet.CloseButton`으로 닫힘
150+
- `"escapeKeyDown"`: <kbd>ESC</kbd> 키 사용
151+
- `"interactOutside"`: 외부 영역 클릭
152+
153+
<ComponentExample name="react/menu-sheet/open-change-reason">
154+
```json doc-gen:file
155+
{
156+
"file": "examples/react/menu-sheet/open-change-reason.tsx",
157+
"codeblock": true
158+
}
159+
```
160+
</ComponentExample>
161+
137162
### Skip Animation
138163

139164
`skipAnimation` prop을 사용하여 MenuSheet의 enter/exit 애니메이션을 건너뛸 수 있습니다.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { HStack, ResponsivePair, Text, VStack } from "@seed-design/react";
2+
import { useState } from "react";
3+
import { ActionButton } from "seed-design/ui/action-button";
4+
import {
5+
AlertDialogAction,
6+
AlertDialogContent,
7+
AlertDialogDescription,
8+
AlertDialogFooter,
9+
AlertDialogHeader,
10+
AlertDialogRoot,
11+
AlertDialogTitle,
12+
AlertDialogTrigger,
13+
} from "seed-design/ui/alert-dialog";
14+
15+
export default function AlertDialogOnOpenChangeReason() {
16+
const [open, setOpen] = useState(false);
17+
const [openReason, setOpenReason] = useState<string | null>(null);
18+
const [closeReason, setCloseReason] = useState<string | null>(null);
19+
20+
return (
21+
<VStack gap="x4" align="center">
22+
<AlertDialogRoot
23+
open={open}
24+
onOpenChange={(open, meta) => {
25+
setOpen(open);
26+
27+
(open ? setOpenReason : setCloseReason)(meta?.reason ?? null);
28+
}}
29+
>
30+
<AlertDialogTrigger asChild>
31+
<ActionButton variant="neutralSolid">열기</ActionButton>
32+
</AlertDialogTrigger>
33+
<AlertDialogContent>
34+
<AlertDialogHeader>
35+
<AlertDialogTitle>알림</AlertDialogTitle>
36+
<AlertDialogDescription>
37+
ESC 키를 누르거나 버튼을 클릭하여 닫아보세요.
38+
</AlertDialogDescription>
39+
</AlertDialogHeader>
40+
<AlertDialogFooter>
41+
<ResponsivePair gap="x2">
42+
<AlertDialogAction variant="neutralWeak">취소</AlertDialogAction>
43+
<AlertDialogAction variant="neutralSolid">확인</AlertDialogAction>
44+
</ResponsivePair>
45+
</AlertDialogFooter>
46+
</AlertDialogContent>
47+
</AlertDialogRoot>
48+
49+
<HStack gap="x4">
50+
<Text fontSize="t3" color="fg.neutralMuted">
51+
마지막 열림 이유: {openReason ?? "-"}
52+
</Text>
53+
<Text fontSize="t3" color="fg.neutralMuted">
54+
마지막 닫힘 이유: {closeReason ?? "-"}
55+
</Text>
56+
</HStack>
57+
</VStack>
58+
);
59+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Box, VStack } from "@seed-design/react";
2+
import { useState } from "react";
3+
import { ActionButton } from "seed-design/ui/action-button";
4+
import {
5+
AlertDialogAction,
6+
AlertDialogContent,
7+
AlertDialogDescription,
8+
AlertDialogFooter,
9+
AlertDialogHeader,
10+
AlertDialogRoot,
11+
AlertDialogTitle,
12+
AlertDialogTrigger,
13+
} from "seed-design/ui/alert-dialog";
14+
import { Switch } from "seed-design/ui/switch";
15+
16+
export default function AlertDialogPreventClose() {
17+
const [preventClose, setPreventClose] = useState(true);
18+
19+
return (
20+
<AlertDialogRoot>
21+
<AlertDialogTrigger asChild>
22+
<ActionButton variant="neutralSolid">열기</ActionButton>
23+
</AlertDialogTrigger>
24+
<AlertDialogContent>
25+
<AlertDialogHeader>
26+
<AlertDialogTitle>닫기 방지</AlertDialogTitle>
27+
<AlertDialogDescription>
28+
확인 버튼을 눌러도 다이얼로그가 닫히지 않도록 설정할 수 있습니다.
29+
</AlertDialogDescription>
30+
</AlertDialogHeader>
31+
<AlertDialogFooter asChild>
32+
<VStack gap="x4">
33+
<Box alignSelf="flex-start">
34+
<Switch
35+
size="16"
36+
tone="neutral"
37+
label="preventDefault 사용"
38+
checked={preventClose}
39+
onCheckedChange={setPreventClose}
40+
/>
41+
</Box>
42+
<AlertDialogAction
43+
variant="neutralSolid"
44+
onClick={(e) => {
45+
if (preventClose) {
46+
e.preventDefault();
47+
}
48+
}}
49+
>
50+
확인
51+
</AlertDialogAction>
52+
</VStack>
53+
</AlertDialogFooter>
54+
</AlertDialogContent>
55+
</AlertDialogRoot>
56+
);
57+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { HStack, Text, VStack } from "@seed-design/react";
2+
import { useState } from "react";
3+
import { ActionButton } from "seed-design/ui/action-button";
4+
import {
5+
BottomSheetBody,
6+
BottomSheetContent,
7+
BottomSheetRoot,
8+
BottomSheetTrigger,
9+
} from "seed-design/ui/bottom-sheet";
10+
11+
const snapPoints = ["200px", "400px", 1];
12+
13+
export default function BottomSheetOnOpenChangeReason() {
14+
const [open, setOpen] = useState(false);
15+
const [snap, setSnap] = useState<number | string | null>(snapPoints[0]);
16+
const [closeReason, setCloseReason] = useState<string | null>(null);
17+
18+
return (
19+
<VStack gap="x4" align="center">
20+
<BottomSheetRoot
21+
open={open}
22+
onOpenChange={(open, details) => {
23+
setOpen(open);
24+
25+
if (open) return;
26+
27+
setCloseReason(details?.reason ?? null);
28+
}}
29+
snapPoints={snapPoints}
30+
activeSnapPoint={snap}
31+
setActiveSnapPoint={setSnap}
32+
>
33+
<BottomSheetTrigger asChild>
34+
<ActionButton variant="neutralSolid">열기</ActionButton>
35+
</BottomSheetTrigger>
36+
<BottomSheetContent title="알림" showHandle style={{ height: "100%", maxHeight: "97%" }}>
37+
<BottomSheetBody minHeight="x16">
38+
<Text textStyle="t4Medium" color="fg.neutralMuted">
39+
ESC 키를 누르거나, 외부 영역을 클릭하거나, 아래로 스와이프하거나, 핸들을 탭하여 snap
40+
point를 순환해보세요.
41+
</Text>
42+
</BottomSheetBody>
43+
</BottomSheetContent>
44+
</BottomSheetRoot>
45+
46+
<HStack gap="x4">
47+
<Text fontSize="t3" color="fg.neutralMuted">
48+
마지막 닫힘 이유: {closeReason ?? "-"}
49+
</Text>
50+
</HStack>
51+
</VStack>
52+
);
53+
}

0 commit comments

Comments
 (0)