@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.
- package/custom-elements.json +101 -81
- package/dist/browser/moni-ui.iife.min.js +1538 -1384
- package/dist/browser/moni-ui.iife.min.js.br +0 -0
- package/dist/browser/moni-ui.iife.min.js.gz +0 -0
- package/dist/browser/moni-ui.iife.min.js.map +1 -1
- package/dist/browser/moni-ui.min.js +2751 -2501
- package/dist/browser/moni-ui.min.js.br +0 -0
- package/dist/browser/moni-ui.min.js.gz +0 -0
- package/dist/browser/moni-ui.min.js.map +1 -1
- package/dist/cdn/chunks/{class-map-DsL1Jr2T.js → class-map-QyT9W7rZ.js} +1 -1
- package/dist/cdn/chunks/{decorate-DxJYS4gw.js → decorate-B7wZvwrn.js} +629 -543
- package/dist/cdn/chunks/if-defined-5hr_ccFw.js +1 -0
- package/dist/cdn/chunks/{live-D52JqeKf.js → live-Hm1Di_ND.js} +1 -1
- package/dist/cdn/components/moni-app-bar.js +1 -1
- package/dist/cdn/components/moni-badge.js +1 -1
- package/dist/cdn/components/moni-bottom-sheet.js +2 -1
- package/dist/cdn/components/moni-button-group.js +1 -1
- package/dist/cdn/components/moni-button-segment.js +1 -1
- package/dist/cdn/components/moni-button.js +1 -1
- package/dist/cdn/components/moni-card.js +1 -1
- package/dist/cdn/components/moni-carousel.js +1 -1
- package/dist/cdn/components/moni-checkbox.js +1 -1
- package/dist/cdn/components/moni-chip.js +1 -1
- package/dist/cdn/components/moni-color-field.js +1 -1
- package/dist/cdn/components/moni-context-menu.js +1 -1
- package/dist/cdn/components/moni-dialog.js +1 -1
- package/dist/cdn/components/moni-divider.js +1 -1
- package/dist/cdn/components/moni-expansion.js +1 -1
- package/dist/cdn/components/moni-fab-menu.js +1 -1
- package/dist/cdn/components/moni-fab.js +1 -1
- package/dist/cdn/components/moni-file-field.js +1 -1
- package/dist/cdn/components/moni-icon.js +1 -1
- package/dist/cdn/components/moni-list-item.js +1 -1
- package/dist/cdn/components/moni-list.js +1 -1
- package/dist/cdn/components/moni-loading-indicator.js +1 -1
- package/dist/cdn/components/moni-menu-item.js +1 -1
- package/dist/cdn/components/moni-menu.js +1 -1
- package/dist/cdn/components/moni-morph-modal.js +3 -2
- package/dist/cdn/components/moni-nav-item.js +1 -1
- package/dist/cdn/components/moni-nav.js +1 -1
- package/dist/cdn/components/moni-progress.js +1 -1
- package/dist/cdn/components/moni-radio.js +1 -1
- package/dist/cdn/components/moni-ripple.js +1 -1
- package/dist/cdn/components/moni-segmented-button.js +1 -1
- package/dist/cdn/components/moni-select-option.js +1 -1
- package/dist/cdn/components/moni-select.js +512 -473
- package/dist/cdn/components/moni-shape.js +1 -1
- package/dist/cdn/components/moni-side-sheet.js +1 -1
- package/dist/cdn/components/moni-slider.js +1 -1
- package/dist/cdn/components/moni-snackbar.js +1 -1
- package/dist/cdn/components/moni-split-button.js +1 -1
- package/dist/cdn/components/moni-step.js +1 -1
- package/dist/cdn/components/moni-stepper.js +1 -1
- package/dist/cdn/components/moni-switch.js +1 -1
- package/dist/cdn/components/moni-tab.js +1 -1
- package/dist/cdn/components/moni-tabs.js +1 -1
- package/dist/cdn/components/moni-text-field.js +122 -95
- package/dist/cdn/components/moni-textarea.js +52 -52
- package/dist/cdn/components/moni-time-picker.js +1 -1
- package/dist/cdn/components/moni-toolbar.js +1 -1
- package/dist/cdn/components/moni-tooltip.js +1 -1
- package/dist/cdn/components/moni-typography.js +1 -1
- package/dist/cdn/importmap.json +98 -98
- package/dist/components/_base/field-styles.d.ts.map +1 -1
- package/dist/components/_base/field-styles.js +662 -576
- package/dist/components/moni-bottom-sheet.d.ts +19 -5
- package/dist/components/moni-bottom-sheet.d.ts.map +1 -1
- package/dist/components/moni-bottom-sheet.js +111 -30
- package/dist/components/moni-morph-modal.d.ts +10 -0
- package/dist/components/moni-morph-modal.d.ts.map +1 -1
- package/dist/components/moni-morph-modal.js +323 -217
- package/dist/components/moni-select.d.ts +16 -16
- package/dist/components/moni-select.d.ts.map +1 -1
- package/dist/components/moni-select.js +797 -727
- package/dist/components/moni-text-field.d.ts +15 -15
- package/dist/components/moni-text-field.d.ts.map +1 -1
- package/dist/components/moni-text-field.js +224 -180
- package/dist/components/moni-textarea.d.ts +13 -13
- package/dist/components/moni-textarea.d.ts.map +1 -1
- package/dist/components/moni-textarea.js +127 -122
- package/docs/api.json +109 -89
- package/docs/assets/moni-color-field/shape--square.png +0 -0
- package/docs/assets/moni-file-field/shape--square.png +0 -0
- package/docs/assets/moni-morph-modal/autoHeight--false.png +0 -0
- package/docs/assets/moni-morph-modal/autoHeight--true.png +0 -0
- package/docs/components/bottom-sheet.md +2 -1
- package/docs/components/morph-modal.md +16 -7
- package/docs/components/select.md +74 -74
- package/docs/components/text-field.md +75 -66
- package/docs/components/textarea.md +51 -51
- package/docs/llms.txt +3 -3
- package/package.json +1 -1
- package/src/components/_base/field-styles.ts +723 -637
- package/src/components/moni-bottom-sheet.test.ts +331 -0
- package/src/components/moni-bottom-sheet.ts +117 -29
- package/src/components/moni-morph-modal.test.ts +490 -413
- package/src/components/moni-morph-modal.ts +2274 -2162
- package/src/components/moni-select.test.ts +489 -466
- package/src/components/moni-select.ts +1963 -1815
- package/src/components/moni-text-field.test.ts +366 -342
- package/src/components/moni-text-field.ts +717 -645
- package/src/components/moni-textarea.test.ts +201 -197
- package/src/components/moni-textarea.ts +476 -460
- 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('
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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('
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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('
|
|
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
|
-
|
|
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(
|
|
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('
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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('
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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(
|
|
454
|
-
|
|
455
|
-
|
|
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.
|
|
479
|
-
|
|
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
|
+
});
|