@versini/ui-panel 8.0.5 → 8.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +221 -498
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /*!
2
- @versini/ui-panel v8.0.5
2
+ @versini/ui-panel v8.0.6
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: "8.0.5",
9
- buildTime: "12/18/2025 04:30 PM EST",
8
+ version: "8.0.6",
9
+ buildTime: "12/18/2025 07:21 PM EST",
10
10
  homepage: "https://www.npmjs.com/package/@versini/ui-panel",
11
11
  license: "MIT",
12
12
  };
@@ -15,10 +15,10 @@ try {
15
15
  // nothing to declare officer
16
16
  }
17
17
 
18
- import { jsx, jsxs } from "react/jsx-runtime";
19
- import { useCallback, useEffect, useId, useRef, useState } from "react";
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ import { FloatingFocusManager, FloatingOverlay, FloatingPortal, useClick, useDismiss, useFloating, useInteractions, useMergeRefs, useRole } from "@floating-ui/react";
20
20
  import clsx from "clsx";
21
- import { createPortal } from "react-dom";
21
+ import { cloneElement, createContext, forwardRef, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
22
22
 
23
23
  ;// CONCATENATED MODULE: ./src/common/constants.ts
24
24
  const MESSAGEBOX_CLASSNAME = "av-messagebox";
@@ -34,450 +34,177 @@ const NONE = "none";
34
34
 
35
35
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
36
36
 
37
- ;// CONCATENATED MODULE: external "react"
37
+ ;// CONCATENATED MODULE: external "@floating-ui/react"
38
38
 
39
39
  ;// CONCATENATED MODULE: external "clsx"
40
40
 
41
- ;// CONCATENATED MODULE: external "react-dom"
41
+ ;// CONCATENATED MODULE: external "react"
42
42
 
43
- ;// CONCATENATED MODULE: ./src/components/Panel/dialogStackManager.ts
44
- /**
45
- * Dialog Stack Manager
46
- *
47
- * Implements the W3C WAI-ARIA APG pattern for managing nested modal dialogs.
48
- * This module maintains a stack of open dialogs and coordinates focus event
49
- * listeners between them.
50
- *
51
- * Key features:
52
- * - Only the topmost dialog has active focus listeners
53
- * - When a nested dialog opens, parent listeners are suspended
54
- * - When a dialog closes, parent listeners are restored
55
- * - Programmatic focus changes are flagged to prevent event interference
56
- *
57
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/
58
- */ /**
59
- * Stack of currently open dialogs. The last item is the topmost dialog.
60
- */ const openDialogStack = [];
61
- /**
62
- * Flag to ignore focus changes during programmatic focus operations.
63
- * When true, focus event handlers should return early to prevent interference.
64
- *
65
- * This is critical for nested dialogs: when a child dialog closes and
66
- * programmatically returns focus to its trigger element inside the parent,
67
- * we don't want the parent's focusin handler to interfere.
68
- */ let ignoreFocusChanges = false;
69
- /**
70
- * Get whether focus changes should be ignored.
71
- * Focus event handlers should check this and return early if true.
72
- */ function shouldIgnoreFocusChanges() {
73
- return ignoreFocusChanges;
74
- }
75
- /**
76
- * Execute a function while ignoring focus change events.
77
- * Use this when programmatically moving focus to prevent event handlers
78
- * from interfering.
79
- *
80
- * @param fn - Function to execute (typically contains focus() calls)
81
- */ function withIgnoredFocusChanges(fn) {
82
- ignoreFocusChanges = true;
83
- try {
84
- fn();
85
- } finally{
86
- // Use setTimeout to ensure the flag stays true through any
87
- // microtasks that might fire focus events
88
- setTimeout(()=>{
89
- ignoreFocusChanges = false;
90
- }, 0);
91
- }
92
- }
93
- /**
94
- * Register a dialog in the stack when it opens.
95
- * If there's a parent dialog, its listeners are suspended.
96
- *
97
- * @param entry - The dialog entry to register
98
- */ function registerDialog(entry) {
99
- // If there's already a dialog open, suspend its listeners
100
- if (openDialogStack.length > 0) {
101
- const currentTop = openDialogStack[openDialogStack.length - 1];
102
- currentTop.removeListeners();
103
- }
104
- // Add the new dialog to the stack and activate its listeners
105
- openDialogStack.push(entry);
106
- entry.addListeners();
107
- }
108
- /**
109
- * Unregister a dialog from the stack when it closes.
110
- * If there's a parent dialog, its listeners are restored.
111
- *
112
- * @param dialogRef - Reference to the dialog element being closed
113
- */ function unregisterDialog(dialogRef) {
114
- const index = openDialogStack.findIndex((entry)=>entry.dialogRef === dialogRef);
115
- /* c8 ignore next 3 - defensive check */ if (index === -1) {
116
- return;
117
- }
118
- // Remove listeners from the dialog being closed
119
- const [removedEntry] = openDialogStack.splice(index, 1);
120
- removedEntry.removeListeners();
121
- // If there's a parent dialog, restore its listeners
122
- if (openDialogStack.length > 0) {
123
- const newTop = openDialogStack[openDialogStack.length - 1];
124
- newTop.addListeners();
125
- }
126
- }
127
- /**
128
- * Get the current number of open dialogs.
129
- * Useful for debugging and testing.
130
- */ function getOpenDialogCount() {
131
- return openDialogStack.length;
132
- }
133
- /**
134
- * Check if a given dialog is the topmost (current) dialog.
135
- *
136
- * @param dialogRef - Reference to the dialog element to check
137
- */ function isTopmostDialog(dialogRef) {
138
- if (openDialogStack.length === 0) {
139
- return false;
140
- }
141
- return openDialogStack[openDialogStack.length - 1].dialogRef === dialogRef;
142
- }
143
- /**
144
- * Reset the stack (for testing purposes only).
145
- * @internal
146
- */ function _resetStackForTesting() {
147
- while(openDialogStack.length > 0){
148
- openDialogStack.pop();
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/18/2025 07:21 PM EST",
52
+ homepage: "https://github.com/aversini/ui-components",
53
+ license: "MIT"
54
+ };
149
55
  }
150
- ignoreFocusChanges = false;
56
+ } catch (error) {
57
+ // nothing to declare officer
151
58
  }
152
59
 
153
- ;// CONCATENATED MODULE: ./src/components/Panel/PanelPortal.tsx
154
-
155
-
156
-
157
60
 
158
61
 
159
62
 
160
- /**
161
- * Selector for all focusable elements within a container. Based on W3C WAI-ARIA
162
- * practices for dialog focus management.
163
- */ const FOCUSABLE_SELECTOR = [
164
- 'a[href]:not([disabled]):not([tabindex="-1"])',
165
- 'button:not([disabled]):not([tabindex="-1"])',
166
- 'textarea:not([disabled]):not([tabindex="-1"])',
167
- 'input:not([disabled]):not([tabindex="-1"])',
168
- 'select:not([disabled]):not([tabindex="-1"])',
169
- '[tabindex]:not([tabindex="-1"]):not([disabled])',
170
- 'audio[controls]:not([tabindex="-1"])',
171
- 'video[controls]:not([tabindex="-1"])',
172
- 'details:not([tabindex="-1"])'
173
- ].join(", ");
174
- /**
175
- * Portal component for rendering the Panel as a modal dialog using the native
176
- * HTML <dialog> element with showModal(). This provides:
177
- * - Native focus trapping (works correctly on iPad with physical keyboard)
178
- * - Native ESC key handling via cancel event
179
- * - Native backdrop via ::backdrop pseudo-element
180
- * - Native inert background (no need for manual scroll lock)
181
- * - Top layer rendering (no need for createPortal)
182
- *
183
- * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
184
- * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
185
- *
186
- */ function PanelPortal({ open, onClose, children, className, style, title, initialFocus = 0, kind = /* inlined export .TYPE_PANEL */ ("panel") }) {
187
- const labelId = useId();
188
- const descriptionId = useId();
189
- const dialogRef = useRef(null);
190
- const previouslyFocusedRef = useRef(null);
191
- /**
192
- * Get all focusable elements within the dialog. Excludes focus sentinel
193
- * elements used for circular focus trapping.
194
- */ const getFocusableElements = useCallback(()=>{
195
- /* c8 ignore next 3 - defensive check, dialogRef is always set when open */ if (!dialogRef.current) {
196
- return [];
197
- }
198
- const elements = dialogRef.current.querySelectorAll(FOCUSABLE_SELECTOR);
199
- return Array.from(elements).filter((el)=>el.offsetParent !== null && // Filter out hidden elements
200
- !el.hasAttribute("data-focus-sentinel"));
201
- }, []);
202
- /**
203
- * Focus a specific element by index, or the element referenced by a ref.
204
- */ const focusElement = useCallback((target)=>{
205
- if (typeof target === "number") {
206
- if (target === -1) {
207
- // -1 means don't focus anything.
208
- return;
209
- }
210
- const focusableElements = getFocusableElements();
211
- if (focusableElements.length > 0) {
212
- const index = Math.min(target, focusableElements.length - 1);
213
- focusableElements[index]?.focus();
214
- }
215
- } else if (target?.current) {
216
- target.current.focus();
217
- }
218
- }, [
219
- getFocusableElements
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
220
87
  ]);
221
- /**
222
- * Handle the native cancel event (fired when ESC is pressed). This replaces
223
- * the custom keydown handler since the native dialog handles ESC
224
- * automatically.
225
- */ const handleCancel = useCallback((event)=>{
226
- // Prevent the default close behavior so we can control it via onClose.
227
- event.preventDefault();
228
- onClose();
229
- }, [
230
- onClose
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,
105
+ initialFocus
231
106
  ]);
232
- /**
233
- * Handle Tab key to implement circular focus trapping. Native dialog focus
234
- * trapping may not be circular in all browsers, so we manually wrap focus from
235
- * last to first element (and vice versa). Uses document-level event listener
236
- * for better iPad physical keyboard support.
237
- *
238
- * IMPORTANT: On iPad Safari with a physical keyboard, the Tab key does not
239
- * automatically navigate between focusable elements. We must manually handle
240
- * ALL Tab key presses, not just wrapping cases.
241
- *
242
- */ const handleKeyDown = useCallback((event)=>{
243
- if (event.key !== "Tab" || !dialogRef.current) {
244
- return;
245
- }
246
- const focusableElements = getFocusableElements();
247
- /* c8 ignore next 4 - edge case: dialog with no focusable elements */ if (focusableElements.length === 0) {
248
- event.preventDefault();
249
- return;
250
- }
251
- const firstElement = focusableElements[0];
252
- const lastElement = focusableElements[focusableElements.length - 1];
253
- const activeElement = document.activeElement;
254
- // Find the current index of the focused element.
255
- const currentIndex = focusableElements.indexOf(activeElement);
256
- /**
257
- * Always prevent default and manually handle focus navigation. This is
258
- * required for iPad Safari with physical keyboard, where Tab key doesn't
259
- * automatically navigate between focusable elements.
260
- */ event.preventDefault();
261
- if (event.shiftKey) {
262
- // Shift+Tab: move to previous element, wrap to last if on first.
263
- if (activeElement === firstElement || currentIndex <= 0) {
264
- lastElement?.focus();
265
- } else {
266
- focusableElements[currentIndex - 1]?.focus();
267
- }
268
- } else {
269
- // Tab: move to next element, wrap to first if on last.
270
- if (activeElement === lastElement || currentIndex >= focusableElements.length - 1) {
271
- firstElement?.focus();
272
- } else {
273
- focusableElements[currentIndex + 1]?.focus();
274
- }
275
- }
276
- }, [
277
- getFocusableElements
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
278
127
  ]);
279
- /**
280
- * Handle focus events to ensure focus stays within the dialog. This catches
281
- * focus that escapes via Tab key on iPad Safari or other means.
282
- *
283
- * Uses the dialog stack manager's ignore flag to prevent interference during
284
- * programmatic focus operations (e.g., when a nested dialog closes and returns
285
- * focus to its trigger element).
286
- *
287
- */ /* v8 ignore next 20 - focus escape handling for iPad Safari, hard to test in jsdom */ const handleFocusIn = useCallback((event)=>{
288
- // Ignore focus changes triggered by programmatic focus operations.
289
- if (shouldIgnoreFocusChanges()) {
290
- return;
291
- }
292
- if (!dialogRef.current || dialogRef.current.contains(event.target)) {
293
- return;
294
- }
295
- // Focus escaped the dialog, bring it back.
296
- const focusableElements = getFocusableElements();
297
- if (focusableElements.length > 0) {
298
- focusableElements[0]?.focus();
299
- }
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);
300
162
  }, [
301
- getFocusableElements
163
+ id,
164
+ setLabelId
302
165
  ]);
303
- /**
304
- * Handle clicks on the backdrop (the area outside the dialog content). Native
305
- * dialog doesn't provide backdrop click handling, so we use the dialog
306
- * element's click event and check if the click target is the dialog itself
307
- * (not a child element).
308
- */ /* v8 ignore next 9 - backdrop clicks are disabled by design in current implementation */ const handleDialogClick = useCallback((_event)=>{
309
- /**
310
- * If the click is directly on the dialog element (the backdrop area), not on
311
- * any child element, then close the dialog. Currently disabled -
312
- * outsidePress is false by design. if (_event.target === dialogRef.current)
313
- * { onClose(); }
314
- */ }, []);
315
- /**
316
- * Focus sentinel handler - when a sentinel element receives focus, redirect
317
- * focus to the appropriate element inside the dialog. This handles iPad
318
- * Safari's Tab key behavior which can bypass event listeners.
319
- */ const handleSentinelFocus = useCallback((position)=>{
320
- const focusableElements = getFocusableElements();
321
- /* c8 ignore next 3 - edge case: dialog with no focusable elements */ if (focusableElements.length === 0) {
322
- return;
323
- }
324
- if (position === "start") {
325
- // Focus came from the end, wrap to last element.
326
- focusableElements[focusableElements.length - 1]?.focus();
327
- } else {
328
- // Focus came from the start, wrap to first element.
329
- focusableElements[0]?.focus();
330
- }
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);
331
181
  }, [
332
- getFocusableElements
182
+ id,
183
+ setDescriptionId
333
184
  ]);
334
- /**
335
- * Effect to show/hide the dialog and manage focus. Uses the dialog stack
336
- * manager to coordinate listeners between nested dialogs.
337
- */ useEffect(()=>{
338
- const dialog = dialogRef.current;
339
- /* c8 ignore next 3 - defensive check */ if (!dialog) {
340
- return;
341
- }
342
- // Store the currently focused element to restore later.
343
- previouslyFocusedRef.current = document.activeElement;
344
- // Show the dialog as a modal.
345
- if (!dialog.open) {
346
- dialog.showModal();
347
- }
348
- /**
349
- * Add cancel event listener for ESC key (always needed, not managed by
350
- * stack).
351
- */ dialog.addEventListener("cancel", handleCancel);
352
- /**
353
- * Define listener management functions for the stack manager. These will be
354
- * called when this dialog becomes/stops being the topmost dialog.
355
- */ const addListeners = ()=>{
356
- document.addEventListener("keydown", handleKeyDown);
357
- document.addEventListener("focusin", handleFocusIn);
358
- };
359
- const removeListeners = ()=>{
360
- document.removeEventListener("keydown", handleKeyDown);
361
- document.removeEventListener("focusin", handleFocusIn);
362
- };
363
- /**
364
- * Register this dialog with the stack manager. This will suspend parent
365
- * dialog listeners if any exist.
366
- */ registerDialog({
367
- dialogRef: dialog,
368
- addListeners,
369
- removeListeners
370
- });
371
- /**
372
- * Set initial focus after a small delay to ensure the DOM is ready. This
373
- * works around React's autoFocus prop not working with native dialog.
374
- */ const focusTimer = setTimeout(()=>{
375
- focusElement(initialFocus);
376
- }, 0);
377
- // Capture the previously focused element for restoration in cleanup.
378
- const previouslyFocused = previouslyFocusedRef.current;
379
- return ()=>{
380
- clearTimeout(focusTimer);
381
- dialog.removeEventListener("cancel", handleCancel);
382
- /**
383
- * Unregister from the stack manager. This will restore parent dialog
384
- * listeners if any exist.
385
- */ unregisterDialog(dialog);
386
- // Close the dialog if it's still open.
387
- if (dialog.open) {
388
- dialog.close();
389
- }
390
- /**
391
- * Restore focus to the previously focused element if it's still in the DOM.
392
- * Use withIgnoredFocusChanges to prevent parent dialog's handleFocusIn from
393
- * interfering with focus restoration.
394
- */ if (previouslyFocused?.isConnected) {
395
- withIgnoredFocusChanges(()=>{
396
- if (previouslyFocused.isConnected) {
397
- previouslyFocused.focus();
398
- }
399
- });
400
- }
401
- };
402
- }, [
403
- handleCancel,
404
- handleKeyDown,
405
- handleFocusIn,
406
- initialFocus,
407
- focusElement
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
408
197
  ]);
409
- /* c8 ignore next 3 - early return when panel is closed */ if (!open) {
410
- return null;
411
- }
412
- const isMessageBox = kind === TYPE_MESSAGEBOX;
413
- const dialogClass = clsx(/**
414
- * Center the dialog on screen with fixed positioning. Native dialog uses
415
- * position: fixed by default.
416
- * - Panel on mobile: inset-0 + no margin for full screen.
417
- * - Panel on desktop: inset-x-0 + top/bottom auto for vertical centering.
418
- * - MessageBox: Always centered (both mobile and desktop) since it doesn't
419
- * take full screen.
420
- */ "fixed max-h-none max-w-none p-0", {
421
- // Panel: full screen on mobile, centered on desktop
422
- "inset-0 m-0 sm:inset-auto sm:inset-x-0 sm:top-1/2 sm:-translate-y-1/2 sm:mx-auto": !isMessageBox,
423
- // MessageBox: always centered at all breakpoints
424
- "inset-auto inset-x-0 top-1/2 -translate-y-1/2 mx-auto z-100": isMessageBox
425
- }, /**
426
- * Backdrop styling via Tailwind's backdrop: variant for ::backdrop
427
- * pseudo-element. Full black on mobile, 80% opacity on desktop (matches
428
- * original overlay).
429
- */ "backdrop:bg-black sm:backdrop:bg-black/80", className);
430
- return /*#__PURE__*/ createPortal(/*#__PURE__*/ jsxs("dialog", {
431
- ref: dialogRef,
432
- "aria-labelledby": labelId,
433
- "aria-describedby": descriptionId,
434
- className: dialogClass,
435
- style: style,
436
- onClick: handleDialogClick,
437
- children: [
438
- /*#__PURE__*/ jsx("span", {
439
- tabIndex: 0,
440
- onFocus: ()=>handleSentinelFocus("start"),
441
- "data-focus-sentinel": "start",
442
- style: {
443
- position: "absolute",
444
- width: 1,
445
- height: 1,
446
- padding: 0,
447
- margin: -1,
448
- overflow: "hidden",
449
- clip: "rect(0, 0, 0, 0)",
450
- whiteSpace: "nowrap",
451
- border: 0
452
- },
453
- "aria-hidden": "true"
454
- }),
455
- /*#__PURE__*/ jsx("span", {
456
- id: labelId,
457
- className: "sr-only",
458
- children: title
459
- }),
460
- children,
461
- /*#__PURE__*/ jsx("span", {
462
- tabIndex: 0,
463
- onFocus: ()=>handleSentinelFocus("end"),
464
- "data-focus-sentinel": "end",
465
- style: {
466
- position: "absolute",
467
- width: 1,
468
- height: 1,
469
- padding: 0,
470
- margin: -1,
471
- overflow: "hidden",
472
- clip: "rect(0, 0, 0, 0)",
473
- whiteSpace: "nowrap",
474
- border: 0
475
- },
476
- "aria-hidden": "true"
477
- })
478
- ]
479
- }), document.body);
480
- }
198
+ return /*#__PURE__*/ jsx("div", {
199
+ className: className,
200
+ children: /*#__PURE__*/ cloneElement(trigger, {
201
+ ref,
202
+ onClick: handleClose,
203
+ ...rest
204
+ })
205
+ });
206
+ });
207
+ /* v8 ignore next 1 */
481
208
 
482
209
  ;// CONCATENATED MODULE: ./src/components/Panel/utilities.ts
483
210
 
@@ -505,10 +232,10 @@ const getPanelClassName = ({ className, kind, borderMode, animation, maxWidth =
505
232
  ["w-full sm:w-[95%] md:max-w-4xl"]: kind === /* inlined export .TYPE_PANEL */ ("panel") && !className && maxWidth === /* inlined export .LARGE */ ("large"),
506
233
  /**
507
234
  * Heights and max heights for Panel
508
- * Mobile: full height (h-full works with inset-0), Desktop: shrink-to-fit with max-height constraint
509
- */ "h-full sm:h-auto min-h-40 sm:min-h-0 max-h-full sm:max-h-[40vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .SMALL */ ("small"),
510
- "h-full sm:h-auto min-h-40 sm:min-h-0 max-h-full sm:max-h-[60vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .MEDIUM */ ("medium"),
511
- "h-full sm:h-auto min-h-40 sm:min-h-0 max-h-full sm:max-h-[95vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .LARGE */ ("large"),
235
+ * Mobile: full height, Desktop: clamp between min and max to allow flexible sizing
236
+ */ "min-h-40 max-h-full sm:max-h-[40vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .SMALL */ ("small"),
237
+ "min-h-40 max-h-full sm:max-h-[60vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .MEDIUM */ ("medium"),
238
+ "min-h-40 max-h-full sm:max-h-[95vh]": kind === /* inlined export .TYPE_PANEL */ ("panel") && effectiveMaxHeight === /* inlined export .LARGE */ ("large"),
512
239
  /**
513
240
  * Panel border colors
514
241
  */ "sm:border-border-dark": borderMode === "dark" && kind === /* inlined export .TYPE_PANEL */ ("panel"),
@@ -530,7 +257,7 @@ const getPanelClassName = ({ className, kind, borderMode, animation, maxWidth =
530
257
  "border-border-accent": borderMode === "light" && kind === TYPE_MESSAGEBOX,
531
258
  [`${className}`]: !!className
532
259
  }),
533
- innerWrapper: "content flex flex-col rounded-[inherit] relative min-h-full isolate",
260
+ innerWrapper: "content flex flex-col rounded-[inherit] relative min-h-full",
534
261
  scrollableContent: clsx("flex-1 overflow-y-auto overflow-x-hidden", "pt-12", {
535
262
  "pb-12": hasFooter
536
263
  }),
@@ -579,15 +306,9 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
579
306
  hasFooter: Boolean(footer)
580
307
  });
581
308
  /**
582
- * Handle close button click.
583
- */ const handleClose = useCallback(()=>{
584
- onOpenChange(false);
585
- }, [
586
- onOpenChange
587
- ]);
588
- /**
589
- * If the panel is opened, set the document title to the panel's title. If it's
590
- * closed, restore the original document.title.
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.
591
312
  */ useEffect(()=>{
592
313
  if (open) {
593
314
  originalTitleRef.current = document.title;
@@ -604,7 +325,7 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
604
325
  ]);
605
326
  /**
606
327
  * Effect to handle the opening and closing animations.
607
- */ /* v8 ignore next 31 */ useEffect(()=>{
328
+ */ /* v8 ignore next 30 */ useEffect(()=>{
608
329
  if (!animation) {
609
330
  return;
610
331
  }
@@ -614,10 +335,9 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
614
335
  } : {
615
336
  transform: "translateY(-666vh)"
616
337
  });
617
- /**
618
- * Small delay to ensure the opening state is applied after the component is
619
- * rendered.
620
- */ const timer = setTimeout(()=>{
338
+ // Small delay to ensure the opening state is applied after
339
+ // the component is rendered.
340
+ const timer = setTimeout(()=>{
621
341
  setAnimationStyles(!animation ? {} : animationType === /* inlined export .ANIMATION_FADE */ ("fade") ? {
622
342
  opacity: 1
623
343
  } : {
@@ -631,62 +351,65 @@ const Panel = ({ open, onOpenChange, title, children, footer, borderMode = "ligh
631
351
  animation,
632
352
  animationType
633
353
  ]);
634
- return /*#__PURE__*/ jsx(PanelPortal, {
635
- open: open,
636
- onClose: handleClose,
637
- className: panelClassName.outerWrapper,
638
- style: animationStyles,
639
- title: title,
640
- initialFocus: initialFocus,
641
- kind: kind,
642
- children: /*#__PURE__*/ jsxs("div", {
643
- className: panelClassName.innerWrapper,
644
- children: [
645
- /*#__PURE__*/ jsxs("div", {
646
- className: panelClassName.header,
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,
647
366
  children: [
648
- /*#__PURE__*/ jsx("div", {
649
- className: panelClassName.closeWrapper,
650
- children: /*#__PURE__*/ jsx("button", {
651
- className: panelClassName.closeButton,
652
- type: "button",
653
- "aria-label": "Close",
654
- onClick: handleClose,
655
- children: /*#__PURE__*/ jsx("span", {
656
- children: /*#__PURE__*/ jsx("svg", {
657
- xmlns: "http://www.w3.org/2000/svg",
658
- className: "size-3",
659
- viewBox: "0 0 384 512",
660
- fill: "currentColor",
661
- role: "img",
662
- "aria-hidden": "true",
663
- focusable: "false",
664
- children: /*#__PURE__*/ jsx("path", {
665
- 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",
666
- opacity: "1"
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
+ })
667
390
  })
668
391
  })
392
+ }),
393
+ /*#__PURE__*/ jsx(Modal_ModalHeading, {
394
+ className: panelClassName.title,
395
+ children: title
669
396
  })
397
+ ]
398
+ }),
399
+ /*#__PURE__*/ jsx("div", {
400
+ className: panelClassName.scrollableContent,
401
+ children: /*#__PURE__*/ jsx("div", {
402
+ className: panelClassName.content,
403
+ children: children
670
404
  })
671
405
  }),
672
- /*#__PURE__*/ jsx("h1", {
673
- className: panelClassName.title,
674
- children: title
406
+ footer && /*#__PURE__*/ jsx("div", {
407
+ className: panelClassName.footer,
408
+ children: footer
675
409
  })
676
410
  ]
677
- }),
678
- /*#__PURE__*/ jsx("div", {
679
- className: panelClassName.scrollableContent,
680
- children: /*#__PURE__*/ jsx("div", {
681
- className: panelClassName.content,
682
- children: children
683
- })
684
- }),
685
- footer && /*#__PURE__*/ jsx("div", {
686
- className: panelClassName.footer,
687
- children: footer
688
411
  })
689
- ]
412
+ })
690
413
  })
691
414
  });
692
415
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-panel",
3
- "version": "8.0.5",
3
+ "version": "8.0.6",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -42,7 +42,8 @@
42
42
  "test": "vitest run"
43
43
  },
44
44
  "devDependencies": {
45
- "@testing-library/jest-dom": "6.9.1"
45
+ "@testing-library/jest-dom": "6.9.1",
46
+ "@versini/ui-modal": "3.2.0"
46
47
  },
47
48
  "dependencies": {
48
49
  "@tailwindcss/typography": "0.5.19",
@@ -52,5 +53,5 @@
52
53
  "sideEffects": [
53
54
  "**/*.css"
54
55
  ],
55
- "gitHead": "7eaf26f57054b515f030a5f13313773e79c59b04"
56
+ "gitHead": "3582aaec11fa1d50f3d6ef280d4f06adaf4746e4"
56
57
  }