From b2cc79e7d5c405a816a8b12bf07d5803495d2b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Tue, 30 Apr 2024 09:49:31 +0900 Subject: [PATCH] =?UTF-8?q?Feat/#566=20overlay=EB=A5=BC=20=EB=8B=A4?= =?UTF-8?q?=EB=A3=B0=20useOverlay=20=ED=9B=85=20=EA=B5=AC=ED=98=84=20(#567?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: useOverlay 구현 * feat: OverlayProvider 감싸기 --- frontend/src/router.tsx | 9 ++-- .../hooks/useOverlay/OverlayController.tsx | 37 +++++++++++++ .../hooks/useOverlay/OverlayProvider.tsx | 42 +++++++++++++++ frontend/src/shared/hooks/useOverlay/index.ts | 2 + frontend/src/shared/hooks/useOverlay/types.ts | 10 ++++ .../shared/hooks/useOverlay/useOverlay.tsx | 54 +++++++++++++++++++ 6 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 frontend/src/shared/hooks/useOverlay/OverlayController.tsx create mode 100644 frontend/src/shared/hooks/useOverlay/OverlayProvider.tsx create mode 100644 frontend/src/shared/hooks/useOverlay/index.ts create mode 100644 frontend/src/shared/hooks/useOverlay/types.ts create mode 100644 frontend/src/shared/hooks/useOverlay/useOverlay.tsx diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 101b076c..608a4b46 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -12,14 +12,17 @@ import SongDetailListPage from './pages/SongDetailListPage'; import AuthLayout from './shared/components/Layout/AuthLayout'; import Layout from './shared/components/Layout/Layout'; import ROUTE_PATH from './shared/constants/path'; +import { OverlayProvider } from './shared/hooks/useOverlay'; const router = createBrowserRouter([ { path: ROUTE_PATH.ROOT, element: ( - - - + + + + + ), children: [ { diff --git a/frontend/src/shared/hooks/useOverlay/OverlayController.tsx b/frontend/src/shared/hooks/useOverlay/OverlayController.tsx new file mode 100644 index 00000000..5dab659f --- /dev/null +++ b/frontend/src/shared/hooks/useOverlay/OverlayController.tsx @@ -0,0 +1,37 @@ +import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react'; +import type { CreateOverlayElement } from './types'; +import type { Ref } from 'react'; + +interface OverlayControllerProps { + overlayElement: CreateOverlayElement; + onExit: () => void; +} + +export interface OverlayControlRef { + close: () => void; +} + +export const OverlayController = forwardRef(function OverlayController( + { overlayElement: OverlayElement, onExit }: OverlayControllerProps, + ref: Ref +) { + const [isOpenOverlay, setIsOpenOverlay] = useState(false); + + const handleOverlayClose = useCallback(() => setIsOpenOverlay(false), []); + + useImperativeHandle( + ref, + () => { + return { close: handleOverlayClose }; + }, + [handleOverlayClose] + ); + + useEffect(() => { + requestAnimationFrame(() => { + setIsOpenOverlay(true); + }); + }, []); + + return ; +}); diff --git a/frontend/src/shared/hooks/useOverlay/OverlayProvider.tsx b/frontend/src/shared/hooks/useOverlay/OverlayProvider.tsx new file mode 100644 index 00000000..d9de553f --- /dev/null +++ b/frontend/src/shared/hooks/useOverlay/OverlayProvider.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useCallback, useMemo, useRef, useState } from 'react'; +import type { Mount, Unmount } from './types'; +import type { MutableRefObject, PropsWithChildren, ReactNode } from 'react'; + +export const OverlayContext = createContext<{ + mount: Mount; + unmount: Unmount; + elementIdRef: MutableRefObject; +} | null>(null); + +export function OverlayProvider({ children }: PropsWithChildren<{ containerId?: string }>) { + const [overlayById, setOverlayById] = useState>(new Map()); + + const elementIdRef = useRef(1); + + const mount = useCallback((id, element) => { + setOverlayById((overlayById) => { + const cloned = new Map(overlayById); + cloned.set(id, element); + return cloned; + }); + }, []); + + const unmount = useCallback((id) => { + setOverlayById((overlayById) => { + const cloned = new Map(overlayById); + cloned.delete(id); + return cloned; + }); + }, []); + + const context = useMemo(() => ({ mount, unmount, elementIdRef }), [mount, unmount]); + + return ( + + {children} + {[...overlayById.entries()].map(([id, element]) => ( + {element} + ))} + + ); +} diff --git a/frontend/src/shared/hooks/useOverlay/index.ts b/frontend/src/shared/hooks/useOverlay/index.ts new file mode 100644 index 00000000..98b244da --- /dev/null +++ b/frontend/src/shared/hooks/useOverlay/index.ts @@ -0,0 +1,2 @@ +export { OverlayProvider, OverlayContext } from './OverlayProvider'; +export { useOverlay } from './useOverlay'; diff --git a/frontend/src/shared/hooks/useOverlay/types.ts b/frontend/src/shared/hooks/useOverlay/types.ts new file mode 100644 index 00000000..de64338e --- /dev/null +++ b/frontend/src/shared/hooks/useOverlay/types.ts @@ -0,0 +1,10 @@ +import type { ReactNode } from 'react'; + +export type Mount = (id: string, element: ReactNode) => void; +export type Unmount = (id: string) => void; + +export type CreateOverlayElement = (props: { + isOpen: boolean; + close: () => void; + exit: () => void; +}) => ReactNode; diff --git a/frontend/src/shared/hooks/useOverlay/useOverlay.tsx b/frontend/src/shared/hooks/useOverlay/useOverlay.tsx new file mode 100644 index 00000000..9577710b --- /dev/null +++ b/frontend/src/shared/hooks/useOverlay/useOverlay.tsx @@ -0,0 +1,54 @@ +import { useContext, useEffect, useMemo, useRef } from 'react'; +import { OverlayController } from './OverlayController'; +import { OverlayContext } from './OverlayProvider'; +import type { OverlayControlRef } from './OverlayController'; +import type { CreateOverlayElement } from './types'; + +interface Options { + exitOnUnmount?: boolean; +} + +export function useOverlay({ exitOnUnmount = true }: Options = {}) { + const context = useContext(OverlayContext); + + if (context === null) { + throw new Error('useOverlay는 OverlayProvider 내부에서 사용 가능합니다.'); + } + + const { mount, unmount, elementIdRef } = context; + + const id = String(elementIdRef.current++); + const overlayRef = useRef(null); + + useEffect(() => { + return () => { + if (exitOnUnmount) { + unmount(id); + } + }; + }, [exitOnUnmount, id, unmount]); + + return useMemo( + () => ({ + open: (overlayElement: CreateOverlayElement) => { + mount( + id, + unmount(id)} + /> + ); + }, + close: () => { + overlayRef.current?.close(); + }, + exit: () => { + unmount(id); + }, + }), + [id, mount, unmount] + ); +}