@moni-labs/moni-ui 0.4.13 → 0.4.16

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 (104) hide show
  1. package/custom-elements.json +101 -81
  2. package/dist/browser/moni-ui.iife.min.js +1538 -1384
  3. package/dist/browser/moni-ui.iife.min.js.br +0 -0
  4. package/dist/browser/moni-ui.iife.min.js.gz +0 -0
  5. package/dist/browser/moni-ui.iife.min.js.map +1 -1
  6. package/dist/browser/moni-ui.min.js +2751 -2501
  7. package/dist/browser/moni-ui.min.js.br +0 -0
  8. package/dist/browser/moni-ui.min.js.gz +0 -0
  9. package/dist/browser/moni-ui.min.js.map +1 -1
  10. package/dist/cdn/chunks/{class-map-DsL1Jr2T.js → class-map-QyT9W7rZ.js} +1 -1
  11. package/dist/cdn/chunks/{decorate-DxJYS4gw.js → decorate-B7wZvwrn.js} +629 -543
  12. package/dist/cdn/chunks/if-defined-5hr_ccFw.js +1 -0
  13. package/dist/cdn/chunks/{live-D52JqeKf.js → live-Hm1Di_ND.js} +1 -1
  14. package/dist/cdn/components/moni-app-bar.js +1 -1
  15. package/dist/cdn/components/moni-badge.js +1 -1
  16. package/dist/cdn/components/moni-bottom-sheet.js +2 -1
  17. package/dist/cdn/components/moni-button-group.js +1 -1
  18. package/dist/cdn/components/moni-button-segment.js +1 -1
  19. package/dist/cdn/components/moni-button.js +1 -1
  20. package/dist/cdn/components/moni-card.js +1 -1
  21. package/dist/cdn/components/moni-carousel.js +1 -1
  22. package/dist/cdn/components/moni-checkbox.js +1 -1
  23. package/dist/cdn/components/moni-chip.js +1 -1
  24. package/dist/cdn/components/moni-color-field.js +1 -1
  25. package/dist/cdn/components/moni-context-menu.js +1 -1
  26. package/dist/cdn/components/moni-dialog.js +1 -1
  27. package/dist/cdn/components/moni-divider.js +1 -1
  28. package/dist/cdn/components/moni-expansion.js +1 -1
  29. package/dist/cdn/components/moni-fab-menu.js +1 -1
  30. package/dist/cdn/components/moni-fab.js +1 -1
  31. package/dist/cdn/components/moni-file-field.js +1 -1
  32. package/dist/cdn/components/moni-icon.js +1 -1
  33. package/dist/cdn/components/moni-list-item.js +1 -1
  34. package/dist/cdn/components/moni-list.js +1 -1
  35. package/dist/cdn/components/moni-loading-indicator.js +1 -1
  36. package/dist/cdn/components/moni-menu-item.js +1 -1
  37. package/dist/cdn/components/moni-menu.js +1 -1
  38. package/dist/cdn/components/moni-morph-modal.js +3 -2
  39. package/dist/cdn/components/moni-nav-item.js +1 -1
  40. package/dist/cdn/components/moni-nav.js +1 -1
  41. package/dist/cdn/components/moni-progress.js +1 -1
  42. package/dist/cdn/components/moni-radio.js +1 -1
  43. package/dist/cdn/components/moni-ripple.js +1 -1
  44. package/dist/cdn/components/moni-segmented-button.js +1 -1
  45. package/dist/cdn/components/moni-select-option.js +1 -1
  46. package/dist/cdn/components/moni-select.js +512 -473
  47. package/dist/cdn/components/moni-shape.js +1 -1
  48. package/dist/cdn/components/moni-side-sheet.js +1 -1
  49. package/dist/cdn/components/moni-slider.js +1 -1
  50. package/dist/cdn/components/moni-snackbar.js +1 -1
  51. package/dist/cdn/components/moni-split-button.js +1 -1
  52. package/dist/cdn/components/moni-step.js +1 -1
  53. package/dist/cdn/components/moni-stepper.js +1 -1
  54. package/dist/cdn/components/moni-switch.js +1 -1
  55. package/dist/cdn/components/moni-tab.js +1 -1
  56. package/dist/cdn/components/moni-tabs.js +1 -1
  57. package/dist/cdn/components/moni-text-field.js +122 -95
  58. package/dist/cdn/components/moni-textarea.js +52 -52
  59. package/dist/cdn/components/moni-time-picker.js +1 -1
  60. package/dist/cdn/components/moni-toolbar.js +1 -1
  61. package/dist/cdn/components/moni-tooltip.js +1 -1
  62. package/dist/cdn/components/moni-typography.js +1 -1
  63. package/dist/cdn/importmap.json +98 -98
  64. package/dist/components/_base/field-styles.d.ts.map +1 -1
  65. package/dist/components/_base/field-styles.js +662 -576
  66. package/dist/components/moni-bottom-sheet.d.ts +19 -5
  67. package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
  68. package/dist/components/moni-bottom-sheet.js +111 -30
  69. package/dist/components/moni-morph-modal.d.ts +10 -0
  70. package/dist/components/moni-morph-modal.d.ts.map +1 -1
  71. package/dist/components/moni-morph-modal.js +323 -217
  72. package/dist/components/moni-select.d.ts +16 -16
  73. package/dist/components/moni-select.d.ts.map +1 -1
  74. package/dist/components/moni-select.js +797 -727
  75. package/dist/components/moni-text-field.d.ts +15 -15
  76. package/dist/components/moni-text-field.d.ts.map +1 -1
  77. package/dist/components/moni-text-field.js +224 -180
  78. package/dist/components/moni-textarea.d.ts +13 -13
  79. package/dist/components/moni-textarea.d.ts.map +1 -1
  80. package/dist/components/moni-textarea.js +127 -122
  81. package/docs/api.json +109 -89
  82. package/docs/assets/moni-color-field/shape--square.png +0 -0
  83. package/docs/assets/moni-file-field/shape--square.png +0 -0
  84. package/docs/assets/moni-morph-modal/autoHeight--false.png +0 -0
  85. package/docs/assets/moni-morph-modal/autoHeight--true.png +0 -0
  86. package/docs/components/bottom-sheet.md +2 -1
  87. package/docs/components/morph-modal.md +16 -7
  88. package/docs/components/select.md +74 -74
  89. package/docs/components/text-field.md +75 -66
  90. package/docs/components/textarea.md +51 -51
  91. package/docs/llms.txt +3 -3
  92. package/package.json +1 -1
  93. package/src/components/_base/field-styles.ts +723 -637
  94. package/src/components/moni-bottom-sheet.test.ts +331 -0
  95. package/src/components/moni-bottom-sheet.ts +117 -29
  96. package/src/components/moni-morph-modal.test.ts +490 -413
  97. package/src/components/moni-morph-modal.ts +2274 -2162
  98. package/src/components/moni-select.test.ts +489 -466
  99. package/src/components/moni-select.ts +1963 -1815
  100. package/src/components/moni-text-field.test.ts +366 -342
  101. package/src/components/moni-text-field.ts +717 -645
  102. package/src/components/moni-textarea.test.ts +201 -197
  103. package/src/components/moni-textarea.ts +476 -460
  104. package/dist/cdn/chunks/if-defined-DdzvcoI7.js +0 -1
@@ -1,116 +1,119 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { gsap } from 'gsap';
3
- import './moni-morph-modal.js';
4
- import './moni-fab.js';
5
- import './moni-fab-menu.js';
6
- import type { MoniMorphModal } from './moni-morph-modal.js';
7
-
8
- vi.mock('gsap', () => {
9
- return {
10
- gsap: {
11
- registerPlugin: vi.fn(),
12
- utils: {
13
- interpolate: vi.fn((_start: unknown, end: unknown) => end)
14
- },
15
- set: vi.fn(),
16
- timeline: vi.fn((config: { onComplete?: () => void } = {}) => {
17
- const timeline = {
18
- addLabel: vi.fn(() => timeline),
19
- to: vi.fn((target: Record<string, unknown>, vars: Record<string, unknown>) => {
20
- if (typeof vars.onStart === 'function') (vars.onStart as () => void)();
21
- if (typeof vars.progress === 'number') target.progress = vars.progress;
22
- if (typeof vars.onUpdate === 'function') (vars.onUpdate as () => void)();
23
- return timeline;
24
- })
25
- };
26
- queueMicrotask(() => config.onComplete?.());
27
- return timeline;
28
- }),
29
- fromTo: vi.fn(
30
- (_target: unknown, _from: object, to: { onComplete?: () => void }) => {
31
- if (to.onComplete) to.onComplete();
32
- return { then: vi.fn((cb: () => void) => cb()) };
33
- }
34
- ),
35
- to: vi.fn((_target: unknown, config: { onComplete?: () => void }) => {
36
- if (config.onComplete) config.onComplete();
37
- return { then: vi.fn((cb: () => void) => cb()) };
38
- })
39
- }
40
- };
41
- });
42
-
43
- vi.mock('gsap/Flip', () => {
44
- return {
45
- Flip: {
46
- getState: vi.fn(() => ({})),
47
- from: vi.fn(
48
- (_state: unknown, config: { onComplete?: () => void }) => {
49
- if (config.onComplete) config.onComplete();
50
- return {};
51
- }
52
- )
53
- }
54
- };
55
- });
56
-
57
- async function waitForRaf() {
58
- await new Promise((resolve) => requestAnimationFrame(resolve));
59
- await new Promise((resolve) => requestAnimationFrame(resolve));
60
- }
61
-
62
- describe('moni-morph-modal', () => {
63
- let el: MoniMorphModal;
64
- let target: HTMLButtonElement;
65
- let originalMatchMedia: typeof window.matchMedia;
66
-
67
- beforeEach(() => {
68
- originalMatchMedia = window.matchMedia;
69
- window.matchMedia = vi.fn().mockImplementation((query: string) => ({
70
- matches: query === '(prefers-reduced-motion: reduce)' ? false : false,
71
- media: query,
72
- onchange: null,
73
- addListener: vi.fn(),
74
- removeListener: vi.fn(),
75
- addEventListener: vi.fn(),
76
- removeEventListener: vi.fn(),
77
- dispatchEvent: vi.fn()
78
- })) as unknown as typeof window.matchMedia;
79
-
80
- target = document.createElement('button');
81
- target.id = 'test-target';
82
- target.textContent = 'Open';
83
- target.getBoundingClientRect = () => new DOMRect(24, 700, 56, 56);
84
- document.body.appendChild(target);
85
-
86
- el = document.createElement('moni-morph-modal') as MoniMorphModal;
87
- el.target = '#test-target';
88
- document.body.appendChild(el);
89
- });
90
-
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { gsap } from 'gsap';
3
+ import './moni-morph-modal.js';
4
+ import './moni-fab.js';
5
+ import './moni-fab-menu.js';
6
+ import type { MoniMorphModal } from './moni-morph-modal.js';
7
+
8
+ vi.mock('gsap', () => {
9
+ return {
10
+ gsap: {
11
+ registerPlugin: vi.fn(),
12
+ utils: {
13
+ interpolate: vi.fn((_start: unknown, end: unknown) => end)
14
+ },
15
+ set: vi.fn(),
16
+ timeline: vi.fn((config: { onComplete?: () => void } = {}) => {
17
+ const timeline = {
18
+ addLabel: vi.fn(() => timeline),
19
+ to: vi.fn((target: Record<string, unknown>, vars: Record<string, unknown>) => {
20
+ if (typeof vars.onStart === 'function') (vars.onStart as () => void)();
21
+ if (typeof vars.progress === 'number') target.progress = vars.progress;
22
+ if (typeof vars.onUpdate === 'function') (vars.onUpdate as () => void)();
23
+ return timeline;
24
+ })
25
+ };
26
+ queueMicrotask(() => config.onComplete?.());
27
+ return timeline;
28
+ }),
29
+ fromTo: vi.fn(
30
+ (_target: unknown, _from: object, to: { onComplete?: () => void }) => {
31
+ if (to.onComplete) to.onComplete();
32
+ return { then: vi.fn((cb: () => void) => cb()) };
33
+ }
34
+ ),
35
+ to: vi.fn((_target: unknown, config: { onComplete?: () => void }) => {
36
+ if (config.onComplete) config.onComplete();
37
+ return { then: vi.fn((cb: () => void) => cb()) };
38
+ })
39
+ }
40
+ };
41
+ });
42
+
43
+ vi.mock('gsap/Flip', () => {
44
+ return {
45
+ Flip: {
46
+ getState: vi.fn(() => ({})),
47
+ from: vi.fn(
48
+ (_state: unknown, config: { onComplete?: () => void }) => {
49
+ if (config.onComplete) config.onComplete();
50
+ return {};
51
+ }
52
+ )
53
+ }
54
+ };
55
+ });
56
+
57
+ async function waitForRaf() {
58
+ await new Promise((resolve) => requestAnimationFrame(resolve));
59
+ await new Promise((resolve) => requestAnimationFrame(resolve));
60
+ }
61
+
62
+ describe('moni-morph-modal', () => {
63
+ let el: MoniMorphModal;
64
+ let target: HTMLButtonElement;
65
+ let originalMatchMedia: typeof window.matchMedia;
66
+
67
+ beforeEach(() => {
68
+ originalMatchMedia = window.matchMedia;
69
+ window.matchMedia = vi.fn().mockImplementation((query: string) => ({
70
+ matches: query === '(prefers-reduced-motion: reduce)' ? false : false,
71
+ media: query,
72
+ onchange: null,
73
+ addListener: vi.fn(),
74
+ removeListener: vi.fn(),
75
+ addEventListener: vi.fn(),
76
+ removeEventListener: vi.fn(),
77
+ dispatchEvent: vi.fn()
78
+ })) as unknown as typeof window.matchMedia;
79
+
80
+ target = document.createElement('button');
81
+ target.id = 'test-target';
82
+ target.textContent = 'Open';
83
+ target.getBoundingClientRect = () => new DOMRect(24, 700, 56, 56);
84
+ document.body.appendChild(target);
85
+
86
+ el = document.createElement('moni-morph-modal') as MoniMorphModal;
87
+ el.target = '#test-target';
88
+ document.body.appendChild(el);
89
+ });
90
+
91
91
  afterEach(() => {
92
92
  window.matchMedia = originalMatchMedia;
93
93
  el.remove();
94
94
  target.remove();
95
+ document.documentElement.style.overflow = '';
96
+ document.body.style.overflow = '';
97
+ document.body.style.overscrollBehavior = '';
95
98
  });
96
-
97
- it('registra el elemento personalizado', () => {
98
- expect(customElements.get('moni-morph-modal')).toBeTruthy();
99
- });
100
-
101
- it('renderiza un panel y un backdrop en el shadow root', async () => {
102
- await el.updateComplete;
103
- const panel = el.shadowRoot?.querySelector('.panel');
104
- const backdrop = el.shadowRoot?.querySelector('.backdrop');
105
- expect(panel).toBeTruthy();
106
- expect(backdrop).toBeTruthy();
107
- });
108
-
109
- it('refleja el atributo target', async () => {
110
- await el.updateComplete;
111
- expect(el.getAttribute('target')).toBe('#test-target');
112
- });
113
-
99
+
100
+ it('registra el elemento personalizado', () => {
101
+ expect(customElements.get('moni-morph-modal')).toBeTruthy();
102
+ });
103
+
104
+ it('renderiza un panel y un backdrop en el shadow root', async () => {
105
+ await el.updateComplete;
106
+ const panel = el.shadowRoot?.querySelector('.panel');
107
+ const backdrop = el.shadowRoot?.querySelector('.backdrop');
108
+ expect(panel).toBeTruthy();
109
+ expect(backdrop).toBeTruthy();
110
+ });
111
+
112
+ it('refleja el atributo target', async () => {
113
+ await el.updateComplete;
114
+ expect(el.getAttribute('target')).toBe('#test-target');
115
+ });
116
+
114
117
  it('refleja el atributo open', async () => {
115
118
  el.open = true;
116
119
  await el.updateComplete;
@@ -143,342 +146,416 @@ describe('moni-morph-modal', () => {
143
146
  vi.unstubAllGlobals();
144
147
  });
145
148
 
146
- it('proyecta el contenido del slot por defecto', async () => {
147
- const content = document.createElement('p');
148
- content.textContent = 'Hello';
149
- el.appendChild(content);
150
- await el.updateComplete;
151
- const body = el.shadowRoot?.querySelector('.body');
152
- expect(body).toBeTruthy();
153
- const slot = body?.querySelector('slot');
154
- expect(slot).toBeTruthy();
155
- });
156
-
157
- it('proyecta el contenido del slot header', async () => {
158
- const header = document.createElement('div');
159
- header.setAttribute('slot', 'header');
160
- header.textContent = 'Title';
161
- el.appendChild(header);
162
- await el.updateComplete;
163
- const headerSlot = el.shadowRoot?.querySelector('slot[name="header"]');
164
- expect(headerSlot).toBeTruthy();
165
- });
149
+ it('auto-height usa la altura natural sin superar expanded-height', () => {
150
+ el.autoHeight = true;
151
+ el.expandedHeight = '600px';
152
+ const compute = (naturalHeight: number) =>
153
+ (el as unknown as {
154
+ _computeFinalRect: (target: DOMRect, natural: { width: number; height: number }) => DOMRect;
155
+ })._computeFinalRect(new DOMRect(24, 100, 56, 56), { width: 400, height: naturalHeight });
166
156
 
167
- it('proyecta el contenido del slot footer', async () => {
168
- const footer = document.createElement('div');
169
- footer.setAttribute('slot', 'footer');
170
- footer.textContent = 'Actions';
171
- el.appendChild(footer);
172
- await el.updateComplete;
173
- const footerSlot = el.shadowRoot?.querySelector('slot[name="footer"]');
174
- expect(footerSlot).toBeTruthy();
157
+ expect(compute(360).height).toBe(360);
158
+ expect(compute(900).height).toBe(600);
175
159
  });
176
-
177
- it('muestra el botón de cerrar cuando show-close-button es true', async () => {
178
- el.showCloseButton = true;
179
- await el.updateComplete;
180
- const closeBtn = el.shadowRoot?.querySelector('.close-btn');
181
- expect(closeBtn).toBeTruthy();
182
- });
183
-
160
+
161
+ it('proyecta el contenido del slot por defecto', async () => {
162
+ const content = document.createElement('p');
163
+ content.textContent = 'Hello';
164
+ el.appendChild(content);
165
+ await el.updateComplete;
166
+ const body = el.shadowRoot?.querySelector('.body');
167
+ expect(body).toBeTruthy();
168
+ const slot = body?.querySelector('slot');
169
+ expect(slot).toBeTruthy();
170
+ });
171
+
172
+ it('proyecta el contenido del slot header', async () => {
173
+ const header = document.createElement('div');
174
+ header.setAttribute('slot', 'header');
175
+ header.textContent = 'Title';
176
+ el.appendChild(header);
177
+ await el.updateComplete;
178
+ const headerSlot = el.shadowRoot?.querySelector('slot[name="header"]');
179
+ expect(headerSlot).toBeTruthy();
180
+ });
181
+
182
+ it('proyecta el contenido del slot footer', async () => {
183
+ const footer = document.createElement('div');
184
+ footer.setAttribute('slot', 'footer');
185
+ footer.textContent = 'Actions';
186
+ el.appendChild(footer);
187
+ await el.updateComplete;
188
+ const footerSlot = el.shadowRoot?.querySelector('slot[name="footer"]');
189
+ expect(footerSlot).toBeTruthy();
190
+ });
191
+
192
+ it('muestra el botón de cerrar cuando show-close-button es true', async () => {
193
+ el.showCloseButton = true;
194
+ await el.updateComplete;
195
+ const closeBtn = el.shadowRoot?.querySelector('.close-btn');
196
+ expect(closeBtn).toBeTruthy();
197
+ });
198
+
184
199
  it('alterna el estado open cuando se llaman a show() y hide()', async () => {
185
- expect(el.open).toBe(false);
186
- el.show();
187
- await waitForRaf();
188
- expect(el.open).toBe(true);
189
- el.hide();
190
- await waitForRaf();
191
- expect(el.open).toBe(false);
200
+ expect(el.open).toBe(false);
201
+ el.show();
202
+ await waitForRaf();
203
+ expect(el.open).toBe(true);
204
+ el.hide();
205
+ await waitForRaf();
206
+ expect(el.open).toBe(false);
192
207
  });
193
208
 
194
- it('desenfoca el contenido mientras desaparece al cerrar', async () => {
209
+ it('bloquea el scroll del documento con backdrop y lo restaura al cerrar', async () => {
210
+ document.documentElement.style.overflow = 'auto';
211
+ document.body.style.overflow = 'scroll';
212
+ el.hasBackdrop = true;
213
+
195
214
  el.show();
196
215
  await waitForRaf();
197
- vi.mocked(gsap.fromTo).mockClear();
216
+ expect(document.documentElement.style.overflow).toBe('hidden');
217
+ expect(document.body.style.overflow).toBe('hidden');
198
218
 
199
219
  el.hide();
200
220
  await waitForRaf();
201
-
202
- expect(gsap.fromTo).toHaveBeenCalledWith(
203
- expect.anything(),
204
- expect.objectContaining({ opacity: 1, filter: 'blur(0px)' }),
205
- expect.objectContaining({ opacity: 0, filter: 'blur(6px)' })
206
- );
221
+ expect(document.documentElement.style.overflow).toBe('auto');
222
+ expect(document.body.style.overflow).toBe('scroll');
207
223
  });
208
224
 
209
- it('recalcula la posición actual del origen al cerrar', async () => {
225
+ it('mantiene bloqueado el scroll hasta cerrar el último morph modal', async () => {
226
+ const secondTarget = document.createElement('button');
227
+ secondTarget.id = 'second-target';
228
+ secondTarget.getBoundingClientRect = () => new DOMRect(100, 100, 56, 56);
229
+ document.body.appendChild(secondTarget);
230
+ const second = document.createElement('moni-morph-modal') as MoniMorphModal;
231
+ second.target = '#second-target';
232
+ document.body.appendChild(second);
233
+
210
234
  el.show();
235
+ second.show();
211
236
  await waitForRaf();
212
- target.getBoundingClientRect = () => new DOMRect(310, 420, 64, 48);
213
-
214
237
  el.hide();
215
238
  await waitForRaf();
239
+ expect(document.body.style.overflow).toBe('hidden');
216
240
 
217
- const panel = el.shadowRoot?.querySelector('.panel') as HTMLElement;
218
- expect(gsap.set).toHaveBeenCalledWith(
219
- panel,
220
- expect.objectContaining({ x: 310, y: 420 })
221
- );
222
- expect(panel.style.width).toBe('64px');
223
- expect(panel.style.height).toBe('48px');
241
+ second.hide();
242
+ await waitForRaf();
243
+ expect(document.body.style.overflow).toBe('');
244
+ second.remove();
245
+ secondTarget.remove();
224
246
  });
225
-
247
+
248
+ it('desenfoca el contenido mientras desaparece al cerrar', async () => {
249
+ el.show();
250
+ await waitForRaf();
251
+ vi.mocked(gsap.fromTo).mockClear();
252
+
253
+ el.hide();
254
+ await waitForRaf();
255
+
256
+ expect(gsap.fromTo).toHaveBeenCalledWith(
257
+ expect.anything(),
258
+ expect.objectContaining({ opacity: 1, filter: 'blur(0px)' }),
259
+ expect.objectContaining({ opacity: 0, filter: 'blur(6px)' })
260
+ );
261
+ });
262
+
263
+ it('recalcula la posición actual del origen al cerrar', async () => {
264
+ el.show();
265
+ await waitForRaf();
266
+ target.getBoundingClientRect = () => new DOMRect(310, 420, 64, 48);
267
+
268
+ el.hide();
269
+ await waitForRaf();
270
+
271
+ const panel = el.shadowRoot?.querySelector('.panel') as HTMLElement;
272
+ expect(gsap.set).toHaveBeenCalledWith(
273
+ panel,
274
+ expect.objectContaining({ x: 310, y: 420 })
275
+ );
276
+ expect(panel.style.width).toBe('64px');
277
+ expect(panel.style.height).toBe('48px');
278
+ });
279
+
226
280
  it('desaparece con escala y opacidad cuando el origen ya no existe', async () => {
227
281
  el.show();
228
282
  await waitForRaf();
229
283
  target.remove();
230
- vi.mocked(gsap.to).mockClear();
231
-
232
- el.hide();
233
- await waitForRaf();
234
-
235
- expect(gsap.to).toHaveBeenCalledWith(
236
- expect.anything(),
237
- expect.objectContaining({ autoAlpha: 0, scale: 0.92 })
238
- );
284
+ vi.mocked(gsap.to).mockClear();
285
+
286
+ el.hide();
287
+ await waitForRaf();
288
+
289
+ expect(gsap.to).toHaveBeenCalledWith(
290
+ expect.anything(),
291
+ expect.objectContaining({ autoAlpha: 0, scale: 0.92 })
292
+ );
239
293
  expect(el.open).toBe(false);
240
294
  });
241
295
 
242
- it('captura la superficie interna real de un FAB para el morph', async () => {
243
- const fab = document.createElement('moni-fab');
244
- document.body.appendChild(fab);
245
- await (fab as HTMLElement & { updateComplete: Promise<unknown> }).updateComplete;
246
- const button = fab.shadowRoot?.querySelector('button') as HTMLButtonElement;
247
- button.style.backgroundColor = 'rgb(103, 80, 164)';
248
- button.style.borderRadius = '20px';
249
- button.style.boxShadow = 'rgba(0, 0, 0, 0.2) 0px 2px 6px';
250
- button.getBoundingClientRect = () => new DOMRect(280, 640, 56, 56);
251
-
252
- const state = (el as unknown as {
253
- _getTargetState: (target: HTMLElement) => {
254
- rect: DOMRect;
255
- backgroundColor: string;
256
- borderRadius: string;
257
- boxShadow: string;
258
- };
259
- })._getTargetState(fab);
260
-
261
- expect(state.rect.x).toBe(280);
262
- expect(state.rect.width).toBe(56);
263
- expect(state.backgroundColor).toBe('rgb(103, 80, 164)');
264
- expect(state.borderRadius).toBe('20px');
265
- expect(state.boxShadow).toContain('2px 6px');
266
- fab.remove();
267
- });
268
-
269
- it('captura todo el contenido compuesto del trigger interno de un FAB Menu', async () => {
270
- const menu = document.createElement('moni-fab-menu') as HTMLElement & {
271
- icon: string;
272
- updateComplete: Promise<unknown>;
273
- };
274
- menu.icon = 'add';
275
- document.body.appendChild(menu);
276
- await menu.updateComplete;
277
- const trigger = menu.shadowRoot?.querySelector('.trigger') as HTMLElement & { updateComplete: Promise<unknown> };
278
- await trigger.updateComplete;
279
-
280
- const visual = (el as unknown as {
281
- _resolveVisualTarget: (target: HTMLElement) => HTMLElement;
282
- })._resolveVisualTarget(menu);
283
- visual.getBoundingClientRect = () => new DOMRect(20, 30, 56, 56);
284
- const snapshot = (el as unknown as {
285
- _createTargetSnapshot: (target: HTMLElement, rect: DOMRect) => HTMLElement;
286
- })._createTargetSnapshot(visual, visual.getBoundingClientRect());
287
-
288
- expect(snapshot.textContent).toContain('add');
289
- expect(snapshot.style.width).toBe('56px');
290
- expect(snapshot.querySelectorAll('*').length).toBeGreaterThan(1);
291
- expect((snapshot.firstElementChild as HTMLElement).style.backgroundColor).toBe('transparent');
292
- expect((snapshot.firstElementChild as HTMLElement).style.transform).toBe('none');
293
- expect((snapshot.firstElementChild as HTMLElement).style.whiteSpace).toBe('nowrap');
294
- menu.remove();
295
- });
296
-
297
- it('carga inmediatamente las imágenes incluidas en el snapshot', () => {
298
- const card = document.createElement('button');
299
- const image = document.createElement('img');
300
- image.loading = 'lazy';
301
- image.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>';
302
- card.appendChild(image);
303
- const snapshot = (el as unknown as {
304
- _createTargetSnapshot: (target: HTMLElement, rect: DOMRect) => HTMLElement;
305
- })._createTargetSnapshot(card, new DOMRect(0, 0, 240, 140));
306
-
307
- expect(snapshot.querySelector('img')?.loading).toBe('eager');
308
- expect(snapshot.querySelector('img')?.decoding).toBe('sync');
309
- });
296
+ it('usa el cierre sin origen cuando el target salio del viewport', async () => {
297
+ const visibleAncestor = document.createElement('div');
298
+ visibleAncestor.getBoundingClientRect = () => new DOMRect(0, 0, 320, 480);
299
+ document.body.appendChild(visibleAncestor);
300
+ visibleAncestor.appendChild(target);
310
301
 
311
- it('descompone rotación y escala del transform vivo del target', () => {
312
- const transformed = document.createElement('button');
313
- transformed.style.transform = 'matrix(1.195128, 0.104587, -0.104587, 1.195128, 80, 0)';
314
- document.body.appendChild(transformed);
315
- const state = (el as unknown as {
316
- _readTransformState: (target: HTMLElement) => { rotation: number; scaleX: number; scaleY: number };
317
- })._readTransformState(transformed);
318
-
319
- expect(state.rotation).toBeCloseTo(5, 1);
320
- expect(state.scaleX).toBeCloseTo(1.2, 1);
321
- expect(state.scaleY).toBeCloseTo(1.2, 1);
322
- transformed.remove();
323
- });
324
-
325
- it('convierte coordenadas del viewport al canvas transformado', () => {
326
- const canvas = document.createElement('div');
327
- canvas.className = 'lab-canvas';
328
- canvas.style.transform = 'scale(0.5)';
329
- canvas.getBoundingClientRect = () => new DOMRect(10, 20, 200, 300);
330
- document.body.appendChild(canvas);
331
- canvas.appendChild(el);
332
-
333
- const local = (el as unknown as {
334
- _toPanelCoordinateRect: (rect: DOMRect) => DOMRect;
335
- })._toPanelCoordinateRect(new DOMRect(60, 70, 20, 30));
336
-
337
- expect(local.x).toBe(100);
338
- expect(local.y).toBe(100);
339
- expect(local.width).toBe(40);
340
- expect(local.height).toBe(60);
341
- canvas.remove();
342
- });
343
-
344
- it('alterna el estado open cuando se llama a toggle()', async () => {
345
- el.toggle();
346
- await waitForRaf();
347
- expect(el.open).toBe(true);
348
- el.toggle();
349
- await waitForRaf();
350
- expect(el.open).toBe(false);
351
- });
352
-
353
- it('advierte cuando no se encuentra el objetivo (target)', () => {
354
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
355
- const modal = document.createElement('moni-morph-modal') as MoniMorphModal;
356
- modal.target = '#missing-target';
357
- document.body.appendChild(modal);
358
- modal.show();
359
- const call = warnSpy.mock.calls.find((c) =>
360
- String(c[0]).includes('[moni-morph-modal]')
361
- );
362
- expect(call).toBeTruthy();
363
- modal.remove();
364
- warnSpy.mockRestore();
365
- });
366
-
367
- it('soporta la declaración recursiva de modales morph anidados', async () => {
368
- const innerTarget = document.createElement('button');
369
- innerTarget.id = 'inner-target';
370
- el.appendChild(innerTarget);
371
-
372
- const innerModal = document.createElement('moni-morph-modal') as MoniMorphModal;
373
- innerModal.target = '#inner-target';
374
- el.appendChild(innerModal);
375
-
376
- await el.updateComplete;
377
- await innerModal.updateComplete;
378
-
379
- expect(innerModal.target).toBe('#inner-target');
380
- innerModal.show();
381
- await waitForRaf();
382
- expect(innerModal.open).toBe(true);
383
-
384
- innerModal.remove();
385
- innerTarget.remove();
386
- });
387
-
388
- it('no crea elementos morph por defecto', async () => {
389
302
  el.show();
390
303
  await waitForRaf();
391
- expect(el.shadowRoot?.querySelector('.morph-text')).toBeFalsy();
392
- expect(el.shadowRoot?.querySelector('.morph-icon')).toBeFalsy();
393
- });
394
-
395
- it('crea y limpia morph-text cuando morph-label es true', async () => {
396
- (el as unknown as { _hasHeader: boolean })._hasHeader = true;
397
- el.morphLabel = true;
398
- await el.updateComplete;
399
-
400
- const createSpy = vi.spyOn(
401
- el as unknown as { _createMorphText: (text: string, source: Element) => HTMLElement },
402
- '_createMorphText'
403
- );
404
- el.show();
405
- await waitForRaf();
406
-
407
- expect(createSpy).toHaveBeenCalled();
408
- expect(el.shadowRoot?.querySelector('.morph-text')).toBeFalsy();
409
- createSpy.mockRestore();
410
- });
411
-
412
- it('usa el contenido del slot trigger-label cuando se proporciona', async () => {
413
- const triggerLabel = document.createElement('span');
414
- triggerLabel.setAttribute('slot', 'trigger-label');
415
- triggerLabel.textContent = 'Custom label';
416
- el.appendChild(triggerLabel);
417
-
418
- (el as unknown as { _hasHeader: boolean })._hasHeader = true;
419
- (el as unknown as { _hasTriggerLabel: boolean })._hasTriggerLabel = true;
420
-
421
- el.morphLabel = true;
422
- await el.updateComplete;
423
-
424
- const getNodes = vi.spyOn(
425
- el as unknown as { _getTriggerLabelNodes: () => Node[] },
426
- '_getTriggerLabelNodes'
427
- );
428
- el.show();
429
- await waitForRaf();
430
-
431
- expect(getNodes).toHaveReturned();
432
- const returned = getNodes.mock.results[0]?.value as Node[] | undefined;
433
- expect(returned?.length).toBeGreaterThan(0);
434
- getNodes.mockRestore();
435
- });
436
-
437
- it('no ejecuta el morph de texto legado al cerrar cuando morph-label es true', async () => {
438
- (el as unknown as { _hasHeader: boolean })._hasHeader = true;
439
- el.morphLabel = true;
440
- await el.updateComplete;
441
-
442
- el.show();
443
- await waitForRaf();
444
- expect(el.open).toBe(true);
304
+ target.getBoundingClientRect = () =>
305
+ new DOMRect(24, window.innerHeight + 100, 56, 56);
306
+ vi.mocked(gsap.to).mockClear();
445
307
 
446
- const closeSpy = vi.spyOn(
447
- el as unknown as { _animateLabelClose: () => void },
448
- '_animateLabelClose'
449
- );
450
308
  el.hide();
451
309
  await waitForRaf();
452
310
 
453
- expect(closeSpy).not.toHaveBeenCalled();
454
- expect(el.shadowRoot?.querySelector('.morph-text, .morph-icon, .morph-target-snapshot')).toBeNull();
455
- closeSpy.mockRestore();
456
- });
457
-
458
- it('no crea elementos morph cuando se prefiere movimiento reducido (reduced motion)', async () => {
459
- (el as unknown as { _hasHeader: boolean })._hasHeader = true;
460
- el.morphLabel = true;
461
- await el.updateComplete;
462
-
463
- window.matchMedia = vi.fn().mockReturnValue({
464
- matches: true,
465
- media: '(prefers-reduced-motion: reduce)',
466
- onchange: null,
467
- addListener: vi.fn(),
468
- removeListener: vi.fn(),
469
- addEventListener: vi.fn(),
470
- removeEventListener: vi.fn(),
471
- dispatchEvent: vi.fn()
472
- } as unknown as MediaQueryList);
473
-
474
- const createSpy = vi.spyOn(
475
- el as unknown as { _createMorphText: (text: string, source: Element) => HTMLElement },
476
- '_createMorphText'
311
+ expect(gsap.to).toHaveBeenCalledWith(
312
+ expect.anything(),
313
+ expect.objectContaining({ autoAlpha: 0, scale: 0.92 })
477
314
  );
478
- el.show();
479
- await waitForRaf();
480
-
481
- expect(createSpy).not.toHaveBeenCalled();
482
- createSpy.mockRestore();
315
+ expect(el.open).toBe(false);
316
+ visibleAncestor.remove();
483
317
  });
484
- });
318
+
319
+ it('captura la superficie interna real de un FAB para el morph', async () => {
320
+ const fab = document.createElement('moni-fab');
321
+ document.body.appendChild(fab);
322
+ await (fab as HTMLElement & { updateComplete: Promise<unknown> }).updateComplete;
323
+ const button = fab.shadowRoot?.querySelector('button') as HTMLButtonElement;
324
+ button.style.backgroundColor = 'rgb(103, 80, 164)';
325
+ button.style.borderRadius = '20px';
326
+ button.style.boxShadow = 'rgba(0, 0, 0, 0.2) 0px 2px 6px';
327
+ button.getBoundingClientRect = () => new DOMRect(280, 640, 56, 56);
328
+
329
+ const state = (el as unknown as {
330
+ _getTargetState: (target: HTMLElement) => {
331
+ rect: DOMRect;
332
+ backgroundColor: string;
333
+ borderRadius: string;
334
+ boxShadow: string;
335
+ };
336
+ })._getTargetState(fab);
337
+
338
+ expect(state.rect.x).toBe(280);
339
+ expect(state.rect.width).toBe(56);
340
+ expect(state.backgroundColor).toBe('rgb(103, 80, 164)');
341
+ expect(state.borderRadius).toBe('20px');
342
+ expect(state.boxShadow).toContain('2px 6px');
343
+ fab.remove();
344
+ });
345
+
346
+ it('captura todo el contenido compuesto del trigger interno de un FAB Menu', async () => {
347
+ const menu = document.createElement('moni-fab-menu') as HTMLElement & {
348
+ icon: string;
349
+ updateComplete: Promise<unknown>;
350
+ };
351
+ menu.icon = 'add';
352
+ document.body.appendChild(menu);
353
+ await menu.updateComplete;
354
+ const trigger = menu.shadowRoot?.querySelector('.trigger') as HTMLElement & { updateComplete: Promise<unknown> };
355
+ await trigger.updateComplete;
356
+
357
+ const visual = (el as unknown as {
358
+ _resolveVisualTarget: (target: HTMLElement) => HTMLElement;
359
+ })._resolveVisualTarget(menu);
360
+ visual.getBoundingClientRect = () => new DOMRect(20, 30, 56, 56);
361
+ const snapshot = (el as unknown as {
362
+ _createTargetSnapshot: (target: HTMLElement, rect: DOMRect) => HTMLElement;
363
+ })._createTargetSnapshot(visual, visual.getBoundingClientRect());
364
+
365
+ expect(snapshot.textContent).toContain('add');
366
+ expect(snapshot.style.width).toBe('56px');
367
+ expect(snapshot.querySelectorAll('*').length).toBeGreaterThan(1);
368
+ expect((snapshot.firstElementChild as HTMLElement).style.backgroundColor).toBe('transparent');
369
+ expect((snapshot.firstElementChild as HTMLElement).style.transform).toBe('none');
370
+ expect((snapshot.firstElementChild as HTMLElement).style.whiteSpace).toBe('nowrap');
371
+ menu.remove();
372
+ });
373
+
374
+ it('carga inmediatamente las imágenes incluidas en el snapshot', () => {
375
+ const card = document.createElement('button');
376
+ const image = document.createElement('img');
377
+ image.loading = 'lazy';
378
+ image.src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>';
379
+ card.appendChild(image);
380
+ const snapshot = (el as unknown as {
381
+ _createTargetSnapshot: (target: HTMLElement, rect: DOMRect) => HTMLElement;
382
+ })._createTargetSnapshot(card, new DOMRect(0, 0, 240, 140));
383
+
384
+ expect(snapshot.querySelector('img')?.loading).toBe('eager');
385
+ expect(snapshot.querySelector('img')?.decoding).toBe('sync');
386
+ });
387
+
388
+ it('descompone rotación y escala del transform vivo del target', () => {
389
+ const transformed = document.createElement('button');
390
+ transformed.style.transform = 'matrix(1.195128, 0.104587, -0.104587, 1.195128, 80, 0)';
391
+ document.body.appendChild(transformed);
392
+ const state = (el as unknown as {
393
+ _readTransformState: (target: HTMLElement) => { rotation: number; scaleX: number; scaleY: number };
394
+ })._readTransformState(transformed);
395
+
396
+ expect(state.rotation).toBeCloseTo(5, 1);
397
+ expect(state.scaleX).toBeCloseTo(1.2, 1);
398
+ expect(state.scaleY).toBeCloseTo(1.2, 1);
399
+ transformed.remove();
400
+ });
401
+
402
+ it('convierte coordenadas del viewport al canvas transformado', () => {
403
+ const canvas = document.createElement('div');
404
+ canvas.className = 'lab-canvas';
405
+ canvas.style.transform = 'scale(0.5)';
406
+ canvas.getBoundingClientRect = () => new DOMRect(10, 20, 200, 300);
407
+ document.body.appendChild(canvas);
408
+ canvas.appendChild(el);
409
+
410
+ const local = (el as unknown as {
411
+ _toPanelCoordinateRect: (rect: DOMRect) => DOMRect;
412
+ })._toPanelCoordinateRect(new DOMRect(60, 70, 20, 30));
413
+
414
+ expect(local.x).toBe(100);
415
+ expect(local.y).toBe(100);
416
+ expect(local.width).toBe(40);
417
+ expect(local.height).toBe(60);
418
+ canvas.remove();
419
+ });
420
+
421
+ it('alterna el estado open cuando se llama a toggle()', async () => {
422
+ el.toggle();
423
+ await waitForRaf();
424
+ expect(el.open).toBe(true);
425
+ el.toggle();
426
+ await waitForRaf();
427
+ expect(el.open).toBe(false);
428
+ });
429
+
430
+ it('advierte cuando no se encuentra el objetivo (target)', () => {
431
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
432
+ const modal = document.createElement('moni-morph-modal') as MoniMorphModal;
433
+ modal.target = '#missing-target';
434
+ document.body.appendChild(modal);
435
+ modal.show();
436
+ const call = warnSpy.mock.calls.find((c) =>
437
+ String(c[0]).includes('[moni-morph-modal]')
438
+ );
439
+ expect(call).toBeTruthy();
440
+ modal.remove();
441
+ warnSpy.mockRestore();
442
+ });
443
+
444
+ it('soporta la declaración recursiva de modales morph anidados', async () => {
445
+ const innerTarget = document.createElement('button');
446
+ innerTarget.id = 'inner-target';
447
+ el.appendChild(innerTarget);
448
+
449
+ const innerModal = document.createElement('moni-morph-modal') as MoniMorphModal;
450
+ innerModal.target = '#inner-target';
451
+ el.appendChild(innerModal);
452
+
453
+ await el.updateComplete;
454
+ await innerModal.updateComplete;
455
+
456
+ expect(innerModal.target).toBe('#inner-target');
457
+ innerModal.show();
458
+ await waitForRaf();
459
+ expect(innerModal.open).toBe(true);
460
+
461
+ innerModal.remove();
462
+ innerTarget.remove();
463
+ });
464
+
465
+ it('no crea elementos morph por defecto', async () => {
466
+ el.show();
467
+ await waitForRaf();
468
+ expect(el.shadowRoot?.querySelector('.morph-text')).toBeFalsy();
469
+ expect(el.shadowRoot?.querySelector('.morph-icon')).toBeFalsy();
470
+ });
471
+
472
+ it('crea y limpia morph-text cuando morph-label es true', async () => {
473
+ (el as unknown as { _hasHeader: boolean })._hasHeader = true;
474
+ el.morphLabel = true;
475
+ await el.updateComplete;
476
+
477
+ const createSpy = vi.spyOn(
478
+ el as unknown as { _createMorphText: (text: string, source: Element) => HTMLElement },
479
+ '_createMorphText'
480
+ );
481
+ el.show();
482
+ await waitForRaf();
483
+
484
+ expect(createSpy).toHaveBeenCalled();
485
+ expect(el.shadowRoot?.querySelector('.morph-text')).toBeFalsy();
486
+ createSpy.mockRestore();
487
+ });
488
+
489
+ it('usa el contenido del slot trigger-label cuando se proporciona', async () => {
490
+ const triggerLabel = document.createElement('span');
491
+ triggerLabel.setAttribute('slot', 'trigger-label');
492
+ triggerLabel.textContent = 'Custom label';
493
+ el.appendChild(triggerLabel);
494
+
495
+ (el as unknown as { _hasHeader: boolean })._hasHeader = true;
496
+ (el as unknown as { _hasTriggerLabel: boolean })._hasTriggerLabel = true;
497
+
498
+ el.morphLabel = true;
499
+ await el.updateComplete;
500
+
501
+ const getNodes = vi.spyOn(
502
+ el as unknown as { _getTriggerLabelNodes: () => Node[] },
503
+ '_getTriggerLabelNodes'
504
+ );
505
+ el.show();
506
+ await waitForRaf();
507
+
508
+ expect(getNodes).toHaveReturned();
509
+ const returned = getNodes.mock.results[0]?.value as Node[] | undefined;
510
+ expect(returned?.length).toBeGreaterThan(0);
511
+ getNodes.mockRestore();
512
+ });
513
+
514
+ it('no ejecuta el morph de texto legado al cerrar cuando morph-label es true', async () => {
515
+ (el as unknown as { _hasHeader: boolean })._hasHeader = true;
516
+ el.morphLabel = true;
517
+ await el.updateComplete;
518
+
519
+ el.show();
520
+ await waitForRaf();
521
+ expect(el.open).toBe(true);
522
+
523
+ const closeSpy = vi.spyOn(
524
+ el as unknown as { _animateLabelClose: () => void },
525
+ '_animateLabelClose'
526
+ );
527
+ el.hide();
528
+ await waitForRaf();
529
+
530
+ expect(closeSpy).not.toHaveBeenCalled();
531
+ expect(el.shadowRoot?.querySelector('.morph-text, .morph-icon, .morph-target-snapshot')).toBeNull();
532
+ closeSpy.mockRestore();
533
+ });
534
+
535
+ it('no crea elementos morph cuando se prefiere movimiento reducido (reduced motion)', async () => {
536
+ (el as unknown as { _hasHeader: boolean })._hasHeader = true;
537
+ el.morphLabel = true;
538
+ await el.updateComplete;
539
+
540
+ window.matchMedia = vi.fn().mockReturnValue({
541
+ matches: true,
542
+ media: '(prefers-reduced-motion: reduce)',
543
+ onchange: null,
544
+ addListener: vi.fn(),
545
+ removeListener: vi.fn(),
546
+ addEventListener: vi.fn(),
547
+ removeEventListener: vi.fn(),
548
+ dispatchEvent: vi.fn()
549
+ } as unknown as MediaQueryList);
550
+
551
+ const createSpy = vi.spyOn(
552
+ el as unknown as { _createMorphText: (text: string, source: Element) => HTMLElement },
553
+ '_createMorphText'
554
+ );
555
+ el.show();
556
+ await waitForRaf();
557
+
558
+ expect(createSpy).not.toHaveBeenCalled();
559
+ createSpy.mockRestore();
560
+ });
561
+ });