react-modal-orchestrator 1.0.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/.idea/ModalLib.iml +9 -0
- package/.idea/copilot.data.migration.agent.xml +6 -0
- package/.idea/copilot.data.migration.ask.xml +6 -0
- package/.idea/copilot.data.migration.ask2agent.xml +6 -0
- package/.idea/copilot.data.migration.edit.xml +6 -0
- package/.idea/misc.xml +15 -0
- package/.idea/modules.xml +8 -0
- package/README.md +134 -0
- package/dist/Modal/ModalHandler.d.ts +2 -0
- package/dist/Modal/ModalHandler.d.ts.map +1 -0
- package/dist/Modal/ModalHandler.js +14 -0
- package/dist/Modal/ModalHandler.js.map +1 -0
- package/dist/Modal/ModalOrchestrator.d.ts +7 -0
- package/dist/Modal/ModalOrchestrator.d.ts.map +1 -0
- package/dist/Modal/ModalOrchestrator.js +13 -0
- package/dist/Modal/ModalOrchestrator.js.map +1 -0
- package/dist/contexts/ModalContext.d.ts +12 -0
- package/dist/contexts/ModalContext.d.ts.map +1 -0
- package/dist/contexts/ModalContext.js +7 -0
- package/dist/contexts/ModalContext.js.map +1 -0
- package/dist/hooks/useModal.d.ts +19 -0
- package/dist/hooks/useModal.d.ts.map +1 -0
- package/dist/hooks/useModal.js +45 -0
- package/dist/hooks/useModal.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
- package/src/MODAL_README.md +97 -0
- package/src/Modal/ModalHandler.tsx +17 -0
- package/src/Modal/ModalOrchestrator.tsx +33 -0
- package/src/contexts/ModalContext.ts +14 -0
- package/src/hooks/useModal.tsx +65 -0
- package/src/index.ts +3 -0
- package/src/types.ts +7 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="JAVA_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="inheritedJdk" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
package/.idea/misc.xml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="MaterialThemeProjectNewConfig">
|
|
4
|
+
<option name="metadata">
|
|
5
|
+
<MTProjectMetadataState>
|
|
6
|
+
<option name="migrated" value="true" />
|
|
7
|
+
<option name="pristineConfig" value="false" />
|
|
8
|
+
<option name="userId" value="1f7785cf:191c2f026d3:-7ffe" />
|
|
9
|
+
</MTProjectMetadataState>
|
|
10
|
+
</option>
|
|
11
|
+
</component>
|
|
12
|
+
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
|
|
13
|
+
<output url="file://$PROJECT_DIR$/out" />
|
|
14
|
+
</component>
|
|
15
|
+
</project>
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ModalLib
|
|
2
|
+
|
|
3
|
+
A centralized and flexible modal management system for React applications, now with lazy loading!
|
|
4
|
+
|
|
5
|
+
`ModalLib` provides a clean, decoupled architecture for managing modals. Instead of controlling modal state with `useState` in every component, you can register all your modals in one place and trigger them from anywhere in your application. By using `React.lazy`, modal components are only loaded when they are first opened, reducing your application's initial bundle size.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
- **ModalOrchestrator**: A provider component that wraps your application and powers the entire modal system.
|
|
10
|
+
- **ModalHandler**: A component that listens for active modals and renders them. It now includes `React.Suspense` to handle lazy-loaded components.
|
|
11
|
+
- **Modal Dictionary**: A simple JavaScript object where you map a unique name to each dynamically imported modal component.
|
|
12
|
+
- **`useContext` for Control**: Use React's `useContext` hook to access the `openModal` and `closeModal` functions from anywhere in your component tree.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install modallib
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
First, you need to wrap your application with the `ModalOrchestrator` and place the `ModalHandler` within it.
|
|
23
|
+
|
|
24
|
+
### 1. Create a Lazy-Loaded Modal Dictionary
|
|
25
|
+
|
|
26
|
+
To take advantage of code-splitting, define your modal dictionary using `React.lazy`. This tells React to load the component code only when it's first rendered.
|
|
27
|
+
|
|
28
|
+
Create a file (e.g., `modals.ts`) to define and register your modals.
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// components/MyFirstModal.tsx
|
|
32
|
+
import React, { useContext } from 'react';
|
|
33
|
+
import { ModalContext } from 'modallib';
|
|
34
|
+
|
|
35
|
+
// Note: The component must be a default export for React.lazy to work
|
|
36
|
+
export const MyFirstModal = ({ title }) => {
|
|
37
|
+
const { closeModal } = useContext(ModalContext);
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<h2>{title}</h2>
|
|
41
|
+
<p>This is my first modal!</p>
|
|
42
|
+
<button onClick={closeModal}>Close</button>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
export default MyFirstModal;
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
// modals.ts
|
|
50
|
+
import React from 'react';
|
|
51
|
+
|
|
52
|
+
export const modalDictionary = {
|
|
53
|
+
MY_FIRST_MODAL: React.lazy(() => import('./components/MyFirstModal')),
|
|
54
|
+
// ... register other lazy-loaded modals here
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Wrap Your App
|
|
59
|
+
|
|
60
|
+
In your main application file (e.g., `App.tsx`), import the `ModalOrchestrator`, `ModalHandler`, and your modal dictionary.
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// App.tsx
|
|
64
|
+
import { ModalOrchestrator, ModalHandler } from 'modallib';
|
|
65
|
+
import { modalDictionary } from './modals';
|
|
66
|
+
|
|
67
|
+
function App() {
|
|
68
|
+
return (
|
|
69
|
+
<ModalOrchestrator modals={modalDictionary}>
|
|
70
|
+
{/* Your application components */}
|
|
71
|
+
<h1>My App</h1>
|
|
72
|
+
<TriggerButton />
|
|
73
|
+
|
|
74
|
+
{/*
|
|
75
|
+
Place the handler at the top level.
|
|
76
|
+
You can customize the loading fallback in your own Suspense boundary
|
|
77
|
+
if you don't like the default "Loading..." text.
|
|
78
|
+
*/}
|
|
79
|
+
<ModalHandler />
|
|
80
|
+
</ModalOrchestrator>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### Opening a Modal
|
|
88
|
+
|
|
89
|
+
To open a modal, import `ModalContext`, get the `openModal` function from the context, and call it with the registered name of the modal you want to open.
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
// components/TriggerButton.tsx
|
|
93
|
+
import React, { useContext } from 'react';
|
|
94
|
+
import { ModalContext } from 'modallib';
|
|
95
|
+
|
|
96
|
+
const TriggerButton = () => {
|
|
97
|
+
const { openModal } = useContext(ModalContext);
|
|
98
|
+
|
|
99
|
+
const handleClick = () => {
|
|
100
|
+
// Open the modal and pass props to it
|
|
101
|
+
openModal('MY_FIRST_MODAL', { title: 'Hello World!' });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return <button onClick={handleClick}>Open Modal</button>;
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Closing a Modal
|
|
109
|
+
|
|
110
|
+
The `ModalHandler` automatically provides a `closeModal` function to your modal component via context. Use the `useContext` hook to access it.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// components/MyFirstModal.tsx
|
|
114
|
+
import React, { useContext } from 'react';
|
|
115
|
+
import { ModalContext } from 'modallib';
|
|
116
|
+
|
|
117
|
+
// Ensure the component is a default export
|
|
118
|
+
const MyFirstModal = ({ title }) => {
|
|
119
|
+
const { closeModal } = useContext(ModalContext); // Get closeModal from context
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<h2>{title}</h2>
|
|
124
|
+
<button onClick={closeModal}>Close Me</button>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default MyFirstModal;
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
This project is licensed under the ISC License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalHandler.d.ts","sourceRoot":"","sources":["../../src/Modal/ModalHandler.tsx"],"names":[],"mappings":"AAGA,QAAA,MAAM,YAAY,sDAYjB,CAAC;AACF,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, Suspense } from "react";
|
|
3
|
+
import { ModalDispatchContext, ModalStateContext } from "../contexts/ModalContext";
|
|
4
|
+
const ModalHandler = () => {
|
|
5
|
+
const { modal, ModalComponent } = useContext(ModalStateContext);
|
|
6
|
+
const { closeModal } = useContext(ModalDispatchContext);
|
|
7
|
+
if (!modal || !ModalComponent)
|
|
8
|
+
return null;
|
|
9
|
+
if (modal)
|
|
10
|
+
document.body.style.removeProperty("overflow");
|
|
11
|
+
return (_jsx(Suspense, { fallback: _jsx("div", { children: "Loading..." }), children: _jsx(ModalComponent, Object.assign({ onClose: closeModal }, modal.props)) }));
|
|
12
|
+
};
|
|
13
|
+
export default ModalHandler;
|
|
14
|
+
//# sourceMappingURL=ModalHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalHandler.js","sourceRoot":"","sources":["../../src/Modal/ModalHandler.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAC,MAAM,0BAA0B,CAAC;AAEjF,MAAM,YAAY,GAAG,GAAG,EAAE;IACxB,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAChE,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAAC,oBAAoB,CAAC,CAAC;IAExD,IAAI,CAAC,KAAK,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,KAAK;QAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;IAE1D,OAAO,CACL,KAAC,QAAQ,IAAC,QAAQ,EAAE,uCAAqB,YACvC,KAAC,cAAc,kBAAC,OAAO,EAAE,UAAU,IAAM,KAAK,CAAC,KAAK,EAAI,GAC/C,CACZ,CAAC;AACJ,CAAC,CAAC;AACF,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type GenericObjectType } from "../types";
|
|
3
|
+
declare const ModalOrchestrator: ({ modals, children, }: {
|
|
4
|
+
modals: GenericObjectType;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export default ModalOrchestrator;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalOrchestrator.d.ts","sourceRoot":"","sources":["../../src/Modal/ModalOrchestrator.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAC,KAAK,iBAAiB,EAAC,MAAM,UAAU,CAAC;AAGhD,QAAA,MAAM,iBAAiB,GAAI,uBAGxB;IACD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB,4CAmBA,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useModal } from "../hooks/useModal";
|
|
4
|
+
import {} from "../types";
|
|
5
|
+
import { ModalDispatchContext, ModalStateContext } from "../contexts/ModalContext";
|
|
6
|
+
const ModalOrchestrator = ({ modals, children, }) => {
|
|
7
|
+
const { modal, ModalComponent, openModal, closeModal } = useModal(modals);
|
|
8
|
+
const state = useMemo(() => ({ modal, ModalComponent }), [modal, ModalComponent]);
|
|
9
|
+
const dispatch = useMemo(() => ({ openModal, closeModal }), [openModal, closeModal]);
|
|
10
|
+
return (_jsx(ModalStateContext.Provider, { value: state, children: _jsx(ModalDispatchContext.Provider, { value: dispatch, children: children }) }));
|
|
11
|
+
};
|
|
12
|
+
export default ModalOrchestrator;
|
|
13
|
+
//# sourceMappingURL=ModalOrchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalOrchestrator.js","sourceRoot":"","sources":["../../src/Modal/ModalOrchestrator.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAkB,OAAO,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAC,QAAQ,EAAC,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAwB,MAAM,UAAU,CAAC;AAChD,OAAO,EAAC,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElF,MAAM,iBAAiB,GAAG,CAAC,EACzB,MAAM,EACN,QAAQ,GAIT,EAAE,EAAE;IACH,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE1E,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,EACjC,CAAC,KAAK,EAAE,cAAc,CAAC,CACxB,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CACtB,GAAG,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,EACjC,CAAC,SAAS,EAAE,UAAU,CAAC,CACxB,CAAC;IAEF,OAAO,CACL,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACtC,KAAC,oBAAoB,CAAC,QAAQ,IAAC,KAAK,EAAE,QAAQ,YAC3C,QAAQ,GACqB,GACL,CAC9B,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const ModalStateContext: import("react").Context<Pick<{
|
|
2
|
+
ModalComponent: import("react").FC<any> | null;
|
|
3
|
+
modal: import("../types").GenericModal | null;
|
|
4
|
+
openModal: (name: string, props?: object) => void;
|
|
5
|
+
closeModal: () => void;
|
|
6
|
+
}, "modal" | "ModalComponent">>;
|
|
7
|
+
export declare const ModalDispatchContext: import("react").Context<Pick<{
|
|
8
|
+
ModalComponent: import("react").FC<any> | null;
|
|
9
|
+
modal: import("../types").GenericModal | null;
|
|
10
|
+
openModal: (name: string, props?: object) => void;
|
|
11
|
+
closeModal: () => void;
|
|
12
|
+
}, "closeModal" | "openModal">>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ModalContext.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,iBAAiB;;;mCAQ2D,CAAC;;+BANlF,CAAC;AAGT,eAAO,MAAM,oBAAoB;;;mCAGwD,CAAC;;+BADlF,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { useModal } from "../hooks/useModal";
|
|
3
|
+
// This context will hold the stateful values that cause re-renders.
|
|
4
|
+
export const ModalStateContext = createContext(null);
|
|
5
|
+
// This context will hold the stable dispatch functions.
|
|
6
|
+
export const ModalDispatchContext = createContext(null);
|
|
7
|
+
//# sourceMappingURL=ModalContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModalContext.js","sourceRoot":"","sources":["../../src/contexts/ModalContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAI7C,oEAAoE;AACpE,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAE5C,IAAK,CAAC,CAAC;AAET,wDAAwD;AACxD,MAAM,CAAC,MAAM,oBAAoB,GAAG,aAAa,CAE/C,IAAK,CAAC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FC } from "react";
|
|
2
|
+
import { type GenericModal, type GenericObjectType } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* A custom hook to manage modals in a React application.
|
|
5
|
+
* It allows opening and closing modals, and keeps track of the modal stack.
|
|
6
|
+
*
|
|
7
|
+
* @param {GenericObjectType} modals - An object containing modal components.
|
|
8
|
+
* @returns {Object} - An object containing the current modal component, openModal and closeModal functions.
|
|
9
|
+
*/
|
|
10
|
+
type UseModalReturnType = {
|
|
11
|
+
ModalComponent: FC<any> | null;
|
|
12
|
+
modal: GenericModal | null;
|
|
13
|
+
openModal: (name: string, props?: object) => void;
|
|
14
|
+
closeModal: () => void;
|
|
15
|
+
};
|
|
16
|
+
export declare const smallGreyFont = "color: grey; font-size: 85%";
|
|
17
|
+
export declare const keyFont = "color: orange;font-weight: bold;";
|
|
18
|
+
export declare const useModal: (modals: GenericObjectType) => UseModalReturnType;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useModal.d.ts","sourceRoot":"","sources":["../../src/hooks/useModal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAoB,MAAM,OAAO,CAAC;AAClD,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAErE;;;;;;GAMG;AAEH,KAAK,kBAAkB,GAAG;IACxB,cAAc,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IAC/B,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AACF,eAAO,MAAM,aAAa,gCAAgC,CAAC;AAC3D,eAAO,MAAM,OAAO,qCAAqC,CAAC;AAE1D,eAAO,MAAM,QAAQ,GAAI,QAAQ,iBAAiB,KAAG,kBA4CpD,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import {} from "../types";
|
|
3
|
+
export const smallGreyFont = "color: grey; font-size: 85%";
|
|
4
|
+
export const keyFont = "color: orange;font-weight: bold;";
|
|
5
|
+
export const useModal = (modals) => {
|
|
6
|
+
const modalStack = useRef([]); // For purpose of keeping track of the modal stack, especially when there are multiple modals open
|
|
7
|
+
const [modal, setModal] = useState(null);
|
|
8
|
+
const ModalLookup = Object.assign({}, modals);
|
|
9
|
+
const modalName = modal ? modal.name : "";
|
|
10
|
+
const ModalComponent = ModalLookup[modalName];
|
|
11
|
+
const openModal = (name, props = {}) => {
|
|
12
|
+
// Check if the modal is already open
|
|
13
|
+
const existingModal = modalStack.current.find((modal) => modal.name === name);
|
|
14
|
+
if (existingModal) {
|
|
15
|
+
// If the modal is already open, just update its props
|
|
16
|
+
// additionally we don't need try/catch here, because we already know the modal name exists
|
|
17
|
+
existingModal.props = Object.assign(Object.assign({}, existingModal.props), props);
|
|
18
|
+
setModal(existingModal);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
if (name in ModalLookup) {
|
|
23
|
+
modalStack.current.push({ name, props });
|
|
24
|
+
setModal({ name, props });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log("%cModalManager %copening:", keyFont, smallGreyFont, name);
|
|
28
|
+
console.log("%cModalStack", smallGreyFont, modalStack.current);
|
|
29
|
+
throw new Error(`Modal with name "${name}" does not exist in ModalLookup.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.log("Failed to open modal:", error);
|
|
34
|
+
console.error("Failed to open modal:", error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const closeModal = () => {
|
|
38
|
+
var _a;
|
|
39
|
+
console.log("closeModal");
|
|
40
|
+
modalStack.current.pop();
|
|
41
|
+
setModal((_a = modalStack.current[modalStack.current.length - 1]) !== null && _a !== void 0 ? _a : null);
|
|
42
|
+
};
|
|
43
|
+
return { ModalComponent, modal, openModal, closeModal };
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=useModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useModal.js","sourceRoot":"","sources":["../../src/hooks/useModal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAW,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,EAA6C,MAAM,UAAU,CAAC;AAgBrE,MAAM,CAAC,MAAM,aAAa,GAAG,6BAA6B,CAAC;AAC3D,MAAM,CAAC,MAAM,OAAO,GAAG,kCAAkC,CAAC;AAE1D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAyB,EAAsB,EAAE;IACxE,MAAM,UAAU,GAAG,MAAM,CAAiB,EAAE,CAAC,CAAC,CAAC,kGAAkG;IACjJ,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAC;IAC9D,MAAM,WAAW,qBAA2B,MAAM,CAAE,CAAC;IACrD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1C,MAAM,cAAc,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAE9C,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,KAAK,GAAG,EAAE,EAAE,EAAE;QAC7C,qCAAqC;QACrC,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAC3C,CAAC,KAAmB,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAC7C,CAAC;QACF,IAAI,aAAa,EAAE,CAAC;YAClB,sDAAsD;YACtD,2FAA2F;YAC3F,aAAa,CAAC,KAAK,mCAAQ,aAAa,CAAC,KAAK,GAAK,KAAK,CAAE,CAAC;YAC3D,QAAQ,CAAC,aAAa,CAAC,CAAC;YACxB,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,IAAI,IAAI,IAAI,WAAW,EAAE,CAAC;gBACxB,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;gBACzC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACJ,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;gBACvE,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,aAAa,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;gBACjE,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,kCAAkC,CAC3D,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,KAAK,CAAC,CAAC;YAC5C,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,KAAK,CAAC,CAAC;QAChD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,GAAG,EAAE;;QACtB,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAC1B,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACzB,QAAQ,CAAC,MAAA,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,mCAAI,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC;IAEF,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC;AAC1D,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACtB,CAAC;AACF,MAAM,MAAM,YAAY,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-modal-orchestrator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"prepublishOnly": "npm run build",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"react",
|
|
13
|
+
"modal",
|
|
14
|
+
"suspense"
|
|
15
|
+
],
|
|
16
|
+
"author": "Rudi Wever: rwever@ford.com",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"description": "A centralized and flexible modal management system for React applications.",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"react": "^18.3.1",
|
|
21
|
+
"react-dom": "^18.3.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^18.3.12",
|
|
25
|
+
"@types/react-dom": "^18.3.1",
|
|
26
|
+
"typescript": "^5.9.3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=16.8.0",
|
|
30
|
+
"react-dom": ">=16.8.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# What to do when creating a modal.
|
|
2
|
+
|
|
3
|
+
# 1. Create a new file named `MyModalComponent.tsx` in the component folder.
|
|
4
|
+
|
|
5
|
+
code template:
|
|
6
|
+
|
|
7
|
+
(do not pass: isOpen or setIsOpen args)
|
|
8
|
+
|
|
9
|
+
- set the `ModalWindow` isOpen to true
|
|
10
|
+
- use `closeModal` from `ModalContext` to close the modal
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
const ModalComponent = () => {
|
|
14
|
+
const { closeModal } = useContext(ModalContext);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<AEMPage pagePath={pagePath} aemHost={getAEMHost()}>
|
|
18
|
+
<ModalWindow variant={"large"} isOpen={true} onClose={closeModal}>
|
|
19
|
+
<ModalHeader onClose={closeModal}>
|
|
20
|
+
<div className={styles.modalHeader}>
|
|
21
|
+
MODAL HEADER
|
|
22
|
+
</div>
|
|
23
|
+
<button onClick={()=> closeModal()}>CLOSE</button>
|
|
24
|
+
</ModalHeader>
|
|
25
|
+
<ModalBody>
|
|
26
|
+
<div>
|
|
27
|
+
MODAL BODY
|
|
28
|
+
</div>
|
|
29
|
+
</ModalBody>
|
|
30
|
+
<ModalFooter placement="right">
|
|
31
|
+
<div>
|
|
32
|
+
MODAL FOOTER
|
|
33
|
+
</div>
|
|
34
|
+
</ModalFooter>
|
|
35
|
+
</ModalWindow>
|
|
36
|
+
</AEMPage>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
export default ModalComponent;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
# 2. Add the new modal to the constant `modals` in `ModalDictionary.tsx`.
|
|
43
|
+
# 3. Add a definition to the RenderingDictionary.
|
|
44
|
+
|
|
45
|
+
When adding the new modal to `bootstrap` for local rendering
|
|
46
|
+
- the aemComponent and/or localComponent should be the ModalHandler
|
|
47
|
+
- the modal will be prefixed with an alias to the group, to specify which modal it is
|
|
48
|
+
|
|
49
|
+
# 4. How to trigger your modal?
|
|
50
|
+
|
|
51
|
+
- use the `openModal` method from `ModalContext` to trigger the modal; `openModal` takes the name of the modal as defined in the
|
|
52
|
+
modalDictionary, optionally you can also pass in props to the modal when you are opening it.
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
const { openModal } = useContext(ModalContext);
|
|
56
|
+
|
|
57
|
+
const handleClick = () => openModal("PST_MODAL", {
|
|
58
|
+
data: "myDatal"
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What if my modal needs data?
|
|
63
|
+
It is not recommended to access state and/or functionality using the`useContext` hook from within your modal component (this includes accessing context from any custom hooks employed by the modal component). It is very likely that the provider supplying the context will not be available when the modal is rendered, leading to errors. Instead, you should use
|
|
64
|
+
optional props of the `openModal` method, allowing your modal to receive data when it is rendered. The props will be accessible in the modal component through the `props` argument. The only `useContext` hook you should use in your modal component is the `ModalContext` to access the `closeModal` method.
|
|
65
|
+
|
|
66
|
+
When passing optional props to the modal, you can access them in the modal component like this:
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
const ModalComponent = ({data}: {data:string}) => {
|
|
70
|
+
console.log(data); // "myData" <<==== ACCESSING THE DATA PASSED TO THE MODAL
|
|
71
|
+
const { closeModal } = useContext(ModalContext); // <<==== ONLY useContext hook useage. ACCESSING THE CLOSE MODAL FUNCTION
|
|
72
|
+
const { data } = modalProps;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<AEMPage pagePath={pagePath} aemHost={getAEMHost()}>
|
|
76
|
+
<ModalWindow variant={"large"} isOpen={true} onClose={closeModal}>
|
|
77
|
+
<ModalHeader onClose={closeModal}>
|
|
78
|
+
<div className={styles.modalHeader}>
|
|
79
|
+
MODAL HEADER
|
|
80
|
+
</div>
|
|
81
|
+
</ModalHeader>
|
|
82
|
+
<ModalBody>
|
|
83
|
+
<div>
|
|
84
|
+
MODAL BODY with data: {data}
|
|
85
|
+
</div>
|
|
86
|
+
</ModalBody>
|
|
87
|
+
<ModalFooter placement="right">
|
|
88
|
+
<div>
|
|
89
|
+
MODAL FOOTER
|
|
90
|
+
</div>
|
|
91
|
+
</ModalFooter>
|
|
92
|
+
</ModalWindow>
|
|
93
|
+
</AEMPage>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
export default ModalComponent;
|
|
97
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useContext, Suspense } from "react";
|
|
2
|
+
import {ModalDispatchContext, ModalStateContext} from "../contexts/ModalContext";
|
|
3
|
+
|
|
4
|
+
const ModalHandler = () => {
|
|
5
|
+
const { modal, ModalComponent } = useContext(ModalStateContext);
|
|
6
|
+
const { closeModal } = useContext(ModalDispatchContext);
|
|
7
|
+
|
|
8
|
+
if (!modal || !ModalComponent) return null;
|
|
9
|
+
if (modal) document.body.style.removeProperty("overflow");
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
13
|
+
<ModalComponent onClose={closeModal} {...modal.props} />
|
|
14
|
+
</Suspense>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
export default ModalHandler;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ReactNode, useMemo } from "react";
|
|
2
|
+
import {useModal} from "../hooks/useModal";
|
|
3
|
+
import {type GenericObjectType} from "../types";
|
|
4
|
+
import {ModalDispatchContext, ModalStateContext } from "../contexts/ModalContext";
|
|
5
|
+
|
|
6
|
+
const ModalOrchestrator = ({
|
|
7
|
+
modals,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
modals: GenericObjectType;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}) => {
|
|
13
|
+
const { modal, ModalComponent, openModal, closeModal } = useModal(modals);
|
|
14
|
+
|
|
15
|
+
const state = useMemo(
|
|
16
|
+
() => ({ modal, ModalComponent }),
|
|
17
|
+
[modal, ModalComponent],
|
|
18
|
+
);
|
|
19
|
+
const dispatch = useMemo(
|
|
20
|
+
() => ({ openModal, closeModal }),
|
|
21
|
+
[openModal, closeModal],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ModalStateContext.Provider value={state}>
|
|
26
|
+
<ModalDispatchContext.Provider value={dispatch}>
|
|
27
|
+
{children}
|
|
28
|
+
</ModalDispatchContext.Provider>
|
|
29
|
+
</ModalStateContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default ModalOrchestrator;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
import { useModal } from "../hooks/useModal";
|
|
3
|
+
|
|
4
|
+
type ModalContextType = ReturnType<typeof useModal>;
|
|
5
|
+
|
|
6
|
+
// This context will hold the stateful values that cause re-renders.
|
|
7
|
+
export const ModalStateContext = createContext<
|
|
8
|
+
Pick<ModalContextType, "modal" | "ModalComponent">
|
|
9
|
+
>(null!);
|
|
10
|
+
|
|
11
|
+
// This context will hold the stable dispatch functions.
|
|
12
|
+
export const ModalDispatchContext = createContext<
|
|
13
|
+
Pick<ModalContextType, "openModal" | "closeModal">
|
|
14
|
+
>(null!);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type FC, useRef, useState } from "react";
|
|
2
|
+
import { type GenericModal, type GenericObjectType } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A custom hook to manage modals in a React application.
|
|
6
|
+
* It allows opening and closing modals, and keeps track of the modal stack.
|
|
7
|
+
*
|
|
8
|
+
* @param {GenericObjectType} modals - An object containing modal components.
|
|
9
|
+
* @returns {Object} - An object containing the current modal component, openModal and closeModal functions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type UseModalReturnType = {
|
|
13
|
+
ModalComponent: FC<any> | null;
|
|
14
|
+
modal: GenericModal | null;
|
|
15
|
+
openModal: (name: string, props?: object) => void;
|
|
16
|
+
closeModal: () => void;
|
|
17
|
+
};
|
|
18
|
+
export const smallGreyFont = "color: grey; font-size: 85%";
|
|
19
|
+
export const keyFont = "color: orange;font-weight: bold;";
|
|
20
|
+
|
|
21
|
+
export const useModal = (modals: GenericObjectType): UseModalReturnType => {
|
|
22
|
+
const modalStack = useRef<GenericModal[]>([]); // For purpose of keeping track of the modal stack, especially when there are multiple modals open
|
|
23
|
+
const [modal, setModal] = useState<GenericModal | null>(null);
|
|
24
|
+
const ModalLookup: GenericObjectType = { ...modals };
|
|
25
|
+
const modalName = modal ? modal.name : "";
|
|
26
|
+
const ModalComponent = ModalLookup[modalName];
|
|
27
|
+
|
|
28
|
+
const openModal = (name: string, props = {}) => {
|
|
29
|
+
// Check if the modal is already open
|
|
30
|
+
const existingModal = modalStack.current.find(
|
|
31
|
+
(modal: GenericModal) => modal.name === name,
|
|
32
|
+
);
|
|
33
|
+
if (existingModal) {
|
|
34
|
+
// If the modal is already open, just update its props
|
|
35
|
+
// additionally we don't need try/catch here, because we already know the modal name exists
|
|
36
|
+
existingModal.props = { ...existingModal.props, ...props };
|
|
37
|
+
setModal(existingModal);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (name in ModalLookup) {
|
|
43
|
+
modalStack.current.push({ name, props });
|
|
44
|
+
setModal({ name, props });
|
|
45
|
+
} else {
|
|
46
|
+
console.log("%cModalManager %copening:", keyFont, smallGreyFont, name);
|
|
47
|
+
console.log("%cModalStack", smallGreyFont, modalStack.current);
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Modal with name "${name}" does not exist in ModalLookup.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.log("Failed to open modal:", error);
|
|
54
|
+
console.error("Failed to open modal:", error);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const closeModal = () => {
|
|
59
|
+
console.log("closeModal");
|
|
60
|
+
modalStack.current.pop();
|
|
61
|
+
setModal(modalStack.current[modalStack.current.length - 1] ?? null);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return { ModalComponent, modal, openModal, closeModal };
|
|
65
|
+
};
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"emitDeclarationOnly": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"forceConsistentCasingInFileNames": true,
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"lib": ["DOM", "ESNext"],
|
|
10
|
+
"module": "CommonJS",
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"noFallthroughCasesInSwitch": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"target": "ESNext"
|
|
16
|
+
}
|
|
17
|
+
}
|