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

Feat/modal #11

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {ComponentStory, ComponentMeta} from "@storybook/react";
import React from "react";

import {Modal} from "./Modal";

export default {
title: "atoms/Modal",
component: Modal,
argTypes: {
},
args: {
title: "title",
children: "content",
confirm: {},
cancel: {},
},
} as ComponentMeta<typeof Modal>;

const Template: ComponentStory<typeof Modal> = (args) => (
<div css={{width: "100%", height: "100vh"}}>
<Modal {...args} />
</div>
);

export const Default = Template.bind({});
Default.args = {

};
120 changes: 120 additions & 0 deletions components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {useTheme} from "@emotion/react"
import React, {useCallback, MouseEventHandler, ReactNode, useRef} from "react"
import {Button} from "components/Button"
import {Icon} from "components/Icon"

export type ModalProps = {
onClose: ()=>void
// header
title?: string
// content
children: ReactNode
// footer
cancel?: {
children?: string
onClick?: ()=>void
}
confirm?: {
disabled?: boolean
children?: string
onClick?: ()=>void
}
}

export const Modal = ({
onClose,
title,
children,
confirm,
cancel,
}: ModalProps) => {
const theme = useTheme()

const modalRef = useRef<HTMLDivElement>(null)

const handleClickOutside: MouseEventHandler<HTMLDivElement> = useCallback((event)=>{
const isClickedInside = (event.target instanceof Node) && modalRef.current?.contains(event.target)

if (isClickedInside) return;

onClose()
},[onClose])

const handleClickClose: MouseEventHandler<HTMLDivElement> = useCallback((event)=>{
onClose()
},[onClose])

return (
<article
onClick={handleClickOutside}
css={{
display: "flex",
flexDirection:"column",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.6)",
}}
>
<div
css={{
display: "flex",
flexDirection:"column",
alignItems: "stretch",
backgroundColor: theme.colors.white,
width: "100%",
maxWidth: "386px",
height: "auto",
borderRadius: "12px",
padding: 16
}}
>
<div
ref={modalRef}
css={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
}}>
<div css={{fontSize: "1.125rem", fontWeight: "bold"}}>
{title}
</div>
<div onClick={handleClickClose} css={{cursor: "pointer"}}>
<Icon name={"close"}/>
</div>
</div>
<div
css={{
width: "100%",
flexGrow: 1,
flexShrink: 1,
marginBottom: 16
}}
>
{children}
</div>
<div css={{width: "100%"}}>
<div css={{margin: "-6px", flex: 1, display: "flex", alignItems: "center"}}>
{cancel && (
<div css={{flex: 1, padding: "6px"}}>
<Button onClick={cancel.onClick}>
{cancel.children || "Cancel"}
</Button>
</div>
)}
{confirm && (
<div css={{flex: 1, padding: "6px"}}>
<Button color={"main"} disabled={confirm.disabled} onClick={confirm.onClick}>
{confirm.children || "Confirm"}
</Button>
</div>
)}
</div>
</div>
</div>
</article>
)
}
2 changes: 2 additions & 0 deletions components/Modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

export * from "./Modal"
24 changes: 24 additions & 0 deletions components/ModalLinkSent/ModalLinkSent.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {ComponentStory, ComponentMeta} from "@storybook/react";
import React from "react";

import {ModalLinkSent} from "./ModalLinkSent";

export default {
title: "atoms/ModalLinkSent",
component: ModalLinkSent,
argTypes: {
},
args: {
},
} as ComponentMeta<typeof ModalLinkSent>;

const Template: ComponentStory<typeof ModalLinkSent> = (args) => (
<div css={{width: "100%", height: "100vh"}}>
<ModalLinkSent {...args} />
</div>
);

export const Default = Template.bind({});
Default.args = {

};
23 changes: 23 additions & 0 deletions components/ModalLinkSent/ModalLinkSent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react"
import {ModalProps} from "components/Modal"
import {Modal} from "components/Modal"

export type ModalLinkSentProps = {
onClose: ModalProps["onClose"]
onConfirm?: ()=>void
}

export const ModalLinkSent = ({
onClose,
onConfirm,
}: ModalLinkSentProps) => {
return (
<Modal
onClose={onClose}
title={"링크 발송"}
confirm={{children: "확인", onClick: onConfirm}}
>
{"가입하신 이메일로 링크를 발송했습니다. 인증확인 후 비밀번호를 변경하세요!"}
</Modal>
)
}
2 changes: 2 additions & 0 deletions components/ModalLinkSent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

export * from "./ModalLinkSent"
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"react-redux": "^8.0.1",
"react-svg": "^14.1.18",
"redux-logger": "^3.0.6",
"redux-saga": "^1.1.3"
"redux-saga": "^1.1.3",
"shallowequal": "^1.1.0",
"utility-types": "^3.10.0"
},
"devDependencies": {
"@babel/core": "^7.17.9",
Expand All @@ -41,6 +43,7 @@
"@types/react": "18.0.1",
"@types/react-dom": "18.0.0",
"@types/redux-logger": "^3.0.9",
"@types/shallowequal": "^1.1.1",
"babel-loader": "^8.2.4",
"eslint": "8.13.0",
"eslint-config-next": "12.1.4",
Expand Down
6 changes: 4 additions & 2 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type {AppProps} from "next/app";
import {Provider} from "react-redux";
import {wrapper} from "modules/store";
import {GlobalStyle} from "styles/globalStyle";
import {ThemeProvider} from "styles/theme";
import {ModalProvider} from "utils/modal";

function MyApp({Component, pageProps}: AppProps) {
return (
<ThemeProvider>
<GlobalStyle />
<Component {...pageProps} />
<ModalProvider>
<Component {...pageProps} />
</ModalProvider>
</ThemeProvider>
);
}
Expand Down
17 changes: 17 additions & 0 deletions pages/test/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type {NextPage} from "next"
import {LayoutCenter} from "components/LayoutCenter"
import {ModalLinkSent} from "components/ModalLinkSent";
import {useModal} from "utils/modal";


const Test: NextPage = () => {
const {toggleModal} = useModal(ModalLinkSent, {})

return (
<LayoutCenter>
<button onClick={()=>toggleModal(true)}>open</button>
</LayoutCenter>
)
}

export default Test
1 change: 1 addition & 0 deletions styles/globalStyle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const GlobalStyle = () => {

body {
width:100%;
line-height: 1.5;
overflow-x:hidden;
font-family: 'Spoqa Han Sans Neo', sans-serif;
}
Expand Down
66 changes: 66 additions & 0 deletions utils/modal/hook.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {FunctionComponent, useCallback, useEffect, useMemo, useState} from "react"
import shallowequal from "shallowequal";
import {Optional} from "utility-types";
import {useModalContext} from "./provider"

type Props<T extends FunctionComponent<any>> = Parameters<T>[0]

export const useModal = <T extends FunctionComponent<any>>(
component: T,
props: Optional<Props<T>, "onClose">,
id?: string
)=>{
type ModalProps = Optional<Props<T>, "onClose">

const {upsertModal, removeModal} = useModalContext()

const [isOpened, setIsOpened] = useState(false)
const [memoizedProps, setMemoizedProps] = useState<ModalProps>(props)

useEffect(()=>{
setMemoizedProps(prev => !shallowequal(props, prev) ? props : prev)
},[props])

const modalId = useMemo(()=>id || component.name, [component.name, id])

const renderModal = useCallback((newProps: Optional<Parameters<T>[0], "onClose">) =>{
upsertModal({
id: modalId,
component,
props: {
...newProps,
onClose: ()=>{
setIsOpened(false)
newProps.onClose?.()
},
},
})
},[component, modalId, upsertModal])

const toggleModal = useCallback((isOpened?: boolean) =>{
if (isOpened){
setIsOpened(isOpened)
}
else {
setIsOpened(prev => !prev)
}
},[])

useEffect(()=>{
if (isOpened){
renderModal({...memoizedProps})
} else {
removeModal(modalId)
}
},[isOpened, modalId, memoizedProps, removeModal, renderModal])

// remove modal when current component is unmounted
useEffect(()=>(()=>{
removeModal(modalId)
}),[modalId, removeModal])

return useMemo(()=>({
id: modalId,
toggleModal,
}), [modalId, toggleModal])
}
2 changes: 2 additions & 0 deletions utils/modal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./provider"
export * from "./hook"
Loading