@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,466 +1,489 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import './moni-select.js';
3
- import './moni-select-option.js';
4
- import type { MoniSelect } from './moni-select.js';
5
-
6
- describe('moni-select', () => {
7
- let el: MoniSelect;
8
-
9
- beforeEach(() => {
10
- el = document.createElement('moni-select') as MoniSelect;
11
- document.body.appendChild(el);
12
- });
13
-
14
- afterEach(() => {
15
- el.remove();
16
- vi.restoreAllMocks();
17
- });
18
-
19
- it('renderiza el input activador (trigger)', async () => {
20
- await el.updateComplete;
21
- const input = el.shadowRoot?.querySelector('input');
22
- expect(input).toBeTruthy();
23
- expect(input?.getAttribute('readonly')).toBe('');
24
- });
25
-
26
- it('aplica la clase small-round para un redondeado compacto', async () => {
27
- el.shape = 'small-round';
28
- await el.updateComplete;
29
- const field = el.shadowRoot?.querySelector('.field');
30
- expect(field?.classList.contains('small-round')).toBe(true);
31
- expect(field?.classList.contains('square')).toBe(false);
32
- });
33
-
34
- it('analiza las opciones insertadas en slots (slotted)', async () => {
35
- const opt1 = document.createElement('moni-select-option');
36
- opt1.setAttribute('value', 'v1');
37
- opt1.textContent = 'Option 1';
38
- const opt2 = document.createElement('option');
39
- opt2.setAttribute('value', 'v2');
40
- opt2.textContent = 'Option 2';
41
-
42
- el.appendChild(opt1);
43
- el.appendChild(opt2);
44
-
45
- // Esperar a slotchange y actualización
46
- await new Promise(resolve => setTimeout(resolve, 50));
47
- await el.updateComplete;
48
-
49
- const items = el.shadowRoot?.querySelectorAll('.option-item');
50
- expect(items?.length).toBe(2);
51
- expect(items?.[0].textContent?.trim()).toBe('Option 1');
52
- expect(items?.[1].textContent?.trim()).toBe('Option 2');
53
- });
54
-
55
- it('alterna abrir/cerrar el dropdown al hacer clic', async () => {
56
- await el.updateComplete;
57
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
58
- const menu = el.shadowRoot?.querySelector('.dropdown-menu');
59
-
60
- expect(menu?.classList.contains('open')).toBe(false);
61
-
62
- input.click();
63
- await el.updateComplete;
64
- expect(menu?.classList.contains('open')).toBe(true);
65
-
66
- input.click();
67
- await el.updateComplete;
68
- expect(menu?.classList.contains('open')).toBe(false);
69
- });
70
-
71
- it('dispara el evento input/change y actualiza el valor al seleccionar', async () => {
72
- const opt1 = document.createElement('moni-select-option');
73
- opt1.setAttribute('value', 'v1');
74
- opt1.textContent = 'Option 1';
75
- el.appendChild(opt1);
76
-
77
- await new Promise(resolve => setTimeout(resolve, 50));
78
- await el.updateComplete;
79
-
80
- let eventFired = false;
81
- el.addEventListener('change', () => {
82
- eventFired = true;
83
- });
84
-
85
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
86
- input.click();
87
- await el.updateComplete;
88
-
89
- const item = el.shadowRoot?.querySelector('.option-item') as HTMLElement;
90
- item.click();
91
- await el.updateComplete;
92
-
93
- expect(el.value).toBe('v1');
94
- expect(eventFired).toBe(true);
95
- });
96
-
97
- it('filtra opciones al buscar y escribir (searchable)', async () => {
98
- el.searchable = true;
99
-
100
- const opt1 = document.createElement('moni-select-option');
101
- opt1.setAttribute('value', 'apple');
102
- opt1.textContent = 'Apple';
103
- const opt2 = document.createElement('moni-select-option');
104
- opt2.setAttribute('value', 'banana');
105
- opt2.textContent = 'Banana';
106
-
107
- el.appendChild(opt1);
108
- el.appendChild(opt2);
109
-
110
- await new Promise(resolve => setTimeout(resolve, 50));
111
- await el.updateComplete;
112
-
113
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
114
- input.click();
115
- await el.updateComplete;
116
-
117
- input.value = 'ap';
118
- input.dispatchEvent(new Event('input'));
119
- await el.updateComplete;
120
-
121
- const items = el.shadowRoot?.querySelectorAll('.option-item');
122
- expect(items?.length).toBe(1);
123
- expect(items?.[0].textContent?.trim()).toBe('Apple');
124
- });
125
-
126
- it('autoselecciona la opción cuando se escribe una coincidencia exacta', async () => {
127
- el.searchable = true;
128
-
129
- const opt1 = document.createElement('moni-select-option');
130
- opt1.setAttribute('value', 'apple');
131
- opt1.textContent = 'Apple';
132
- const opt2 = document.createElement('moni-select-option');
133
- opt2.setAttribute('value', 'banana');
134
- opt2.textContent = 'Banana';
135
-
136
- el.appendChild(opt1);
137
- el.appendChild(opt2);
138
-
139
- await new Promise(resolve => setTimeout(resolve, 50));
140
- await el.updateComplete;
141
-
142
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
143
- input.click();
144
- await el.updateComplete;
145
-
146
- input.value = 'banana';
147
- input.dispatchEvent(new Event('input'));
148
- await el.updateComplete;
149
-
150
- expect(el.value).toBe('banana');
151
- });
152
-
153
- it('renderiza encabezados de subcategoría de grupo', async () => {
154
- const opt1 = document.createElement('moni-select-option');
155
- opt1.setAttribute('value', 'v1');
156
- opt1.setAttribute('group', 'Fruits');
157
- opt1.textContent = 'Apple';
158
-
159
- const opt2 = document.createElement('moni-select-option');
160
- opt2.setAttribute('value', 'v2');
161
- opt2.setAttribute('group', 'Vegetables');
162
- opt2.textContent = 'Carrot';
163
-
164
- el.appendChild(opt1);
165
- el.appendChild(opt2);
166
-
167
- await new Promise(resolve => setTimeout(resolve, 50));
168
- await el.updateComplete;
169
-
170
- const headers = el.shadowRoot?.querySelectorAll('.group-header');
171
- expect(headers?.length).toBe(2);
172
- expect(headers?.[0].querySelector('span')?.textContent?.trim()).toBe('Fruits');
173
- expect(headers?.[1].querySelector('span')?.textContent?.trim()).toBe('Vegetables');
174
- });
175
-
176
- it('mantiene flyouts anidados cuando todos los niveles caben en pantalla', async () => {
177
- vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(900);
178
- vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1600);
179
- vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
180
- top: 80,
181
- bottom: 136,
182
- left: 100,
183
- right: 300,
184
- width: 200,
185
- height: 56,
186
- x: 100,
187
- y: 80,
188
- toJSON: () => ({})
189
- } as DOMRect);
190
-
191
- const option = document.createElement('moni-select-option');
192
- option.setAttribute('value', 'nested');
193
- option.setAttribute('group', 'Nivel 1/Nivel 2');
194
- option.textContent = 'Opción anidada';
195
- el.appendChild(option);
196
-
197
- await new Promise(resolve => setTimeout(resolve, 50));
198
- await el.updateComplete;
199
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
200
- input.click();
201
- await el.updateComplete;
202
-
203
- const menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
204
- expect(menu.classList.contains('inline-categories')).toBe(false);
205
- expect(menu.querySelectorAll('.submenu').length).toBe(2);
206
- expect(menu.querySelector('.submenu .submenu')).toBeTruthy();
207
- });
208
-
209
- it('convierte subcategorías profundas en un drilldown navegable sin recortarlas', async () => {
210
- vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
211
- vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(900);
212
- vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
213
- top: 80,
214
- bottom: 136,
215
- left: 350,
216
- right: 550,
217
- width: 200,
218
- height: 56,
219
- x: 350,
220
- y: 80,
221
- toJSON: () => ({})
222
- } as DOMRect);
223
-
224
- const option = document.createElement('moni-select-option');
225
- option.setAttribute('value', 'deep');
226
- option.setAttribute('group', 'Nivel 1/Nivel 2/Nivel 3/Nivel 4/Nivel 5');
227
- option.textContent = 'Opción profunda';
228
- el.appendChild(option);
229
-
230
- await new Promise(resolve => setTimeout(resolve, 50));
231
- await el.updateComplete;
232
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
233
- input.click();
234
- await el.updateComplete;
235
-
236
- const menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
237
- expect(menu.classList.contains('inline-categories')).toBe(true);
238
- expect(menu.classList.contains('scrollable')).toBe(true);
239
- expect(menu.querySelector('.drilldown-wrapper')).toBeTruthy();
240
- expect(menu.querySelector('.submenu')).toBeNull();
241
-
242
- const firstGroup = menu.querySelector('.group-header') as HTMLElement;
243
- firstGroup.click();
244
- await el.updateComplete;
245
- expect(menu.textContent).toContain('Regresar');
246
- expect(menu.textContent).toContain('Nivel 2');
247
- });
248
-
249
- it('limpia la selección cuando el input se vacía y clearable es true', async () => {
250
- el.searchable = true;
251
- el.clearable = true;
252
- el.value = 'apple';
253
-
254
- const opt1 = document.createElement('moni-select-option');
255
- opt1.setAttribute('value', 'apple');
256
- opt1.textContent = 'Apple';
257
- el.appendChild(opt1);
258
-
259
- await new Promise(resolve => setTimeout(resolve, 50));
260
- await el.updateComplete;
261
-
262
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
263
- input.click();
264
- await el.updateComplete;
265
-
266
- input.value = '';
267
- input.dispatchEvent(new Event('input'));
268
- await el.updateComplete;
269
-
270
- expect(el.value).toBe('');
271
- });
272
-
273
- it('renderiza el cajón bottom sheet cuando sheet es true', async () => {
274
- el.sheet = true;
275
- await el.updateComplete;
276
-
277
- const drawer = el.shadowRoot?.querySelector('.sheet-drawer');
278
- expect(drawer).toBeTruthy();
279
- });
280
-
281
- it('aplica estilos de posicionamiento fijo cuando positioning es fixed', async () => {
282
- el.positioning = 'fixed';
283
- await el.updateComplete;
284
-
285
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
286
- input.click();
287
- await el.updateComplete;
288
-
289
- const menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
290
- expect(menu?.style.position).toBe('fixed');
291
- });
292
-
293
- it('cierra cualquier otro select antes de abrir uno nuevo', async () => {
294
- const other = document.createElement('moni-select') as MoniSelect;
295
- document.body.appendChild(other);
296
-
297
- try {
298
- await Promise.all([el.updateComplete, other.updateComplete]);
299
- const firstInput = el.shadowRoot?.querySelector('input') as HTMLInputElement;
300
- const secondInput = other.shadowRoot?.querySelector('input') as HTMLInputElement;
301
-
302
- firstInput.click();
303
- await el.updateComplete;
304
- expect(el.shadowRoot?.querySelector('.dropdown-menu')?.classList.contains('open')).toBe(true);
305
-
306
- secondInput.click();
307
- await Promise.all([el.updateComplete, other.updateComplete]);
308
- expect(el.shadowRoot?.querySelector('.dropdown-menu')?.classList.contains('open')).toBe(false);
309
- expect(other.shadowRoot?.querySelector('.dropdown-menu')?.classList.contains('open')).toBe(true);
310
- } finally {
311
- other.remove();
312
- }
313
- });
314
-
315
- it('reinicia la búsqueda al abrir aunque exista una opción seleccionada', async () => {
316
- el.searchable = true;
317
- el.value = 'banana';
318
-
319
- for (const [value, label] of [['apple', 'Apple'], ['banana', 'Banana']]) {
320
- const option = document.createElement('moni-select-option');
321
- option.setAttribute('value', value);
322
- option.textContent = label;
323
- el.appendChild(option);
324
- }
325
-
326
- await new Promise(resolve => setTimeout(resolve, 50));
327
- await el.updateComplete;
328
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
329
- expect(input.value).toBe('Banana');
330
-
331
- input.click();
332
- await el.updateComplete;
333
- expect(input.value).toBe('');
334
- expect(el.shadowRoot?.querySelectorAll('.option-item').length).toBe(2);
335
- });
336
-
337
- it('limita la altura según el viewport, permite un máximo manual y habilita scroll', async () => {
338
- vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
339
- vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200);
340
- vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
341
- top: 100,
342
- bottom: 156,
343
- left: 100,
344
- right: 300,
345
- width: 200,
346
- height: 56,
347
- x: 100,
348
- y: 100,
349
- toJSON: () => ({})
350
- } as DOMRect);
351
-
352
- for (let index = 1; index <= 30; index += 1) {
353
- const option = document.createElement('moni-select-option');
354
- option.setAttribute('value', String(index));
355
- option.textContent = `Option ${index}`;
356
- el.appendChild(option);
357
- }
358
-
359
- await new Promise(resolve => setTimeout(resolve, 50));
360
- await el.updateComplete;
361
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
362
- input.click();
363
- await el.updateComplete;
364
-
365
- let menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
366
- expect(menu.style.maxHeight).toBe('436px');
367
- expect(menu.classList.contains('scrollable')).toBe(true);
368
-
369
- input.click();
370
- el.dropdownMaxHeight = '120px';
371
- await el.updateComplete;
372
- input.click();
373
- await el.updateComplete;
374
- menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
375
- expect(menu.style.maxHeight).toContain('120px');
376
- });
377
-
378
- it('cambia hacia arriba y recalcula el alto cuando no hay espacio debajo', async () => {
379
- vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
380
- vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200);
381
- vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
382
- top: 650,
383
- bottom: 706,
384
- left: 100,
385
- right: 300,
386
- width: 200,
387
- height: 56,
388
- x: 100,
389
- y: 650,
390
- toJSON: () => ({})
391
- } as DOMRect);
392
-
393
- await el.updateComplete;
394
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
395
- input.click();
396
- await el.updateComplete;
397
-
398
- const menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
399
- expect(menu.classList.contains('placement-top')).toBe(true);
400
- expect(menu.style.maxHeight).toBe('442px');
401
- });
402
-
403
- it('usa la capa superior cuando positioning=body', async () => {
404
- el.positioning = 'body';
405
- await el.updateComplete;
406
-
407
- const menu = el.shadowRoot?.querySelector('.dropdown-menu') as HTMLElement;
408
- expect(menu.getAttribute('popover')).toBe('manual');
409
-
410
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
411
- input.click();
412
- await el.updateComplete;
413
- expect(menu.style.position).toBe('fixed');
414
- expect(menu.classList.contains('open')).toBe(true);
415
- });
416
- });
417
-
418
- describe('moni-select · nombre accesible', () => {
419
- it('asocia la etiqueta con el input mediante for/id', async () => {
420
- const el = document.createElement('moni-select') as MoniSelect;
421
- el.label = 'Estado';
422
- document.body.appendChild(el);
423
- await el.updateComplete;
424
-
425
- const input = el.shadowRoot?.querySelector('input');
426
- const label = el.shadowRoot?.querySelector('label');
427
- expect(input?.id).toBe('input');
428
- expect(label?.getAttribute('for')).toBe('input');
429
- expect(input?.nextElementSibling).toBe(label);
430
- el.remove();
431
- });
432
- })
433
-
434
- describe('moni-select · required', () => {
435
- /*
436
- * Su input visible es readonly y por tanto queda fuera de la validación de
437
- * restricciones, así que `required` se declara en el host.
438
- */
439
- it('declara valueMissing mientras no haya valor', async () => {
440
- // Igual que en text-field: jsdom no expone checkValidity() aquí.
441
- const calls: Array<Record<string, unknown>> = [];
442
- const originalAttach = HTMLElement.prototype.attachInternals;
443
- HTMLElement.prototype.attachInternals = function spy(this: HTMLElement) {
444
- const internals = originalAttach.call(this);
445
- const setValidity = internals.setValidity.bind(internals);
446
- internals.setValidity = ((flags, message, anchor) => {
447
- calls.push({ valueMissing: flags?.valueMissing === true });
448
- return setValidity(flags, message, anchor);
449
- }) as typeof internals.setValidity;
450
- return internals;
451
- };
452
- const el = document.createElement('moni-select') as MoniSelect;
453
- HTMLElement.prototype.attachInternals = originalAttach;
454
-
455
- el.required = true;
456
- document.body.appendChild(el);
457
- await el.updateComplete;
458
- expect(calls.some((c) => c.valueMissing === true)).toBe(true);
459
-
460
- calls.length = 0;
461
- el.value = 'uno';
462
- await el.updateComplete;
463
- expect(calls.at(-1)).toEqual({ valueMissing: false });
464
- el.remove();
465
- });
466
- })
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import "./moni-select.js";
3
+ import "./moni-select-option.js";
4
+ import type { MoniSelect } from "./moni-select.js";
5
+
6
+ describe("moni-select", () => {
7
+ let el: MoniSelect;
8
+
9
+ beforeEach(() => {
10
+ el = document.createElement("moni-select") as MoniSelect;
11
+ document.body.appendChild(el);
12
+ });
13
+
14
+ afterEach(() => {
15
+ el.remove();
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ it("renderiza el input activador (trigger)", async () => {
20
+ await el.updateComplete;
21
+ const input = el.shadowRoot?.querySelector("input");
22
+ expect(input).toBeTruthy();
23
+ expect(input?.getAttribute("readonly")).toBe("");
24
+ });
25
+
26
+ it("aplica la clase small-round para un redondeado compacto", async () => {
27
+ el.shape = "small-round";
28
+ await el.updateComplete;
29
+ const field = el.shadowRoot?.querySelector(".field");
30
+ expect(field?.classList.contains("small-round")).toBe(true);
31
+ expect(field?.classList.contains("square")).toBe(false);
32
+ });
33
+
34
+ it("analiza las opciones insertadas en slots (slotted)", async () => {
35
+ const opt1 = document.createElement("moni-select-option");
36
+ opt1.setAttribute("value", "v1");
37
+ opt1.textContent = "Option 1";
38
+ const opt2 = document.createElement("option");
39
+ opt2.setAttribute("value", "v2");
40
+ opt2.textContent = "Option 2";
41
+
42
+ el.appendChild(opt1);
43
+ el.appendChild(opt2);
44
+
45
+ // Esperar a slotchange y actualización
46
+ await new Promise((resolve) => setTimeout(resolve, 50));
47
+ await el.updateComplete;
48
+
49
+ const items = el.shadowRoot?.querySelectorAll(".option-item");
50
+ expect(items?.length).toBe(2);
51
+ expect(items?.[0].textContent?.trim()).toBe("Option 1");
52
+ expect(items?.[1].textContent?.trim()).toBe("Option 2");
53
+ });
54
+
55
+ it("alterna abrir/cerrar el dropdown al hacer clic", async () => {
56
+ await el.updateComplete;
57
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
58
+ const menu = el.shadowRoot?.querySelector(".dropdown-menu");
59
+
60
+ expect(menu?.classList.contains("open")).toBe(false);
61
+
62
+ input.click();
63
+ await el.updateComplete;
64
+ expect(menu?.classList.contains("open")).toBe(true);
65
+
66
+ input.click();
67
+ await el.updateComplete;
68
+ expect(menu?.classList.contains("open")).toBe(false);
69
+ });
70
+
71
+ it("dispara el evento input/change y actualiza el valor al seleccionar", async () => {
72
+ const opt1 = document.createElement("moni-select-option");
73
+ opt1.setAttribute("value", "v1");
74
+ opt1.textContent = "Option 1";
75
+ el.appendChild(opt1);
76
+
77
+ await new Promise((resolve) => setTimeout(resolve, 50));
78
+ await el.updateComplete;
79
+
80
+ let eventFired = false;
81
+ el.addEventListener("change", () => {
82
+ eventFired = true;
83
+ });
84
+
85
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
86
+ input.click();
87
+ await el.updateComplete;
88
+
89
+ const item = el.shadowRoot?.querySelector(".option-item") as HTMLElement;
90
+ item.click();
91
+ await el.updateComplete;
92
+
93
+ expect(el.value).toBe("v1");
94
+ expect(eventFired).toBe(true);
95
+ });
96
+
97
+ it("filtra opciones al buscar y escribir (searchable)", async () => {
98
+ el.searchable = true;
99
+
100
+ const opt1 = document.createElement("moni-select-option");
101
+ opt1.setAttribute("value", "apple");
102
+ opt1.textContent = "Apple";
103
+ const opt2 = document.createElement("moni-select-option");
104
+ opt2.setAttribute("value", "banana");
105
+ opt2.textContent = "Banana";
106
+
107
+ el.appendChild(opt1);
108
+ el.appendChild(opt2);
109
+
110
+ await new Promise((resolve) => setTimeout(resolve, 50));
111
+ await el.updateComplete;
112
+
113
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
114
+ input.click();
115
+ await el.updateComplete;
116
+
117
+ input.value = "ap";
118
+ input.dispatchEvent(new Event("input"));
119
+ await el.updateComplete;
120
+
121
+ const items = el.shadowRoot?.querySelectorAll(".option-item");
122
+ expect(items?.length).toBe(1);
123
+ expect(items?.[0].textContent?.trim()).toBe("Apple");
124
+ });
125
+
126
+ it("autoselecciona la opción cuando se escribe una coincidencia exacta", async () => {
127
+ el.searchable = true;
128
+
129
+ const opt1 = document.createElement("moni-select-option");
130
+ opt1.setAttribute("value", "apple");
131
+ opt1.textContent = "Apple";
132
+ const opt2 = document.createElement("moni-select-option");
133
+ opt2.setAttribute("value", "banana");
134
+ opt2.textContent = "Banana";
135
+
136
+ el.appendChild(opt1);
137
+ el.appendChild(opt2);
138
+
139
+ await new Promise((resolve) => setTimeout(resolve, 50));
140
+ await el.updateComplete;
141
+
142
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
143
+ input.click();
144
+ await el.updateComplete;
145
+
146
+ input.value = "banana";
147
+ input.dispatchEvent(new Event("input"));
148
+ await el.updateComplete;
149
+
150
+ expect(el.value).toBe("banana");
151
+ });
152
+
153
+ it("renderiza encabezados de subcategoría de grupo", async () => {
154
+ const opt1 = document.createElement("moni-select-option");
155
+ opt1.setAttribute("value", "v1");
156
+ opt1.setAttribute("group", "Fruits");
157
+ opt1.textContent = "Apple";
158
+
159
+ const opt2 = document.createElement("moni-select-option");
160
+ opt2.setAttribute("value", "v2");
161
+ opt2.setAttribute("group", "Vegetables");
162
+ opt2.textContent = "Carrot";
163
+
164
+ el.appendChild(opt1);
165
+ el.appendChild(opt2);
166
+
167
+ await new Promise((resolve) => setTimeout(resolve, 50));
168
+ await el.updateComplete;
169
+
170
+ const headers = el.shadowRoot?.querySelectorAll(".group-header");
171
+ expect(headers?.length).toBe(2);
172
+ expect(headers?.[0].querySelector("span")?.textContent?.trim()).toBe(
173
+ "Fruits",
174
+ );
175
+ expect(headers?.[1].querySelector("span")?.textContent?.trim()).toBe(
176
+ "Vegetables",
177
+ );
178
+ });
179
+
180
+ it("mantiene flyouts anidados cuando todos los niveles caben en pantalla", async () => {
181
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(900);
182
+ vi.spyOn(window, "innerWidth", "get").mockReturnValue(1600);
183
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
184
+ top: 80,
185
+ bottom: 136,
186
+ left: 100,
187
+ right: 300,
188
+ width: 200,
189
+ height: 56,
190
+ x: 100,
191
+ y: 80,
192
+ toJSON: () => ({}),
193
+ } as DOMRect);
194
+
195
+ const option = document.createElement("moni-select-option");
196
+ option.setAttribute("value", "nested");
197
+ option.setAttribute("group", "Nivel 1/Nivel 2");
198
+ option.textContent = "Opción anidada";
199
+ el.appendChild(option);
200
+
201
+ await new Promise((resolve) => setTimeout(resolve, 50));
202
+ await el.updateComplete;
203
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
204
+ input.click();
205
+ await el.updateComplete;
206
+
207
+ const menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
208
+ expect(menu.classList.contains("inline-categories")).toBe(false);
209
+ expect(menu.querySelectorAll(".submenu").length).toBe(2);
210
+ expect(menu.querySelector(".submenu .submenu")).toBeTruthy();
211
+ });
212
+
213
+ it("convierte subcategorías profundas en un drilldown navegable sin recortarlas", async () => {
214
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(800);
215
+ vi.spyOn(window, "innerWidth", "get").mockReturnValue(900);
216
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
217
+ top: 80,
218
+ bottom: 136,
219
+ left: 350,
220
+ right: 550,
221
+ width: 200,
222
+ height: 56,
223
+ x: 350,
224
+ y: 80,
225
+ toJSON: () => ({}),
226
+ } as DOMRect);
227
+
228
+ const option = document.createElement("moni-select-option");
229
+ option.setAttribute("value", "deep");
230
+ option.setAttribute("group", "Nivel 1/Nivel 2/Nivel 3/Nivel 4/Nivel 5");
231
+ option.textContent = "Opción profunda";
232
+ el.appendChild(option);
233
+
234
+ await new Promise((resolve) => setTimeout(resolve, 50));
235
+ await el.updateComplete;
236
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
237
+ input.click();
238
+ await el.updateComplete;
239
+
240
+ const menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
241
+ expect(menu.classList.contains("inline-categories")).toBe(true);
242
+ expect(menu.classList.contains("scrollable")).toBe(true);
243
+ expect(menu.querySelector(".drilldown-wrapper")).toBeTruthy();
244
+ expect(menu.querySelector(".submenu")).toBeNull();
245
+
246
+ const firstGroup = menu.querySelector(".group-header") as HTMLElement;
247
+ firstGroup.click();
248
+ await el.updateComplete;
249
+ expect(menu.textContent).toContain("Regresar");
250
+ expect(menu.textContent).toContain("Nivel 2");
251
+ });
252
+
253
+ it("limpia la selección cuando el input se vacía y clearable es true", async () => {
254
+ el.searchable = true;
255
+ el.clearable = true;
256
+ el.value = "apple";
257
+
258
+ const opt1 = document.createElement("moni-select-option");
259
+ opt1.setAttribute("value", "apple");
260
+ opt1.textContent = "Apple";
261
+ el.appendChild(opt1);
262
+
263
+ await new Promise((resolve) => setTimeout(resolve, 50));
264
+ await el.updateComplete;
265
+
266
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
267
+ input.click();
268
+ await el.updateComplete;
269
+
270
+ input.value = "";
271
+ input.dispatchEvent(new Event("input"));
272
+ await el.updateComplete;
273
+
274
+ expect(el.value).toBe("");
275
+ });
276
+
277
+ it("renderiza el cajón bottom sheet cuando sheet es true", async () => {
278
+ el.sheet = true;
279
+ await el.updateComplete;
280
+
281
+ const drawer = el.shadowRoot?.querySelector(".sheet-drawer");
282
+ expect(drawer).toBeTruthy();
283
+ });
284
+
285
+ it("aplica estilos de posicionamiento fijo cuando positioning es fixed", async () => {
286
+ el.positioning = "fixed";
287
+ await el.updateComplete;
288
+
289
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
290
+ input.click();
291
+ await el.updateComplete;
292
+
293
+ const menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
294
+ expect(menu?.style.position).toBe("fixed");
295
+ });
296
+
297
+ it("cierra cualquier otro select antes de abrir uno nuevo", async () => {
298
+ const other = document.createElement("moni-select") as MoniSelect;
299
+ document.body.appendChild(other);
300
+
301
+ try {
302
+ await Promise.all([el.updateComplete, other.updateComplete]);
303
+ const firstInput = el.shadowRoot?.querySelector(
304
+ "input",
305
+ ) as HTMLInputElement;
306
+ const secondInput = other.shadowRoot?.querySelector(
307
+ "input",
308
+ ) as HTMLInputElement;
309
+
310
+ firstInput.click();
311
+ await el.updateComplete;
312
+ expect(
313
+ el.shadowRoot
314
+ ?.querySelector(".dropdown-menu")
315
+ ?.classList.contains("open"),
316
+ ).toBe(true);
317
+
318
+ secondInput.click();
319
+ await Promise.all([el.updateComplete, other.updateComplete]);
320
+ expect(
321
+ el.shadowRoot
322
+ ?.querySelector(".dropdown-menu")
323
+ ?.classList.contains("open"),
324
+ ).toBe(false);
325
+ expect(
326
+ other.shadowRoot
327
+ ?.querySelector(".dropdown-menu")
328
+ ?.classList.contains("open"),
329
+ ).toBe(true);
330
+ } finally {
331
+ other.remove();
332
+ }
333
+ });
334
+
335
+ it("reinicia la búsqueda al abrir aunque exista una opción seleccionada", async () => {
336
+ el.searchable = true;
337
+ el.value = "banana";
338
+
339
+ for (const [value, label] of [
340
+ ["apple", "Apple"],
341
+ ["banana", "Banana"],
342
+ ]) {
343
+ const option = document.createElement("moni-select-option");
344
+ option.setAttribute("value", value);
345
+ option.textContent = label;
346
+ el.appendChild(option);
347
+ }
348
+
349
+ await new Promise((resolve) => setTimeout(resolve, 50));
350
+ await el.updateComplete;
351
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
352
+ expect(input.value).toBe("Banana");
353
+
354
+ input.click();
355
+ await el.updateComplete;
356
+ expect(input.value).toBe("");
357
+ expect(el.shadowRoot?.querySelectorAll(".option-item").length).toBe(2);
358
+ });
359
+
360
+ it("limita la altura según el viewport, permite un máximo manual y habilita scroll", async () => {
361
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(800);
362
+ vi.spyOn(window, "innerWidth", "get").mockReturnValue(1200);
363
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
364
+ top: 100,
365
+ bottom: 156,
366
+ left: 100,
367
+ right: 300,
368
+ width: 200,
369
+ height: 56,
370
+ x: 100,
371
+ y: 100,
372
+ toJSON: () => ({}),
373
+ } as DOMRect);
374
+
375
+ for (let index = 1; index <= 30; index += 1) {
376
+ const option = document.createElement("moni-select-option");
377
+ option.setAttribute("value", String(index));
378
+ option.textContent = `Option ${index}`;
379
+ el.appendChild(option);
380
+ }
381
+
382
+ await new Promise((resolve) => setTimeout(resolve, 50));
383
+ await el.updateComplete;
384
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
385
+ input.click();
386
+ await el.updateComplete;
387
+
388
+ let menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
389
+ expect(menu.style.maxHeight).toBe("436px");
390
+ expect(menu.classList.contains("scrollable")).toBe(true);
391
+
392
+ input.click();
393
+ el.dropdownMaxHeight = "120px";
394
+ await el.updateComplete;
395
+ input.click();
396
+ await el.updateComplete;
397
+ menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
398
+ expect(menu.style.maxHeight).toContain("120px");
399
+ });
400
+
401
+ it("cambia hacia arriba y recalcula el alto cuando no hay espacio debajo", async () => {
402
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(800);
403
+ vi.spyOn(window, "innerWidth", "get").mockReturnValue(1200);
404
+ vi.spyOn(el, "getBoundingClientRect").mockReturnValue({
405
+ top: 650,
406
+ bottom: 706,
407
+ left: 100,
408
+ right: 300,
409
+ width: 200,
410
+ height: 56,
411
+ x: 100,
412
+ y: 650,
413
+ toJSON: () => ({}),
414
+ } as DOMRect);
415
+
416
+ await el.updateComplete;
417
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
418
+ input.click();
419
+ await el.updateComplete;
420
+
421
+ const menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
422
+ expect(menu.classList.contains("placement-top")).toBe(true);
423
+ expect(menu.style.maxHeight).toBe("442px");
424
+ });
425
+
426
+ it("usa la capa superior cuando positioning=body", async () => {
427
+ el.positioning = "body";
428
+ await el.updateComplete;
429
+
430
+ const menu = el.shadowRoot?.querySelector(".dropdown-menu") as HTMLElement;
431
+ expect(menu.getAttribute("popover")).toBe("manual");
432
+
433
+ const input = el.shadowRoot?.querySelector("input") as HTMLInputElement;
434
+ input.click();
435
+ await el.updateComplete;
436
+ expect(menu.style.position).toBe("fixed");
437
+ expect(menu.classList.contains("open")).toBe(true);
438
+ });
439
+ });
440
+
441
+ describe("moni-select · nombre accesible", () => {
442
+ it("asocia la etiqueta con el input mediante for/id", async () => {
443
+ const el = document.createElement("moni-select") as MoniSelect;
444
+ el.label = "Estado";
445
+ document.body.appendChild(el);
446
+ await el.updateComplete;
447
+
448
+ const input = el.shadowRoot?.querySelector("input");
449
+ const label = el.shadowRoot?.querySelector("label");
450
+ expect(input?.id).toBe("input");
451
+ expect(label?.getAttribute("for")).toBe("input");
452
+ expect(input?.nextElementSibling).toBe(label);
453
+ el.remove();
454
+ });
455
+ });
456
+
457
+ describe("moni-select · required", () => {
458
+ /*
459
+ * Su input visible es readonly y por tanto queda fuera de la validación de
460
+ * restricciones, así que `required` se declara en el host.
461
+ */
462
+ it("declara valueMissing mientras no haya valor", async () => {
463
+ // Igual que en text-field: jsdom no expone checkValidity() aquí.
464
+ const calls: Array<Record<string, unknown>> = [];
465
+ const originalAttach = HTMLElement.prototype.attachInternals;
466
+ HTMLElement.prototype.attachInternals = function spy(this: HTMLElement) {
467
+ const internals = originalAttach.call(this);
468
+ const setValidity = internals.setValidity.bind(internals);
469
+ internals.setValidity = ((flags, message, anchor) => {
470
+ calls.push({ valueMissing: flags?.valueMissing === true });
471
+ return setValidity(flags, message, anchor);
472
+ }) as typeof internals.setValidity;
473
+ return internals;
474
+ };
475
+ const el = document.createElement("moni-select") as MoniSelect;
476
+ HTMLElement.prototype.attachInternals = originalAttach;
477
+
478
+ el.required = true;
479
+ document.body.appendChild(el);
480
+ await el.updateComplete;
481
+ expect(calls.some((c) => c.valueMissing === true)).toBe(true);
482
+
483
+ calls.length = 0;
484
+ el.value = "uno";
485
+ await el.updateComplete;
486
+ expect(calls.at(-1)).toEqual({ valueMissing: false });
487
+ el.remove();
488
+ });
489
+ });