|
1 | 1 | import * as React from 'react'; |
2 | | -import { atom, useAtomValue, useSetAtom } from 'jotai'; |
| 2 | +import { useRouter } from 'next/router'; |
3 | 3 | import { Button, Group, Modal, Text } from '@mantine/core'; |
4 | 4 |
|
5 | 5 | type ConfirmOptions = { |
6 | 6 | variant?: 'primary' | 'danger'; |
7 | 7 | }; |
8 | 8 |
|
9 | | -type ConfirmAtom = { |
| 9 | +type ConfirmState = { |
10 | 10 | message: React.ReactNode; |
11 | 11 | confirmLabel?: string; |
12 | 12 | confirmVariant?: 'primary' | 'danger'; |
13 | 13 | onConfirm: () => void; |
14 | | - onClose?: () => void; |
| 14 | + onClose: () => void; |
15 | 15 | } | null; |
16 | 16 |
|
17 | | -const confirmAtom = atom<ConfirmAtom>(null); |
| 17 | +type ConfirmFn = ( |
| 18 | + message: React.ReactNode, |
| 19 | + confirmLabel?: string, |
| 20 | + options?: ConfirmOptions, |
| 21 | +) => Promise<boolean>; |
18 | 22 |
|
19 | | -export const useConfirm = () => { |
20 | | - const setConfirm = useSetAtom(confirmAtom); |
| 23 | +const ConfirmContext = React.createContext<ConfirmFn | null>(null); |
| 24 | + |
| 25 | +export function ConfirmProvider({ children }: { children: React.ReactNode }) { |
| 26 | + const [state, setState] = React.useState<ConfirmState>(null); |
| 27 | + const router = useRouter(); |
| 28 | + |
| 29 | + // Keep a ref so the route-change handler always sees the latest state |
| 30 | + // without needing to be re-registered on every render. |
| 31 | + const stateRef = React.useRef(state); |
| 32 | + React.useEffect(() => { |
| 33 | + stateRef.current = state; |
| 34 | + }, [state]); |
21 | 35 |
|
22 | | - return React.useCallback( |
23 | | - async ( |
24 | | - message: React.ReactNode, |
25 | | - confirmLabel?: string, |
26 | | - options?: ConfirmOptions, |
27 | | - ): Promise<boolean> => { |
28 | | - return new Promise(resolve => { |
29 | | - setConfirm({ |
| 36 | + React.useEffect(() => { |
| 37 | + const dismiss = () => { |
| 38 | + stateRef.current?.onClose(); |
| 39 | + }; |
| 40 | + router.events.on('routeChangeStart', dismiss); |
| 41 | + return () => router.events.off('routeChangeStart', dismiss); |
| 42 | + }, [router.events]); |
| 43 | + |
| 44 | + const confirm = React.useCallback<ConfirmFn>( |
| 45 | + (message, confirmLabel, options) => { |
| 46 | + return new Promise<boolean>(resolve => { |
| 47 | + setState({ |
30 | 48 | message, |
31 | 49 | confirmLabel, |
32 | 50 | confirmVariant: options?.variant ?? 'primary', |
33 | 51 | onConfirm: () => { |
34 | 52 | resolve(true); |
35 | | - setConfirm(null); |
| 53 | + setState(null); |
36 | 54 | }, |
37 | 55 | onClose: () => { |
38 | 56 | resolve(false); |
39 | | - setConfirm(null); |
| 57 | + setState(null); |
40 | 58 | }, |
41 | 59 | }); |
42 | 60 | }); |
43 | 61 | }, |
44 | | - [setConfirm], |
| 62 | + [], |
45 | 63 | ); |
46 | | -}; |
47 | | - |
48 | | -export const useConfirmModal = () => { |
49 | | - const confirm = useAtomValue(confirmAtom); |
50 | | - const setConfirm = useSetAtom(confirmAtom); |
51 | | - |
52 | | - const handleClose = React.useCallback(() => { |
53 | | - confirm?.onClose?.(); |
54 | | - setConfirm(null); |
55 | | - }, [confirm, setConfirm]); |
56 | 64 |
|
57 | 65 | return ( |
58 | | - <Modal |
59 | | - data-testid="confirm-modal" |
60 | | - opened={!!confirm} |
61 | | - onClose={handleClose} |
62 | | - centered |
63 | | - withCloseButton={false} |
64 | | - > |
65 | | - <Text size="sm" opacity={0.7}> |
66 | | - {confirm?.message} |
67 | | - </Text> |
68 | | - <Group justify="flex-end" mt="md" gap="xs"> |
69 | | - <Button |
70 | | - data-testid="confirm-cancel-button" |
71 | | - size="xs" |
72 | | - variant="secondary" |
73 | | - onClick={handleClose} |
74 | | - > |
75 | | - Cancel |
76 | | - </Button> |
77 | | - <Button |
78 | | - data-testid="confirm-confirm-button" |
79 | | - size="xs" |
80 | | - variant={confirm?.confirmVariant ?? 'primary'} |
81 | | - onClick={confirm?.onConfirm} |
82 | | - > |
83 | | - {confirm?.confirmLabel || 'Confirm'} |
84 | | - </Button> |
85 | | - </Group> |
86 | | - </Modal> |
| 66 | + <ConfirmContext.Provider value={confirm}> |
| 67 | + {children} |
| 68 | + <Modal |
| 69 | + data-testid="confirm-modal" |
| 70 | + opened={!!state} |
| 71 | + onClose={state?.onClose ?? (() => {})} |
| 72 | + centered |
| 73 | + withCloseButton={false} |
| 74 | + > |
| 75 | + <Text size="sm" opacity={0.7}> |
| 76 | + {state?.message} |
| 77 | + </Text> |
| 78 | + <Group justify="flex-end" mt="md" gap="xs"> |
| 79 | + <Button |
| 80 | + data-testid="confirm-cancel-button" |
| 81 | + size="xs" |
| 82 | + variant="secondary" |
| 83 | + onClick={state?.onClose} |
| 84 | + > |
| 85 | + Cancel |
| 86 | + </Button> |
| 87 | + <Button |
| 88 | + data-testid="confirm-confirm-button" |
| 89 | + size="xs" |
| 90 | + variant={state?.confirmVariant ?? 'primary'} |
| 91 | + onClick={state?.onConfirm} |
| 92 | + > |
| 93 | + {state?.confirmLabel || 'Confirm'} |
| 94 | + </Button> |
| 95 | + </Group> |
| 96 | + </Modal> |
| 97 | + </ConfirmContext.Provider> |
87 | 98 | ); |
| 99 | +} |
| 100 | + |
| 101 | +export const useConfirm = () => { |
| 102 | + const confirm = React.useContext(ConfirmContext); |
| 103 | + if (confirm == null) { |
| 104 | + throw new Error('useConfirm must be used within a ConfirmProvider'); |
| 105 | + } |
| 106 | + return confirm; |
88 | 107 | }; |
0 commit comments