Skip to content

Commit

Permalink
[Feature] 모달 컴포넌트 (#25)
Browse files Browse the repository at this point in the history
* feat: 모달 기본 UI 구현

* feat: 라우팅으로 모달을 열고 닫기 위한 로직 추가

* feat: modal 병렬 라우팅 레이아웃 추가 및 JotaiProvider 삭제

* fix: Modal 닫기 버튼 onClick 함수 변경 및 title 정렬

* feat: Modal ui 패키지로 이동, hook 분리

* fix: 타입 경고 해결

* fix: hook 내보내기 위치 변경

* feat: ui style 재생성

* feat: 라우팅 활용 모달 예시 추가

* feat: jsdoc 추가 및 태그 수정

* fix: JotaiProvider 원복

* fix: title prop 삭제

* fix: open 함수 네이밍 변경

* fix: title 삭제에 따른 예시 prop 삭제

* feat: close 아이콘 적용

* refactor: PropsWithChildren 타입 사용

* feat: useClickOutside 훅 분리

* fix: 불필요한 코드 삭제 및 스토리북 수정

* fix: closeModal props 이름 변경

* refactor: any 타입 좁히기

* feat: setIsOpen 내보내기 추가

* fix: 빌드 에러 해결
  • Loading branch information
hamo-o authored Aug 20, 2024
1 parent 9d0c1a4 commit ded17a5
Show file tree
Hide file tree
Showing 14 changed files with 1,318 additions and 1,269 deletions.
22 changes: 22 additions & 0 deletions apps/admin/app/@modal/(.)participants/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { Flex } from "@styled-system/jsx";
import { Modal } from "@wow-class/ui";
import { useModalRoute } from "@wow-class/ui/hooks";
import Button from "wowds-ui/Button";

const TestModal = () => {
const { closeModal } = useModalRoute();
return (
<Modal onClose={closeModal}>
<Flex gap="sm" width="21rem">
<Button variant="outline" onClick={closeModal}>
취소
</Button>
<Button>저장하기</Button>
</Flex>
</Modal>
);
};

export default TestModal;
5 changes: 5 additions & 0 deletions apps/admin/app/@modal/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Default = () => {
return null;
};

export default Default;
15 changes: 11 additions & 4 deletions apps/admin/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import "./global.css";
import "wowds-ui/styles.css";
import "@wow-class/ui/styles.css";

import { JotaiProvider } from "components/JotaiProvider";
import Navbar from "components/Navbar";
import type { Metadata } from "next";

import { JotaiProvider } from "../components/JotaiProvider";
import type { ReactNode } from "react";

export const metadata: Metadata = {
title: "Create Next App",
Expand All @@ -13,13 +14,19 @@ export const metadata: Metadata = {

const RootLayout = ({
children,
modal,
}: Readonly<{
children: React.ReactNode;
children: ReactNode;
modal: ReactNode;
}>) => {
return (
<html lang="ko">
<body>
<JotaiProvider>{children}</JotaiProvider>
<JotaiProvider>
<Navbar />
{children}
{modal}
</JotaiProvider>
</body>
</html>
);
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"exports": {
".": "./src/components/index.ts",
"./hooks": "./src/hooks/index.ts",
"./styles.css": "./src/styles.css"
},
"scripts": {
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/assets/images/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions packages/ui/src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useModalState } from "src/hooks";

import Text from "../Text";
import Modal from ".";

const meta = {
title: "Shared/Modal",
component: Modal,
tags: ["autodocs"],
parameters: {
componentSubtitle: "Modal 컴포넌트",
},
argTypes: {
onClose: {
description: "Modal 컴포넌트를 닫을 수 있는 함수를 나타냅니다.",
table: {
type: { summary: "function" },
control: false,
},
},
children: {
description: "Modal 컴포넌트의 자식 컴포넌트를 나타냅니다.",
table: {
type: { summary: "ReactNode" },
control: false,
},
},
},
} satisfies Meta<typeof Modal>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
children: (
<Text as="h1" typo="h1">
상세 정보가 등록되었어요.
</Text>
),
onClose: () => {
console.log("모달 닫기");
},
},
};

export const StateModal = () => {
const { isOpen, openModal, closeModal } = useModalState();

return (
<>
<button onClick={openModal}>모달 열기</button>
{isOpen && (
<Modal onClose={closeModal}>
<Text as="h1" typo="h1">
상세 정보가 등록되었어요.
</Text>
</Modal>
)}
</>
);
};
74 changes: 74 additions & 0 deletions packages/ui/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";

import { css } from "@styled-system/css";
import { Flex, styled } from "@styled-system/jsx";
import Image from "next/image";
import type { PropsWithChildren } from "react";

import closeUrl from "../../assets/images/close.svg";
import { useClickOutside } from "../../hooks";

/**
* @description 모달 컴포넌트입니다.
*
* @param {() => void} onClose - 모달 컴포넌트를 닫기 위한 함수.
* @param {ReactNode} [children] - 모달 컴포넌트에 들어갈 자식 요소.
*/

export interface ModalProps extends PropsWithChildren {
onClose: () => void;
}

const Modal = ({ children, onClose }: ModalProps) => {
const modal = useClickOutside<HTMLDialogElement>(onClose);

return (
<Flex alignItems="center" className={backDropStyle} justifyContent="center">
<styled.dialog className={dialogStyle} ref={modal}>
<Image
alt="close-icon"
className={closeButtonStyle}
height={24}
src={closeUrl}
width={24}
onClick={onClose}
/>
{children}
</styled.dialog>
</Flex>
);
};

const dialogStyle = css({
width: "40.75rem",
height: "28.125rem",

display: "flex",
alignItems: "center",
justifyContent: "center",

position: "relative",

borderRadius: "md",
shadow: "mono",
});

const backDropStyle = css({
width: "100vw",
height: "100vh",

position: "absolute",
top: 0,
left: 0,

background: "backgroundDimmer",
});

const closeButtonStyle = css({
position: "absolute",
top: "xl",
right: "xl",
cursor: "pointer",
});

export default Modal;
1 change: 1 addition & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as Header } from "./Header";
export { default as Modal } from "./Modal";
export { default as NavItem } from "./NavItem";
export { default as Space } from "./Space";
export { default as Table } from "./Table";
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as useClickOutside } from "./useClickOutside";
export { default as useModalRoute } from "./useModalRoute";
export { default as useModalState } from "./useModalState";
28 changes: 28 additions & 0 deletions packages/ui/src/hooks/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { RefObject } from "react";
import { useEffect, useRef } from "react";

const useClickOutside = <T extends HTMLElement>(
onClickOutside: (event: MouseEvent) => void
) => {
const notClickableRef: RefObject<T> = useRef<T>(null);

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Node;
const isOutside =
notClickableRef.current && !notClickableRef.current.contains(target);
if (!isOutside) return;

onClickOutside(e);
};

document.addEventListener("click", handleClickOutside, true);
return () => {
document.removeEventListener("click", handleClickOutside, true);
};
}, [notClickableRef, onClickOutside]);

return notClickableRef;
};

export default useClickOutside;
14 changes: 14 additions & 0 deletions packages/ui/src/hooks/useModalRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useRouter } from "next/navigation";
import { useCallback } from "react";

const useModalRoute = () => {
const router = useRouter();

const closeModal = useCallback(() => {
router.back();
}, [router]);

return { closeModal };
};

export default useModalRoute;
17 changes: 17 additions & 0 deletions packages/ui/src/hooks/useModalState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useCallback, useState } from "react";

const useModalState = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);

const openModal = useCallback(() => setIsOpen(() => true), []);
const closeModal = useCallback(() => setIsOpen(() => false), []);

return {
isOpen,
setIsOpen,
openModal,
closeModal,
};
};

export default useModalState;
63 changes: 57 additions & 6 deletions packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,10 @@ progress {
--colors-mono-950: #121212;
--colors-white: #ffffff;
--colors-black: #000000;
--spacing-xl: 1.5rem;
--radii-md: 0.5rem;
--border-widths-button: 1px;
--shadows-mono: 0px 4px 8px 0px rgba(0, 0, 0, 0.2);
--colors-primary: #368ff7;
--colors-success: #2a8642;
--colors-error: #bb362a;
Expand Down Expand Up @@ -270,6 +273,12 @@ progress {
--colors-blue-disabled: #d7e9fd;
--colors-text-blue-disabled: #afd2fc;
}
.textStyle_h1:not(#\#):not(#\#):not(#\#):not(#\#) {
letter-spacing: -0.015rem;
font-size: 1.5rem;
line-height: 130%;
font-weight: 600;
}
.textStyle_body1:not(#\#):not(#\#):not(#\#):not(#\#) {
letter-spacing: -0.01rem;
font-size: 1rem;
Expand Down Expand Up @@ -305,12 +314,6 @@ progress {
line-height: 130%;
font-weight: 700;
}
.textStyle_h1:not(#\#):not(#\#):not(#\#):not(#\#) {
letter-spacing: -0.015rem;
font-size: 1.5rem;
line-height: 130%;
font-weight: 600;
}
.textStyle_h2:not(#\#):not(#\#):not(#\#):not(#\#) {
letter-spacing: -0.01125rem;
font-size: 1.125rem;
Expand Down Expand Up @@ -370,6 +373,36 @@ progress {
.c_primary:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
color: var(--colors-primary);
}
.ta_center:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
text-align: center;
}
.w_40\.75rem:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
width: 40.75rem;
}
.h_28\.125rem:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
height: 28.125rem;
}
.gap_1\.75rem:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
gap: 1.75rem;
}
.pos_relative:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
position: relative;
}
.bdr_md:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
border-radius: var(--radii-md);
}
.bx-sh_mono:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
box-shadow: var(--shadows-mono);
}
.h_100vh:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
height: 100vh;
}
.pos_absolute:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
position: absolute;
}
.bg_backgroundDimmer:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
background: var(--colors-background-dimmer);
}
.li-s_none:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
list-style: none;
}
Expand Down Expand Up @@ -764,6 +797,24 @@ progress {
.fs_14px:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
font-size: 14px;
}
.jc_center:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
justify-content: center;
}
.flex-d_column:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
flex-direction: column;
}
.top_0:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
top: 0;
}
.left_0:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
left: 0;
}
.top_xl:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
top: var(--spacing-xl);
}
.right_xl:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
right: var(--spacing-xl);
}
.ml_auto:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) {
margin-left: auto;
}
Expand Down
Loading

0 comments on commit ded17a5

Please sign in to comment.