Skip to content

Commit 87ea65e

Browse files
committed
feat: add Modal context
1 parent 2af3a62 commit 87ea65e

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

components/modal/demo/context.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<demo>
2+
3+
### Context
4+
5+
Manage multiple modals by ID with `Modal.Provider` and `Modal.useModal`.
6+
7+
<!--start-code-->
8+
9+
```jsx live
10+
() => {
11+
const ConfirmModal = () => {
12+
const { visible, close } = Modal.useModal('confirm');
13+
return (
14+
<Modal header="Confirm" visible={visible} onClose={close} onConfirm={close}>
15+
<p>Are you sure you want to proceed?</p>
16+
</Modal>
17+
);
18+
};
19+
20+
const SettingsModal = () => {
21+
const { visible, close } = Modal.useModal('settings');
22+
return (
23+
<Modal header="Settings" visible={visible} onClose={close} footer={null}>
24+
<p>Application settings go here.</p>
25+
</Modal>
26+
);
27+
};
28+
29+
const Toolbar = () => {
30+
const confirm = Modal.useModal('confirm');
31+
const settings = Modal.useModal('settings');
32+
return (
33+
<Space>
34+
<Button btnType="primary" onClick={confirm.show}>Open Confirm</Button>
35+
<Button onClick={settings.show}>Open Settings</Button>
36+
</Space>
37+
);
38+
};
39+
40+
return (
41+
<Modal.Provider>
42+
<Toolbar />
43+
<ConfirmModal />
44+
<SettingsModal />
45+
</Modal.Provider>
46+
);
47+
}
48+
```
49+
50+
</demo>

components/modal/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Animation from './demo/animation.md'
22
import Basic from './demo/basic.md'
3+
import Context from './demo/context.md'
34
import CustomisedFooter from './demo/customised-footer.md'
45
import Position from './demo/position.md'
56

@@ -27,6 +28,7 @@ import { Modal } from 'tiny-ui';
2728
<column>
2829
<Position/>
2930
<Animation/>
31+
<Context/>
3032
</column>
3133
</layout>
3234

components/modal/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { ConfigContext } from '../config-provider/config-context';
77
import { getPrefixCls } from '../_utils/general';
88
import { useLocale } from '../_utils/use-locale';
99
import { ModalProps } from './types';
10+
import { ModalProvider, useModal } from './modal-context';
11+
import type { ModalProviderProps, UseModalReturn } from './modal-context';
1012

1113
const Modal = React.forwardRef<HTMLDivElement, ModalProps>((props, ref) => {
1214
const locale = useLocale();
@@ -168,4 +170,14 @@ const Modal = React.forwardRef<HTMLDivElement, ModalProps>((props, ref) => {
168170

169171
Modal.displayName = 'Modal';
170172

171-
export default Modal;
173+
type ModalComponent = typeof Modal & {
174+
Provider: typeof ModalProvider;
175+
useModal: typeof useModal;
176+
};
177+
178+
const ModalWithContext = Modal as ModalComponent;
179+
ModalWithContext.Provider = ModalProvider;
180+
ModalWithContext.useModal = useModal;
181+
182+
export default ModalWithContext;
183+
export type { ModalProviderProps, UseModalReturn };

components/modal/modal-context.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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

Comments
 (0)