@versini/ui-panel 6.1.1 → 6.2.1

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 +160 -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.1
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.1",
9
+ buildTime: "12/15/2025 06:38 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,114 @@ 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
71
  });
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
- });
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
+ const activeElement = document.activeElement;
79
+ /* c8 ignore next 4 - menu focus check requires external menu component */ // Check if focus is inside an open menu - if so, let the menu handle Escape first.
80
+ if (activeElement?.closest('[role="menu"]')) {
81
+ return;
82
+ }
83
+ // Only handle if focus is within this panel's container.
84
+ if (containerRef.current?.contains(activeElement)) {
85
+ event.preventDefault();
86
+ event.stopPropagation();
87
+ onClose();
88
+ }
89
+ }
162
90
  }, [
163
- id,
164
- setLabelId
91
+ onClose,
92
+ containerRef.current?.contains
165
93
  ]);
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);
94
+ // Backdrop clicks are intentionally disabled (outsidePress: false).
95
+ /* c8 ignore next 1 - handler is disabled by design, no-op function */ const handleBackdropClick = useCallback(()=>{}, []);
96
+ // Lock body scroll and add escape key listener when open.
97
+ useEffect(()=>{
98
+ /* c8 ignore next 3 - effect cleanup path when not open */ if (!open) {
99
+ return;
100
+ }
101
+ // Lock body scroll.
102
+ const originalOverflow = document.body.style.overflow;
103
+ document.body.style.overflow = "hidden";
104
+ document.body.classList.add(DIALOG_OPEN_CLASS);
105
+ // Add escape key listener.
106
+ document.addEventListener("keydown", handleKeyDown);
107
+ return ()=>{
108
+ // Restore body scroll.
109
+ document.body.style.overflow = originalOverflow;
110
+ document.body.classList.remove(DIALOG_OPEN_CLASS);
111
+ // Remove escape key listener.
112
+ document.removeEventListener("keydown", handleKeyDown);
113
+ };
181
114
  }, [
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
115
+ open,
116
+ handleKeyDown
197
117
  ]);
198
- return /*#__PURE__*/ jsx("div", {
199
- className: className,
200
- children: /*#__PURE__*/ cloneElement(trigger, {
201
- ref,
202
- onClick: handleClose,
203
- ...rest
118
+ /* c8 ignore next 3 - early return when panel is closed */ if (!open) {
119
+ return null;
120
+ }
121
+ const overlayClass = clsx("fixed inset-0 z-50 grid place-items-center", "bg-black sm:bg-black/80");
122
+ return /*#__PURE__*/ createPortal(/*#__PURE__*/ jsx("div", {
123
+ className: overlayClass,
124
+ onClick: handleBackdropClick,
125
+ // Prevent clicks on the overlay from triggering parent handlers.
126
+ /* c8 ignore next 1 - event propagation handler */ onMouseDown: (e)=>e.stopPropagation(),
127
+ children: /*#__PURE__*/ jsxs("div", {
128
+ ref: containerRef,
129
+ role: "dialog",
130
+ "aria-modal": "true",
131
+ "aria-labelledby": labelId,
132
+ "aria-describedby": descriptionId,
133
+ className: className,
134
+ style: style,
135
+ children: [
136
+ /*#__PURE__*/ jsx("span", {
137
+ id: labelId,
138
+ className: "sr-only",
139
+ children: title
140
+ }),
141
+ children
142
+ ]
204
143
  })
205
- });
206
- });
207
- /* v8 ignore next 1 */
144
+ }), document.body);
145
+ }
208
146
 
209
147
  ;// CONCATENATED MODULE: ./src/components/Panel/utilities.ts
210
148
 
@@ -306,9 +244,15 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
306
244
  hasFooter: Boolean(footer)
307
245
  });
308
246
  /**
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.
247
+ * Handle close button click.
248
+ */ const handleClose = useCallback(()=>{
249
+ onOpenChange(false);
250
+ }, [
251
+ onOpenChange
252
+ ]);
253
+ /**
254
+ * If the panel is opened, set the document title to the panel's title. If it's
255
+ * closed, restore the original document.title.
312
256
  */ useEffect(()=>{
313
257
  if (open) {
314
258
  originalTitleRef.current = document.title;
@@ -325,7 +269,7 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
325
269
  ]);
326
270
  /**
327
271
  * Effect to handle the opening and closing animations.
328
- */ /* v8 ignore next 30 */ useEffect(()=>{
272
+ */ /* v8 ignore next 31 */ useEffect(()=>{
329
273
  if (!animation) {
330
274
  return;
331
275
  }
@@ -335,9 +279,10 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
335
279
  } : {
336
280
  transform: "translateY(-666vh)"
337
281
  });
338
- // Small delay to ensure the opening state is applied after
339
- // the component is rendered.
340
- const timer = setTimeout(()=>{
282
+ /**
283
+ * Small delay to ensure the opening state is applied after the component is
284
+ * rendered.
285
+ */ const timer = setTimeout(()=>{
341
286
  setAnimationStyles(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
342
287
  opacity: 1
343
288
  } : {
@@ -351,65 +296,61 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
351
296
  animation,
352
297
  animationType
353
298
  ]);
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,
299
+ return /*#__PURE__*/ jsx(PanelPortal, {
300
+ open: open,
301
+ onClose: handleClose,
302
+ className: panelClassName.outerWrapper,
303
+ style: animationStyles,
304
+ title: title,
305
+ initialFocus: initialFocus,
306
+ children: /*#__PURE__*/ jsxs("div", {
307
+ className: panelClassName.innerWrapper,
308
+ children: [
309
+ /*#__PURE__*/ jsxs("div", {
310
+ className: panelClassName.header,
366
311
  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
- })
312
+ /*#__PURE__*/ jsx("div", {
313
+ className: panelClassName.closeWrapper,
314
+ children: /*#__PURE__*/ jsx("button", {
315
+ className: panelClassName.closeButton,
316
+ type: "button",
317
+ "aria-label": "Close",
318
+ onClick: handleClose,
319
+ children: /*#__PURE__*/ jsx("span", {
320
+ children: /*#__PURE__*/ jsx("svg", {
321
+ xmlns: "http://www.w3.org/2000/svg",
322
+ className: "size-3",
323
+ viewBox: "0 0 384 512",
324
+ fill: "currentColor",
325
+ role: "img",
326
+ "aria-hidden": "true",
327
+ focusable: "false",
328
+ children: /*#__PURE__*/ jsx("path", {
329
+ 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",
330
+ opacity: "1"
390
331
  })
391
332
  })
392
- }),
393
- /*#__PURE__*/ jsx(Modal_ModalHeading, {
394
- className: panelClassName.title,
395
- children: title
396
333
  })
397
- ]
398
- }),
399
- /*#__PURE__*/ jsx("div", {
400
- className: panelClassName.scrollableContent,
401
- children: /*#__PURE__*/ jsx("div", {
402
- className: panelClassName.content,
403
- children: children
404
334
  })
405
335
  }),
406
- footer && /*#__PURE__*/ jsx("div", {
407
- className: panelClassName.footer,
408
- children: footer
336
+ /*#__PURE__*/ jsx("h1", {
337
+ className: panelClassName.title,
338
+ children: title
409
339
  })
410
340
  ]
341
+ }),
342
+ /*#__PURE__*/ jsx("div", {
343
+ className: panelClassName.scrollableContent,
344
+ children: /*#__PURE__*/ jsx("div", {
345
+ className: panelClassName.content,
346
+ children: children
347
+ })
348
+ }),
349
+ footer && /*#__PURE__*/ jsx("div", {
350
+ className: panelClassName.footer,
351
+ children: footer
411
352
  })
412
- })
353
+ ]
413
354
  })
414
355
  });
415
356
  };
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.1",
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.1",
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": "e525a00f1bf7288dffb5c49451e075cedc22aaa0"
53
53
  }