@mshafiqyajid/react-modal 0.0.0 → 0.2.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/dist/styled.cjs CHANGED
@@ -66,9 +66,11 @@ function useFocusTrap() {
66
66
  }
67
67
  var ModalStyled = react.forwardRef(
68
68
  function ModalStyled2({
69
+ open,
69
70
  isOpen,
70
71
  onClose,
71
72
  title,
73
+ description,
72
74
  children,
73
75
  footer,
74
76
  size = "md",
@@ -80,83 +82,133 @@ var ModalStyled = react.forwardRef(
80
82
  overlayColor,
81
83
  padding = "md",
82
84
  scrollable = true,
83
- className
85
+ className,
86
+ initialFocusRef,
87
+ finalFocusRef,
88
+ onAfterOpen,
89
+ onAfterClose,
90
+ preventClose,
91
+ lockBodyScroll = true,
92
+ container
84
93
  }, ref) {
85
94
  const titleId = react.useId();
95
+ const descId = react.useId();
96
+ const isActuallyOpen = open ?? isOpen ?? false;
86
97
  const [mounted, setMounted] = react.useState(false);
87
98
  const [rendered, setRendered] = react.useState(false);
88
99
  const [visible, setVisible] = react.useState(false);
89
100
  const panelRef = react.useRef(null);
90
101
  const originalOverflowRef = react.useRef("");
91
102
  const exitTimerRef = react.useRef(null);
103
+ const enterTimerRef = react.useRef(null);
92
104
  const { activate, deactivate, handleKeyDown } = useFocusTrap();
105
+ const onAfterOpenRef = react.useRef(onAfterOpen);
106
+ onAfterOpenRef.current = onAfterOpen;
107
+ const onAfterCloseRef = react.useRef(onAfterClose);
108
+ onAfterCloseRef.current = onAfterClose;
93
109
  react.useEffect(() => {
94
110
  setMounted(true);
95
111
  }, []);
112
+ const requestClose = react.useCallback(
113
+ (reason) => {
114
+ if (preventClose && !preventClose(reason)) return;
115
+ onClose();
116
+ },
117
+ [preventClose, onClose]
118
+ );
96
119
  react.useEffect(() => {
97
120
  if (exitTimerRef.current) {
98
121
  clearTimeout(exitTimerRef.current);
99
122
  exitTimerRef.current = null;
100
123
  }
101
- if (isOpen) {
124
+ if (enterTimerRef.current) {
125
+ clearTimeout(enterTimerRef.current);
126
+ enterTimerRef.current = null;
127
+ }
128
+ if (isActuallyOpen) {
102
129
  setRendered(true);
103
130
  requestAnimationFrame(() => {
104
131
  requestAnimationFrame(() => {
105
132
  setVisible(true);
106
133
  });
107
134
  });
108
- originalOverflowRef.current = document.body.style.overflow;
109
- document.body.style.overflow = "hidden";
135
+ if (lockBodyScroll) {
136
+ originalOverflowRef.current = document.body.style.overflow;
137
+ document.body.style.overflow = "hidden";
138
+ }
139
+ enterTimerRef.current = setTimeout(() => {
140
+ onAfterOpenRef.current?.();
141
+ }, 320);
110
142
  } else {
111
143
  setVisible(false);
112
144
  exitTimerRef.current = setTimeout(() => {
113
145
  setRendered(false);
146
+ onAfterCloseRef.current?.();
114
147
  }, 300);
115
- document.body.style.overflow = originalOverflowRef.current;
148
+ if (lockBodyScroll) {
149
+ document.body.style.overflow = originalOverflowRef.current;
150
+ }
116
151
  }
117
- }, [isOpen]);
152
+ }, [isActuallyOpen, lockBodyScroll]);
118
153
  react.useEffect(() => {
119
154
  return () => {
120
155
  if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
121
- document.body.style.overflow = originalOverflowRef.current;
156
+ if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
157
+ if (lockBodyScroll) {
158
+ document.body.style.overflow = originalOverflowRef.current;
159
+ }
122
160
  };
123
- }, []);
161
+ }, [lockBodyScroll]);
162
+ const previouslyFocusedRef = react.useRef(null);
124
163
  react.useEffect(() => {
125
- if (isOpen && panelRef.current) {
126
- activate(panelRef.current);
127
- } else if (!isOpen) {
164
+ if (isActuallyOpen) {
165
+ previouslyFocusedRef.current = document.activeElement;
166
+ if (panelRef.current) {
167
+ activate(panelRef.current);
168
+ requestAnimationFrame(() => {
169
+ if (initialFocusRef?.current) {
170
+ initialFocusRef.current.focus();
171
+ }
172
+ });
173
+ }
174
+ } else {
128
175
  deactivate();
176
+ const target = finalFocusRef?.current ?? previouslyFocusedRef.current;
177
+ if (target && typeof target.focus === "function") {
178
+ target.focus();
179
+ }
129
180
  }
130
- }, [isOpen, activate, deactivate]);
181
+ }, [isActuallyOpen, activate, deactivate, initialFocusRef, finalFocusRef]);
131
182
  const onKeyDown = react.useCallback(
132
183
  (e) => {
133
184
  if (closeOnEsc && e.key === "Escape") {
134
- onClose();
185
+ requestClose("esc");
135
186
  return;
136
187
  }
137
188
  handleKeyDown(e.nativeEvent);
138
189
  },
139
- [closeOnEsc, onClose, handleKeyDown]
190
+ [closeOnEsc, requestClose, handleKeyDown]
140
191
  );
141
192
  const handleOverlayClick = react.useCallback(
142
193
  (e) => {
143
194
  if (closeOnOverlayClick && e.target === e.currentTarget) {
144
- onClose();
195
+ requestClose("overlay");
145
196
  }
146
197
  },
147
- [closeOnOverlayClick, onClose]
198
+ [closeOnOverlayClick, requestClose]
148
199
  );
149
200
  const setPanelRef = react.useCallback(
150
201
  (el) => {
151
202
  panelRef.current = el;
152
203
  if (typeof ref === "function") ref(el);
153
204
  else if (ref) ref.current = el;
154
- if (el && isOpen) activate(el);
205
+ if (el && isActuallyOpen) activate(el);
155
206
  },
156
- [ref, isOpen, activate]
207
+ [ref, isActuallyOpen, activate]
157
208
  );
158
209
  if (!mounted || !rendered) return null;
159
210
  const hasHeader = title !== void 0 || showCloseButton;
211
+ const portalTarget = container ?? document.body;
160
212
  return reactDom.createPortal(
161
213
  /* @__PURE__ */ jsxRuntime.jsx(
162
214
  "div",
@@ -167,6 +219,7 @@ var ModalStyled = react.forwardRef(
167
219
  ].filter(Boolean).join(" "),
168
220
  "data-variant": variant,
169
221
  "data-blur": blur,
222
+ "data-state": visible ? "open" : "closed",
170
223
  style: overlayColor ? { "--rmod-overlay-bg": overlayColor } : void 0,
171
224
  onClick: handleOverlayClick,
172
225
  onKeyDown,
@@ -177,6 +230,7 @@ var ModalStyled = react.forwardRef(
177
230
  role: "dialog",
178
231
  "aria-modal": "true",
179
232
  "aria-labelledby": title ? titleId : void 0,
233
+ "aria-describedby": description ? descId : void 0,
180
234
  tabIndex: -1,
181
235
  className: [
182
236
  "rmod-panel",
@@ -187,11 +241,22 @@ var ModalStyled = react.forwardRef(
187
241
  "data-variant": variant,
188
242
  "data-padding": padding,
189
243
  "data-scrollable": scrollable ? "true" : void 0,
244
+ "data-state": visible ? "open" : "closed",
190
245
  children: [
191
246
  hasHeader && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rmod-header", children: [
192
247
  title ? /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, className: "rmod-title", children: title }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
193
- showCloseButton && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "rmod-close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsxRuntime.jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4L4 12M4 4l8 8", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" }) }) })
248
+ showCloseButton && /* @__PURE__ */ jsxRuntime.jsx(
249
+ "button",
250
+ {
251
+ type: "button",
252
+ className: "rmod-close",
253
+ "aria-label": "Close",
254
+ onClick: () => requestClose("close-button"),
255
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4L4 12M4 4l8 8", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" }) })
256
+ }
257
+ )
194
258
  ] }),
259
+ description !== void 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { id: descId, className: "rmod-description", children: description }),
195
260
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rmod-body", children }),
196
261
  footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rmod-footer", children: footer })
197
262
  ]
@@ -199,7 +264,7 @@ var ModalStyled = react.forwardRef(
199
264
  )
200
265
  }
201
266
  ),
202
- document.body
267
+ portalTarget
203
268
  );
204
269
  }
205
270
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useFocusTrap.ts","../src/styled/ModalStyled.tsx"],"names":["useRef","useCallback","forwardRef","ModalStyled","useId","useState","useEffect","createPortal","jsx","jsxs"],"mappings":"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","file":"styled.cjs","sourcesContent":["import { useCallback, useRef } from \"react\";\n\nconst FOCUSABLE_SELECTORS = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n \"details > summary\",\n].join(\", \");\n\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)).filter(\n (el) => !el.closest(\"[inert]\") && getComputedStyle(el).display !== \"none\",\n );\n}\n\nexport interface UseFocusTrapResult {\n activate: (container: HTMLElement) => void;\n deactivate: () => void;\n handleKeyDown: (e: KeyboardEvent) => void;\n}\n\nexport function useFocusTrap(): UseFocusTrapResult {\n const containerRef = useRef<HTMLElement | null>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n\n const activate = useCallback((container: HTMLElement) => {\n containerRef.current = container;\n previousFocusRef.current = document.activeElement as HTMLElement | null;\n\n const focusable = getFocusableElements(container);\n const first = focusable[0];\n if (first) {\n first.focus();\n } else {\n container.focus();\n }\n }, []);\n\n const deactivate = useCallback(() => {\n containerRef.current = null;\n const prev = previousFocusRef.current;\n if (prev && typeof prev.focus === \"function\") {\n prev.focus();\n }\n previousFocusRef.current = null;\n }, []);\n\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\n if (e.key !== \"Tab\" || !containerRef.current) return;\n\n const focusable = getFocusableElements(containerRef.current);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n\n const first = focusable[0]!;\n const last = focusable[focusable.length - 1]!;\n\n if (e.shiftKey) {\n if (document.activeElement === first) {\n e.preventDefault();\n last.focus();\n }\n } else {\n if (document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n }, []);\n\n return { activate, deactivate, handleKeyDown };\n}\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useFocusTrap } from \"../useFocusTrap\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"full\";\nexport type ModalVariant = \"dialog\" | \"drawer-left\" | \"drawer-right\" | \"drawer-bottom\";\n\nexport interface ModalStyledProps {\n isOpen: boolean;\n onClose: () => void;\n /** Modal heading — omit to hide the header entirely */\n title?: ReactNode;\n children: ReactNode;\n /** Footer content — omit to hide footer */\n footer?: ReactNode;\n size?: ModalSize;\n variant?: ModalVariant;\n closeOnOverlayClick?: boolean;\n closeOnEsc?: boolean;\n showCloseButton?: boolean;\n /** Backdrop blur intensity. Default: \"md\" */\n blur?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Custom overlay color e.g. \"rgba(0,0,0,0.6)\" */\n overlayColor?: string;\n /** Padding inside the body. Default: \"md\" */\n padding?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Max height of scrollable body. Default: auto */\n scrollable?: boolean;\n className?: string;\n}\n\nexport const ModalStyled = forwardRef<HTMLDivElement, ModalStyledProps>(\n function ModalStyled(\n {\n isOpen,\n onClose,\n title,\n children,\n footer,\n size = \"md\",\n variant = \"dialog\",\n closeOnOverlayClick = true,\n closeOnEsc = true,\n showCloseButton = true,\n blur = \"md\",\n overlayColor,\n padding = \"md\",\n scrollable = true,\n className,\n },\n ref,\n ) {\n const titleId = useId();\n const [mounted, setMounted] = useState(false);\n const [rendered, setRendered] = useState(false);\n const [visible, setVisible] = useState(false);\n const panelRef = useRef<HTMLDivElement | null>(null);\n const originalOverflowRef = useRef(\"\");\n const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const { activate, deactivate, handleKeyDown } = useFocusTrap();\n\n useEffect(() => { setMounted(true); }, []);\n\n useEffect(() => {\n if (exitTimerRef.current) {\n clearTimeout(exitTimerRef.current);\n exitTimerRef.current = null;\n }\n\n if (isOpen) {\n setRendered(true);\n // Double RAF ensures the element is in the DOM before we add the visible class\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n setVisible(true);\n });\n });\n originalOverflowRef.current = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n } else {\n // Remove visible class first (triggers CSS exit transition)\n setVisible(false);\n // Then unmount after transition finishes\n exitTimerRef.current = setTimeout(() => {\n setRendered(false);\n }, 300);\n document.body.style.overflow = originalOverflowRef.current;\n }\n }, [isOpen]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (exitTimerRef.current) clearTimeout(exitTimerRef.current);\n document.body.style.overflow = originalOverflowRef.current;\n };\n }, []);\n\n useEffect(() => {\n if (isOpen && panelRef.current) {\n activate(panelRef.current);\n } else if (!isOpen) {\n deactivate();\n }\n }, [isOpen, activate, deactivate]);\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (closeOnEsc && e.key === \"Escape\") {\n onClose();\n return;\n }\n handleKeyDown(e.nativeEvent);\n },\n [closeOnEsc, onClose, handleKeyDown],\n );\n\n const handleOverlayClick = useCallback(\n (e: React.MouseEvent<HTMLDivElement>) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n onClose();\n }\n },\n [closeOnOverlayClick, onClose],\n );\n\n const setPanelRef = useCallback(\n (el: HTMLDivElement | null) => {\n panelRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;\n if (el && isOpen) activate(el);\n },\n [ref, isOpen, activate],\n );\n\n if (!mounted || !rendered) return null;\n\n const hasHeader = title !== undefined || showCloseButton;\n\n return createPortal(\n <div\n className={[\n \"rmod-overlay\",\n visible ? \"rmod-overlay--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-variant={variant}\n data-blur={blur}\n style={overlayColor ? { \"--rmod-overlay-bg\": overlayColor } as React.CSSProperties : undefined}\n onClick={handleOverlayClick}\n onKeyDown={onKeyDown}\n >\n <div\n ref={setPanelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n tabIndex={-1}\n className={[\n \"rmod-panel\",\n className,\n visible ? \"rmod-panel--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-size={size}\n data-variant={variant}\n data-padding={padding}\n data-scrollable={scrollable ? \"true\" : undefined}\n >\n {hasHeader && (\n <div className=\"rmod-header\">\n {title ? (\n <h2 id={titleId} className=\"rmod-title\">{title}</h2>\n ) : (\n <span />\n )}\n {showCloseButton && (\n <button type=\"button\" className=\"rmod-close\" aria-label=\"Close\" onClick={onClose}>\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M12 4L4 12M4 4l8 8\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </button>\n )}\n </div>\n )}\n <div className=\"rmod-body\">{children}</div>\n {footer && <div className=\"rmod-footer\">{footer}</div>}\n </div>\n </div>,\n document.body,\n );\n },\n);\n"]}
1
+ {"version":3,"sources":["../src/useFocusTrap.ts","../src/styled/ModalStyled.tsx"],"names":["useRef","useCallback","forwardRef","ModalStyled","useId","useState","useEffect","createPortal","jsx","jsxs"],"mappings":"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","file":"styled.cjs","sourcesContent":["import { useCallback, useRef } from \"react\";\n\nconst FOCUSABLE_SELECTORS = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n \"[tabindex]:not([tabindex='-1'])\",\n \"details > summary\",\n].join(\", \");\n\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)).filter(\n (el) => !el.closest(\"[inert]\") && getComputedStyle(el).display !== \"none\",\n );\n}\n\nexport interface UseFocusTrapResult {\n activate: (container: HTMLElement) => void;\n deactivate: () => void;\n handleKeyDown: (e: KeyboardEvent) => void;\n}\n\nexport function useFocusTrap(): UseFocusTrapResult {\n const containerRef = useRef<HTMLElement | null>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n\n const activate = useCallback((container: HTMLElement) => {\n containerRef.current = container;\n previousFocusRef.current = document.activeElement as HTMLElement | null;\n\n const focusable = getFocusableElements(container);\n const first = focusable[0];\n if (first) {\n first.focus();\n } else {\n container.focus();\n }\n }, []);\n\n const deactivate = useCallback(() => {\n containerRef.current = null;\n const prev = previousFocusRef.current;\n if (prev && typeof prev.focus === \"function\") {\n prev.focus();\n }\n previousFocusRef.current = null;\n }, []);\n\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\n if (e.key !== \"Tab\" || !containerRef.current) return;\n\n const focusable = getFocusableElements(containerRef.current);\n if (focusable.length === 0) {\n e.preventDefault();\n return;\n }\n\n const first = focusable[0]!;\n const last = focusable[focusable.length - 1]!;\n\n if (e.shiftKey) {\n if (document.activeElement === first) {\n e.preventDefault();\n last.focus();\n }\n } else {\n if (document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n }, []);\n\n return { activate, deactivate, handleKeyDown };\n}\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useFocusTrap } from \"../useFocusTrap\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"full\";\nexport type ModalVariant = \"dialog\" | \"drawer-left\" | \"drawer-right\" | \"drawer-bottom\";\nexport type CloseReason = \"esc\" | \"overlay\" | \"close-button\" | \"programmatic\";\n\nexport interface ModalStyledProps {\n /** Open state. `open` is the canonical name; `isOpen` continues to work. */\n open?: boolean;\n isOpen?: boolean;\n onClose: () => void;\n /** Modal heading — omit to hide the header entirely */\n title?: ReactNode;\n /** Optional accessible description. Renders below the title and links via aria-describedby. */\n description?: ReactNode;\n children: ReactNode;\n /** Footer content — omit to hide footer */\n footer?: ReactNode;\n size?: ModalSize;\n variant?: ModalVariant;\n closeOnOverlayClick?: boolean;\n closeOnEsc?: boolean;\n showCloseButton?: boolean;\n /** Backdrop blur intensity. Default: \"md\" */\n blur?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Custom overlay color e.g. \"rgba(0,0,0,0.6)\" */\n overlayColor?: string;\n /** Padding inside the body. Default: \"md\" */\n padding?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Max height of scrollable body. Default: auto */\n scrollable?: boolean;\n className?: string;\n /** Element to focus when the modal opens. Defaults to the first focusable child inside the panel. */\n initialFocusRef?: RefObject<HTMLElement | null>;\n /** Element to focus when the modal closes. Defaults to whatever was focused before opening. */\n finalFocusRef?: RefObject<HTMLElement | null>;\n /** Fires after the open transition completes. */\n onAfterOpen?: () => void;\n /** Fires after the close transition completes (and unmount). */\n onAfterClose?: () => void;\n /** Return false to veto a close attempt. Receives the trigger reason. */\n preventClose?: (reason: CloseReason) => boolean;\n /** Disable body scroll lock while open. Default: true (locked). */\n lockBodyScroll?: boolean;\n /** Override the portal container. Default: document.body. */\n container?: HTMLElement | null;\n}\n\nexport const ModalStyled = forwardRef<HTMLDivElement, ModalStyledProps>(\n function ModalStyled(\n {\n open,\n isOpen,\n onClose,\n title,\n description,\n children,\n footer,\n size = \"md\",\n variant = \"dialog\",\n closeOnOverlayClick = true,\n closeOnEsc = true,\n showCloseButton = true,\n blur = \"md\",\n overlayColor,\n padding = \"md\",\n scrollable = true,\n className,\n initialFocusRef,\n finalFocusRef,\n onAfterOpen,\n onAfterClose,\n preventClose,\n lockBodyScroll = true,\n container,\n },\n ref,\n ) {\n const titleId = useId();\n const descId = useId();\n const isActuallyOpen = open ?? isOpen ?? false;\n const [mounted, setMounted] = useState(false);\n const [rendered, setRendered] = useState(false);\n const [visible, setVisible] = useState(false);\n const panelRef = useRef<HTMLDivElement | null>(null);\n const originalOverflowRef = useRef(\"\");\n const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const enterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const { activate, deactivate, handleKeyDown } = useFocusTrap();\n\n const onAfterOpenRef = useRef(onAfterOpen);\n onAfterOpenRef.current = onAfterOpen;\n const onAfterCloseRef = useRef(onAfterClose);\n onAfterCloseRef.current = onAfterClose;\n\n useEffect(() => { setMounted(true); }, []);\n\n const requestClose = useCallback(\n (reason: CloseReason) => {\n if (preventClose && !preventClose(reason)) return;\n onClose();\n },\n [preventClose, onClose],\n );\n\n useEffect(() => {\n if (exitTimerRef.current) {\n clearTimeout(exitTimerRef.current);\n exitTimerRef.current = null;\n }\n if (enterTimerRef.current) {\n clearTimeout(enterTimerRef.current);\n enterTimerRef.current = null;\n }\n\n if (isActuallyOpen) {\n setRendered(true);\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n setVisible(true);\n });\n });\n if (lockBodyScroll) {\n originalOverflowRef.current = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n // Fire onAfterOpen once the enter transition has had time to settle.\n enterTimerRef.current = setTimeout(() => {\n onAfterOpenRef.current?.();\n }, 320);\n } else {\n setVisible(false);\n exitTimerRef.current = setTimeout(() => {\n setRendered(false);\n onAfterCloseRef.current?.();\n }, 300);\n if (lockBodyScroll) {\n document.body.style.overflow = originalOverflowRef.current;\n }\n }\n }, [isActuallyOpen, lockBodyScroll]);\n\n useEffect(() => {\n return () => {\n if (exitTimerRef.current) clearTimeout(exitTimerRef.current);\n if (enterTimerRef.current) clearTimeout(enterTimerRef.current);\n if (lockBodyScroll) {\n document.body.style.overflow = originalOverflowRef.current;\n }\n };\n }, [lockBodyScroll]);\n\n // Focus management\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n useEffect(() => {\n if (isActuallyOpen) {\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n if (panelRef.current) {\n activate(panelRef.current);\n // Honor initialFocusRef after the panel is mounted+visible\n requestAnimationFrame(() => {\n if (initialFocusRef?.current) {\n initialFocusRef.current.focus();\n }\n });\n }\n } else {\n deactivate();\n // Honor finalFocusRef on close, otherwise restore previous focus\n const target = finalFocusRef?.current ?? previouslyFocusedRef.current;\n if (target && typeof target.focus === \"function\") {\n target.focus();\n }\n }\n }, [isActuallyOpen, activate, deactivate, initialFocusRef, finalFocusRef]);\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (closeOnEsc && e.key === \"Escape\") {\n requestClose(\"esc\");\n return;\n }\n handleKeyDown(e.nativeEvent);\n },\n [closeOnEsc, requestClose, handleKeyDown],\n );\n\n const handleOverlayClick = useCallback(\n (e: React.MouseEvent<HTMLDivElement>) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n requestClose(\"overlay\");\n }\n },\n [closeOnOverlayClick, requestClose],\n );\n\n const setPanelRef = useCallback(\n (el: HTMLDivElement | null) => {\n panelRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;\n if (el && isActuallyOpen) activate(el);\n },\n [ref, isActuallyOpen, activate],\n );\n\n if (!mounted || !rendered) return null;\n\n const hasHeader = title !== undefined || showCloseButton;\n const portalTarget = container ?? document.body;\n\n return createPortal(\n <div\n className={[\n \"rmod-overlay\",\n visible ? \"rmod-overlay--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-variant={variant}\n data-blur={blur}\n data-state={visible ? \"open\" : \"closed\"}\n style={overlayColor ? { \"--rmod-overlay-bg\": overlayColor } as React.CSSProperties : undefined}\n onClick={handleOverlayClick}\n onKeyDown={onKeyDown}\n >\n <div\n ref={setPanelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n aria-describedby={description ? descId : undefined}\n tabIndex={-1}\n className={[\n \"rmod-panel\",\n className,\n visible ? \"rmod-panel--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-size={size}\n data-variant={variant}\n data-padding={padding}\n data-scrollable={scrollable ? \"true\" : undefined}\n data-state={visible ? \"open\" : \"closed\"}\n >\n {hasHeader && (\n <div className=\"rmod-header\">\n {title ? (\n <h2 id={titleId} className=\"rmod-title\">{title}</h2>\n ) : (\n <span />\n )}\n {showCloseButton && (\n <button\n type=\"button\"\n className=\"rmod-close\"\n aria-label=\"Close\"\n onClick={() => requestClose(\"close-button\")}\n >\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M12 4L4 12M4 4l8 8\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </button>\n )}\n </div>\n )}\n {description !== undefined && (\n <p id={descId} className=\"rmod-description\">{description}</p>\n )}\n <div className=\"rmod-body\">{children}</div>\n {footer && <div className=\"rmod-footer\">{footer}</div>}\n </div>\n </div>,\n portalTarget,\n );\n },\n);\n"]}
package/dist/styled.d.cts CHANGED
@@ -1,13 +1,18 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, RefObject } from 'react';
3
3
 
4
4
  type ModalSize = "sm" | "md" | "lg" | "full";
5
5
  type ModalVariant = "dialog" | "drawer-left" | "drawer-right" | "drawer-bottom";
6
+ type CloseReason = "esc" | "overlay" | "close-button" | "programmatic";
6
7
  interface ModalStyledProps {
7
- isOpen: boolean;
8
+ /** Open state. `open` is the canonical name; `isOpen` continues to work. */
9
+ open?: boolean;
10
+ isOpen?: boolean;
8
11
  onClose: () => void;
9
12
  /** Modal heading — omit to hide the header entirely */
10
13
  title?: ReactNode;
14
+ /** Optional accessible description. Renders below the title and links via aria-describedby. */
15
+ description?: ReactNode;
11
16
  children: ReactNode;
12
17
  /** Footer content — omit to hide footer */
13
18
  footer?: ReactNode;
@@ -25,6 +30,20 @@ interface ModalStyledProps {
25
30
  /** Max height of scrollable body. Default: auto */
26
31
  scrollable?: boolean;
27
32
  className?: string;
33
+ /** Element to focus when the modal opens. Defaults to the first focusable child inside the panel. */
34
+ initialFocusRef?: RefObject<HTMLElement | null>;
35
+ /** Element to focus when the modal closes. Defaults to whatever was focused before opening. */
36
+ finalFocusRef?: RefObject<HTMLElement | null>;
37
+ /** Fires after the open transition completes. */
38
+ onAfterOpen?: () => void;
39
+ /** Fires after the close transition completes (and unmount). */
40
+ onAfterClose?: () => void;
41
+ /** Return false to veto a close attempt. Receives the trigger reason. */
42
+ preventClose?: (reason: CloseReason) => boolean;
43
+ /** Disable body scroll lock while open. Default: true (locked). */
44
+ lockBodyScroll?: boolean;
45
+ /** Override the portal container. Default: document.body. */
46
+ container?: HTMLElement | null;
28
47
  }
29
48
  declare const ModalStyled: react.ForwardRefExoticComponent<ModalStyledProps & react.RefAttributes<HTMLDivElement>>;
30
49
 
package/dist/styled.d.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, RefObject } from 'react';
3
3
 
4
4
  type ModalSize = "sm" | "md" | "lg" | "full";
5
5
  type ModalVariant = "dialog" | "drawer-left" | "drawer-right" | "drawer-bottom";
6
+ type CloseReason = "esc" | "overlay" | "close-button" | "programmatic";
6
7
  interface ModalStyledProps {
7
- isOpen: boolean;
8
+ /** Open state. `open` is the canonical name; `isOpen` continues to work. */
9
+ open?: boolean;
10
+ isOpen?: boolean;
8
11
  onClose: () => void;
9
12
  /** Modal heading — omit to hide the header entirely */
10
13
  title?: ReactNode;
14
+ /** Optional accessible description. Renders below the title and links via aria-describedby. */
15
+ description?: ReactNode;
11
16
  children: ReactNode;
12
17
  /** Footer content — omit to hide footer */
13
18
  footer?: ReactNode;
@@ -25,6 +30,20 @@ interface ModalStyledProps {
25
30
  /** Max height of scrollable body. Default: auto */
26
31
  scrollable?: boolean;
27
32
  className?: string;
33
+ /** Element to focus when the modal opens. Defaults to the first focusable child inside the panel. */
34
+ initialFocusRef?: RefObject<HTMLElement | null>;
35
+ /** Element to focus when the modal closes. Defaults to whatever was focused before opening. */
36
+ finalFocusRef?: RefObject<HTMLElement | null>;
37
+ /** Fires after the open transition completes. */
38
+ onAfterOpen?: () => void;
39
+ /** Fires after the close transition completes (and unmount). */
40
+ onAfterClose?: () => void;
41
+ /** Return false to veto a close attempt. Receives the trigger reason. */
42
+ preventClose?: (reason: CloseReason) => boolean;
43
+ /** Disable body scroll lock while open. Default: true (locked). */
44
+ lockBodyScroll?: boolean;
45
+ /** Override the portal container. Default: document.body. */
46
+ container?: HTMLElement | null;
28
47
  }
29
48
  declare const ModalStyled: react.ForwardRefExoticComponent<ModalStyledProps & react.RefAttributes<HTMLDivElement>>;
30
49
 
package/dist/styled.js CHANGED
@@ -5,9 +5,11 @@ import { jsx, jsxs } from 'react/jsx-runtime';
5
5
 
6
6
  var ModalStyled = forwardRef(
7
7
  function ModalStyled2({
8
+ open,
8
9
  isOpen,
9
10
  onClose,
10
11
  title,
12
+ description,
11
13
  children,
12
14
  footer,
13
15
  size = "md",
@@ -19,83 +21,133 @@ var ModalStyled = forwardRef(
19
21
  overlayColor,
20
22
  padding = "md",
21
23
  scrollable = true,
22
- className
24
+ className,
25
+ initialFocusRef,
26
+ finalFocusRef,
27
+ onAfterOpen,
28
+ onAfterClose,
29
+ preventClose,
30
+ lockBodyScroll = true,
31
+ container
23
32
  }, ref) {
24
33
  const titleId = useId();
34
+ const descId = useId();
35
+ const isActuallyOpen = open ?? isOpen ?? false;
25
36
  const [mounted, setMounted] = useState(false);
26
37
  const [rendered, setRendered] = useState(false);
27
38
  const [visible, setVisible] = useState(false);
28
39
  const panelRef = useRef(null);
29
40
  const originalOverflowRef = useRef("");
30
41
  const exitTimerRef = useRef(null);
42
+ const enterTimerRef = useRef(null);
31
43
  const { activate, deactivate, handleKeyDown } = useFocusTrap();
44
+ const onAfterOpenRef = useRef(onAfterOpen);
45
+ onAfterOpenRef.current = onAfterOpen;
46
+ const onAfterCloseRef = useRef(onAfterClose);
47
+ onAfterCloseRef.current = onAfterClose;
32
48
  useEffect(() => {
33
49
  setMounted(true);
34
50
  }, []);
51
+ const requestClose = useCallback(
52
+ (reason) => {
53
+ if (preventClose && !preventClose(reason)) return;
54
+ onClose();
55
+ },
56
+ [preventClose, onClose]
57
+ );
35
58
  useEffect(() => {
36
59
  if (exitTimerRef.current) {
37
60
  clearTimeout(exitTimerRef.current);
38
61
  exitTimerRef.current = null;
39
62
  }
40
- if (isOpen) {
63
+ if (enterTimerRef.current) {
64
+ clearTimeout(enterTimerRef.current);
65
+ enterTimerRef.current = null;
66
+ }
67
+ if (isActuallyOpen) {
41
68
  setRendered(true);
42
69
  requestAnimationFrame(() => {
43
70
  requestAnimationFrame(() => {
44
71
  setVisible(true);
45
72
  });
46
73
  });
47
- originalOverflowRef.current = document.body.style.overflow;
48
- document.body.style.overflow = "hidden";
74
+ if (lockBodyScroll) {
75
+ originalOverflowRef.current = document.body.style.overflow;
76
+ document.body.style.overflow = "hidden";
77
+ }
78
+ enterTimerRef.current = setTimeout(() => {
79
+ onAfterOpenRef.current?.();
80
+ }, 320);
49
81
  } else {
50
82
  setVisible(false);
51
83
  exitTimerRef.current = setTimeout(() => {
52
84
  setRendered(false);
85
+ onAfterCloseRef.current?.();
53
86
  }, 300);
54
- document.body.style.overflow = originalOverflowRef.current;
87
+ if (lockBodyScroll) {
88
+ document.body.style.overflow = originalOverflowRef.current;
89
+ }
55
90
  }
56
- }, [isOpen]);
91
+ }, [isActuallyOpen, lockBodyScroll]);
57
92
  useEffect(() => {
58
93
  return () => {
59
94
  if (exitTimerRef.current) clearTimeout(exitTimerRef.current);
60
- document.body.style.overflow = originalOverflowRef.current;
95
+ if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
96
+ if (lockBodyScroll) {
97
+ document.body.style.overflow = originalOverflowRef.current;
98
+ }
61
99
  };
62
- }, []);
100
+ }, [lockBodyScroll]);
101
+ const previouslyFocusedRef = useRef(null);
63
102
  useEffect(() => {
64
- if (isOpen && panelRef.current) {
65
- activate(panelRef.current);
66
- } else if (!isOpen) {
103
+ if (isActuallyOpen) {
104
+ previouslyFocusedRef.current = document.activeElement;
105
+ if (panelRef.current) {
106
+ activate(panelRef.current);
107
+ requestAnimationFrame(() => {
108
+ if (initialFocusRef?.current) {
109
+ initialFocusRef.current.focus();
110
+ }
111
+ });
112
+ }
113
+ } else {
67
114
  deactivate();
115
+ const target = finalFocusRef?.current ?? previouslyFocusedRef.current;
116
+ if (target && typeof target.focus === "function") {
117
+ target.focus();
118
+ }
68
119
  }
69
- }, [isOpen, activate, deactivate]);
120
+ }, [isActuallyOpen, activate, deactivate, initialFocusRef, finalFocusRef]);
70
121
  const onKeyDown = useCallback(
71
122
  (e) => {
72
123
  if (closeOnEsc && e.key === "Escape") {
73
- onClose();
124
+ requestClose("esc");
74
125
  return;
75
126
  }
76
127
  handleKeyDown(e.nativeEvent);
77
128
  },
78
- [closeOnEsc, onClose, handleKeyDown]
129
+ [closeOnEsc, requestClose, handleKeyDown]
79
130
  );
80
131
  const handleOverlayClick = useCallback(
81
132
  (e) => {
82
133
  if (closeOnOverlayClick && e.target === e.currentTarget) {
83
- onClose();
134
+ requestClose("overlay");
84
135
  }
85
136
  },
86
- [closeOnOverlayClick, onClose]
137
+ [closeOnOverlayClick, requestClose]
87
138
  );
88
139
  const setPanelRef = useCallback(
89
140
  (el) => {
90
141
  panelRef.current = el;
91
142
  if (typeof ref === "function") ref(el);
92
143
  else if (ref) ref.current = el;
93
- if (el && isOpen) activate(el);
144
+ if (el && isActuallyOpen) activate(el);
94
145
  },
95
- [ref, isOpen, activate]
146
+ [ref, isActuallyOpen, activate]
96
147
  );
97
148
  if (!mounted || !rendered) return null;
98
149
  const hasHeader = title !== void 0 || showCloseButton;
150
+ const portalTarget = container ?? document.body;
99
151
  return createPortal(
100
152
  /* @__PURE__ */ jsx(
101
153
  "div",
@@ -106,6 +158,7 @@ var ModalStyled = forwardRef(
106
158
  ].filter(Boolean).join(" "),
107
159
  "data-variant": variant,
108
160
  "data-blur": blur,
161
+ "data-state": visible ? "open" : "closed",
109
162
  style: overlayColor ? { "--rmod-overlay-bg": overlayColor } : void 0,
110
163
  onClick: handleOverlayClick,
111
164
  onKeyDown,
@@ -116,6 +169,7 @@ var ModalStyled = forwardRef(
116
169
  role: "dialog",
117
170
  "aria-modal": "true",
118
171
  "aria-labelledby": title ? titleId : void 0,
172
+ "aria-describedby": description ? descId : void 0,
119
173
  tabIndex: -1,
120
174
  className: [
121
175
  "rmod-panel",
@@ -126,11 +180,22 @@ var ModalStyled = forwardRef(
126
180
  "data-variant": variant,
127
181
  "data-padding": padding,
128
182
  "data-scrollable": scrollable ? "true" : void 0,
183
+ "data-state": visible ? "open" : "closed",
129
184
  children: [
130
185
  hasHeader && /* @__PURE__ */ jsxs("div", { className: "rmod-header", children: [
131
186
  title ? /* @__PURE__ */ jsx("h2", { id: titleId, className: "rmod-title", children: title }) : /* @__PURE__ */ jsx("span", {}),
132
- showCloseButton && /* @__PURE__ */ jsx("button", { type: "button", className: "rmod-close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M12 4L4 12M4 4l8 8", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" }) }) })
187
+ showCloseButton && /* @__PURE__ */ jsx(
188
+ "button",
189
+ {
190
+ type: "button",
191
+ className: "rmod-close",
192
+ "aria-label": "Close",
193
+ onClick: () => requestClose("close-button"),
194
+ children: /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", children: /* @__PURE__ */ jsx("path", { d: "M12 4L4 12M4 4l8 8", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round" }) })
195
+ }
196
+ )
133
197
  ] }),
198
+ description !== void 0 && /* @__PURE__ */ jsx("p", { id: descId, className: "rmod-description", children: description }),
134
199
  /* @__PURE__ */ jsx("div", { className: "rmod-body", children }),
135
200
  footer && /* @__PURE__ */ jsx("div", { className: "rmod-footer", children: footer })
136
201
  ]
@@ -138,7 +203,7 @@ var ModalStyled = forwardRef(
138
203
  )
139
204
  }
140
205
  ),
141
- document.body
206
+ portalTarget
142
207
  );
143
208
  }
144
209
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/styled/ModalStyled.tsx"],"names":["ModalStyled"],"mappings":"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","file":"styled.js","sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useFocusTrap } from \"../useFocusTrap\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"full\";\nexport type ModalVariant = \"dialog\" | \"drawer-left\" | \"drawer-right\" | \"drawer-bottom\";\n\nexport interface ModalStyledProps {\n isOpen: boolean;\n onClose: () => void;\n /** Modal heading — omit to hide the header entirely */\n title?: ReactNode;\n children: ReactNode;\n /** Footer content — omit to hide footer */\n footer?: ReactNode;\n size?: ModalSize;\n variant?: ModalVariant;\n closeOnOverlayClick?: boolean;\n closeOnEsc?: boolean;\n showCloseButton?: boolean;\n /** Backdrop blur intensity. Default: \"md\" */\n blur?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Custom overlay color e.g. \"rgba(0,0,0,0.6)\" */\n overlayColor?: string;\n /** Padding inside the body. Default: \"md\" */\n padding?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Max height of scrollable body. Default: auto */\n scrollable?: boolean;\n className?: string;\n}\n\nexport const ModalStyled = forwardRef<HTMLDivElement, ModalStyledProps>(\n function ModalStyled(\n {\n isOpen,\n onClose,\n title,\n children,\n footer,\n size = \"md\",\n variant = \"dialog\",\n closeOnOverlayClick = true,\n closeOnEsc = true,\n showCloseButton = true,\n blur = \"md\",\n overlayColor,\n padding = \"md\",\n scrollable = true,\n className,\n },\n ref,\n ) {\n const titleId = useId();\n const [mounted, setMounted] = useState(false);\n const [rendered, setRendered] = useState(false);\n const [visible, setVisible] = useState(false);\n const panelRef = useRef<HTMLDivElement | null>(null);\n const originalOverflowRef = useRef(\"\");\n const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const { activate, deactivate, handleKeyDown } = useFocusTrap();\n\n useEffect(() => { setMounted(true); }, []);\n\n useEffect(() => {\n if (exitTimerRef.current) {\n clearTimeout(exitTimerRef.current);\n exitTimerRef.current = null;\n }\n\n if (isOpen) {\n setRendered(true);\n // Double RAF ensures the element is in the DOM before we add the visible class\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n setVisible(true);\n });\n });\n originalOverflowRef.current = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n } else {\n // Remove visible class first (triggers CSS exit transition)\n setVisible(false);\n // Then unmount after transition finishes\n exitTimerRef.current = setTimeout(() => {\n setRendered(false);\n }, 300);\n document.body.style.overflow = originalOverflowRef.current;\n }\n }, [isOpen]);\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (exitTimerRef.current) clearTimeout(exitTimerRef.current);\n document.body.style.overflow = originalOverflowRef.current;\n };\n }, []);\n\n useEffect(() => {\n if (isOpen && panelRef.current) {\n activate(panelRef.current);\n } else if (!isOpen) {\n deactivate();\n }\n }, [isOpen, activate, deactivate]);\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (closeOnEsc && e.key === \"Escape\") {\n onClose();\n return;\n }\n handleKeyDown(e.nativeEvent);\n },\n [closeOnEsc, onClose, handleKeyDown],\n );\n\n const handleOverlayClick = useCallback(\n (e: React.MouseEvent<HTMLDivElement>) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n onClose();\n }\n },\n [closeOnOverlayClick, onClose],\n );\n\n const setPanelRef = useCallback(\n (el: HTMLDivElement | null) => {\n panelRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;\n if (el && isOpen) activate(el);\n },\n [ref, isOpen, activate],\n );\n\n if (!mounted || !rendered) return null;\n\n const hasHeader = title !== undefined || showCloseButton;\n\n return createPortal(\n <div\n className={[\n \"rmod-overlay\",\n visible ? \"rmod-overlay--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-variant={variant}\n data-blur={blur}\n style={overlayColor ? { \"--rmod-overlay-bg\": overlayColor } as React.CSSProperties : undefined}\n onClick={handleOverlayClick}\n onKeyDown={onKeyDown}\n >\n <div\n ref={setPanelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n tabIndex={-1}\n className={[\n \"rmod-panel\",\n className,\n visible ? \"rmod-panel--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-size={size}\n data-variant={variant}\n data-padding={padding}\n data-scrollable={scrollable ? \"true\" : undefined}\n >\n {hasHeader && (\n <div className=\"rmod-header\">\n {title ? (\n <h2 id={titleId} className=\"rmod-title\">{title}</h2>\n ) : (\n <span />\n )}\n {showCloseButton && (\n <button type=\"button\" className=\"rmod-close\" aria-label=\"Close\" onClick={onClose}>\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M12 4L4 12M4 4l8 8\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </button>\n )}\n </div>\n )}\n <div className=\"rmod-body\">{children}</div>\n {footer && <div className=\"rmod-footer\">{footer}</div>}\n </div>\n </div>,\n document.body,\n );\n },\n);\n"]}
1
+ {"version":3,"sources":["../src/styled/ModalStyled.tsx"],"names":["ModalStyled"],"mappings":";;;;;AA2DO,IAAM,WAAA,GAAc,UAAA;AAAA,EACzB,SAASA,YAAAA,CACP;AAAA,IACE,IAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,KAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA,GAAO,IAAA;AAAA,IACP,OAAA,GAAU,QAAA;AAAA,IACV,mBAAA,GAAsB,IAAA;AAAA,IACtB,UAAA,GAAa,IAAA;AAAA,IACb,eAAA,GAAkB,IAAA;AAAA,IAClB,IAAA,GAAO,IAAA;AAAA,IACP,YAAA;AAAA,IACA,OAAA,GAAU,IAAA;AAAA,IACV,UAAA,GAAa,IAAA;AAAA,IACb,SAAA;AAAA,IACA,eAAA;AAAA,IACA,aAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAA;AAAA,IACA,YAAA;AAAA,IACA,cAAA,GAAiB,IAAA;AAAA,IACjB;AAAA,KAEF,GAAA,EACA;AACA,IAAA,MAAM,UAAU,KAAA,EAAM;AACtB,IAAA,MAAM,SAAS,KAAA,EAAM;AACrB,IAAA,MAAM,cAAA,GAAiB,QAAQ,MAAA,IAAU,KAAA;AACzC,IAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,IAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,SAAS,KAAK,CAAA;AAC9C,IAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,IAAA,MAAM,QAAA,GAAW,OAA8B,IAAI,CAAA;AACnD,IAAA,MAAM,mBAAA,GAAsB,OAAO,EAAE,CAAA;AACrC,IAAA,MAAM,YAAA,GAAe,OAA6C,IAAI,CAAA;AACtE,IAAA,MAAM,aAAA,GAAgB,OAA6C,IAAI,CAAA;AACvE,IAAA,MAAM,EAAE,QAAA,EAAU,UAAA,EAAY,aAAA,KAAkB,YAAA,EAAa;AAE7D,IAAA,MAAM,cAAA,GAAiB,OAAO,WAAW,CAAA;AACzC,IAAA,cAAA,CAAe,OAAA,GAAU,WAAA;AACzB,IAAA,MAAM,eAAA,GAAkB,OAAO,YAAY,CAAA;AAC3C,IAAA,eAAA,CAAgB,OAAA,GAAU,YAAA;AAE1B,IAAA,SAAA,CAAU,MAAM;AAAE,MAAA,UAAA,CAAW,IAAI,CAAA;AAAA,IAAG,CAAA,EAAG,EAAE,CAAA;AAEzC,IAAA,MAAM,YAAA,GAAe,WAAA;AAAA,MACnB,CAAC,MAAA,KAAwB;AACvB,QAAA,IAAI,YAAA,IAAgB,CAAC,YAAA,CAAa,MAAM,CAAA,EAAG;AAC3C,QAAA,OAAA,EAAQ;AAAA,MACV,CAAA;AAAA,MACA,CAAC,cAAc,OAAO;AAAA,KACxB;AAEA,IAAA,SAAA,CAAU,MAAM;AACd,MAAA,IAAI,aAAa,OAAA,EAAS;AACxB,QAAA,YAAA,CAAa,aAAa,OAAO,CAAA;AACjC,QAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAAA,MACzB;AACA,MAAA,IAAI,cAAc,OAAA,EAAS;AACzB,QAAA,YAAA,CAAa,cAAc,OAAO,CAAA;AAClC,QAAA,aAAA,CAAc,OAAA,GAAU,IAAA;AAAA,MAC1B;AAEA,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,WAAA,CAAY,IAAI,CAAA;AAChB,QAAA,qBAAA,CAAsB,MAAM;AAC1B,UAAA,qBAAA,CAAsB,MAAM;AAC1B,YAAA,UAAA,CAAW,IAAI,CAAA;AAAA,UACjB,CAAC,CAAA;AAAA,QACH,CAAC,CAAA;AACD,QAAA,IAAI,cAAA,EAAgB;AAClB,UAAA,mBAAA,CAAoB,OAAA,GAAU,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AAClD,UAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAAA,QACjC;AAEA,QAAA,aAAA,CAAc,OAAA,GAAU,WAAW,MAAM;AACvC,UAAA,cAAA,CAAe,OAAA,IAAU;AAAA,QAC3B,GAAG,GAAG,CAAA;AAAA,MACR,CAAA,MAAO;AACL,QAAA,UAAA,CAAW,KAAK,CAAA;AAChB,QAAA,YAAA,CAAa,OAAA,GAAU,WAAW,MAAM;AACtC,UAAA,WAAA,CAAY,KAAK,CAAA;AACjB,UAAA,eAAA,CAAgB,OAAA,IAAU;AAAA,QAC5B,GAAG,GAAG,CAAA;AACN,QAAA,IAAI,cAAA,EAAgB;AAClB,UAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA,GAAW,mBAAA,CAAoB,OAAA;AAAA,QACrD;AAAA,MACF;AAAA,IACF,CAAA,EAAG,CAAC,cAAA,EAAgB,cAAc,CAAC,CAAA;AAEnC,IAAA,SAAA,CAAU,MAAM;AACd,MAAA,OAAO,MAAM;AACX,QAAA,IAAI,YAAA,CAAa,OAAA,EAAS,YAAA,CAAa,YAAA,CAAa,OAAO,CAAA;AAC3D,QAAA,IAAI,aAAA,CAAc,OAAA,EAAS,YAAA,CAAa,aAAA,CAAc,OAAO,CAAA;AAC7D,QAAA,IAAI,cAAA,EAAgB;AAClB,UAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA,GAAW,mBAAA,CAAoB,OAAA;AAAA,QACrD;AAAA,MACF,CAAA;AAAA,IACF,CAAA,EAAG,CAAC,cAAc,CAAC,CAAA;AAGnB,IAAA,MAAM,oBAAA,GAAuB,OAA2B,IAAI,CAAA;AAC5D,IAAA,SAAA,CAAU,MAAM;AACd,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,oBAAA,CAAqB,UAAU,QAAA,CAAS,aAAA;AACxC,QAAA,IAAI,SAAS,OAAA,EAAS;AACpB,UAAA,QAAA,CAAS,SAAS,OAAO,CAAA;AAEzB,UAAA,qBAAA,CAAsB,MAAM;AAC1B,YAAA,IAAI,iBAAiB,OAAA,EAAS;AAC5B,cAAA,eAAA,CAAgB,QAAQ,KAAA,EAAM;AAAA,YAChC;AAAA,UACF,CAAC,CAAA;AAAA,QACH;AAAA,MACF,CAAA,MAAO;AACL,QAAA,UAAA,EAAW;AAEX,QAAA,MAAM,MAAA,GAAS,aAAA,EAAe,OAAA,IAAW,oBAAA,CAAqB,OAAA;AAC9D,QAAA,IAAI,MAAA,IAAU,OAAO,MAAA,CAAO,KAAA,KAAU,UAAA,EAAY;AAChD,UAAA,MAAA,CAAO,KAAA,EAAM;AAAA,QACf;AAAA,MACF;AAAA,IACF,GAAG,CAAC,cAAA,EAAgB,UAAU,UAAA,EAAY,eAAA,EAAiB,aAAa,CAAC,CAAA;AAEzE,IAAA,MAAM,SAAA,GAAY,WAAA;AAAA,MAChB,CAAC,CAAA,KAA2B;AAC1B,QAAA,IAAI,UAAA,IAAc,CAAA,CAAE,GAAA,KAAQ,QAAA,EAAU;AACpC,UAAA,YAAA,CAAa,KAAK,CAAA;AAClB,UAAA;AAAA,QACF;AACA,QAAA,aAAA,CAAc,EAAE,WAAW,CAAA;AAAA,MAC7B,CAAA;AAAA,MACA,CAAC,UAAA,EAAY,YAAA,EAAc,aAAa;AAAA,KAC1C;AAEA,IAAA,MAAM,kBAAA,GAAqB,WAAA;AAAA,MACzB,CAAC,CAAA,KAAwC;AACvC,QAAA,IAAI,mBAAA,IAAuB,CAAA,CAAE,MAAA,KAAW,CAAA,CAAE,aAAA,EAAe;AACvD,UAAA,YAAA,CAAa,SAAS,CAAA;AAAA,QACxB;AAAA,MACF,CAAA;AAAA,MACA,CAAC,qBAAqB,YAAY;AAAA,KACpC;AAEA,IAAA,MAAM,WAAA,GAAc,WAAA;AAAA,MAClB,CAAC,EAAA,KAA8B;AAC7B,QAAA,QAAA,CAAS,OAAA,GAAU,EAAA;AACnB,QAAA,IAAI,OAAO,GAAA,KAAQ,UAAA,EAAY,GAAA,CAAI,EAAE,CAAA;AAAA,aAAA,IAC5B,GAAA,EAAM,GAAA,CAAsD,OAAA,GAAU,EAAA;AAC/E,QAAA,IAAI,EAAA,IAAM,cAAA,EAAgB,QAAA,CAAS,EAAE,CAAA;AAAA,MACvC,CAAA;AAAA,MACA,CAAC,GAAA,EAAK,cAAA,EAAgB,QAAQ;AAAA,KAChC;AAEA,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,QAAA,EAAU,OAAO,IAAA;AAElC,IAAA,MAAM,SAAA,GAAY,UAAU,MAAA,IAAa,eAAA;AACzC,IAAA,MAAM,YAAA,GAAe,aAAa,QAAA,CAAS,IAAA;AAE3C,IAAA,OAAO,YAAA;AAAA,sBACL,GAAA;AAAA,QAAC,KAAA;AAAA,QAAA;AAAA,UACC,SAAA,EAAW;AAAA,YACT,cAAA;AAAA,YACA,UAAU,uBAAA,GAA0B;AAAA,WACtC,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AAAA,UAC1B,cAAA,EAAc,OAAA;AAAA,UACd,WAAA,EAAW,IAAA;AAAA,UACX,YAAA,EAAY,UAAU,MAAA,GAAS,QAAA;AAAA,UAC/B,KAAA,EAAO,YAAA,GAAe,EAAE,mBAAA,EAAqB,cAAa,GAA2B,MAAA;AAAA,UACrF,OAAA,EAAS,kBAAA;AAAA,UACT,SAAA;AAAA,UAEA,QAAA,kBAAA,IAAA;AAAA,YAAC,KAAA;AAAA,YAAA;AAAA,cACC,GAAA,EAAK,WAAA;AAAA,cACL,IAAA,EAAK,QAAA;AAAA,cACL,YAAA,EAAW,MAAA;AAAA,cACX,iBAAA,EAAiB,QAAQ,OAAA,GAAU,MAAA;AAAA,cACnC,kBAAA,EAAkB,cAAc,MAAA,GAAS,MAAA;AAAA,cACzC,QAAA,EAAU,EAAA;AAAA,cACV,SAAA,EAAW;AAAA,gBACT,YAAA;AAAA,gBACA,SAAA;AAAA,gBACA,UAAU,qBAAA,GAAwB;AAAA,eACpC,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AAAA,cAC1B,WAAA,EAAW,IAAA;AAAA,cACX,cAAA,EAAc,OAAA;AAAA,cACd,cAAA,EAAc,OAAA;AAAA,cACd,iBAAA,EAAiB,aAAa,MAAA,GAAS,MAAA;AAAA,cACvC,YAAA,EAAY,UAAU,MAAA,GAAS,QAAA;AAAA,cAE9B,QAAA,EAAA;AAAA,gBAAA,SAAA,oBACC,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,aAAA,EACZ,QAAA,EAAA;AAAA,kBAAA,KAAA,mBACC,GAAA,CAAC,QAAG,EAAA,EAAI,OAAA,EAAS,WAAU,YAAA,EAAc,QAAA,EAAA,KAAA,EAAM,CAAA,mBAE/C,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA;AAAA,kBAEP,eAAA,oBACC,GAAA;AAAA,oBAAC,QAAA;AAAA,oBAAA;AAAA,sBACC,IAAA,EAAK,QAAA;AAAA,sBACL,SAAA,EAAU,YAAA;AAAA,sBACV,YAAA,EAAW,OAAA;AAAA,sBACX,OAAA,EAAS,MAAM,YAAA,CAAa,cAAc,CAAA;AAAA,sBAE1C,QAAA,kBAAA,GAAA,CAAC,SAAI,aAAA,EAAY,MAAA,EAAO,OAAM,IAAA,EAAK,MAAA,EAAO,IAAA,EAAK,OAAA,EAAQ,WAAA,EAAY,IAAA,EAAK,QACtE,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,GAAE,oBAAA,EAAqB,MAAA,EAAO,gBAAe,WAAA,EAAY,MAAA,EAAO,aAAA,EAAc,OAAA,EAAQ,CAAA,EAC9F;AAAA;AAAA;AACF,iBAAA,EAEJ,CAAA;AAAA,gBAED,WAAA,KAAgB,0BACf,GAAA,CAAC,GAAA,EAAA,EAAE,IAAI,MAAA,EAAQ,SAAA,EAAU,oBAAoB,QAAA,EAAA,WAAA,EAAY,CAAA;AAAA,gCAE3D,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,WAAA,EAAa,QAAA,EAAS,CAAA;AAAA,gBACpC,MAAA,oBAAU,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,eAAe,QAAA,EAAA,MAAA,EAAO;AAAA;AAAA;AAAA;AAClD;AAAA,OACF;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACF","file":"styled.js","sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useFocusTrap } from \"../useFocusTrap\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"full\";\nexport type ModalVariant = \"dialog\" | \"drawer-left\" | \"drawer-right\" | \"drawer-bottom\";\nexport type CloseReason = \"esc\" | \"overlay\" | \"close-button\" | \"programmatic\";\n\nexport interface ModalStyledProps {\n /** Open state. `open` is the canonical name; `isOpen` continues to work. */\n open?: boolean;\n isOpen?: boolean;\n onClose: () => void;\n /** Modal heading — omit to hide the header entirely */\n title?: ReactNode;\n /** Optional accessible description. Renders below the title and links via aria-describedby. */\n description?: ReactNode;\n children: ReactNode;\n /** Footer content — omit to hide footer */\n footer?: ReactNode;\n size?: ModalSize;\n variant?: ModalVariant;\n closeOnOverlayClick?: boolean;\n closeOnEsc?: boolean;\n showCloseButton?: boolean;\n /** Backdrop blur intensity. Default: \"md\" */\n blur?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Custom overlay color e.g. \"rgba(0,0,0,0.6)\" */\n overlayColor?: string;\n /** Padding inside the body. Default: \"md\" */\n padding?: \"none\" | \"sm\" | \"md\" | \"lg\";\n /** Max height of scrollable body. Default: auto */\n scrollable?: boolean;\n className?: string;\n /** Element to focus when the modal opens. Defaults to the first focusable child inside the panel. */\n initialFocusRef?: RefObject<HTMLElement | null>;\n /** Element to focus when the modal closes. Defaults to whatever was focused before opening. */\n finalFocusRef?: RefObject<HTMLElement | null>;\n /** Fires after the open transition completes. */\n onAfterOpen?: () => void;\n /** Fires after the close transition completes (and unmount). */\n onAfterClose?: () => void;\n /** Return false to veto a close attempt. Receives the trigger reason. */\n preventClose?: (reason: CloseReason) => boolean;\n /** Disable body scroll lock while open. Default: true (locked). */\n lockBodyScroll?: boolean;\n /** Override the portal container. Default: document.body. */\n container?: HTMLElement | null;\n}\n\nexport const ModalStyled = forwardRef<HTMLDivElement, ModalStyledProps>(\n function ModalStyled(\n {\n open,\n isOpen,\n onClose,\n title,\n description,\n children,\n footer,\n size = \"md\",\n variant = \"dialog\",\n closeOnOverlayClick = true,\n closeOnEsc = true,\n showCloseButton = true,\n blur = \"md\",\n overlayColor,\n padding = \"md\",\n scrollable = true,\n className,\n initialFocusRef,\n finalFocusRef,\n onAfterOpen,\n onAfterClose,\n preventClose,\n lockBodyScroll = true,\n container,\n },\n ref,\n ) {\n const titleId = useId();\n const descId = useId();\n const isActuallyOpen = open ?? isOpen ?? false;\n const [mounted, setMounted] = useState(false);\n const [rendered, setRendered] = useState(false);\n const [visible, setVisible] = useState(false);\n const panelRef = useRef<HTMLDivElement | null>(null);\n const originalOverflowRef = useRef(\"\");\n const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const enterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const { activate, deactivate, handleKeyDown } = useFocusTrap();\n\n const onAfterOpenRef = useRef(onAfterOpen);\n onAfterOpenRef.current = onAfterOpen;\n const onAfterCloseRef = useRef(onAfterClose);\n onAfterCloseRef.current = onAfterClose;\n\n useEffect(() => { setMounted(true); }, []);\n\n const requestClose = useCallback(\n (reason: CloseReason) => {\n if (preventClose && !preventClose(reason)) return;\n onClose();\n },\n [preventClose, onClose],\n );\n\n useEffect(() => {\n if (exitTimerRef.current) {\n clearTimeout(exitTimerRef.current);\n exitTimerRef.current = null;\n }\n if (enterTimerRef.current) {\n clearTimeout(enterTimerRef.current);\n enterTimerRef.current = null;\n }\n\n if (isActuallyOpen) {\n setRendered(true);\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n setVisible(true);\n });\n });\n if (lockBodyScroll) {\n originalOverflowRef.current = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n // Fire onAfterOpen once the enter transition has had time to settle.\n enterTimerRef.current = setTimeout(() => {\n onAfterOpenRef.current?.();\n }, 320);\n } else {\n setVisible(false);\n exitTimerRef.current = setTimeout(() => {\n setRendered(false);\n onAfterCloseRef.current?.();\n }, 300);\n if (lockBodyScroll) {\n document.body.style.overflow = originalOverflowRef.current;\n }\n }\n }, [isActuallyOpen, lockBodyScroll]);\n\n useEffect(() => {\n return () => {\n if (exitTimerRef.current) clearTimeout(exitTimerRef.current);\n if (enterTimerRef.current) clearTimeout(enterTimerRef.current);\n if (lockBodyScroll) {\n document.body.style.overflow = originalOverflowRef.current;\n }\n };\n }, [lockBodyScroll]);\n\n // Focus management\n const previouslyFocusedRef = useRef<HTMLElement | null>(null);\n useEffect(() => {\n if (isActuallyOpen) {\n previouslyFocusedRef.current = document.activeElement as HTMLElement | null;\n if (panelRef.current) {\n activate(panelRef.current);\n // Honor initialFocusRef after the panel is mounted+visible\n requestAnimationFrame(() => {\n if (initialFocusRef?.current) {\n initialFocusRef.current.focus();\n }\n });\n }\n } else {\n deactivate();\n // Honor finalFocusRef on close, otherwise restore previous focus\n const target = finalFocusRef?.current ?? previouslyFocusedRef.current;\n if (target && typeof target.focus === \"function\") {\n target.focus();\n }\n }\n }, [isActuallyOpen, activate, deactivate, initialFocusRef, finalFocusRef]);\n\n const onKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (closeOnEsc && e.key === \"Escape\") {\n requestClose(\"esc\");\n return;\n }\n handleKeyDown(e.nativeEvent);\n },\n [closeOnEsc, requestClose, handleKeyDown],\n );\n\n const handleOverlayClick = useCallback(\n (e: React.MouseEvent<HTMLDivElement>) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n requestClose(\"overlay\");\n }\n },\n [closeOnOverlayClick, requestClose],\n );\n\n const setPanelRef = useCallback(\n (el: HTMLDivElement | null) => {\n panelRef.current = el;\n if (typeof ref === \"function\") ref(el);\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;\n if (el && isActuallyOpen) activate(el);\n },\n [ref, isActuallyOpen, activate],\n );\n\n if (!mounted || !rendered) return null;\n\n const hasHeader = title !== undefined || showCloseButton;\n const portalTarget = container ?? document.body;\n\n return createPortal(\n <div\n className={[\n \"rmod-overlay\",\n visible ? \"rmod-overlay--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-variant={variant}\n data-blur={blur}\n data-state={visible ? \"open\" : \"closed\"}\n style={overlayColor ? { \"--rmod-overlay-bg\": overlayColor } as React.CSSProperties : undefined}\n onClick={handleOverlayClick}\n onKeyDown={onKeyDown}\n >\n <div\n ref={setPanelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleId : undefined}\n aria-describedby={description ? descId : undefined}\n tabIndex={-1}\n className={[\n \"rmod-panel\",\n className,\n visible ? \"rmod-panel--visible\" : \"\",\n ].filter(Boolean).join(\" \")}\n data-size={size}\n data-variant={variant}\n data-padding={padding}\n data-scrollable={scrollable ? \"true\" : undefined}\n data-state={visible ? \"open\" : \"closed\"}\n >\n {hasHeader && (\n <div className=\"rmod-header\">\n {title ? (\n <h2 id={titleId} className=\"rmod-title\">{title}</h2>\n ) : (\n <span />\n )}\n {showCloseButton && (\n <button\n type=\"button\"\n className=\"rmod-close\"\n aria-label=\"Close\"\n onClick={() => requestClose(\"close-button\")}\n >\n <svg aria-hidden=\"true\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M12 4L4 12M4 4l8 8\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </button>\n )}\n </div>\n )}\n {description !== undefined && (\n <p id={descId} className=\"rmod-description\">{description}</p>\n )}\n <div className=\"rmod-body\">{children}</div>\n {footer && <div className=\"rmod-footer\">{footer}</div>}\n </div>\n </div>,\n portalTarget,\n );\n },\n);\n"]}
package/dist/styles.css CHANGED
@@ -245,3 +245,17 @@
245
245
  @media (prefers-reduced-motion: reduce) {
246
246
  .rmod-overlay, .rmod-panel { transition-duration: 0ms !important; }
247
247
  }
248
+
249
+ /* ============================================================================
250
+ * 0.2.0 additions: description below title
251
+ * ========================================================================== */
252
+ .rmod-description {
253
+ margin: 0 0 0.6rem;
254
+ padding: 0 1rem;
255
+ color: var(--rmod-fg-muted);
256
+ font-size: 0.875rem;
257
+ line-height: 1.55;
258
+ }
259
+ .rmod-panel[data-padding="none"] .rmod-description { padding: 0; }
260
+ .rmod-panel[data-padding="sm"] .rmod-description { padding: 0 0.75rem; }
261
+ .rmod-panel[data-padding="lg"] .rmod-description { padding: 0 1.5rem; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mshafiqyajid/react-modal",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Headless modal hook and styled component for React. Accessible, focus-trapped, scroll-locked, animated, SSR-safe, fully typed.",
5
5
  "keywords": [
6
6
  "react",