@versini/ui-panel 6.1.1 → 6.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.
Files changed (2) hide show
  1. package/dist/index.js +155 -219
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /*!
2
- @versini/ui-panel v6.1.1
2
+ @versini/ui-panel v6.2.0
3
3
  © 2025 gizmette.com
4
4
  */
5
5
  try {
6
6
  if (!window.__VERSINI_UI_PANEL__) {
7
7
  window.__VERSINI_UI_PANEL__ = {
8
- version: "6.1.1",
9
- buildTime: "12/12/2025 10:28 AM EST",
8
+ version: "6.2.0",
9
+ buildTime: "12/14/2025 08:05 PM EST",
10
10
  homepage: "https://www.npmjs.com/package/@versini/ui-panel",
11
11
  license: "MIT",
12
12
  };
@@ -15,10 +15,11 @@ try {
15
15
  // nothing to declare officer
16
16
  }
17
17
 
18
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
- import { FloatingFocusManager, FloatingOverlay, FloatingPortal, useClick, useDismiss, useFloating, useInteractions, useMergeRefs, useRole } from "@floating-ui/react";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
20
+ import { useFocusTrap } from "@versini/ui-hooks";
20
21
  import clsx from "clsx";
21
- import { cloneElement, createContext, forwardRef, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
22
+ import { createPortal } from "react-dom";
22
23
 
23
24
  ;// CONCATENATED MODULE: ./src/common/constants.ts
24
25
  const MESSAGEBOX_CLASSNAME = "av-messagebox";
@@ -34,177 +35,109 @@ const NONE = "none";
34
35
 
35
36
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
36
37
 
37
- ;// CONCATENATED MODULE: external "@floating-ui/react"
38
+ ;// CONCATENATED MODULE: external "react"
39
+
40
+ ;// CONCATENATED MODULE: external "@versini/ui-hooks"
38
41
 
39
42
  ;// CONCATENATED MODULE: external "clsx"
40
43
 
41
- ;// CONCATENATED MODULE: external "react"
44
+ ;// CONCATENATED MODULE: external "react-dom"
42
45
 
43
- ;// CONCATENATED MODULE: ../ui-modal/dist/index.js
44
- /*!
45
- @versini/ui-modal v3.2.0
46
- © 2025 gizmette.com
47
- */ try {
48
- if (!window.__VERSINI_UI_MODAL__) {
49
- window.__VERSINI_UI_MODAL__ = {
50
- version: "3.2.0",
51
- buildTime: "12/12/2025 10:28 AM EST",
52
- homepage: "https://www.npmjs.com/package/@versini/ui-modal",
53
- license: "MIT"
54
- };
55
- }
56
- } catch (error) {
57
- // nothing to declare officer
58
- }
46
+ ;// CONCATENATED MODULE: ./src/components/Panel/PanelPortal.tsx
59
47
 
60
48
 
61
49
 
62
50
 
63
- const ModalContext = /*#__PURE__*/ createContext(null);
64
- function useModal({ initialOpen = false, open: controlledOpen, onOpenChange: setControlledOpen, initialFocus } = {}) {
65
- const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
66
- const [labelId, setLabelId] = useState();
67
- const [descriptionId, setDescriptionId] = useState();
68
- /* v8 ignore next 2 */ const open = controlledOpen ?? uncontrolledOpen;
69
- const setOpen = setControlledOpen ?? setUncontrolledOpen;
70
- const data = useFloating({
71
- open,
72
- onOpenChange: setOpen
73
- });
74
- const context = data.context;
75
- const click = useClick(context, {
76
- enabled: controlledOpen == null
77
- });
78
- const dismiss = useDismiss(context, {
79
- outsidePress: false,
80
- outsidePressEvent: "mousedown"
81
- });
82
- const role = useRole(context);
83
- const interactions = useInteractions([
84
- click,
85
- dismiss,
86
- role
87
- ]);
88
- return useMemo(()=>({
89
- open,
90
- setOpen,
91
- ...interactions,
92
- ...data,
93
- labelId,
94
- descriptionId,
95
- setLabelId,
96
- setDescriptionId,
97
- initialFocus
98
- }), [
99
- open,
100
- setOpen,
101
- interactions,
102
- data,
103
- labelId,
104
- descriptionId,
51
+
52
+ const DIALOG_OPEN_CLASS = "av-dialog-open";
53
+ /**
54
+ * Portal component for rendering the Panel as a modal dialog. Implements W3C
55
+ * WAI-ARIA dialog patterns:
56
+ * - Renders in a portal outside the DOM hierarchy
57
+ * - Locks body scroll when open
58
+ * - Traps focus within the dialog
59
+ * - Handles Escape key to close
60
+ * - Proper ARIA attributes (role="dialog", aria-modal="true", aria-labelledby)
61
+ *
62
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
63
+ *
64
+ */ function PanelPortal({ open, onClose, children, className, style, title, initialFocus = 0 }) {
65
+ const labelId = useId();
66
+ const descriptionId = useId();
67
+ // Focus trap hook for managing focus within the dialog.
68
+ const { containerRef } = useFocusTrap({
69
+ enabled: open,
105
70
  initialFocus
106
- ]);
107
- }
108
- const useModalContext = ()=>{
109
- const context = useContext(ModalContext);
110
- /* v8 ignore next 3 */ if (context == null) {
111
- throw new Error("Modal components must be wrapped in <Modal />");
112
- }
113
- return context;
114
- };
115
- function Modal({ children, ...options }) {
116
- const dialog = useModal(options);
117
- return /*#__PURE__*/ jsx(ModalContext.Provider, {
118
- value: dialog,
119
- children: children
120
- });
121
- }
122
- const Modal_ModalContent = /*#__PURE__*/ forwardRef(function ModalContent(props, propRef) {
123
- const { context: floatingContext, ...context } = useModalContext();
124
- const ref = useMergeRefs([
125
- context.refs.setFloating,
126
- propRef
127
- ]);
128
- /* v8 ignore next 3 */ if (!floatingContext.open) {
129
- return null;
130
- }
131
- const { overlayBackground, ...rest } = props;
132
- const overlayClass = clsx("grid place-items-center", {
133
- [`${overlayBackground}`]: overlayBackground,
134
- "bg-black sm:bg-black/[.8]": !overlayBackground
135
71
  });
136
- return /*#__PURE__*/ jsx(FloatingPortal, {
137
- children: /*#__PURE__*/ jsx(FloatingOverlay, {
138
- className: overlayClass,
139
- lockScroll: true,
140
- children: /*#__PURE__*/ jsx(FloatingFocusManager, {
141
- context: floatingContext,
142
- initialFocus: context.initialFocus,
143
- children: /*#__PURE__*/ jsx("div", {
144
- ref: ref,
145
- "aria-labelledby": context.labelId,
146
- "aria-describedby": context.descriptionId,
147
- ...context.getFloatingProps(rest),
148
- children: rest.children
149
- })
150
- })
151
- })
152
- });
153
- });
154
- const Modal_ModalHeading = /*#__PURE__*/ forwardRef(function ModalHeading({ children, ...props }, ref) {
155
- const { setLabelId } = useModalContext();
156
- const id = useId();
157
- // Only sets `aria-labelledby` on the Modal root element
158
- // if this component is mounted inside it.
159
- useLayoutEffect(()=>{
160
- setLabelId(id);
161
- return ()=>setLabelId(undefined);
72
+ /**
73
+ * Handle Escape key to close the dialog. Only handles the event if focus is
74
+ * within this panel's container to support nested panels (child panel should
75
+ * close first).
76
+ */ const handleKeyDown = useCallback((event)=>{
77
+ if (event.key === "Escape") {
78
+ // Only handle if focus is within this panel's container.
79
+ if (containerRef.current?.contains(document.activeElement)) {
80
+ event.preventDefault();
81
+ event.stopPropagation();
82
+ onClose();
83
+ }
84
+ }
162
85
  }, [
163
- id,
164
- setLabelId
86
+ onClose,
87
+ containerRef.current?.contains
165
88
  ]);
166
- return /*#__PURE__*/ jsx("h1", {
167
- ...props,
168
- ref: ref,
169
- id: id,
170
- children: children
171
- });
172
- });
173
- const Modal_ModalDescription = /*#__PURE__*/ forwardRef(function ModalDescription({ children, ...props }, ref) {
174
- const { setDescriptionId } = useModalContext();
175
- const id = useId();
176
- // Only sets `aria-describedby` on the Modal root element
177
- // if this component is mounted inside it.
178
- useLayoutEffect(()=>{
179
- setDescriptionId(id);
180
- return ()=>setDescriptionId(undefined);
89
+ // Backdrop clicks are intentionally disabled (outsidePress: false).
90
+ /* c8 ignore next 1 - handler is disabled by design, no-op function */ const handleBackdropClick = useCallback(()=>{}, []);
91
+ // Lock body scroll and add escape key listener when open.
92
+ useEffect(()=>{
93
+ /* c8 ignore next 3 - effect cleanup path when not open */ if (!open) {
94
+ return;
95
+ }
96
+ // Lock body scroll.
97
+ const originalOverflow = document.body.style.overflow;
98
+ document.body.style.overflow = "hidden";
99
+ document.body.classList.add(DIALOG_OPEN_CLASS);
100
+ // Add escape key listener.
101
+ document.addEventListener("keydown", handleKeyDown);
102
+ return ()=>{
103
+ // Restore body scroll.
104
+ document.body.style.overflow = originalOverflow;
105
+ document.body.classList.remove(DIALOG_OPEN_CLASS);
106
+ // Remove escape key listener.
107
+ document.removeEventListener("keydown", handleKeyDown);
108
+ };
181
109
  }, [
182
- id,
183
- setDescriptionId
184
- ]);
185
- return /*#__PURE__*/ jsx("div", {
186
- ...props,
187
- ref: ref,
188
- id: id,
189
- children: children
190
- });
191
- });
192
- const Modal_ModalClose = /*#__PURE__*/ forwardRef(function ModalClose(props, ref) {
193
- const { setOpen } = useModalContext();
194
- const { trigger, className, ...rest } = props;
195
- const handleClose = useCallback(()=>setOpen(false), [
196
- setOpen
110
+ open,
111
+ handleKeyDown
197
112
  ]);
198
- return /*#__PURE__*/ jsx("div", {
199
- className: className,
200
- children: /*#__PURE__*/ cloneElement(trigger, {
201
- ref,
202
- onClick: handleClose,
203
- ...rest
113
+ /* c8 ignore next 3 - early return when panel is closed */ if (!open) {
114
+ return null;
115
+ }
116
+ const overlayClass = clsx("fixed inset-0 z-50 grid place-items-center", "bg-black sm:bg-black/80");
117
+ return /*#__PURE__*/ createPortal(/*#__PURE__*/ jsx("div", {
118
+ className: overlayClass,
119
+ onClick: handleBackdropClick,
120
+ // Prevent clicks on the overlay from triggering parent handlers.
121
+ /* c8 ignore next 1 - event propagation handler */ onMouseDown: (e)=>e.stopPropagation(),
122
+ children: /*#__PURE__*/ jsxs("div", {
123
+ ref: containerRef,
124
+ role: "dialog",
125
+ "aria-modal": "true",
126
+ "aria-labelledby": labelId,
127
+ "aria-describedby": descriptionId,
128
+ className: className,
129
+ style: style,
130
+ children: [
131
+ /*#__PURE__*/ jsx("span", {
132
+ id: labelId,
133
+ className: "sr-only",
134
+ children: title
135
+ }),
136
+ children
137
+ ]
204
138
  })
205
- });
206
- });
207
- /* v8 ignore next 1 */
139
+ }), document.body);
140
+ }
208
141
 
209
142
  ;// CONCATENATED MODULE: ./src/components/Panel/utilities.ts
210
143
 
@@ -306,9 +239,15 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
306
239
  hasFooter: Boolean(footer)
307
240
  });
308
241
  /**
309
- * If the panel is opened, set the document
310
- * title to the panel's title. If it's closed,
311
- * restore the original document.title.
242
+ * Handle close button click.
243
+ */ const handleClose = useCallback(()=>{
244
+ onOpenChange(false);
245
+ }, [
246
+ onOpenChange
247
+ ]);
248
+ /**
249
+ * If the panel is opened, set the document title to the panel's title. If it's
250
+ * closed, restore the original document.title.
312
251
  */ useEffect(()=>{
313
252
  if (open) {
314
253
  originalTitleRef.current = document.title;
@@ -325,7 +264,7 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
325
264
  ]);
326
265
  /**
327
266
  * Effect to handle the opening and closing animations.
328
- */ /* v8 ignore next 30 */ useEffect(()=>{
267
+ */ /* v8 ignore next 31 */ useEffect(()=>{
329
268
  if (!animation) {
330
269
  return;
331
270
  }
@@ -335,9 +274,10 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
335
274
  } : {
336
275
  transform: "translateY(-666vh)"
337
276
  });
338
- // Small delay to ensure the opening state is applied after
339
- // the component is rendered.
340
- const timer = setTimeout(()=>{
277
+ /**
278
+ * Small delay to ensure the opening state is applied after the component is
279
+ * rendered.
280
+ */ const timer = setTimeout(()=>{
341
281
  setAnimationStyles(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
342
282
  opacity: 1
343
283
  } : {
@@ -351,65 +291,61 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
351
291
  animation,
352
292
  animationType
353
293
  ]);
354
- return /*#__PURE__*/ jsx(Fragment, {
355
- children: open && /*#__PURE__*/ jsx(Modal, {
356
- open: open,
357
- onOpenChange: onOpenChange,
358
- initialFocus: initialFocus,
359
- children: /*#__PURE__*/ jsx(Modal_ModalContent, {
360
- className: panelClassName.outerWrapper,
361
- style: {
362
- ...animationStyles
363
- },
364
- children: /*#__PURE__*/ jsxs(Modal_ModalDescription, {
365
- className: panelClassName.innerWrapper,
294
+ return /*#__PURE__*/ jsx(PanelPortal, {
295
+ open: open,
296
+ onClose: handleClose,
297
+ className: panelClassName.outerWrapper,
298
+ style: animationStyles,
299
+ title: title,
300
+ initialFocus: initialFocus,
301
+ children: /*#__PURE__*/ jsxs("div", {
302
+ className: panelClassName.innerWrapper,
303
+ children: [
304
+ /*#__PURE__*/ jsxs("div", {
305
+ className: panelClassName.header,
366
306
  children: [
367
- /*#__PURE__*/ jsxs("div", {
368
- className: panelClassName.header,
369
- children: [
370
- /*#__PURE__*/ jsx(Modal_ModalClose, {
371
- className: panelClassName.closeWrapper,
372
- trigger: /*#__PURE__*/ jsx("button", {
373
- className: panelClassName.closeButton,
374
- type: "button",
375
- "aria-label": "Close",
376
- children: /*#__PURE__*/ jsx("span", {
377
- children: /*#__PURE__*/ jsx("svg", {
378
- xmlns: "http://www.w3.org/2000/svg",
379
- className: "size-3",
380
- viewBox: "0 0 384 512",
381
- fill: "currentColor",
382
- role: "img",
383
- "aria-hidden": "true",
384
- focusable: "false",
385
- children: /*#__PURE__*/ jsx("path", {
386
- d: "M297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256l105.3-105.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3z",
387
- opacity: "1"
388
- })
389
- })
307
+ /*#__PURE__*/ jsx("div", {
308
+ className: panelClassName.closeWrapper,
309
+ children: /*#__PURE__*/ jsx("button", {
310
+ className: panelClassName.closeButton,
311
+ type: "button",
312
+ "aria-label": "Close",
313
+ onClick: handleClose,
314
+ children: /*#__PURE__*/ jsx("span", {
315
+ children: /*#__PURE__*/ jsx("svg", {
316
+ xmlns: "http://www.w3.org/2000/svg",
317
+ className: "size-3",
318
+ viewBox: "0 0 384 512",
319
+ fill: "currentColor",
320
+ role: "img",
321
+ "aria-hidden": "true",
322
+ focusable: "false",
323
+ children: /*#__PURE__*/ jsx("path", {
324
+ d: "M297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256l105.3-105.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3z",
325
+ opacity: "1"
390
326
  })
391
327
  })
392
- }),
393
- /*#__PURE__*/ jsx(Modal_ModalHeading, {
394
- className: panelClassName.title,
395
- children: title
396
328
  })
397
- ]
398
- }),
399
- /*#__PURE__*/ jsx("div", {
400
- className: panelClassName.scrollableContent,
401
- children: /*#__PURE__*/ jsx("div", {
402
- className: panelClassName.content,
403
- children: children
404
329
  })
405
330
  }),
406
- footer && /*#__PURE__*/ jsx("div", {
407
- className: panelClassName.footer,
408
- children: footer
331
+ /*#__PURE__*/ jsx("h1", {
332
+ className: panelClassName.title,
333
+ children: title
409
334
  })
410
335
  ]
336
+ }),
337
+ /*#__PURE__*/ jsx("div", {
338
+ className: panelClassName.scrollableContent,
339
+ children: /*#__PURE__*/ jsx("div", {
340
+ className: panelClassName.content,
341
+ children: children
342
+ })
343
+ }),
344
+ footer && /*#__PURE__*/ jsx("div", {
345
+ className: panelClassName.footer,
346
+ children: footer
411
347
  })
412
- })
348
+ ]
413
349
  })
414
350
  });
415
351
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-panel",
3
- "version": "6.1.1",
3
+ "version": "6.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -38,16 +38,16 @@
38
38
  "test": "vitest run"
39
39
  },
40
40
  "devDependencies": {
41
- "@testing-library/jest-dom": "6.9.1",
42
- "@versini/ui-modal": "3.2.0"
41
+ "@testing-library/jest-dom": "6.9.1"
43
42
  },
44
43
  "dependencies": {
45
44
  "@tailwindcss/typography": "0.5.19",
45
+ "@versini/ui-hooks": "5.3.0",
46
46
  "clsx": "2.1.1",
47
47
  "tailwindcss": "4.1.18"
48
48
  },
49
49
  "sideEffects": [
50
50
  "**/*.css"
51
51
  ],
52
- "gitHead": "012dc1e7cb83b4a3d15b73dd78f9663b8616d8f9"
52
+ "gitHead": "15f3028acf21328c70773147a6bedfbafaa3a2a4"
53
53
  }