From b3749c616ef5c3cd62e7ebad652286eeb91a1d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Tue, 23 Apr 2024 13:28:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20useOverlay=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../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 +++++++++++++++++++ 5 files changed, 145 insertions(+) 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/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] + ); +} From 32dd51c75a3829718e92224470039f58f4b47d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Tue, 23 Apr 2024 14:01:51 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20OverlayProvider=20=EA=B0=90?= =?UTF-8?q?=EC=8B=B8=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/router.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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: [ {