@axzydev/axzy_ui_system 1.3.6 → 1.4.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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "packageManager": "pnpm@10.34.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
- "version": "1.3.6",
7
+ "version": "1.4.1",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
@@ -292,6 +292,7 @@ export default function ITDatePicker({
292
292
  createPortal(
293
293
  <div
294
294
  ref={panelRef}
295
+ data-it-floating="true"
295
296
  className={clsx(
296
297
  calendarClassName,
297
298
  range ? "w-[320px]" : "w-[280px]"
@@ -172,6 +172,7 @@ export default function ITDropdownMenu({
172
172
  createPortal(
173
173
  <div
174
174
  ref={panelRef}
175
+ data-it-floating="true"
175
176
  role="menu"
176
177
  aria-label={triggerLabel}
177
178
  style={panelStyle}
@@ -323,6 +323,7 @@ export default function ITMultiSelect({
323
323
  createPortal(
324
324
  <div
325
325
  ref={panelRef}
326
+ data-it-floating="true"
326
327
  role="listbox"
327
328
  aria-multiselectable="true"
328
329
  style={panelStyle}
@@ -283,6 +283,7 @@ export default function ITSearchSelect({
283
283
  createPortal(
284
284
  <div
285
285
  ref={panelRef}
286
+ data-it-floating="true"
286
287
  style={panelStyle}
287
288
  className="bg-white dark:bg-slate-900 border border-secondary-200 dark:border-slate-800 rounded-lg shadow-xl overflow-hidden animate-in fade-in zoom-in duration-200 origin-top"
288
289
  >
@@ -217,6 +217,7 @@ export default function ITTimePicker({
217
217
  {isOpen && !disabled && createPortal(
218
218
  <div
219
219
  ref={dropdownRef}
220
+ data-it-floating="true"
220
221
  className="bg-white border border-secondary-100 shadow-xl rounded-xl w-64 overflow-hidden flex flex-col animate-in fade-in zoom-in-95 duration-200 origin-top it-timepicker-dropdown"
221
222
  style={panelStyle}
222
223
  >
@@ -1,6 +1,8 @@
1
1
  import type { Meta } from '@storybook/react';
2
2
  import ITDialog from '@/components/organisms/dialog/dialog';
3
+ import ITSearchSelect from '@/components/molecules/search-select/search-select';
3
4
  import { useState } from 'react';
5
+ import { expect, userEvent, within } from 'storybook/test';
4
6
  import ITButton from '@/components/atoms/button/button';
5
7
 
6
8
  const meta = {
@@ -76,3 +78,49 @@ export const LongContent: any = {
76
78
  className: 'w-[600px]',
77
79
  } as any,
78
80
  };
81
+
82
+ // Dialog opened on mount but controlled, so an unexpected close is observable.
83
+ const DialogWithSearchSelect = () => {
84
+ const [fruit, setFruit] = useState('');
85
+ const [isOpen, setIsOpen] = useState(true);
86
+
87
+ return (
88
+ <ITDialog isOpen={isOpen} onClose={() => setIsOpen(false)} title="Fruta favorita">
89
+ <ITSearchSelect
90
+ label="Fruta"
91
+ placeholder="Busca una fruta..."
92
+ options={[
93
+ { value: 'apple', label: 'Apple' },
94
+ { value: 'banana', label: 'Banana' },
95
+ ]}
96
+ value={fruit}
97
+ onChange={(next) => setFruit(String(next))}
98
+ />
99
+ </ITDialog>
100
+ );
101
+ };
102
+
103
+ /**
104
+ * Regression test: floating panels (selects, pickers, menus) are portaled to
105
+ * `<body>`, so they live outside the dialog DOM. Picking an option there must
106
+ * not count as a click outside the dialog.
107
+ *
108
+ * Note the whole dialog and its dropdown are portaled, so they are queried
109
+ * from `document.body` instead of the story canvas.
110
+ */
111
+ export const FloatingPanelInsideDialog: any = {
112
+ render: () => <DialogWithSearchSelect />,
113
+ play: async () => {
114
+ const view = within(document.body);
115
+
116
+ const input = await view.findByPlaceholderText('Busca una fruta...');
117
+ await userEvent.click(input);
118
+
119
+ await userEvent.click(await view.findByText('Banana'));
120
+
121
+ // The dialog must still be mounted after picking an option.
122
+ await expect(view.getByText('Fruta favorita')).toBeVisible();
123
+ await expect((input as HTMLInputElement).value).toBe('Banana');
124
+ },
125
+ };
126
+
@@ -14,6 +14,12 @@ const useClickOutside = (
14
14
  // child overlay or its contents.
15
15
  if (clickedDialog && clickedDialog !== currentDialog) return;
16
16
 
17
+ // Floating panels (select/date/time pickers, menus) are portaled to
18
+ // <body>, so they sit outside the DOM of the dialog/drawer that opened
19
+ // them. Picking an option there must not count as an outside click.
20
+ const clickedFloating = target?.closest("[data-it-floating]");
21
+ if (clickedFloating && !ref.current?.contains(clickedFloating)) return;
22
+
17
23
  if (ref.current && !ref.current.contains(event.target as Node)) {
18
24
  callback();
19
25
  }
@@ -57,7 +57,10 @@ export function useFloatingPanel(
57
57
  const flipAbove = spaceBelow < panelHeight + offset && spaceAbove > spaceBelow;
58
58
 
59
59
  const width = matchWidth ? rect.width : undefined;
60
- const panelWidth = panelRef.current?.offsetWidth ?? width ?? 0;
60
+ // With matchWidth the final width is the anchor's. Measuring the panel
61
+ // here would read its pre-style width (a full-width block on first
62
+ // paint) and clamp `left` to the viewport edge until the next update.
63
+ const panelWidth = width ?? panelRef.current?.offsetWidth ?? 0;
61
64
  let left =
62
65
  align === "end" && panelWidth
63
66
  ? rect.right - panelWidth
@@ -2,6 +2,7 @@ import React, { useState } from "react";
2
2
  import {
3
3
  ITDialog,
4
4
  ITButton,
5
+ ITSearchSelect,
5
6
  ITSlideToggle,
6
7
  ITToast,
7
8
  ITInput,
@@ -18,8 +19,9 @@ import { ShowcaseLayout } from "./ShowcaseLayout";
18
19
  export const DialogShowcase = () => {
19
20
  const [isOpen, setIsOpen] = useState(false);
20
21
  const [useFormHeader, setUseFormHeader] = useState(true);
22
+ const [auditor, setAuditor] = useState("");
21
23
 
22
- const code = `<ITDialog\n isOpen={${isOpen}}\n onClose={() => setIsOpen(false)}\n title="Confirmación de Auditoría"\n useFormHeader={${useFormHeader}}\n>\n <p>¿Estás seguro de registrar esta auditoría?</p>\n</ITDialog>`;
24
+ const code = `<ITDialog\n isOpen={${isOpen}}\n onClose={() => setIsOpen(false)}\n title="Confirmación de Auditoría"\n useFormHeader={${useFormHeader}}\n>\n <p>¿Estás seguro de registrar esta auditoría?</p>\n <ITSearchSelect placeholder="Selecciona un auditor" ... />\n</ITDialog>`;
23
25
 
24
26
  return (
25
27
  <ShowcaseLayout
@@ -39,6 +41,19 @@ export const DialogShowcase = () => {
39
41
  <p className="text-sm text-slate-600 dark:text-slate-300">
40
42
  Se guardará un registro inmutable en los logs de la blockchain corporativa. Esta acción es irreversible.
41
43
  </p>
44
+ {/* Los paneles flotantes (selects, pickers, menús) se renderizan en
45
+ un portal fuera del diálogo: elegir una opción no debe cerrarlo. */}
46
+ <ITSearchSelect
47
+ label="Auditor responsable"
48
+ placeholder="Busca un auditor..."
49
+ options={[
50
+ { value: "ana", label: "Ana Torres" },
51
+ { value: "luis", label: "Luis Paredes" },
52
+ { value: "marta", label: "Marta Ruiz" },
53
+ ]}
54
+ value={auditor}
55
+ onChange={(next) => setAuditor(String(next))}
56
+ />
42
57
  <div className="flex justify-end gap-3 pt-4 border-t">
43
58
  <ITButton label="Cancelar" variant="ghost" onClick={() => setIsOpen(false)} />
44
59
  <ITButton label="Autorizar" color="success" onClick={() => { alert("Transacción firmada!"); setIsOpen(false); }} />