modalio 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/dist/index.cjs +912 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +618 -0
- package/dist/index.d.ts +618 -0
- package/dist/index.js +874 -0
- package/dist/index.js.map +1 -0
- package/package.json +76 -0
- package/src/adapters.ts +567 -0
- package/src/api.ts +418 -0
- package/src/context.tsx +206 -0
- package/src/core.ts +226 -0
- package/src/hoc.tsx +114 -0
- package/src/hooks.ts +250 -0
- package/src/index.ts +81 -0
- package/src/modal-manager.ts +136 -0
- package/src/types.ts +243 -0
package/src/core.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { ComponentType, Dispatch } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
DeferredPromise,
|
|
4
|
+
ModalAction,
|
|
5
|
+
ModalHocProps,
|
|
6
|
+
ModalRegistryEntry,
|
|
7
|
+
ModalStore,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
/** Symbol for storing modal IDs on component functions */
|
|
11
|
+
const symModalId: unique symbol = Symbol("ModalManagerId");
|
|
12
|
+
|
|
13
|
+
/** Type for components with attached modal ID */
|
|
14
|
+
type ComponentWithModalId = ComponentType<Record<string, unknown>> & {
|
|
15
|
+
[symModalId]?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Initial empty state
|
|
19
|
+
export const initialState: ModalStore = {};
|
|
20
|
+
|
|
21
|
+
// Modal registry - maps IDs to component definitions
|
|
22
|
+
export const MODAL_REGISTRY: Record<string, ModalRegistryEntry> = {};
|
|
23
|
+
|
|
24
|
+
// Track already mounted modals for delayed visibility
|
|
25
|
+
export const ALREADY_MOUNTED: Record<string, boolean> = {};
|
|
26
|
+
|
|
27
|
+
/** Promise callbacks for modal open/close resolution */
|
|
28
|
+
export const modalCallbacks: Record<string, DeferredPromise<unknown>> = {};
|
|
29
|
+
export const hideModalCallbacks: Record<string, DeferredPromise<unknown>> = {};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cleanup all callbacks for a modal (prevents memory leaks)
|
|
33
|
+
*/
|
|
34
|
+
export const cleanupCallbacks = (modalId: string): void => {
|
|
35
|
+
delete modalCallbacks[modalId];
|
|
36
|
+
delete hideModalCallbacks[modalId];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cleanup all callbacks (useful for testing or full reset)
|
|
41
|
+
*/
|
|
42
|
+
export const cleanupAllCallbacks = (): void => {
|
|
43
|
+
for (const key of Object.keys(modalCallbacks)) {
|
|
44
|
+
delete modalCallbacks[key];
|
|
45
|
+
}
|
|
46
|
+
for (const key of Object.keys(hideModalCallbacks)) {
|
|
47
|
+
delete hideModalCallbacks[key];
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// UID counter for generating unique IDs
|
|
52
|
+
let uidSeed = 0;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reset the UID seed (useful for SSR to prevent hydration mismatches)
|
|
56
|
+
* Call this on the server before rendering to ensure consistent IDs
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export const resetUidSeed = (): void => {
|
|
60
|
+
uidSeed = 0;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Note: If you need external store subscriptions, implement useSyncExternalStore support here
|
|
64
|
+
|
|
65
|
+
// Global dispatch function - set by Provider
|
|
66
|
+
let dispatchFn: Dispatch<ModalAction> = () => {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"No dispatch method detected. Did you wrap your app with ModalManager.Provider?",
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a unique modal ID
|
|
74
|
+
*/
|
|
75
|
+
export const getUid = (): string => `_modal_${uidSeed++}`;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the modal ID from a string or component
|
|
79
|
+
*/
|
|
80
|
+
export const getModalId = (
|
|
81
|
+
modal: string | ComponentType<Record<string, unknown>>,
|
|
82
|
+
): string => {
|
|
83
|
+
if (typeof modal === "string") {
|
|
84
|
+
return modal;
|
|
85
|
+
}
|
|
86
|
+
const modalWithId = modal as ComponentWithModalId;
|
|
87
|
+
if (!modalWithId[symModalId]) {
|
|
88
|
+
modalWithId[symModalId] = getUid();
|
|
89
|
+
}
|
|
90
|
+
return modalWithId[symModalId];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get a registered modal component by ID
|
|
95
|
+
*/
|
|
96
|
+
export const getModal = (
|
|
97
|
+
modalId: string,
|
|
98
|
+
): ComponentType<ModalHocProps & Record<string, unknown>> | undefined => {
|
|
99
|
+
return MODAL_REGISTRY[modalId]?.comp;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Set the global dispatch function
|
|
104
|
+
*/
|
|
105
|
+
export const setDispatch = (fn: Dispatch<ModalAction>): void => {
|
|
106
|
+
dispatchFn = fn;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the current dispatch function
|
|
111
|
+
*/
|
|
112
|
+
export const getDispatch = (): Dispatch<ModalAction> => dispatchFn;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Action creators
|
|
116
|
+
*/
|
|
117
|
+
export const actions = {
|
|
118
|
+
open: (modalId: string, data?: Record<string, unknown>): ModalAction => ({
|
|
119
|
+
type: "modalio/show",
|
|
120
|
+
payload: { modalId, data },
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
close: (modalId: string): ModalAction => ({
|
|
124
|
+
type: "modalio/hide",
|
|
125
|
+
payload: { modalId },
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
remove: (modalId: string): ModalAction => ({
|
|
129
|
+
type: "modalio/remove",
|
|
130
|
+
payload: { modalId },
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
setFlags: (modalId: string, flags: Record<string, unknown>): ModalAction => ({
|
|
134
|
+
type: "modalio/set-flags",
|
|
135
|
+
payload: { modalId, flags },
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reducer for modal state management
|
|
141
|
+
*/
|
|
142
|
+
export const reducer = (state: ModalStore, action: ModalAction): ModalStore => {
|
|
143
|
+
switch (action.type) {
|
|
144
|
+
case "modalio/show": {
|
|
145
|
+
const { modalId, data } = action.payload;
|
|
146
|
+
return {
|
|
147
|
+
...state,
|
|
148
|
+
[modalId]: {
|
|
149
|
+
...state[modalId],
|
|
150
|
+
modalId,
|
|
151
|
+
data,
|
|
152
|
+
// If already mounted, show immediately; otherwise delay
|
|
153
|
+
isOpen: !!ALREADY_MOUNTED[modalId],
|
|
154
|
+
delayOpen: !ALREADY_MOUNTED[modalId],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "modalio/hide": {
|
|
160
|
+
const { modalId } = action.payload;
|
|
161
|
+
const modalState = state[modalId];
|
|
162
|
+
if (!modalState) {
|
|
163
|
+
return state;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
...state,
|
|
167
|
+
[modalId]: {
|
|
168
|
+
...modalState,
|
|
169
|
+
isOpen: false,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case "modalio/remove": {
|
|
175
|
+
const { modalId } = action.payload;
|
|
176
|
+
const newState = { ...state };
|
|
177
|
+
delete newState[modalId];
|
|
178
|
+
return newState;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case "modalio/set-flags": {
|
|
182
|
+
const { modalId, flags } = action.payload;
|
|
183
|
+
const existingState = state[modalId];
|
|
184
|
+
// Only set flags if the modal exists
|
|
185
|
+
if (!existingState) {
|
|
186
|
+
return state;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...state,
|
|
190
|
+
[modalId]: {
|
|
191
|
+
...existingState,
|
|
192
|
+
...flags,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
default:
|
|
198
|
+
return state;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Register a modal component
|
|
204
|
+
*/
|
|
205
|
+
export const register = (
|
|
206
|
+
id: string,
|
|
207
|
+
// biome-ignore lint/suspicious/noExplicitAny: component props vary
|
|
208
|
+
comp: ComponentType<any>,
|
|
209
|
+
props?: Record<string, unknown>,
|
|
210
|
+
): void => {
|
|
211
|
+
if (MODAL_REGISTRY[id]) {
|
|
212
|
+
MODAL_REGISTRY[id].props = props;
|
|
213
|
+
} else {
|
|
214
|
+
MODAL_REGISTRY[id] = {
|
|
215
|
+
comp: comp as ComponentType<ModalHocProps & Record<string, unknown>>,
|
|
216
|
+
props,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Unregister a modal component
|
|
223
|
+
*/
|
|
224
|
+
export const unregister = (id: string): void => {
|
|
225
|
+
delete MODAL_REGISTRY[id];
|
|
226
|
+
};
|
package/src/hoc.tsx
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type ComponentType, type Ref, useContext, useEffect } from "react";
|
|
2
|
+
import { setFlags } from "./api";
|
|
3
|
+
import { ModalContext, ModalIdContext } from "./context";
|
|
4
|
+
import { ALREADY_MOUNTED } from "./core";
|
|
5
|
+
import { useModal } from "./hooks";
|
|
6
|
+
import type { ModalHocProps } from "./types";
|
|
7
|
+
|
|
8
|
+
/** Props passed directly to the HOC wrapper */
|
|
9
|
+
type HocOwnProps = ModalHocProps;
|
|
10
|
+
|
|
11
|
+
/** Props for components that accept a ref (React 19+ style) */
|
|
12
|
+
export interface RefProp<T> {
|
|
13
|
+
ref?: Ref<T>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* HOC to create a modal component with automatic registration and lifecycle management.
|
|
18
|
+
* Uses React 19's ref-as-prop pattern (no forwardRef needed).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* interface MyModalProps {
|
|
23
|
+
* title: string;
|
|
24
|
+
* onSave: () => void;
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* const MyModal = createModal<MyModalProps>(({ title, onSave }) => {
|
|
28
|
+
* const modal = useModal();
|
|
29
|
+
*
|
|
30
|
+
* return (
|
|
31
|
+
* <Dialog open={modal.open} onOpenChange={(open) => !open && modal.dismiss()}>
|
|
32
|
+
* <DialogContent onAnimationEnd={modal.onAnimationEnd}>
|
|
33
|
+
* <h2>{title}</h2>
|
|
34
|
+
* <Button onClick={() => { onSave(); modal.close(); }}>Save</Button>
|
|
35
|
+
* </DialogContent>
|
|
36
|
+
* </Dialog>
|
|
37
|
+
* );
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Usage
|
|
41
|
+
* open(MyModal, { data: { title: 'Edit Item', onSave: handleSave } });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const createModal = <
|
|
45
|
+
TProps extends Record<string, unknown> = Record<string, unknown>,
|
|
46
|
+
TRef = unknown,
|
|
47
|
+
>(
|
|
48
|
+
Comp: ComponentType<TProps & RefProp<TRef>>,
|
|
49
|
+
): ComponentType<TProps & HocOwnProps & RefProp<TRef>> => {
|
|
50
|
+
function WrappedComponent(allProps: TProps & HocOwnProps & RefProp<TRef>) {
|
|
51
|
+
// Extract HOC props
|
|
52
|
+
const { defaultOpen, keepMounted, modalId, ref, ...restProps } =
|
|
53
|
+
allProps as HocOwnProps & RefProp<TRef> & Record<string, unknown>;
|
|
54
|
+
const componentProps = restProps as TProps;
|
|
55
|
+
|
|
56
|
+
const { data, open: openModal } = useModal(modalId);
|
|
57
|
+
const modals = useContext(ModalContext);
|
|
58
|
+
|
|
59
|
+
// Only mount if this modal exists in the store
|
|
60
|
+
const shouldMount = modalId in modals;
|
|
61
|
+
|
|
62
|
+
// Handle default visibility and track mounted state
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (defaultOpen) {
|
|
65
|
+
openModal();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ALREADY_MOUNTED[modalId] = true;
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
delete ALREADY_MOUNTED[modalId];
|
|
72
|
+
};
|
|
73
|
+
}, [modalId, openModal, defaultOpen]);
|
|
74
|
+
|
|
75
|
+
// Handle keepMounted flag
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (keepMounted) {
|
|
78
|
+
setFlags(modalId, { keepMounted: true });
|
|
79
|
+
}
|
|
80
|
+
}, [modalId, keepMounted]);
|
|
81
|
+
|
|
82
|
+
// Handle delayed visibility (for modals shown before mount)
|
|
83
|
+
const delayOpen = modals[modalId]?.delayOpen;
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (delayOpen) {
|
|
86
|
+
openModal(data);
|
|
87
|
+
}
|
|
88
|
+
}, [delayOpen, data, openModal]);
|
|
89
|
+
|
|
90
|
+
if (!shouldMount) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Merge static props with dynamic data from open()
|
|
95
|
+
const mergedProps: TProps & RefProp<TRef> = {
|
|
96
|
+
...componentProps,
|
|
97
|
+
...(data as Partial<TProps>),
|
|
98
|
+
ref,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<ModalIdContext.Provider value={modalId}>
|
|
103
|
+
<Comp {...mergedProps} />
|
|
104
|
+
</ModalIdContext.Provider>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Copy display name for debugging
|
|
109
|
+
WrappedComponent.displayName = `ModalManager(${
|
|
110
|
+
Comp.displayName || Comp.name || "Component"
|
|
111
|
+
})`;
|
|
112
|
+
|
|
113
|
+
return WrappedComponent;
|
|
114
|
+
};
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
closeModal,
|
|
4
|
+
markClosed,
|
|
5
|
+
notifyOpened,
|
|
6
|
+
openModal,
|
|
7
|
+
removeModal,
|
|
8
|
+
} from "./api";
|
|
9
|
+
import { ModalContext, ModalIdContext } from "./context";
|
|
10
|
+
import {
|
|
11
|
+
getModalId,
|
|
12
|
+
hideModalCallbacks,
|
|
13
|
+
MODAL_REGISTRY,
|
|
14
|
+
modalCallbacks,
|
|
15
|
+
register,
|
|
16
|
+
} from "./core";
|
|
17
|
+
import type { ModalHandler, ModalHocProps, ModalProps } from "./types";
|
|
18
|
+
import { type InternalModalConfig, MODAL_CONFIG_KEY } from "./types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook to control a modal from within the modal component or from anywhere.
|
|
22
|
+
* Returns an enhanced handler with state and control methods.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // Inside a modal component (uses context)
|
|
27
|
+
* const modal = useModal();
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <Dialog open={modal.open} onOpenChange={(open) => !open && modal.dismiss()}>
|
|
31
|
+
* <DialogContent onAnimationEnd={modal.onAnimationEnd}>
|
|
32
|
+
* <Button onClick={() => modal.close(result)}>Save</Button>
|
|
33
|
+
* <Button onClick={modal.dismiss}>Cancel</Button>
|
|
34
|
+
* </DialogContent>
|
|
35
|
+
* </Dialog>
|
|
36
|
+
* );
|
|
37
|
+
*
|
|
38
|
+
* // With a specific modal component
|
|
39
|
+
* const modal = useModal(MyModal);
|
|
40
|
+
* modal.open({ userId: '123' });
|
|
41
|
+
*
|
|
42
|
+
* // With a string ID
|
|
43
|
+
* const modal = useModal('my-modal');
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export function useModal(
|
|
48
|
+
modal?: string,
|
|
49
|
+
data?: Record<string, unknown>,
|
|
50
|
+
): ModalHandler;
|
|
51
|
+
|
|
52
|
+
export function useModal<
|
|
53
|
+
TProps extends Record<string, unknown>,
|
|
54
|
+
P extends Partial<ModalProps<React.ComponentType<TProps & ModalHocProps>>>,
|
|
55
|
+
>(
|
|
56
|
+
modal: React.ComponentType<TProps & ModalHocProps>,
|
|
57
|
+
data?: P,
|
|
58
|
+
): ModalHandler<TProps>;
|
|
59
|
+
|
|
60
|
+
export function useModal(
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: component props vary
|
|
62
|
+
modal?: React.ComponentType<any> | string,
|
|
63
|
+
initialData?: Record<string, unknown>,
|
|
64
|
+
): ModalHandler {
|
|
65
|
+
const modals = useContext(ModalContext);
|
|
66
|
+
const contextModalId = useContext(ModalIdContext);
|
|
67
|
+
|
|
68
|
+
// Determine modal ID - from argument or context
|
|
69
|
+
const modalId = modal ? getModalId(modal) : contextModalId;
|
|
70
|
+
|
|
71
|
+
if (!modalId) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"[ModalManager] No modal id found in useModal. " +
|
|
74
|
+
"Either pass a modal component/id or use inside a modal component.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if modal argument is a component reference (not a string ID)
|
|
79
|
+
// Note: ForwardRefExoticComponent is an object, not a function
|
|
80
|
+
const isComponentRef = modal !== undefined && typeof modal !== "string";
|
|
81
|
+
|
|
82
|
+
// Register component if passed and not already registered
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (isComponentRef && !MODAL_REGISTRY[modalId]) {
|
|
85
|
+
register(
|
|
86
|
+
modalId,
|
|
87
|
+
modal as React.ComponentType<Record<string, unknown>>,
|
|
88
|
+
initialData,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}, [isComponentRef, modalId, modal, initialData]);
|
|
92
|
+
|
|
93
|
+
const modalInfo = modals[modalId];
|
|
94
|
+
|
|
95
|
+
// Use refs to avoid stale closures in animation handlers
|
|
96
|
+
const modalInfoRef = useRef(modalInfo);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
modalInfoRef.current = modalInfo;
|
|
99
|
+
}, [modalInfo]);
|
|
100
|
+
|
|
101
|
+
// Memoized control methods
|
|
102
|
+
const openCallback = useCallback(
|
|
103
|
+
(data?: Record<string, unknown>) => {
|
|
104
|
+
const ref = openModal(modalId, { data });
|
|
105
|
+
return ref.afterClosed();
|
|
106
|
+
},
|
|
107
|
+
[modalId],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const closeCallback = useCallback(
|
|
111
|
+
(result?: unknown) => {
|
|
112
|
+
modalCallbacks[modalId]?.resolve(result);
|
|
113
|
+
delete modalCallbacks[modalId];
|
|
114
|
+
closeModal(modalId);
|
|
115
|
+
},
|
|
116
|
+
[modalId],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const dismissCallback = useCallback(() => {
|
|
120
|
+
modalCallbacks[modalId]?.resolve(undefined);
|
|
121
|
+
delete modalCallbacks[modalId];
|
|
122
|
+
closeModal(modalId);
|
|
123
|
+
}, [modalId]);
|
|
124
|
+
|
|
125
|
+
const removeCallback = useCallback(() => removeModal(modalId), [modalId]);
|
|
126
|
+
|
|
127
|
+
// Animation completion handler
|
|
128
|
+
const onAnimationEnd = useCallback(() => {
|
|
129
|
+
const current = modalInfoRef.current;
|
|
130
|
+
|
|
131
|
+
if (current?.isOpen) {
|
|
132
|
+
// Modal is opening - notify afterOpened promise
|
|
133
|
+
notifyOpened(modalId);
|
|
134
|
+
} else {
|
|
135
|
+
// Modal is closing - mark as fully closed
|
|
136
|
+
markClosed(modalId);
|
|
137
|
+
|
|
138
|
+
// Resolve close promise when closing animation completes
|
|
139
|
+
hideModalCallbacks[modalId]?.resolve(undefined);
|
|
140
|
+
delete hideModalCallbacks[modalId];
|
|
141
|
+
|
|
142
|
+
// Remove if not keepMounted (use ref for latest value)
|
|
143
|
+
if (!current?.keepMounted) {
|
|
144
|
+
removeModal(modalId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}, [modalId]);
|
|
148
|
+
|
|
149
|
+
return useMemo(
|
|
150
|
+
() => ({
|
|
151
|
+
// State
|
|
152
|
+
modalId,
|
|
153
|
+
data: modalInfo?.data,
|
|
154
|
+
isOpen: !!modalInfo?.isOpen,
|
|
155
|
+
keepMounted: !!modalInfo?.keepMounted,
|
|
156
|
+
// Controls
|
|
157
|
+
open: openCallback,
|
|
158
|
+
close: closeCallback,
|
|
159
|
+
dismiss: dismissCallback,
|
|
160
|
+
remove: removeCallback,
|
|
161
|
+
// Animation handler
|
|
162
|
+
onAnimationEnd,
|
|
163
|
+
}),
|
|
164
|
+
[
|
|
165
|
+
modalId,
|
|
166
|
+
modalInfo?.data,
|
|
167
|
+
modalInfo?.isOpen,
|
|
168
|
+
modalInfo?.keepMounted,
|
|
169
|
+
openCallback,
|
|
170
|
+
closeCallback,
|
|
171
|
+
dismissCallback,
|
|
172
|
+
removeCallback,
|
|
173
|
+
onAnimationEnd,
|
|
174
|
+
],
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Hook to get typed data passed to a modal
|
|
180
|
+
* Must be used inside a modal component (within ModalIdContext)
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```tsx
|
|
184
|
+
* interface MyModalData {
|
|
185
|
+
* userId: string;
|
|
186
|
+
* userName: string;
|
|
187
|
+
* }
|
|
188
|
+
*
|
|
189
|
+
* const MyModal = createModal(() => {
|
|
190
|
+
* const data = useModalData<MyModalData>();
|
|
191
|
+
* const modal = useModal();
|
|
192
|
+
*
|
|
193
|
+
* return (
|
|
194
|
+
* <Dialog open={modal.open}>
|
|
195
|
+
* <DialogContent>
|
|
196
|
+
* <p>User: {data?.userName}</p>
|
|
197
|
+
* </DialogContent>
|
|
198
|
+
* </Dialog>
|
|
199
|
+
* );
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* // Usage:
|
|
203
|
+
* open(MyModal, { data: { userId: '123', userName: 'John' } });
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function useModalData<TData = Record<string, unknown>>():
|
|
207
|
+
| TData
|
|
208
|
+
| undefined {
|
|
209
|
+
const modals = useContext(ModalContext);
|
|
210
|
+
const modalId = useContext(ModalIdContext);
|
|
211
|
+
|
|
212
|
+
if (!modalId) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"[ModalManager] useModalData must be used inside a modal component. " +
|
|
215
|
+
"Make sure you're using createModal() to wrap your modal.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const modalInfo = modals[modalId];
|
|
220
|
+
const data = modalInfo?.data;
|
|
221
|
+
|
|
222
|
+
// Filter out internal modal config from user data
|
|
223
|
+
if (!data) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const { [MODAL_CONFIG_KEY]: _, ...userData } = data;
|
|
227
|
+
return userData as TData;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Hook to get the modal config options (disableClose, keepMounted, etc.)
|
|
232
|
+
* Must be used inside a modal component
|
|
233
|
+
*/
|
|
234
|
+
export function useModalConfig(): InternalModalConfig {
|
|
235
|
+
const modals = useContext(ModalContext);
|
|
236
|
+
const modalId = useContext(ModalIdContext);
|
|
237
|
+
|
|
238
|
+
if (!modalId) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"[ModalManager] useModalConfig must be used inside a modal component.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const modalInfo = modals[modalId];
|
|
245
|
+
const config = modalInfo?.data?.[MODAL_CONFIG_KEY] as
|
|
246
|
+
| InternalModalConfig
|
|
247
|
+
| undefined;
|
|
248
|
+
|
|
249
|
+
return config ?? {};
|
|
250
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Primary API - ModalManager namespace
|
|
2
|
+
|
|
3
|
+
// Re-export adapter types
|
|
4
|
+
export type {
|
|
5
|
+
AdapterOptions,
|
|
6
|
+
BaseUiDialogPopupProps,
|
|
7
|
+
BaseUiDialogPortalProps,
|
|
8
|
+
BaseUiDialogRootProps,
|
|
9
|
+
BaseUiPopoverPopupProps,
|
|
10
|
+
BaseUiPopoverPortalProps,
|
|
11
|
+
BaseUiPopoverRootProps,
|
|
12
|
+
ShadcnUiDrawerContentProps,
|
|
13
|
+
ShadcnUiDrawerRootProps,
|
|
14
|
+
} from "./adapters";
|
|
15
|
+
// Re-export adapters
|
|
16
|
+
export {
|
|
17
|
+
// Base UI adapters
|
|
18
|
+
baseUiAlertDialog,
|
|
19
|
+
baseUiAlertDialogPopup,
|
|
20
|
+
baseUiAlertDialogPortal,
|
|
21
|
+
baseUiDialog,
|
|
22
|
+
baseUiDialogPopup,
|
|
23
|
+
baseUiDialogPortal,
|
|
24
|
+
// Base UI Popover adapters
|
|
25
|
+
baseUiPopover,
|
|
26
|
+
baseUiPopoverPopup,
|
|
27
|
+
baseUiPopoverPortal,
|
|
28
|
+
// Base UI Sheet adapters
|
|
29
|
+
baseUiSheet,
|
|
30
|
+
baseUiSheetPopup,
|
|
31
|
+
baseUiSheetPortal,
|
|
32
|
+
// Radix UI adapters
|
|
33
|
+
radixUiAlertDialog,
|
|
34
|
+
radixUiAlertDialogContent,
|
|
35
|
+
radixUiDialog,
|
|
36
|
+
radixUiDialogContent,
|
|
37
|
+
radixUiPopover,
|
|
38
|
+
radixUiSheet,
|
|
39
|
+
radixUiSheetContent,
|
|
40
|
+
// Shadcn adapters
|
|
41
|
+
shadcnUiAlertDialog,
|
|
42
|
+
shadcnUiAlertDialogContent,
|
|
43
|
+
shadcnUiDialog,
|
|
44
|
+
shadcnUiDialogContent,
|
|
45
|
+
shadcnUiDrawer,
|
|
46
|
+
shadcnUiDrawerContent,
|
|
47
|
+
shadcnUiPopover,
|
|
48
|
+
shadcnUiSheet,
|
|
49
|
+
shadcnUiSheetContent,
|
|
50
|
+
} from "./adapters";
|
|
51
|
+
// Re-export context (for advanced usage)
|
|
52
|
+
export { ModalContext, ModalIdContext } from "./context";
|
|
53
|
+
// Re-export core utilities (for advanced usage)
|
|
54
|
+
export { getModalId, reducer } from "./core";
|
|
55
|
+
// Re-export hooks
|
|
56
|
+
export { useModal, useModalConfig, useModalData } from "./hooks";
|
|
57
|
+
export { ModalManager } from "./modal-manager";
|
|
58
|
+
// Re-export types
|
|
59
|
+
export type {
|
|
60
|
+
DeferredPromise,
|
|
61
|
+
InternalModalConfig,
|
|
62
|
+
ModalAction,
|
|
63
|
+
ModalActionType,
|
|
64
|
+
ModalAnimationHandlers,
|
|
65
|
+
ModalConfig,
|
|
66
|
+
ModalControls,
|
|
67
|
+
ModalHandler,
|
|
68
|
+
ModalHocProps,
|
|
69
|
+
ModalLifecycleState,
|
|
70
|
+
ModalProps,
|
|
71
|
+
ModalProviderProps,
|
|
72
|
+
ModalReadState,
|
|
73
|
+
ModalRef,
|
|
74
|
+
ModalState,
|
|
75
|
+
ModalStore,
|
|
76
|
+
RadixDialogContentProps,
|
|
77
|
+
RadixDialogProps,
|
|
78
|
+
ShadcnDialogProps,
|
|
79
|
+
} from "./types";
|
|
80
|
+
// Re-export constants
|
|
81
|
+
export { MODAL_CONFIG_KEY } from "./types";
|