Skip to content

Commit efb2f24

Browse files
committed
refactor(#93): 기존 BottomSheet를 Compound Component 패턴으로 분리
1 parent 2a1eca3 commit efb2f24

File tree

9 files changed

+241
-64
lines changed

9 files changed

+241
-64
lines changed

src/components/ui/BottomSheet/BottomSheet.css.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { style } from "@vanilla-extract/css";
22

3-
import { colors, radius, semantic, typography } from "@/styles";
3+
import { colors, semantic } from "@/styles";
4+
import { zIndex } from "@/styles/zIndex.css";
45

56
export const overlay = style({
67
position: "fixed",
78
inset: 0,
89
backgroundColor: semantic.background.dim,
10+
zIndex: zIndex.overlay,
911
});
1012

1113
export const content = style({
@@ -15,25 +17,19 @@ export const content = style({
1517
right: 0,
1618
maxWidth: "48rem",
1719
margin: "0 auto",
18-
display: "flex",
19-
justifyContent: "center",
2020
backgroundColor: colors.common[100],
21-
borderTopLeftRadius: radius[120],
22-
borderTopRightRadius: radius[120],
21+
borderRadius: "2.8rem 2.8rem 0 0",
22+
zIndex: zIndex.modal,
2323
});
2424

2525
export const innerContent = style({
2626
width: "100%",
27-
minHeight: "32.6rem",
28-
maxHeight: "100vh",
27+
minHeight: "37.5rem",
28+
maxHeight: "calc(100dvh - 52px)",
2929
display: "flex",
3030
flexDirection: "column",
3131
});
3232

33-
export const handleContainer = style({
34-
padding: "1.2rem 16.2rem 1rem",
35-
});
36-
3733
export const handle = style({
3834
width: "5.1rem",
3935
height: "0.4rem",
@@ -43,29 +39,16 @@ export const handle = style({
4339
});
4440

4541
export const title = style({
46-
display: "flex",
47-
gap: "1rem",
42+
width: "100%",
4843
padding: "1.4rem 2rem",
49-
...typography.title2Sb,
50-
color: semantic.text.normal,
5144
});
5245

5346
export const sheetBody = style({
5447
display: "flex",
48+
flex: 1,
5549
flexDirection: "column",
5650
gap: "0.8rem",
57-
padding: "1.4rem 2rem 6rem",
58-
overflowY: "auto",
59-
});
60-
61-
export const sheetBodyTitle = style({
62-
...typography.title3Sb,
63-
color: semantic.text.normal,
64-
});
65-
66-
export const sheetBodyDescription = style({
67-
...typography.body2Rg,
68-
color: semantic.text.alternative,
51+
padding: "2rem",
6952
});
7053

7154
export const buttonContainer = style({
Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,79 @@
1-
import { type ReactNode } from "react";
2-
import { type DialogProps,Drawer } from "vaul";
3-
4-
import * as styles from "./BottomSheet.css";
5-
6-
export type BottomSheetProps = {
7-
title: string;
8-
trigger?: ReactNode;
9-
footer?: ReactNode;
10-
content?: ReactNode;
11-
} & DialogProps;
12-
13-
export const BottomSheet = ({
14-
title,
15-
trigger,
16-
footer,
17-
content,
18-
...props
19-
}: BottomSheetProps) => {
20-
return (
21-
<Drawer.Root {...props}>
22-
{trigger && <Drawer.Trigger asChild>{trigger}</Drawer.Trigger>}
23-
<Drawer.Portal>
24-
<Drawer.Overlay className={styles.overlay} />
25-
<Drawer.Content className={styles.content}>
26-
<section className={styles.innerContent}>
27-
<div className={styles.handleContainer}>
28-
<div className={styles.handle} />
29-
</div>
30-
<Drawer.Title className={styles.title}>{title}</Drawer.Title>
31-
<div className={styles.sheetBody}>{content}</div>
32-
{footer && <div className={styles.buttonContainer}>{footer}</div>}
33-
</section>
34-
</Drawer.Content>
35-
</Drawer.Portal>
36-
</Drawer.Root>
37-
);
1+
"use client";
2+
3+
import { BottomSheetBody } from "./BottomSheetBody";
4+
import { BottomSheetContent } from "./BottomSheetContent";
5+
import { BottomSheetFooter } from "./BottomSheetFooter";
6+
import { BottomSheetRoot } from "./BottomSheetRoot";
7+
import { BottomSheetTitle } from "./BottomSheetTitle";
8+
import { BottomSheetTrigger } from "./BottomSheetTrigger";
9+
10+
type BottomSheetComposition = {
11+
/**
12+
* 바텀시트의 상태 및 컨텍스트를 관리하는 최상위 컴포넌트 (`vaul`의 Drawer.Root 래핑)
13+
*/
14+
Root: typeof BottomSheetRoot;
15+
16+
/**
17+
* 바텀시트를 열기 위한 트리거 (버튼 등)
18+
* `asChild`로 감싸면 외부 요소를 그대로 트리거로 사용할 수 있음
19+
*/
20+
Trigger: typeof BottomSheetTrigger;
21+
22+
/**
23+
* 바텀시트의 콘텐츠를 감싸는 영역. Portal을 통해 렌더링되며 Overlay와 Content를 포함
24+
*/
25+
Content: typeof BottomSheetContent;
26+
27+
/**
28+
* 바텀시트의 제목 영역. 상단 핸들바와 함께 사용
29+
*/
30+
Title: typeof BottomSheetTitle;
31+
32+
/**
33+
* 바텀시트의 메인 콘텐츠 영역
34+
*/
35+
Body: typeof BottomSheetBody;
36+
37+
/**
38+
* 바텀시트의 하단 영역. 주로 버튼/액션을 배치하는 데 사용
39+
*/
40+
Footer: typeof BottomSheetFooter;
41+
};
42+
43+
/**
44+
* BottomSheet 컴포넌트
45+
* @description Compound Component Pattern을 사용하여 화면 하단에서 나타나는 패널을 구현한 컴포넌트입니다.
46+
*
47+
* @see vaul 라이브러리를 기반으로 구현되었습니다. {@link https://vaul.emilkowal.ski/}
48+
*
49+
* @example
50+
* ```tsx
51+
* <BottomSheet.Root>
52+
* <BottomSheet.Trigger asChild>
53+
* <Button>바텀시트 열기</Button>
54+
* </BottomSheet.Trigger>
55+
*
56+
* <BottomSheet.Content>
57+
* <BottomSheet.Title>
58+
* 제목
59+
* </BottomSheet.Title>
60+
*
61+
* <BottomSheet.Body>
62+
* <p>여기에 컨텐츠가 들어갑니다.</p>
63+
* </BottomSheet.Body>
64+
*
65+
* <BottomSheet.Footer>
66+
* <Button size="fullWidth">확인</Button>
67+
* </BottomSheet.Footer>
68+
* </BottomSheet.Content>
69+
* </BottomSheet.Root>
70+
* ```
71+
*/
72+
export const BottomSheet: BottomSheetComposition = {
73+
Root: BottomSheetRoot,
74+
Title: BottomSheetTitle,
75+
Trigger: BottomSheetTrigger,
76+
Content: BottomSheetContent,
77+
Body: BottomSheetBody,
78+
Footer: BottomSheetFooter,
3879
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import { type ReactNode } from "react";
4+
5+
import * as styles from "./BottomSheet.css";
6+
7+
export type BottomSheetBodyProps = {
8+
children: ReactNode;
9+
className?: string;
10+
};
11+
12+
export function BottomSheetBody({ children, className }: BottomSheetBodyProps) {
13+
return (
14+
<div
15+
className={
16+
className ? `${styles.sheetBody} ${className}` : styles.sheetBody
17+
}
18+
>
19+
{children}
20+
</div>
21+
);
22+
}
23+
24+
BottomSheetBody.displayName = "BottomSheet.Body";
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { type ReactNode } from "react";
4+
import { Drawer } from "vaul";
5+
6+
import * as styles from "./BottomSheet.css";
7+
8+
export type BottomSheetContentProps = {
9+
children: ReactNode;
10+
className?: string;
11+
};
12+
13+
export function BottomSheetContent({
14+
children,
15+
className,
16+
}: BottomSheetContentProps) {
17+
return (
18+
<Drawer.Portal>
19+
<Drawer.Overlay className={styles.overlay} />
20+
<Drawer.Content className={styles.content}>
21+
<section
22+
className={
23+
className
24+
? `${styles.innerContent} ${className}`
25+
: styles.innerContent
26+
}
27+
>
28+
{children}
29+
</section>
30+
</Drawer.Content>
31+
</Drawer.Portal>
32+
);
33+
}
34+
35+
BottomSheetContent.displayName = "BottomSheet.Content";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import { type ReactNode } from "react";
4+
5+
import * as styles from "./BottomSheet.css";
6+
7+
export type BottomSheetFooterProps = {
8+
children: ReactNode;
9+
className?: string;
10+
};
11+
12+
export function BottomSheetFooter({
13+
children,
14+
className,
15+
...props
16+
}: BottomSheetFooterProps) {
17+
return (
18+
<div
19+
className={
20+
className
21+
? `${styles.buttonContainer} ${className}`
22+
: styles.buttonContainer
23+
}
24+
{...props}
25+
>
26+
{children}
27+
</div>
28+
);
29+
}
30+
31+
BottomSheetFooter.displayName = "BottomSheet.Footer";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type ReactNode } from "react";
2+
import { type DialogProps, Drawer } from "vaul";
3+
4+
export type BottomSheetRootProps = {
5+
children: ReactNode;
6+
} & DialogProps;
7+
8+
export function BottomSheetRoot({ children, ...props }: BottomSheetRootProps) {
9+
return <Drawer.Root {...props}>{children}</Drawer.Root>;
10+
}
11+
12+
BottomSheetRoot.displayName = "BottomSheet.Root";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import { type ReactNode } from "react";
4+
import { Drawer } from "vaul";
5+
6+
import * as styles from "./BottomSheet.css";
7+
8+
export type BottomSheetTitleProps = {
9+
children: ReactNode;
10+
className?: string;
11+
};
12+
13+
export function BottomSheetTitle({
14+
children,
15+
className,
16+
...props
17+
}: BottomSheetTitleProps) {
18+
return (
19+
<div>
20+
<div className={styles.handle} />
21+
<Drawer.Title
22+
className={className ? `${styles.title} ${className}` : styles.title}
23+
{...props}
24+
>
25+
{children}
26+
</Drawer.Title>
27+
</div>
28+
);
29+
}
30+
31+
BottomSheetTitle.displayName = "BottomSheet.Title";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use client";
2+
3+
import { type ReactNode } from "react";
4+
import { Drawer } from "vaul";
5+
6+
export type BottomSheetTriggerProps = {
7+
children: ReactNode;
8+
};
9+
10+
export function BottomSheetTrigger({ children }: BottomSheetTriggerProps) {
11+
return <Drawer.Trigger asChild>{children}</Drawer.Trigger>;
12+
}
13+
14+
BottomSheetTrigger.displayName = "BottomSheet.Trigger";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export { BottomSheet } from "./BottomSheet";
2+
export { BottomSheetBody } from "./BottomSheetBody";
3+
export { BottomSheetContent } from "./BottomSheetContent";
4+
export { BottomSheetFooter } from "./BottomSheetFooter";
5+
export { BottomSheetRoot } from "./BottomSheetRoot";
6+
export { BottomSheetTitle } from "./BottomSheetTitle";
7+
export { BottomSheetTrigger } from "./BottomSheetTrigger";

0 commit comments

Comments
 (0)