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: useModal 훅 구현 #24

Merged
merged 10 commits into from
Jun 11, 2024
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,44 @@ const SomeComponent = () => {
);
};
```

### useModal

A hook for easily managing an animated modal through a portal.

#### Function Arguments

modalProps object is accepted. This object is structured as follows:

```ts
interface UseModalProps {
modalRoot?: ModalRoot;
overlayClose?: boolean;
overlayAnimation?: {
showClassName?: string;
hideClassName?: string;
};
modalAnimation?: {
showClassName?: string;
hideClassName?: string;
};
}
```

`modalRoot`: The HTMLElement where the modal will be rendered. The default is `document.body`.

`overlayClose`: Sets whether clicking on the overlay closes the modal. The default is `true`.

`overlayAnimation`: The animation className applied to the overlay. It can accept two key-value pairs: `showClassName` and `hideClassName`.

`modalAnimation`: The animation className applied to the modal. It can accept two key-value pairs: `showClassName` and `hideClassName`.

#### Return Values

`Modal`: A component that renders its children to the specified root through a portal.

`show`: Opens the modal.

`hide`: Closes the modal.

`isShow`: Indicates whether the modal is open.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useThrottle from './useThrottle/useThrottle';
import useDebounce from './useDebounce/useDebounce';
import useLocalStorage from './useLocalStorage/useLocalStorage';
import useDisclosure from './useDisclosure/useDisclosure';
import useModal from './useModal/useModal';

export {
useInput,
Expand All @@ -26,4 +27,5 @@ export {
useDebounce,
useLocalStorage,
useDisclosure,
useModal,
};
77 changes: 77 additions & 0 deletions src/stories/useModal/Docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Canvas, Meta, Description } from '@storybook/blocks';
import * as Modal from './Modal.stories';

<Meta of={Modal} />

# useModal

애니메이션이 적용된 Modal을 portal을 통해 간편하게 관리하기 위한 훅입니다.

## 함수 인자

modalProps객체를 받습니다. 해당 객체는 아래와 같이 구성됩니다.

```ts
interface UseModalProps {
modalRoot?: ModalRoot;
overlayClose?: boolean;
overlayAnimation?: {
showClassName?: string;
hideClassName?: string;
};
modalAnimation?: {
showClassName?: string;
hideClassName?: string;
};
}
```

`modalRoot`: 모달을 렌더링할 HTMLElement입니다. default는 `document.body`입니다.

`overlayClose`: overlay를 눌러 modal을 닫을지를 설정합니다. default는 `true`입니다.

`overlayAnimation`: Overlay에 적용될 애니메이션 className입니다. `showClassName`과 `hideClassName` 두 가지 key-value를 받을 수 있습니다.

`modalAnimation`: Modal에 적용될 애니메이션 className입니다. `showClassName`과 `hideClassName` 두 가지 key-value를 받을 수 있습니다.

## 반환값

`Modal`: 컴포넌트로,해당 컴포넌트로 감싸진 children이 지정한 root에 portal을 통해 렌더링 됩니다.

`show`: 모달을 엽니다.

`hide`: 모달을 닫습니다.

`isShow`: 모달이 열려있는지 상태를 나타냅니다.

```tsx
function Modal() {
const { Modal, show, isShow, hide } = useModal({
modalAnimation: {
showClassName: showStyle,
hideClassName: hideStyle,
},
overlayAnimation: {
showClassName: overlayShow,
hideClassName: overlayHide,
},
});

const handleClick = () => {
if (isShow) hide();
show();
};

return (
<div>
<button onClick={handleClick}>{isShow ? 'hide' : 'show'}</button>
<Modal overlayClassName={Overlay} modalClassName={ModalContainer}>
<div>모달!</div>
<button onClick={hide}>닫기</button>
</Modal>
</div>
);
}
```

<Canvas of={Modal.defaultStory} />
78 changes: 78 additions & 0 deletions src/stories/useModal/Modal.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { keyframes, style } from '@vanilla-extract/css';

export const Overlay = style({
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});

export const ModalContainer = style({
backgroundColor: 'white',
padding: '30px 60px 30px 60px',
borderRadius: 25,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
gap: 10,
});

const showKeyframe = keyframes({
from: {
opacity: 0,
transform: 'scale(0)',
},
to: {
opacity: 1,
transform: 'scale(1)',
},
});

const hideKeyframe = keyframes({
from: {
opacity: 1,
transform: ' scale(1)',
},
to: {
opacity: 0,
transform: 'scale(0)',
},
});
const overlayShowKeyframe = keyframes({
from: {
opacity: 0,
},
to: {
opacity: 1,
},
});

const overlayHideKeyframe = keyframes({
from: {
opacity: 1,
},
to: {
opacity: 0,
},
});

export const showStyle = style({
animation: `${showKeyframe} 500ms forwards`,
});

export const hideStyle = style({
animation: `${hideKeyframe} 500ms forwards`,
});

export const overlayShow = style({
animation: `${overlayShowKeyframe} 500ms forwards`,
});
export const overlayHide = style({
animation: `${overlayHideKeyframe} 500ms forwards`,
});
21 changes: 21 additions & 0 deletions src/stories/useModal/Modal.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Meta, StoryObj } from '@storybook/react';
import Modal from './Modal';

const meta = {
title: 'hooks/useModal',
component: Modal,
parameters: {
layout: 'centered',
docs: {
canvas: {},
},
},
} satisfies Meta<typeof Modal>;

export default meta;

type Story = StoryObj<typeof meta>;

export const defaultStory: Story = {
args: {},
};
38 changes: 38 additions & 0 deletions src/stories/useModal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import useModal from '@/useModal/useModal';
import React from 'react';
import { ModalContainer, Overlay, hideStyle, overlayHide, overlayShow, showStyle } from './Modal.css';

export default function Modal() {
const { Modal, show, isShow, hide } = useModal({
modalAnimation: {
showClassName: showStyle,
hideClassName: hideStyle,
},
overlayAnimation: {
showClassName: overlayShow,
hideClassName: overlayHide,
},
});

const handleClick = () => {
if (isShow) hide();
show();
};

return (
<div>
<button onClick={handleClick}>{isShow ? 'hide' : 'show'}</button>
<Modal overlayClassName={Overlay} modalClassName={ModalContainer}>
<div>모달!</div>
<button
style={{
fontSize: 10,
}}
onClick={hide}
>
닫기
</button>
</Modal>
</div>
);
}
6 changes: 3 additions & 3 deletions src/useAnimation/useAnimation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { CSSProperties, ReactNode, useState } from 'react';
import React, { useState } from 'react';

export function _useAnimation(mountAnimationClassName?: string, unmountAnimationClassName?: string, unmountCallback?: () => void) {
const [animationClassName, setAnimationClassName] = useState<string | undefined>(mountAnimationClassName);
Expand Down Expand Up @@ -31,10 +31,10 @@ export default function useAnimation({ mountClassName, unmountClassName }: { mou
const show = () => setIsShow(true);
const hide = () => triggerUnmountAnimation();

const AnimationWrapper = ({ children, style, className }: { children: ReactNode; style?: CSSProperties; className?: string }) => {
const AnimationWrapper = ({ children, className, ...rest }: { className?: string } & React.ComponentProps<'div'>) => {
return (
isShow && (
<div className={`${animationClassName} ${className}`} onAnimationEnd={handleUnmountAnimationEnd} style={style}>
<div className={`${animationClassName} ${className}`} onAnimationEnd={handleUnmountAnimationEnd} {...rest}>
{children}
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion src/useBoolean/_useBoolean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react';

describe('useBoolean 기능테스트', () => {
it('useBoolean은 boolean상태를 나타내는 값과 그 boolean을 변경할 수 있는 값을 배열로 반환한다.', () => {
const { result } = renderHook(() => useBoolean(false));
const { result } = renderHook(() => useBoolean());

expect(result.current[0]).toBe(false);
act(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/useDisclosure/_useDisclosure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { renderHook, act } from '@testing-library/react';

describe('useDisclosure 기능테스트', () => {
it('useDisclosure는 modal, disclosure와 같이 컴포넌트의 열림과 닫힘 상태를 조절할 수 있는 기능들을 반환한다.', () => {
const { result } = renderHook(() => useDisclosure(false));
const { result } = renderHook(() => useDisclosure());

expect(result.current.isOpen).toBe(false);
act(() => {
Expand Down
Loading
Loading