Skip to content

Commit

Permalink
merge: useModal 훅 구현 (#24)
Browse files Browse the repository at this point in the history
* feat: useAnimation의 AnimationWrapper가 div 타입 props를 받을 수 있도록 변경

* feat: 애니메이션을 지원하는useModal 기능 추가

* test: useModal 기능 테스트 추가

* docs: useModal Storybook 추가

* docs: README useModal 추가

* test: branch test coverage 개선

* feat: overlayClose옵션 추가

* test: overlayClose test 추가

* docs: Storybook, README 업데이트

* chore: UseModalAnimation type interface로 변경
  • Loading branch information
d0422 authored Jun 11, 2024
1 parent fec3b6e commit 34dac06
Show file tree
Hide file tree
Showing 12 changed files with 480 additions and 6 deletions.
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

0 comments on commit 34dac06

Please sign in to comment.