|
| 1 | +import React, { createContext, useCallback, useContext, useMemo, useReducer } from 'react'; |
| 2 | + |
| 3 | +// --- Reducer --- |
| 4 | + |
| 5 | +type ModalState = Record<string, boolean>; |
| 6 | + |
| 7 | +type ModalAction = |
| 8 | + | { type: 'SHOW'; id: string } |
| 9 | + | { type: 'HIDE'; id: string } |
| 10 | + | { type: 'TOGGLE'; id: string } |
| 11 | + | { type: 'PURGE' }; |
| 12 | + |
| 13 | +function modalReducer(state: ModalState, action: ModalAction): ModalState { |
| 14 | + switch (action.type) { |
| 15 | + case 'SHOW': |
| 16 | + return state[action.id] === true ? state : { ...state, [action.id]: true }; |
| 17 | + case 'HIDE': |
| 18 | + return state[action.id] === false ? state : { ...state, [action.id]: false }; |
| 19 | + case 'TOGGLE': |
| 20 | + return { ...state, [action.id]: !state[action.id] }; |
| 21 | + case 'PURGE': |
| 22 | + return {}; |
| 23 | + default: |
| 24 | + return state; |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +// --- Context --- |
| 29 | + |
| 30 | +interface ModalContextValue { |
| 31 | + modals: ModalState; |
| 32 | + dispatch: React.Dispatch<ModalAction>; |
| 33 | +} |
| 34 | + |
| 35 | +const ModalContext = createContext<ModalContextValue | null>(null); |
| 36 | + |
| 37 | +// --- Provider --- |
| 38 | + |
| 39 | +interface ModalProviderProps { |
| 40 | + modals?: ModalState; |
| 41 | + dispatch?: React.Dispatch<ModalAction>; |
| 42 | + children: React.ReactNode; |
| 43 | +} |
| 44 | + |
| 45 | +function ModalProvider({ modals: controlledModals, dispatch: controlledDispatch, children }: ModalProviderProps) { |
| 46 | + const [internalModals, internalDispatch] = useReducer(modalReducer, {}); |
| 47 | + |
| 48 | + const modals = controlledModals ?? internalModals; |
| 49 | + const dispatch = controlledDispatch ?? internalDispatch; |
| 50 | + |
| 51 | + const value = useMemo(() => ({ modals, dispatch }), [modals, dispatch]); |
| 52 | + |
| 53 | + return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>; |
| 54 | +} |
| 55 | + |
| 56 | +// --- Hook --- |
| 57 | + |
| 58 | +interface UseModalReturn { |
| 59 | + visible: boolean; |
| 60 | + show: () => void; |
| 61 | + close: () => void; |
| 62 | + toggle: () => void; |
| 63 | +} |
| 64 | + |
| 65 | +function useModal(id: string): UseModalReturn { |
| 66 | + const ctx = useContext(ModalContext); |
| 67 | + if (!ctx) { |
| 68 | + throw new Error('useModal must be used within a <Modal.Provider>'); |
| 69 | + } |
| 70 | + |
| 71 | + const { modals, dispatch } = ctx; |
| 72 | + const visible = !!modals[id]; |
| 73 | + |
| 74 | + const show = useCallback(() => dispatch({ type: 'SHOW', id }), [dispatch, id]); |
| 75 | + const close = useCallback(() => dispatch({ type: 'HIDE', id }), [dispatch, id]); |
| 76 | + const toggle = useCallback(() => dispatch({ type: 'TOGGLE', id }), [dispatch, id]); |
| 77 | + |
| 78 | + return useMemo(() => ({ visible, show, close, toggle }), [visible, show, close, toggle]); |
| 79 | +} |
| 80 | + |
| 81 | +export { ModalProvider, useModal, modalReducer }; |
| 82 | +export type { ModalState, ModalAction, ModalProviderProps, UseModalReturn }; |
0 commit comments