custome-modal 1.0.6
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 +98 -0
- package/package.json +30 -0
- package/src/custom-modal.js +244 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +1 -0
- package/src/react-modal.d.ts +25 -0
- package/src/react-modal.js +216 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Custom Modal
|
|
2
|
+
|
|
3
|
+
Lightweight modal component for React. Focused on rendering any React children, with simple API and optional imperative control.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @firstzxd/custome-modal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with yarn/pnpm:
|
|
12
|
+
```bash
|
|
13
|
+
yarn add @firstzxd/custome-modal
|
|
14
|
+
pnpm add @firstzxd/custome-modal
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## QuickStart
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
"use client";
|
|
21
|
+
|
|
22
|
+
import { useModal } from "@firstzxd/custome-modal/react";
|
|
23
|
+
|
|
24
|
+
export default function Example() {
|
|
25
|
+
const { openModal, closeModal, Modal } = useModal();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<button onClick={openModal}>Open modal</button>
|
|
30
|
+
|
|
31
|
+
<Modal width="lg">
|
|
32
|
+
<h2>Confirm action</h2>
|
|
33
|
+
<p>Custom content here</p>
|
|
34
|
+
<div className="mt-6 flex gap-2 justify-end">
|
|
35
|
+
<button onClick={closeModal}>Cancel</button>
|
|
36
|
+
<button onClick={closeModal}>Confirm</button>
|
|
37
|
+
</div>
|
|
38
|
+
</Modal>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Multiple Modals
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
"use client";
|
|
48
|
+
|
|
49
|
+
import { useModal } from "@firstzxd/custome-modal/react";
|
|
50
|
+
|
|
51
|
+
export default function Example() {
|
|
52
|
+
const confirmModal = useModal();
|
|
53
|
+
const detailModal = useModal();
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-3">
|
|
57
|
+
<button onClick={confirmModal.openModal}>Open confirm</button>
|
|
58
|
+
<button onClick={detailModal.openModal}>Open detail</button>
|
|
59
|
+
|
|
60
|
+
<confirmModal.Modal width="md">
|
|
61
|
+
<h2>Confirm delete</h2>
|
|
62
|
+
<p>Are you sure?</p>
|
|
63
|
+
<div className="mt-4 flex gap-2 justify-end">
|
|
64
|
+
<button onClick={confirmModal.closeModal}>Cancel</button>
|
|
65
|
+
<button onClick={confirmModal.closeModal}>Delete</button>
|
|
66
|
+
</div>
|
|
67
|
+
</confirmModal.Modal>
|
|
68
|
+
|
|
69
|
+
<detailModal.Modal width="lg">
|
|
70
|
+
<h2>Order detail</h2>
|
|
71
|
+
<p>Any custom content here</p>
|
|
72
|
+
<div className="mt-4 flex justify-end">
|
|
73
|
+
<button onClick={detailModal.closeModal}>Close</button>
|
|
74
|
+
</div>
|
|
75
|
+
</detailModal.Modal>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Props
|
|
82
|
+
|
|
83
|
+
- `children` (ReactNode) - custom content. Default: `undefined`.
|
|
84
|
+
- `dismissOnBackdrop` (boolean) - click backdrop to close. Default: `true`.
|
|
85
|
+
- `align` (`left` | `center` | `right`) - horizontal alignment inside viewport. Default: `center`.
|
|
86
|
+
- `justify` (`top` | `center` | `bottom`) - vertical alignment inside viewport. Default: `center`.
|
|
87
|
+
- `width` (`sm` | `md` | `lg` | `xl` | `2xl` | `3xl` | `4xl` | `5xl` | number | string) - modal width. Default: `lg`.
|
|
88
|
+
|
|
89
|
+
## Methods (from useModal)
|
|
90
|
+
|
|
91
|
+
- `openModal()` - open the modal.
|
|
92
|
+
- `closeModal()` - close the modal.
|
|
93
|
+
- `setOpen(value: boolean)` - set open state directly.
|
|
94
|
+
|
|
95
|
+
## Notes
|
|
96
|
+
|
|
97
|
+
- Works with any React app (Next.js, Vite, CRA, etc.)
|
|
98
|
+
- For Next.js, use within `"use client"` components only.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "custome-modal",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Custom modal component for react",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./src/react-modal.d.ts",
|
|
15
|
+
"default": "./src/react-modal.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"@firstzxd:registry": "https://npm.pkg.github.com"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"modal",
|
|
26
|
+
"nextjs",
|
|
27
|
+
"react",
|
|
28
|
+
"dialog"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
const template = document.createElement("template");
|
|
2
|
+
|
|
3
|
+
template.innerHTML = `
|
|
4
|
+
<style>
|
|
5
|
+
:host {
|
|
6
|
+
--cm-overlay-bg: rgba(0, 0, 0, 0.55);
|
|
7
|
+
--cm-bg: #ffffff;
|
|
8
|
+
--cm-text: #1a1a1a;
|
|
9
|
+
--cm-radius: 16px;
|
|
10
|
+
--cm-width: 720px;
|
|
11
|
+
--cm-max-width: 92vw;
|
|
12
|
+
--cm-max-height: 85vh;
|
|
13
|
+
--cm-padding: 12px;
|
|
14
|
+
--cm-shadow: 0 30px 80px rgba(0, 0, 0, 0.22);
|
|
15
|
+
--cm-z-index: 1000;
|
|
16
|
+
--cm-anim-duration: 180ms;
|
|
17
|
+
--cm-align-items: center;
|
|
18
|
+
--cm-justify-content: center;
|
|
19
|
+
|
|
20
|
+
display: none;
|
|
21
|
+
position: fixed;
|
|
22
|
+
inset: 0;
|
|
23
|
+
z-index: var(--cm-z-index);
|
|
24
|
+
font: inherit;
|
|
25
|
+
color: var(--cm-text);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:host([open]) {
|
|
29
|
+
display: block;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.overlay {
|
|
33
|
+
position: absolute;
|
|
34
|
+
inset: 0;
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: var(--cm-align-items);
|
|
37
|
+
justify-content: var(--cm-justify-content);
|
|
38
|
+
padding: var(--cm-padding);
|
|
39
|
+
background: var(--cm-overlay-bg);
|
|
40
|
+
opacity: 0;
|
|
41
|
+
transition: opacity var(--cm-anim-duration) ease;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:host([open]) .overlay {
|
|
45
|
+
opacity: 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dialog {
|
|
49
|
+
position: relative;
|
|
50
|
+
width: var(--cm-width);
|
|
51
|
+
max-width: var(--cm-max-width);
|
|
52
|
+
max-height: var(--cm-max-height);
|
|
53
|
+
overflow: auto;
|
|
54
|
+
background: var(--cm-bg);
|
|
55
|
+
border-radius: var(--cm-radius);
|
|
56
|
+
box-shadow: var(--cm-shadow);
|
|
57
|
+
transform: translateY(10px) scale(0.98);
|
|
58
|
+
opacity: 0;
|
|
59
|
+
transition: transform var(--cm-anim-duration) ease, opacity var(--cm-anim-duration) ease;
|
|
60
|
+
outline: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:host([open]) .dialog {
|
|
64
|
+
transform: translateY(0) scale(1);
|
|
65
|
+
opacity: 1;
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
|
|
69
|
+
<div class="overlay" part="overlay">
|
|
70
|
+
<div class="dialog" part="dialog" role="dialog" aria-modal="true" tabindex="-1">
|
|
71
|
+
<slot></slot>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
class CustomModal extends HTMLElement {
|
|
77
|
+
static get observedAttributes() {
|
|
78
|
+
return ["open", "width", "align", "justify", "dismiss-on-backdrop"];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
this.attachShadow({ mode: "open" });
|
|
84
|
+
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
85
|
+
|
|
86
|
+
this._overlayEl = this.shadowRoot.querySelector(".overlay");
|
|
87
|
+
this._dialogEl = this.shadowRoot.querySelector(".dialog");
|
|
88
|
+
|
|
89
|
+
this._handleOverlayClick = this._handleOverlayClick.bind(this);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
connectedCallback() {
|
|
93
|
+
this._upgradeProperty("open");
|
|
94
|
+
this._upgradeProperty("width");
|
|
95
|
+
this._upgradeProperty("align");
|
|
96
|
+
this._upgradeProperty("justify");
|
|
97
|
+
this._upgradeProperty("dismissOnBackdrop");
|
|
98
|
+
|
|
99
|
+
this._overlayEl.addEventListener("click", this._handleOverlayClick);
|
|
100
|
+
this._syncLayout();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
disconnectedCallback() {
|
|
104
|
+
this._overlayEl.removeEventListener("click", this._handleOverlayClick);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
108
|
+
if (oldValue === newValue) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (name === "open") {
|
|
113
|
+
if (this.open) {
|
|
114
|
+
this._onOpen();
|
|
115
|
+
} else {
|
|
116
|
+
this._onClose();
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (name === "width" || name === "align" || name === "justify") {
|
|
122
|
+
this._syncLayout();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get open() {
|
|
127
|
+
return this.hasAttribute("open");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set open(value) {
|
|
131
|
+
if (value) {
|
|
132
|
+
this.setAttribute("open", "");
|
|
133
|
+
} else {
|
|
134
|
+
this.removeAttribute("open");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
openModal() {
|
|
139
|
+
this.open = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
closeModal() {
|
|
143
|
+
this.open = false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get dismissOnBackdrop() {
|
|
147
|
+
return !this._isFalseAttr("dismiss-on-backdrop");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
set dismissOnBackdrop(value) {
|
|
151
|
+
if (value === false) {
|
|
152
|
+
this.setAttribute("dismiss-on-backdrop", "false");
|
|
153
|
+
} else {
|
|
154
|
+
this.removeAttribute("dismiss-on-backdrop");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_upgradeProperty(prop) {
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(this, prop)) {
|
|
160
|
+
const value = this[prop];
|
|
161
|
+
delete this[prop];
|
|
162
|
+
this[prop] = value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_isFalseAttr(name) {
|
|
167
|
+
const attr = this.getAttribute(name);
|
|
168
|
+
return attr !== null && attr.toLowerCase() === "false";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_handleOverlayClick(event) {
|
|
172
|
+
if (event.target !== this._overlayEl) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (this.dismissOnBackdrop) {
|
|
176
|
+
this.closeModal();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
_onOpen() {
|
|
181
|
+
this._lastActive = document.activeElement;
|
|
182
|
+
this.dispatchEvent(new CustomEvent("modal-opened", { bubbles: true }));
|
|
183
|
+
|
|
184
|
+
window.requestAnimationFrame(() => {
|
|
185
|
+
this._dialogEl.focus();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_onClose() {
|
|
190
|
+
this.dispatchEvent(new CustomEvent("modal-closed", { bubbles: true }));
|
|
191
|
+
|
|
192
|
+
if (this._lastActive && typeof this._lastActive.focus === "function") {
|
|
193
|
+
this._lastActive.focus();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_syncLayout() {
|
|
198
|
+
const align = (this.getAttribute("align") || "center").toLowerCase();
|
|
199
|
+
const justify = (this.getAttribute("justify") || "center").toLowerCase();
|
|
200
|
+
const width = (this.getAttribute("width") || "lg").toLowerCase();
|
|
201
|
+
|
|
202
|
+
const alignMap = {
|
|
203
|
+
left: "flex-start",
|
|
204
|
+
center: "center",
|
|
205
|
+
right: "flex-end"
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const justifyMap = {
|
|
209
|
+
top: "flex-start",
|
|
210
|
+
center: "center",
|
|
211
|
+
bottom: "flex-end"
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const widthMap = {
|
|
215
|
+
sm: "300px",
|
|
216
|
+
md: "500px",
|
|
217
|
+
lg: "720px",
|
|
218
|
+
xl: "960px",
|
|
219
|
+
"2xl": "1140px",
|
|
220
|
+
"3xl": "1280px",
|
|
221
|
+
"4xl": "1440px",
|
|
222
|
+
"5xl": "1600px"
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const widthValue = widthMap[width] || width;
|
|
226
|
+
this.style.setProperty("--cm-align-items", alignMap[align] || "center");
|
|
227
|
+
this.style.setProperty("--cm-justify-content", justifyMap[justify] || "center");
|
|
228
|
+
this.style.setProperty("--cm-width", this._formatWidth(widthValue));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_formatWidth(value) {
|
|
232
|
+
if (!value) {
|
|
233
|
+
return "720px";
|
|
234
|
+
}
|
|
235
|
+
if (/^\d+$/.test(value)) {
|
|
236
|
+
return `${value}px`;
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
customElements.define("custom-modal", CustomModal);
|
|
243
|
+
|
|
244
|
+
export { CustomModal };
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CustomModal } from "./custom-modal.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
type Align = "left" | "center" | "right";
|
|
4
|
+
|
|
5
|
+
type Justify = "top" | "center" | "bottom";
|
|
6
|
+
|
|
7
|
+
type Width = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | number | string;
|
|
8
|
+
|
|
9
|
+
export type ModalProps = {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
dismissOnBackdrop?: boolean;
|
|
12
|
+
align?: Align;
|
|
13
|
+
justify?: Justify;
|
|
14
|
+
width?: Width;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function Modal(props: ModalProps): JSX.Element | null;
|
|
18
|
+
|
|
19
|
+
export function useModal(options?: Partial<ModalProps>): {
|
|
20
|
+
open: boolean;
|
|
21
|
+
setOpen: (value: boolean) => void;
|
|
22
|
+
openModal: () => void;
|
|
23
|
+
closeModal: () => void;
|
|
24
|
+
Modal: (props: ModalProps) => JSX.Element | null;
|
|
25
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
|
|
4
|
+
const ALIGN_MAP = {
|
|
5
|
+
left: "flex-start",
|
|
6
|
+
center: "center",
|
|
7
|
+
right: "flex-end"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const JUSTIFY_MAP = {
|
|
11
|
+
top: "flex-start",
|
|
12
|
+
center: "center",
|
|
13
|
+
bottom: "flex-end"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const WIDTH_MAP = {
|
|
17
|
+
sm: "300px",
|
|
18
|
+
md: "500px",
|
|
19
|
+
lg: "720px",
|
|
20
|
+
xl: "960px",
|
|
21
|
+
"2xl": "1140px",
|
|
22
|
+
"3xl": "1280px",
|
|
23
|
+
"4xl": "1440px",
|
|
24
|
+
"5xl": "1600px"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let scrollLockCount = 0;
|
|
28
|
+
|
|
29
|
+
function enableScrollLock() {
|
|
30
|
+
if (scrollLockCount === 0) {
|
|
31
|
+
const prevOverflow = document.body.style.overflow;
|
|
32
|
+
document.body.dataset.scrollLockPrev = prevOverflow || "auto";
|
|
33
|
+
document.body.style.overflow = "hidden";
|
|
34
|
+
}
|
|
35
|
+
scrollLockCount += 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function disableScrollLock() {
|
|
39
|
+
scrollLockCount = Math.max(0, scrollLockCount - 1);
|
|
40
|
+
if (scrollLockCount === 0) {
|
|
41
|
+
const prevOverflow = document.body.dataset.scrollLockPrev || "auto";
|
|
42
|
+
document.body.style.overflow = prevOverflow;
|
|
43
|
+
delete document.body.dataset.scrollLockPrev;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Portal({ children, selector = "body" }) {
|
|
48
|
+
const container = useMemo(() => document.createElement("div"), []);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
let host = document.body;
|
|
52
|
+
if (typeof selector === "string" && selector.length > 0) {
|
|
53
|
+
try {
|
|
54
|
+
host = document.querySelector(selector) ?? document.body;
|
|
55
|
+
} catch {
|
|
56
|
+
host = document.body;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
host.appendChild(container);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
if (host.contains(container)) {
|
|
64
|
+
host.removeChild(container);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, [selector, container]);
|
|
68
|
+
|
|
69
|
+
return createPortal(children, container);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function Modal({
|
|
73
|
+
children,
|
|
74
|
+
dismissOnBackdrop = true,
|
|
75
|
+
align = "center",
|
|
76
|
+
justify = "center",
|
|
77
|
+
width = "lg",
|
|
78
|
+
_open = false,
|
|
79
|
+
_onClose,
|
|
80
|
+
_lockScroll = true,
|
|
81
|
+
_closeOnEsc = true
|
|
82
|
+
} = {}) {
|
|
83
|
+
const dialogRef = useRef(null);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!_open || !_lockScroll) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
enableScrollLock();
|
|
90
|
+
return () => disableScrollLock();
|
|
91
|
+
}, [_open, _lockScroll]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!_open || !_closeOnEsc) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onKey(event) {
|
|
99
|
+
if (event.key === "Escape") {
|
|
100
|
+
_onClose?.();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
document.addEventListener("keydown", onKey);
|
|
105
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
106
|
+
}, [_open, _closeOnEsc, _onClose]);
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if (!_open) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const alignItems = ALIGN_MAP[align] || "center";
|
|
114
|
+
const justifyContent = JUSTIFY_MAP[justify] || "center";
|
|
115
|
+
const widthValue = typeof width === "number" ? `${width}px` : (WIDTH_MAP[width] || width);
|
|
116
|
+
|
|
117
|
+
const safeContainerStyle = {
|
|
118
|
+
position: "fixed",
|
|
119
|
+
inset: 0,
|
|
120
|
+
zIndex: 1000,
|
|
121
|
+
display: "flex",
|
|
122
|
+
alignItems,
|
|
123
|
+
justifyContent,
|
|
124
|
+
padding: "12px"
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const safeOverlayStyle = {
|
|
128
|
+
position: "absolute",
|
|
129
|
+
inset: 0,
|
|
130
|
+
background: "rgba(0, 0, 0, 0.5)"
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const safeDialogStyle = {
|
|
134
|
+
position: "relative",
|
|
135
|
+
zIndex: 1,
|
|
136
|
+
background: "#ffffff",
|
|
137
|
+
borderRadius: "12px",
|
|
138
|
+
boxShadow: "0 20px 60px rgba(0, 0, 0, 0.2)",
|
|
139
|
+
width: widthValue || "720px",
|
|
140
|
+
maxWidth: "92vw",
|
|
141
|
+
maxHeight: "85vh",
|
|
142
|
+
overflow: "auto"
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return React.createElement(
|
|
146
|
+
Portal,
|
|
147
|
+
{ selector: "body" },
|
|
148
|
+
React.createElement(
|
|
149
|
+
"div",
|
|
150
|
+
{
|
|
151
|
+
className: "",
|
|
152
|
+
style: safeContainerStyle
|
|
153
|
+
},
|
|
154
|
+
React.createElement("div", {
|
|
155
|
+
className: "",
|
|
156
|
+
style: safeOverlayStyle,
|
|
157
|
+
onClick: () => dismissOnBackdrop && _onClose?.()
|
|
158
|
+
}),
|
|
159
|
+
React.createElement(
|
|
160
|
+
"div",
|
|
161
|
+
{
|
|
162
|
+
ref: dialogRef,
|
|
163
|
+
role: "dialog",
|
|
164
|
+
"aria-modal": "true",
|
|
165
|
+
className: "",
|
|
166
|
+
style: safeDialogStyle,
|
|
167
|
+
onClick: (event) => event.stopPropagation()
|
|
168
|
+
},
|
|
169
|
+
children
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function useModal(options = {}) {
|
|
176
|
+
const [open, setOpen] = useState(false);
|
|
177
|
+
|
|
178
|
+
const openModal = () => setOpen(true);
|
|
179
|
+
const closeModal = () => setOpen(false);
|
|
180
|
+
|
|
181
|
+
const stateRef = useRef({
|
|
182
|
+
open,
|
|
183
|
+
closeModal,
|
|
184
|
+
options
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
stateRef.current = {
|
|
188
|
+
open,
|
|
189
|
+
closeModal,
|
|
190
|
+
options
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const modalSlotRef = useRef(null);
|
|
194
|
+
if (!modalSlotRef.current) {
|
|
195
|
+
modalSlotRef.current = function ModalSlot(props) {
|
|
196
|
+
const current = stateRef.current;
|
|
197
|
+
return React.createElement(Modal, {
|
|
198
|
+
_open: current.open,
|
|
199
|
+
_onClose: current.closeModal,
|
|
200
|
+
_lockScroll: true,
|
|
201
|
+
_closeOnEsc: true,
|
|
202
|
+
...current.options,
|
|
203
|
+
...props
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
modalSlotRef.current.displayName = "ModalSlot";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
open,
|
|
211
|
+
setOpen,
|
|
212
|
+
openModal,
|
|
213
|
+
closeModal,
|
|
214
|
+
Modal: modalSlotRef.current
|
|
215
|
+
};
|
|
216
|
+
}
|