Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] 액션시트 컴포넌트 #165

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/scripts/generateBuildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const excludedComponents = [
"CollectionContext",
"DropDownOptionList",
"pickerComponents",
"ActionSheetOverlay",
];

const getFilteredComponentFiles = async (directoryPath: string) => {
Expand Down
20 changes: 20 additions & 0 deletions packages/wow-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@
"types": "./dist/components/Box/index.d.ts",
"require": "./dist/Box.cjs",
"import": "./dist/Box.js"
},
"./ActionSheetBody": {
"types": "./dist/components/ActionSheet/ActionSheetBody.d.ts",
"require": "./dist/ActionSheetBody.cjs",
"import": "./dist/ActionSheetBody.js"
},
"./ActionSheetFooter": {
"types": "./dist/components/ActionSheet/ActionSheetFooter.d.ts",
"require": "./dist/ActionSheetFooter.cjs",
"import": "./dist/ActionSheetFooter.js"
},
"./ActionSheetHeader": {
"types": "./dist/components/ActionSheet/ActionSheetHeader.d.ts",
"require": "./dist/ActionSheetHeader.cjs",
"import": "./dist/ActionSheetHeader.js"
},
"./ActionSheet": {
"types": "./dist/components/ActionSheet/index.d.ts",
"require": "./dist/ActionSheet.cjs",
"import": "./dist/ActionSheet.js"
}
},
"keywords": [],
Expand Down
4 changes: 4 additions & 0 deletions packages/wow-ui/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export default {
Checkbox: "./src/components/Checkbox",
Button: "./src/components/Button",
Box: "./src/components/Box",
ActionSheetBody: "./src/components/ActionSheet/ActionSheetBody",
ActionSheetFooter: "./src/components/ActionSheet/ActionSheetFooter",
ActionSheetHeader: "./src/components/ActionSheet/ActionSheetHeader",
ActionSheet: "./src/components/ActionSheet",
},
output: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Meta } from "@storybook/react";

import ActionSheet from "@/components/ActionSheet";
import Box from "@/components/Box";
import Button from "@/components/Button";
import useOpenState from "@/hooks/useOpenState";

const meta = {
title: "UI/ActionSheet",
component: ActionSheet,
tags: ["autodocs"],
parameters: {
componentSubtitle: "액션시트 컴포넌트",
a11y: {
config: {
rules: [{ id: "color-contrast", enabled: false }],
},
},
},
argTypes: {
isOpen: {
description: "액션시트의 표시 여부를 설정합니다.",
control: {
type: "boolean",
},
},
onClose: {
description: "액션시트를 닫는 함수입니다.",
control: false,
},
style: {
description: "액션시트의 커스텀 스타일을 설정합니다.",
table: {
type: { summary: "CSSProperties" },
defaultValue: { summary: "{}" },
},
control: false,
},
className: {
description: "액션시트에 전달하는 커스텀 클래스를 설정합니다.",
table: {
type: { summary: "string" },
},
control: false,
},
},
} satisfies Meta<typeof ActionSheet>;

export default meta;

export const Default = () => {
const { onClose } = useOpenState();

return (
<ActionSheet isOpen={true} onClose={onClose}>
<ActionSheet.Header
style={{ paddingBottom: "1rem" }}
subText="subtext"
text="Text"
/>
<ActionSheet.Footer>
<Button style={{ minWidth: "100%" }}>Button</Button>
</ActionSheet.Footer>
</ActionSheet>
);
};

export const Controlled = () => {
const { open, onClose, onOpen } = useOpenState();

return (
<>
<Button onClick={onOpen}>Open</Button>
<ActionSheet isOpen={open} onClose={onClose}>
<ActionSheet.Header subText="subtext" text="Text" />
<ActionSheet.Body gap="md" paddingY="md">
<Box text="Box" />
<Box text="Box" />
</ActionSheet.Body>
<ActionSheet.Footer gap="md">
<Button variant="outline">Button</Button>
<Button>Button</Button>
</ActionSheet.Footer>
</ActionSheet>
</>
);
};
24 changes: 24 additions & 0 deletions packages/wow-ui/src/components/ActionSheet/ActionSheetBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";
import type { FlexProps } from "@styled-system/jsx";
import { Flex } from "@styled-system/jsx";
import type { ReactNode } from "react";

/**
* @description ActionSheet의 바디 요소를 나타내는 ActionSheetBody 컴포넌트입니다.
*
* @param {string} children 액션시트의 바디 요소.
*/
export interface ActionSheetBodyProps extends FlexProps {
children: ReactNode;
}

const ActionSheetBody = ({ children, ...rest }: ActionSheetBodyProps) => {
return (
<Flex direction="column" width="100%" {...rest}>
{children}
</Flex>
);
};

ActionSheetBody.displayName = "ActionSheetBody";
export default ActionSheetBody;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createContext } from "react";

import useSafeContext from "../../hooks/useSafeContext";

export interface ActionSheetContextProps {
onClose: () => void;
}

export const ActionSheetContext = createContext<ActionSheetContextProps | null>(
null
);

export const useActionSheetContext = () => {
const context = useSafeContext(ActionSheetContext);
return context;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import type { FlexProps } from "@styled-system/jsx";
import { Flex } from "@styled-system/jsx";
import type { ReactElement } from "react";

import type Button from "@/components/Button";
import type TextButton from "@/components/TextButton";

/**
* @description ActionSheet의 푸터 요소를 나타내는 ActionSheetFooter 컴포넌트입니다.
*
* @param {children} children 액션시트의 푸터에 들어갈 버튼.
*/
export interface ActionSheetFooterProps extends FlexProps {
children:
| ReactElement<typeof Button | typeof TextButton>
| ReactElement<typeof Button | typeof TextButton>[];
}

const ActionSheetFooter = ({ children, ...rest }: ActionSheetFooterProps) => {
return (
<Flex width="100%" {...rest}>
{children}
</Flex>
);
};

ActionSheetFooter.displayName = "ActionSheetFooter";
export default ActionSheetFooter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { styled } from "@styled-system/jsx";
import type { CSSProperties } from "react";
import { Close } from "wowds-icons";

import { useActionSheetContext } from "@/components/ActionSheet/ActionSheetContext";

/**
* @description ActionSheet의 헤더 요소를 나타내는 ActionSheetHeader 컴포넌트입니다.
*
* @param {string} text 액션시트의 헤더 텍스트.
* @param {string} subText 액션시트의 헤더 서브 텍스트.
* @param {CSSProperties} [style] 액션시트 헤더의 커스텀 스타일.
* @param {string} [className] 액션시트 헤더에 전달하는 커스텀 클래스.
*/
export interface ActionSheetHeaderProps {
text: string;
subText: string;
style?: CSSProperties;
className?: string;
}

const ActionSheetHeader = ({
text,
subText,
...rest
}: ActionSheetHeaderProps) => {
const { onClose } = useActionSheetContext();

return (
<styled.header
alignItems="flex-end"
display="flex"
flexDir="column"
width="100%"
{...rest}
>
<Close stroke="outline" style={{ cursor: "pointer" }} onClick={onClose} />
<styled.h1 textStyle="h1" width="100%">
{text}
</styled.h1>
<styled.p textStyle="body1" width="100%">
{subText}
</styled.p>
</styled.header>
);
};

ActionSheetHeader.displayName = "ActionSheetHeader";
export default ActionSheetHeader;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { styled } from "@styled-system/jsx";

const ActionSheetOverlay = () => {
return (
<styled.div
backgroundColor="backgroundDimmer"
height="100vh"
left={0}
position="fixed"
top={0}
width="100vw"
zIndex={9998}
/>
);
};

export default ActionSheetOverlay;
122 changes: 122 additions & 0 deletions packages/wow-ui/src/components/ActionSheet/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import { cva } from "@styled-system/css";
import type { CSSProperties, ReactElement } from "react";
import { useEffect, useRef, useState } from "react";

import useClickOutside from "@/hooks/useClickOutside";

import ActionSheetBody from "./ActionSheetBody";
import { ActionSheetContext } from "./ActionSheetContext";
import ActionSheetFooter from "./ActionSheetFooter";
import ActionSheetHeader from "./ActionSheetHeader";
import ActionSheetOverlay from "./ActionSheetOverlay";

/**
* @description ActionSheet 컴포넌트입니다.
*
* @param {boolean} isOpen 액션시트의 표시 여부.
* @param {onClose} onClose 액션시트를 닫는 함수.
* @param {CSSProperties} [style] 액션시트의 커스텀 스타일.
* @param {string} [className] 액션시트에 전달하는 커스텀 클래스.
*/

export interface ActionSheetProps {
children:
| [
ReactElement<typeof ActionSheetHeader>,
ReactElement<typeof ActionSheetFooter>,
]
| [
ReactElement<typeof ActionSheetHeader>,
ReactElement<typeof ActionSheetBody>,
ReactElement<typeof ActionSheetFooter>,
];
isOpen: boolean;
onClose: () => void;
style?: CSSProperties;
className?: string;
}

const ActionSheet = ({
isOpen,
onClose,
children,
...rest
}: ActionSheetProps) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const [state, setState] = useState<"open" | "close">("close");

const handleClickClose = () => {
setState("close");
setTimeout(() => {
onClose();
}, 100);
};

useClickOutside(dialogRef, handleClickClose);

useEffect(() => {
if (isOpen) {
const timer = setTimeout(() => {
setState("open");
}, 100);
return () => clearTimeout(timer);
}
}, [isOpen]);

return (
isOpen && (
<ActionSheetContext.Provider value={{ onClose: handleClickClose }}>
<dialog className={dialogStyle({ state })} ref={dialogRef} {...rest}>
{children}
</dialog>
{/* TODO: 공통 컴포넌트? */}
<ActionSheetOverlay />
</ActionSheetContext.Provider>
)
);
};

ActionSheet.Header = ActionSheetHeader;
ActionSheet.Body = ActionSheetBody;
ActionSheet.Footer = ActionSheetFooter;

const dialogStyle = cva({
base: {
width: 390,

padding: "1.25rem 1rem",

display: "flex",
flexDir: "column",
alignItems: "center",

borderTopRadius: "md",
overflow: "hidden",

position: "fixed",
bottom: 0,
left: "50%",
translate: "-50%",

transition: "transform",
transitionDelay: "0.8",
transitionTimingFunction: "ease-in-out",

zIndex: 9999,
},
variants: {
state: {
open: {
transform: "translateY(0)",
},
close: {
transform: "translateY(100%)",
},
},
},
});

ActionSheet.displayName = "ActionSheet";
export default ActionSheet;
Loading
Loading