mtrl 0.3.1 → 0.3.3

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 (162) hide show
  1. package/.env +15 -0
  2. package/CONTRIBUTING.md +62 -23
  3. package/DOCS.md +3 -3
  4. package/README.md +43 -20
  5. package/TESTING.md +128 -18
  6. package/dist/index.js +14865 -0
  7. package/git-user-stats.js +545 -0
  8. package/index.ts +9 -67
  9. package/package.json +8 -3
  10. package/src/components/badge/api.ts +15 -1
  11. package/src/components/badge/badge.ts +43 -4
  12. package/src/components/badge/config.ts +40 -8
  13. package/src/components/badge/index.ts +64 -3
  14. package/src/components/badge/types.ts +175 -33
  15. package/src/components/button/api.ts +63 -1
  16. package/src/components/button/button.ts +39 -3
  17. package/src/components/button/config.ts +21 -4
  18. package/src/components/button/index.ts +26 -1
  19. package/src/components/button/types.ts +7 -1
  20. package/src/components/card/api.ts +78 -9
  21. package/src/components/card/card.ts +58 -3
  22. package/src/components/card/config.ts +41 -11
  23. package/src/components/card/features.ts +39 -12
  24. package/src/components/card/index.ts +84 -19
  25. package/src/components/card/types.ts +218 -29
  26. package/src/components/carousel/carousel.ts +92 -28
  27. package/src/components/carousel/constants.ts +107 -21
  28. package/src/components/carousel/index.ts +31 -13
  29. package/src/components/checkbox/checkbox.ts +83 -16
  30. package/src/components/checkbox/index.ts +43 -1
  31. package/src/components/checkbox/types.ts +219 -32
  32. package/src/components/chips/api.ts +194 -0
  33. package/src/components/{chip → chips/chip}/api.ts +42 -2
  34. package/src/components/chips/chip/chip.ts +131 -0
  35. package/src/components/{chip → chips/chip}/config.ts +3 -3
  36. package/src/components/chips/chip/index.ts +3 -0
  37. package/src/components/chips/chips.md +481 -0
  38. package/src/components/chips/chips.ts +75 -0
  39. package/src/components/chips/config.ts +109 -0
  40. package/src/components/chips/constants.ts +61 -0
  41. package/src/components/chips/features/chip-items.ts +33 -0
  42. package/src/components/chips/features/container.ts +77 -0
  43. package/src/components/chips/features/controller.ts +448 -0
  44. package/src/components/chips/features/index.ts +5 -0
  45. package/src/components/chips/features/label.ts +108 -0
  46. package/src/components/chips/index.ts +11 -0
  47. package/src/components/chips/schema.ts +61 -0
  48. package/src/components/{chip → chips}/types.ts +203 -92
  49. package/src/components/dialog/dialog.ts +99 -16
  50. package/src/components/dialog/index.ts +97 -1
  51. package/src/components/dialog/types.ts +375 -69
  52. package/src/components/divider/config.ts +90 -6
  53. package/src/components/divider/divider.ts +32 -2
  54. package/src/components/divider/features.ts +26 -0
  55. package/src/components/divider/index.ts +30 -0
  56. package/src/components/divider/types.ts +86 -9
  57. package/src/components/extended-fab/api.ts +53 -1
  58. package/src/components/extended-fab/config.ts +29 -1
  59. package/src/components/extended-fab/extended-fab.ts +28 -0
  60. package/src/components/extended-fab/index.ts +36 -0
  61. package/src/components/extended-fab/types.ts +458 -13
  62. package/src/components/fab/api.ts +42 -2
  63. package/src/components/fab/config.ts +29 -1
  64. package/src/components/fab/fab.ts +16 -2
  65. package/src/components/fab/index.ts +35 -0
  66. package/src/components/fab/types.ts +374 -10
  67. package/src/components/list/api.ts +12 -2
  68. package/src/components/list/config.ts +21 -0
  69. package/src/components/list/features.ts +6 -0
  70. package/src/components/list/index.ts +56 -1
  71. package/src/components/list/list-item.ts +46 -2
  72. package/src/components/list/list.ts +73 -2
  73. package/src/components/list/types.ts +172 -0
  74. package/src/components/list/utils.ts +26 -2
  75. package/src/components/menu/api.ts +217 -20
  76. package/src/components/menu/config.ts +27 -0
  77. package/src/components/menu/features/visibility.ts +55 -6
  78. package/src/components/menu/index.ts +64 -0
  79. package/src/components/menu/menu-item.ts +46 -3
  80. package/src/components/menu/menu.ts +77 -1
  81. package/src/components/menu/types.ts +404 -39
  82. package/src/components/navigation/nav-item.ts +13 -2
  83. package/src/components/sheet/config.ts +1 -2
  84. package/src/components/sheet/features/gestures.ts +1 -1
  85. package/src/components/sheet/features/position.ts +1 -2
  86. package/src/components/sheet/features/state.ts +1 -1
  87. package/src/components/sheet/index.ts +10 -2
  88. package/src/components/sheet/sheet.ts +1 -2
  89. package/src/components/sheet/types.ts +29 -1
  90. package/src/components/slider/api.ts +1 -1
  91. package/src/components/slider/config.ts +1 -1
  92. package/src/components/slider/features/controller.ts +1 -1
  93. package/src/components/slider/features/handlers.ts +1 -1
  94. package/src/components/slider/features/states.ts +1 -1
  95. package/src/components/slider/index.ts +12 -5
  96. package/src/components/slider/schema.ts +1 -1
  97. package/src/components/slider/types.ts +31 -0
  98. package/src/components/tabs/tab-api.ts +1 -1
  99. package/src/components/tabs/types.ts +1 -1
  100. package/src/components/tooltip/api.ts +6 -2
  101. package/src/components/tooltip/config.ts +9 -28
  102. package/src/components/tooltip/index.ts +10 -1
  103. package/src/components/tooltip/types.ts +38 -3
  104. package/src/core/dom/create.ts +57 -51
  105. package/src/index.ts +129 -31
  106. package/src/styles/abstract/_mixins.scss +23 -9
  107. package/src/styles/abstract/_variables.scss +14 -4
  108. package/src/styles/components/_card.scss +1 -1
  109. package/src/styles/components/_chip.scss +323 -113
  110. package/src/styles/components/_tabs.scss +1 -1
  111. package/src/styles/themes/_autumn.scss +3 -0
  112. package/CLAUDE.md +0 -33
  113. package/src/components/checkbox/constants.ts +0 -37
  114. package/src/components/chip/chip-set.ts +0 -225
  115. package/src/components/chip/chip.ts +0 -118
  116. package/src/components/chip/constants.ts +0 -28
  117. package/src/components/chip/index.ts +0 -12
  118. package/src/components/list/constants.ts +0 -116
  119. package/src/components/sheet/constants.ts +0 -20
  120. package/src/components/slider/constants.ts +0 -32
  121. package/src/components/tooltip/constants.ts +0 -27
  122. package/test/components/badge.test.ts +0 -545
  123. package/test/components/bottom-app-bar.test.ts +0 -303
  124. package/test/components/button.test.ts +0 -233
  125. package/test/components/card.test.ts +0 -560
  126. package/test/components/carousel.test.ts +0 -951
  127. package/test/components/checkbox.test.ts +0 -462
  128. package/test/components/chip.test.ts +0 -692
  129. package/test/components/datepicker.test.ts +0 -1124
  130. package/test/components/dialog.test.ts +0 -990
  131. package/test/components/divider.test.ts +0 -412
  132. package/test/components/extended-fab.test.ts +0 -672
  133. package/test/components/fab.test.ts +0 -561
  134. package/test/components/list.test.ts +0 -365
  135. package/test/components/menu.test.ts +0 -718
  136. package/test/components/navigation.test.ts +0 -186
  137. package/test/components/progress.test.ts +0 -567
  138. package/test/components/radios.test.ts +0 -699
  139. package/test/components/search.test.ts +0 -1135
  140. package/test/components/segmented-button.test.ts +0 -732
  141. package/test/components/sheet.test.ts +0 -641
  142. package/test/components/slider.test.ts +0 -1220
  143. package/test/components/snackbar.test.ts +0 -461
  144. package/test/components/switch.test.ts +0 -452
  145. package/test/components/tabs.test.ts +0 -1369
  146. package/test/components/textfield.test.ts +0 -400
  147. package/test/components/timepicker.test.ts +0 -592
  148. package/test/components/tooltip.test.ts +0 -630
  149. package/test/components/top-app-bar.test.ts +0 -566
  150. package/test/core/dom.attributes.test.ts +0 -148
  151. package/test/core/dom.classes.test.ts +0 -152
  152. package/test/core/dom.events.test.ts +0 -243
  153. package/test/core/emitter.test.ts +0 -141
  154. package/test/core/ripple.test.ts +0 -99
  155. package/test/core/state.store.test.ts +0 -189
  156. package/test/core/utils.normalize.test.ts +0 -61
  157. package/test/core/utils.object.test.ts +0 -120
  158. package/test/setup.js +0 -371
  159. package/test/setup.ts +0 -451
  160. package/tsconfig.json +0 -22
  161. package/typedoc.json +0 -28
  162. package/typedoc.simple.json +0 -14
@@ -1,1369 +0,0 @@
1
- // test/components/tabs.test.ts
2
- import { describe, test, expect } from 'bun:test';
3
- import {
4
- type TabComponent,
5
- type TabConfig,
6
- type TabsComponent,
7
- type TabsConfig,
8
- type TabsVariant,
9
- type TabStates
10
- } from '../../src/components/tabs/types';
11
-
12
- // Constants for tabs variants
13
- const TABS_VARIANTS = {
14
- PRIMARY: 'primary',
15
- SECONDARY: 'secondary'
16
- } as const;
17
-
18
- // Constants for tab states
19
- const TAB_STATES = {
20
- ACTIVE: 'active',
21
- INACTIVE: 'inactive',
22
- DISABLED: 'disabled'
23
- } as const;
24
-
25
- // Mock tab implementation
26
- const createMockTab = (config: TabConfig = {}): TabComponent => {
27
- // Create main element
28
- const element = document.createElement('button');
29
- element.className = 'mtrl-tab';
30
- element.type = 'button';
31
-
32
- // Default settings
33
- const settings = {
34
- state: config.state || TAB_STATES.INACTIVE,
35
- disabled: config.disabled || false,
36
- text: config.text || '',
37
- icon: config.icon || '',
38
- badge: config.badge || '',
39
- value: config.value || '',
40
- prefix: config.prefix || 'mtrl',
41
- componentName: config.componentName || 'tab',
42
- ripple: config.ripple !== undefined ? config.ripple : true,
43
- variant: config.variant || 'primary'
44
- };
45
-
46
- // Apply state class
47
- element.classList.add(`mtrl-tab--${settings.state}`);
48
-
49
- // Apply variant class
50
- element.classList.add(`mtrl-tab--${settings.variant}`);
51
-
52
- // Apply disabled state
53
- if (settings.disabled) {
54
- element.disabled = true;
55
- element.classList.add('mtrl-tab--disabled');
56
- }
57
-
58
- // Set value attribute
59
- if (settings.value) {
60
- element.setAttribute('data-value', settings.value);
61
- }
62
-
63
- // Apply additional classes
64
- if (config.class) {
65
- const classes = config.class.split(' ');
66
- classes.forEach(className => element.classList.add(className));
67
- }
68
-
69
- // Create container for content (used for layout)
70
- const contentContainer = document.createElement('div');
71
- contentContainer.className = 'mtrl-tab__content';
72
-
73
- // Create icon if provided
74
- let iconElement: HTMLElement | null = null;
75
- if (settings.icon) {
76
- iconElement = document.createElement('span');
77
- iconElement.className = 'mtrl-tab__icon';
78
- iconElement.innerHTML = settings.icon;
79
-
80
- if (config.iconSize) {
81
- iconElement.style.width = config.iconSize;
82
- iconElement.style.height = config.iconSize;
83
- }
84
-
85
- contentContainer.appendChild(iconElement);
86
- }
87
-
88
- // Create text element
89
- const textElement = document.createElement('span');
90
- textElement.className = 'mtrl-tab__text';
91
- if (settings.text) {
92
- textElement.textContent = settings.text;
93
- }
94
- contentContainer.appendChild(textElement);
95
-
96
- // Add content container to tab
97
- element.appendChild(contentContainer);
98
-
99
- // Create ripple element if enabled
100
- if (settings.ripple) {
101
- const ripple = document.createElement('span');
102
- ripple.className = 'mtrl-tab__ripple';
103
- element.appendChild(ripple);
104
- }
105
-
106
- // Badge component and elements
107
- let badgeComponent: any = undefined;
108
- let badgeElement: HTMLElement | null = null;
109
-
110
- // Create badge if configured
111
- if (settings.badge) {
112
- badgeElement = document.createElement('span');
113
- badgeElement.className = 'mtrl-badge';
114
- badgeElement.textContent = settings.badge.toString();
115
- element.appendChild(badgeElement);
116
-
117
- // Simple mock badge component
118
- badgeComponent = {
119
- element: badgeElement,
120
- setValue: (value: string | number) => {
121
- badgeElement!.textContent = value.toString();
122
- return badgeComponent;
123
- },
124
- getValue: () => badgeElement!.textContent || '',
125
- show: () => {
126
- badgeElement!.style.display = '';
127
- return badgeComponent;
128
- },
129
- hide: () => {
130
- badgeElement!.style.display = 'none';
131
- return badgeComponent;
132
- }
133
- };
134
- }
135
-
136
- // Track event handlers
137
- const eventHandlers: Record<string, Function[]> = {};
138
-
139
- // Icon API
140
- const iconAPI = {
141
- setIcon: (html: string) => {
142
- settings.icon = html;
143
-
144
- if (html) {
145
- if (!iconElement) {
146
- iconElement = document.createElement('span');
147
- iconElement.className = 'mtrl-tab__icon';
148
- contentContainer.insertBefore(iconElement, contentContainer.firstChild);
149
- }
150
-
151
- iconElement.innerHTML = html;
152
- } else if (iconElement) {
153
- iconElement.remove();
154
- iconElement = null;
155
- }
156
-
157
- return iconAPI;
158
- },
159
-
160
- getIcon: () => settings.icon,
161
-
162
- getElement: () => iconElement
163
- };
164
-
165
- // Text API
166
- const textAPI = {
167
- setText: (content: string) => {
168
- settings.text = content;
169
- textElement.textContent = content;
170
- return textAPI;
171
- },
172
-
173
- getText: () => settings.text,
174
-
175
- getElement: () => textElement
176
- };
177
-
178
- // Create the tab component
179
- const tab: TabComponent = {
180
- element,
181
- badge: badgeComponent,
182
-
183
- getClass: (name: string) => {
184
- const prefix = settings.prefix;
185
- return name ? `${prefix}-${name}` : `${prefix}-tab`;
186
- },
187
-
188
- getValue: () => settings.value,
189
-
190
- setValue: (value: string) => {
191
- settings.value = value;
192
- element.setAttribute('data-value', value);
193
- return tab;
194
- },
195
-
196
- activate: () => {
197
- // Remove current state class
198
- element.classList.remove(`mtrl-tab--${settings.state}`);
199
-
200
- // Set new state
201
- settings.state = TAB_STATES.ACTIVE;
202
- element.classList.add(`mtrl-tab--${settings.state}`);
203
-
204
- return tab;
205
- },
206
-
207
- deactivate: () => {
208
- // Remove current state class
209
- element.classList.remove(`mtrl-tab--${settings.state}`);
210
-
211
- // Set new state
212
- settings.state = TAB_STATES.INACTIVE;
213
- element.classList.add(`mtrl-tab--${settings.state}`);
214
-
215
- return tab;
216
- },
217
-
218
- isActive: () => settings.state === TAB_STATES.ACTIVE,
219
-
220
- enable: () => {
221
- if (settings.disabled) {
222
- settings.disabled = false;
223
- element.disabled = false;
224
- element.classList.remove('mtrl-tab--disabled');
225
- }
226
-
227
- return tab;
228
- },
229
-
230
- disable: () => {
231
- if (!settings.disabled) {
232
- settings.disabled = true;
233
- element.disabled = true;
234
- element.classList.add('mtrl-tab--disabled');
235
- }
236
-
237
- return tab;
238
- },
239
-
240
- setText: (content: string) => {
241
- textAPI.setText(content);
242
- return tab;
243
- },
244
-
245
- getText: () => textAPI.getText(),
246
-
247
- setIcon: (icon: string) => {
248
- iconAPI.setIcon(icon);
249
- return tab;
250
- },
251
-
252
- getIcon: () => iconAPI.getIcon(),
253
-
254
- setBadge: (content: string | number) => {
255
- settings.badge = content;
256
-
257
- if (!badgeComponent) {
258
- badgeElement = document.createElement('span');
259
- badgeElement.className = 'mtrl-badge';
260
- element.appendChild(badgeElement);
261
-
262
- badgeComponent = {
263
- element: badgeElement,
264
- setValue: (value: string | number) => {
265
- badgeElement!.textContent = value.toString();
266
- return badgeComponent;
267
- },
268
- getValue: () => badgeElement!.textContent || '',
269
- show: () => {
270
- badgeElement!.style.display = '';
271
- return badgeComponent;
272
- },
273
- hide: () => {
274
- badgeElement!.style.display = 'none';
275
- return badgeComponent;
276
- }
277
- };
278
-
279
- tab.badge = badgeComponent;
280
- }
281
-
282
- badgeComponent.setValue(content);
283
-
284
- return tab;
285
- },
286
-
287
- getBadge: () => {
288
- return badgeComponent ? badgeComponent.getValue() : '';
289
- },
290
-
291
- showBadge: () => {
292
- if (badgeComponent) {
293
- badgeComponent.show();
294
- }
295
-
296
- return tab;
297
- },
298
-
299
- hideBadge: () => {
300
- if (badgeComponent) {
301
- badgeComponent.hide();
302
- }
303
-
304
- return tab;
305
- },
306
-
307
- getBadgeComponent: () => badgeComponent,
308
-
309
- updateLayoutStyle: () => {
310
- // Adjust layout based on content
311
- if (settings.icon && settings.text) {
312
- element.classList.add('mtrl-tab--with-icon-and-text');
313
- } else if (settings.icon) {
314
- element.classList.add('mtrl-tab--icon-only');
315
- } else if (settings.text) {
316
- element.classList.add('mtrl-tab--text-only');
317
- }
318
- },
319
-
320
- on: (event: string, handler: Function) => {
321
- if (!eventHandlers[event]) {
322
- eventHandlers[event] = [];
323
- }
324
-
325
- eventHandlers[event].push(handler);
326
-
327
- element.addEventListener(event, handler as EventListener);
328
- return tab;
329
- },
330
-
331
- off: (event: string, handler: Function) => {
332
- if (eventHandlers[event]) {
333
- eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
334
- }
335
-
336
- element.removeEventListener(event, handler as EventListener);
337
- return tab;
338
- },
339
-
340
- destroy: () => {
341
- // Remove element from DOM if it has a parent
342
- if (element.parentNode) {
343
- element.parentNode.removeChild(element);
344
- }
345
-
346
- // Remove event listeners
347
- for (const event in eventHandlers) {
348
- eventHandlers[event].forEach(handler => {
349
- element.removeEventListener(event, handler as EventListener);
350
- });
351
-
352
- eventHandlers[event] = [];
353
- }
354
- },
355
-
356
- disabled: {
357
- enable: () => {
358
- if (settings.disabled) {
359
- settings.disabled = false;
360
- element.disabled = false;
361
- element.classList.remove('mtrl-tab--disabled');
362
- }
363
- },
364
-
365
- disable: () => {
366
- if (!settings.disabled) {
367
- settings.disabled = true;
368
- element.disabled = true;
369
- element.classList.add('mtrl-tab--disabled');
370
- }
371
- },
372
-
373
- isDisabled: () => settings.disabled
374
- },
375
-
376
- lifecycle: {
377
- destroy: () => {
378
- tab.destroy();
379
- }
380
- }
381
- };
382
-
383
- // Update layout style based on content
384
- tab.updateLayoutStyle();
385
-
386
- return tab;
387
- };
388
-
389
- // Mock tabs implementation
390
- const createMockTabs = (config: TabsConfig = {}): TabsComponent => {
391
- // Create main container element
392
- const element = document.createElement('div');
393
- element.className = 'mtrl-tabs';
394
-
395
- // Default settings
396
- const settings = {
397
- variant: config.variant || TABS_VARIANTS.PRIMARY,
398
- showDivider: config.showDivider !== undefined ? config.showDivider : true,
399
- scrollable: config.scrollable !== undefined ? config.scrollable : true,
400
- prefix: config.prefix || 'mtrl',
401
- tabs: []
402
- };
403
-
404
- // Apply variant class
405
- element.classList.add(`mtrl-tabs--${settings.variant}`);
406
-
407
- // Apply scrollable class if enabled
408
- if (settings.scrollable) {
409
- element.classList.add('mtrl-tabs--scrollable');
410
- }
411
-
412
- // Apply additional classes
413
- if (config.class) {
414
- const classes = config.class.split(' ');
415
- classes.forEach(className => element.classList.add(className));
416
- }
417
-
418
- // Create tabs container
419
- const tabsContainer = document.createElement('div');
420
- tabsContainer.className = 'mtrl-tabs__container';
421
-
422
- // Create scroll container if scrollable
423
- let scrollContainer: HTMLElement | undefined;
424
- if (settings.scrollable) {
425
- scrollContainer = document.createElement('div');
426
- scrollContainer.className = 'mtrl-tabs__scroll-container';
427
- element.appendChild(scrollContainer);
428
- scrollContainer.appendChild(tabsContainer);
429
- } else {
430
- element.appendChild(tabsContainer);
431
- }
432
-
433
- // Create divider if enabled
434
- if (settings.showDivider) {
435
- const divider = document.createElement('div');
436
- divider.className = 'mtrl-tabs__divider';
437
- element.appendChild(divider);
438
- }
439
-
440
- // Create indicator element
441
- const indicator = document.createElement('div');
442
- indicator.className = 'mtrl-tabs__indicator';
443
- tabsContainer.appendChild(indicator);
444
-
445
- // Simple mock indicator component
446
- const indicatorComponent = {
447
- element: indicator,
448
- updatePosition: (targetTab: TabComponent) => {
449
- // Set indicator position to match the active tab
450
- if (targetTab.element.offsetLeft !== undefined) {
451
- indicator.style.left = `${targetTab.element.offsetLeft}px`;
452
- indicator.style.width = `${targetTab.element.offsetWidth}px`;
453
- }
454
- },
455
- hide: () => {
456
- indicator.style.opacity = '0';
457
- },
458
- show: () => {
459
- indicator.style.opacity = '1';
460
- }
461
- };
462
-
463
- // Track tabs and event handlers
464
- const tabs: TabComponent[] = [];
465
- const eventHandlers: Record<string, Function[]> = {};
466
-
467
- // Emit an event
468
- const emit = (event: string, data?: any) => {
469
- if (eventHandlers[event]) {
470
- eventHandlers[event].forEach(handler => handler(data));
471
- }
472
-
473
- // Call the on.change callback from config if available
474
- if (event === 'change' && config.on?.change) {
475
- config.on.change(data);
476
- }
477
- };
478
-
479
- // Handle tab click
480
- const handleTabClick = (event: Event, tab: TabComponent) => {
481
- if (tab.disabled?.isDisabled()) {
482
- return;
483
- }
484
-
485
- // Set as active tab
486
- tabs.forEach(t => t.deactivate());
487
- tab.activate();
488
-
489
- // Update indicator position
490
- indicatorComponent.updatePosition(tab);
491
-
492
- // Emit change event
493
- emit('change', { tab, value: tab.getValue() });
494
- };
495
-
496
- // Create the tabs component
497
- const tabsComponent: TabsComponent = {
498
- element,
499
- scrollContainer,
500
-
501
- addTab: (config: TabConfig) => {
502
- // Set variant from tabs component
503
- config.variant = settings.variant;
504
-
505
- // Create the tab
506
- const tab = createMockTab(config);
507
-
508
- // Add tab to container
509
- tabsContainer.appendChild(tab.element);
510
-
511
- // Add click event handler
512
- tab.element.addEventListener('click', (event) => {
513
- tabsComponent.handleTabClick(event, tab);
514
- });
515
-
516
- // Add to tabs array
517
- tabs.push(tab);
518
-
519
- // If this is the first tab with active state, position the indicator
520
- if (tab.isActive()) {
521
- indicatorComponent.updatePosition(tab);
522
- }
523
-
524
- return tab;
525
- },
526
-
527
- add: (tab: TabComponent) => {
528
- // Add tab to container
529
- tabsContainer.appendChild(tab.element);
530
-
531
- // Add click event handler
532
- tab.element.addEventListener('click', (event) => {
533
- tabsComponent.handleTabClick(event, tab);
534
- });
535
-
536
- // Add to tabs array
537
- tabs.push(tab);
538
-
539
- // If this is an active tab, position the indicator
540
- if (tab.isActive()) {
541
- indicatorComponent.updatePosition(tab);
542
- }
543
-
544
- return tabsComponent;
545
- },
546
-
547
- getTabs: () => [...tabs],
548
-
549
- getActiveTab: () => {
550
- return tabs.find(tab => tab.isActive()) || null;
551
- },
552
-
553
- getIndicator: () => indicatorComponent,
554
-
555
- setActiveTab: (tabOrValue: TabComponent | string) => {
556
- let targetTab: TabComponent | undefined;
557
-
558
- if (typeof tabOrValue === 'string') {
559
- // Find tab by value
560
- targetTab = tabs.find(tab => tab.getValue() === tabOrValue);
561
- } else {
562
- // Tab component provided directly
563
- targetTab = tabOrValue;
564
- }
565
-
566
- if (targetTab && !targetTab.disabled?.isDisabled()) {
567
- // Deactivate all tabs
568
- tabs.forEach(tab => tab.deactivate());
569
-
570
- // Activate the target tab
571
- targetTab.activate();
572
-
573
- // Update indicator position
574
- indicatorComponent.updatePosition(targetTab);
575
-
576
- // Emit change event
577
- emit('change', { tab: targetTab, value: targetTab.getValue() });
578
- }
579
-
580
- return tabsComponent;
581
- },
582
-
583
- removeTab: (tabOrValue: TabComponent | string) => {
584
- let targetTabIndex = -1;
585
- let targetTab: TabComponent | undefined;
586
-
587
- if (typeof tabOrValue === 'string') {
588
- // Find tab by value
589
- targetTabIndex = tabs.findIndex(tab => tab.getValue() === tabOrValue);
590
- if (targetTabIndex !== -1) {
591
- targetTab = tabs[targetTabIndex];
592
- }
593
- } else {
594
- // Tab component provided directly
595
- targetTabIndex = tabs.indexOf(tabOrValue);
596
- targetTab = tabOrValue;
597
- }
598
-
599
- if (targetTabIndex !== -1 && targetTab) {
600
- // Check if this is the active tab
601
- const wasActive = targetTab.isActive();
602
-
603
- // Remove tab from the DOM
604
- targetTab.destroy();
605
-
606
- // Remove from tabs array
607
- tabs.splice(targetTabIndex, 1);
608
-
609
- // If removed tab was active, activate the first remaining tab
610
- if (wasActive && tabs.length > 0) {
611
- tabsComponent.setActiveTab(tabs[0]);
612
- } else if (tabs.length === 0) {
613
- // No tabs left, hide the indicator
614
- indicatorComponent.hide();
615
- }
616
- }
617
-
618
- return tabsComponent;
619
- },
620
-
621
- on: (event: string, handler: Function) => {
622
- if (!eventHandlers[event]) {
623
- eventHandlers[event] = [];
624
- }
625
-
626
- eventHandlers[event].push(handler);
627
- return tabsComponent;
628
- },
629
-
630
- off: (event: string, handler: Function) => {
631
- if (eventHandlers[event]) {
632
- eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
633
- }
634
-
635
- return tabsComponent;
636
- },
637
-
638
- emit: (event: string, data: any) => {
639
- emit(event, data);
640
- return tabsComponent;
641
- },
642
-
643
- destroy: () => {
644
- // Clean up tabs
645
- tabs.forEach(tab => tab.destroy());
646
- tabs.length = 0;
647
-
648
- // Remove element from DOM if it has a parent
649
- if (element.parentNode) {
650
- element.parentNode.removeChild(element);
651
- }
652
-
653
- // Clear event handlers
654
- for (const event in eventHandlers) {
655
- eventHandlers[event] = [];
656
- }
657
- },
658
-
659
- handleTabClick
660
- };
661
-
662
- // Initialize tabs from config
663
- if (config.tabs) {
664
- config.tabs.forEach(tabConfig => {
665
- // Set variant from tabs component
666
- tabConfig.variant = settings.variant;
667
-
668
- tabsComponent.addTab(tabConfig);
669
- });
670
- }
671
-
672
- return tabsComponent;
673
- };
674
-
675
- describe('Tab Component', () => {
676
- test('should create a tab element', () => {
677
- const tab = createMockTab({
678
- text: 'Home',
679
- value: 'home'
680
- });
681
-
682
- expect(tab.element).toBeDefined();
683
- expect(tab.element.tagName).toBe('BUTTON');
684
- expect(tab.element.className).toContain('mtrl-tab');
685
-
686
- const textElement = tab.element.querySelector('.mtrl-tab__text');
687
- expect(textElement).toBeDefined();
688
- expect(textElement?.textContent).toBe('Home');
689
-
690
- expect(tab.element.getAttribute('data-value')).toBe('home');
691
- });
692
-
693
- test('should apply inactive state by default', () => {
694
- const tab = createMockTab();
695
- expect(tab.element.className).toContain('mtrl-tab--inactive');
696
- expect(tab.isActive()).toBe(false);
697
- });
698
-
699
- test('should apply active state when configured', () => {
700
- const tab = createMockTab({
701
- state: TAB_STATES.ACTIVE
702
- });
703
-
704
- expect(tab.element.className).toContain('mtrl-tab--active');
705
- expect(tab.isActive()).toBe(true);
706
- });
707
-
708
- test('should apply disabled state when configured', () => {
709
- const tab = createMockTab({
710
- disabled: true
711
- });
712
-
713
- expect(tab.element.disabled).toBe(true);
714
- expect(tab.element.className).toContain('mtrl-tab--disabled');
715
- expect(tab.disabled?.isDisabled()).toBe(true);
716
- });
717
-
718
- test('should render icon when provided', () => {
719
- const iconHtml = '<svg>home</svg>';
720
- const tab = createMockTab({
721
- icon: iconHtml
722
- });
723
-
724
- const iconElement = tab.element.querySelector('.mtrl-tab__icon');
725
- expect(iconElement).toBeDefined();
726
- expect(iconElement?.innerHTML).toBe(iconHtml);
727
- expect(tab.getIcon()).toBe(iconHtml);
728
- });
729
-
730
- test('should apply custom icon size', () => {
731
- const tab = createMockTab({
732
- icon: '<svg>home</svg>',
733
- iconSize: '32px'
734
- });
735
-
736
- const iconElement = tab.element.querySelector('.mtrl-tab__icon') as HTMLElement;
737
- expect(iconElement.style.width).toBe('32px');
738
- expect(iconElement.style.height).toBe('32px');
739
- });
740
-
741
- test('should create badge when configured', () => {
742
- const tab = createMockTab({
743
- badge: '5'
744
- });
745
-
746
- expect(tab.badge).toBeDefined();
747
-
748
- const badgeElement = tab.element.querySelector('.mtrl-badge');
749
- expect(badgeElement).toBeDefined();
750
- expect(badgeElement?.textContent).toBe('5');
751
- expect(tab.getBadge()).toBe('5');
752
- });
753
-
754
- test('should create ripple by default', () => {
755
- const tab = createMockTab();
756
-
757
- const ripple = tab.element.querySelector('.mtrl-tab__ripple');
758
- expect(ripple).toBeDefined();
759
- });
760
-
761
- test('should not create ripple when disabled', () => {
762
- const tab = createMockTab({
763
- ripple: false
764
- });
765
-
766
- const ripple = tab.element.querySelector('.mtrl-tab__ripple');
767
- expect(ripple).toBeNull();
768
- });
769
-
770
- test('should activate and deactivate', () => {
771
- const tab = createMockTab();
772
-
773
- expect(tab.isActive()).toBe(false);
774
- expect(tab.element.className).toContain('mtrl-tab--inactive');
775
-
776
- tab.activate();
777
-
778
- expect(tab.isActive()).toBe(true);
779
- expect(tab.element.className).toContain('mtrl-tab--active');
780
- expect(tab.element.className).not.toContain('mtrl-tab--inactive');
781
-
782
- tab.deactivate();
783
-
784
- expect(tab.isActive()).toBe(false);
785
- expect(tab.element.className).toContain('mtrl-tab--inactive');
786
- expect(tab.element.className).not.toContain('mtrl-tab--active');
787
- });
788
-
789
- test('should enable and disable', () => {
790
- const tab = createMockTab({
791
- disabled: true
792
- });
793
-
794
- expect(tab.disabled?.isDisabled()).toBe(true);
795
- expect(tab.element.disabled).toBe(true);
796
-
797
- tab.enable();
798
-
799
- expect(tab.disabled?.isDisabled()).toBe(false);
800
- expect(tab.element.disabled).toBe(false);
801
- expect(tab.element.className).not.toContain('mtrl-tab--disabled');
802
-
803
- tab.disable();
804
-
805
- expect(tab.disabled?.isDisabled()).toBe(true);
806
- expect(tab.element.disabled).toBe(true);
807
- expect(tab.element.className).toContain('mtrl-tab--disabled');
808
- });
809
-
810
- test('should update text content', () => {
811
- const tab = createMockTab({
812
- text: 'Initial'
813
- });
814
-
815
- expect(tab.getText()).toBe('Initial');
816
-
817
- tab.setText('Updated');
818
-
819
- expect(tab.getText()).toBe('Updated');
820
-
821
- const textElement = tab.element.querySelector('.mtrl-tab__text');
822
- expect(textElement?.textContent).toBe('Updated');
823
- });
824
-
825
- test('should update icon', () => {
826
- const tab = createMockTab();
827
-
828
- expect(tab.getIcon()).toBe('');
829
-
830
- const iconHtml = '<svg>home</svg>';
831
- tab.setIcon(iconHtml);
832
-
833
- expect(tab.getIcon()).toBe(iconHtml);
834
-
835
- const iconElement = tab.element.querySelector('.mtrl-tab__icon');
836
- expect(iconElement).toBeDefined();
837
- expect(iconElement?.innerHTML).toBe(iconHtml);
838
- });
839
-
840
- test('should update badge', () => {
841
- const tab = createMockTab();
842
-
843
- expect(tab.getBadge()).toBe('');
844
- expect(tab.badge).toBeUndefined();
845
-
846
- tab.setBadge('5');
847
-
848
- expect(tab.getBadge()).toBe('5');
849
- expect(tab.badge).toBeDefined();
850
-
851
- const badgeElement = tab.element.querySelector('.mtrl-badge');
852
- expect(badgeElement).toBeDefined();
853
- expect(badgeElement?.textContent).toBe('5');
854
-
855
- // Update existing badge
856
- tab.setBadge('10');
857
-
858
- expect(tab.getBadge()).toBe('10');
859
- expect(badgeElement?.textContent).toBe('10');
860
- });
861
-
862
- test('should show and hide badge', () => {
863
- const tab = createMockTab({
864
- badge: '5'
865
- });
866
-
867
- const badgeElement = tab.element.querySelector('.mtrl-badge') as HTMLElement;
868
- expect(badgeElement.style.display).not.toBe('none');
869
-
870
- tab.hideBadge();
871
-
872
- expect(badgeElement.style.display).toBe('none');
873
-
874
- tab.showBadge();
875
-
876
- expect(badgeElement.style.display).not.toBe('none');
877
- });
878
-
879
- test('should get badge component', () => {
880
- const tab = createMockTab({
881
- badge: '5'
882
- });
883
-
884
- const badgeComponent = tab.getBadgeComponent();
885
- expect(badgeComponent).toBeDefined();
886
- expect(badgeComponent).toBe(tab.badge);
887
- });
888
-
889
- test('should apply layout styles based on content', () => {
890
- // Tab with both icon and text
891
- const tabWithBoth = createMockTab({
892
- icon: '<svg>home</svg>',
893
- text: 'Home'
894
- });
895
- expect(tabWithBoth.element.className).toContain('mtrl-tab--with-icon-and-text');
896
-
897
- // Tab with only icon
898
- const tabWithIcon = createMockTab({
899
- icon: '<svg>home</svg>'
900
- });
901
- expect(tabWithIcon.element.className).toContain('mtrl-tab--icon-only');
902
-
903
- // Tab with only text
904
- const tabWithText = createMockTab({
905
- text: 'Home'
906
- });
907
- expect(tabWithText.element.className).toContain('mtrl-tab--text-only');
908
- });
909
-
910
- test('should add event listeners', () => {
911
- const tab = createMockTab();
912
- let clicked = false;
913
-
914
- tab.on('click', () => {
915
- clicked = true;
916
- });
917
-
918
- // Simulate click
919
- tab.element.dispatchEvent(new Event('click'));
920
-
921
- expect(clicked).toBe(true);
922
- });
923
-
924
- test('should remove event listeners', () => {
925
- const tab = createMockTab();
926
- let count = 0;
927
-
928
- const handler = () => {
929
- count++;
930
- };
931
-
932
- tab.on('click', handler);
933
-
934
- // First click
935
- tab.element.dispatchEvent(new Event('click'));
936
- expect(count).toBe(1);
937
-
938
- // Remove listener
939
- tab.off('click', handler);
940
-
941
- // Second click
942
- tab.element.dispatchEvent(new Event('click'));
943
- expect(count).toBe(1); // Count should not increase
944
- });
945
-
946
- test('should be properly destroyed', () => {
947
- const tab = createMockTab();
948
- document.body.appendChild(tab.element);
949
-
950
- expect(document.body.contains(tab.element)).toBe(true);
951
-
952
- tab.destroy();
953
-
954
- expect(document.body.contains(tab.element)).toBe(false);
955
- });
956
- });
957
-
958
- describe('Tabs Component', () => {
959
- test('should create a tabs container', () => {
960
- const tabs = createMockTabs();
961
-
962
- expect(tabs.element).toBeDefined();
963
- expect(tabs.element.tagName).toBe('DIV');
964
- expect(tabs.element.className).toContain('mtrl-tabs');
965
-
966
- const container = tabs.element.querySelector('.mtrl-tabs__container');
967
- expect(container).toBeDefined();
968
-
969
- const indicator = tabs.element.querySelector('.mtrl-tabs__indicator');
970
- expect(indicator).toBeDefined();
971
- });
972
-
973
- test('should apply primary variant by default', () => {
974
- const tabs = createMockTabs();
975
- expect(tabs.element.className).toContain('mtrl-tabs--primary');
976
- });
977
-
978
- test('should apply different variants', () => {
979
- const variants: TabsVariant[] = [
980
- TABS_VARIANTS.PRIMARY,
981
- TABS_VARIANTS.SECONDARY
982
- ];
983
-
984
- variants.forEach(variant => {
985
- const tabs = createMockTabs({ variant });
986
- expect(tabs.element.className).toContain(`mtrl-tabs--${variant}`);
987
- });
988
- });
989
-
990
- test('should create divider by default', () => {
991
- const tabs = createMockTabs();
992
-
993
- const divider = tabs.element.querySelector('.mtrl-tabs__divider');
994
- expect(divider).toBeDefined();
995
- });
996
-
997
- test('should not create divider when configured', () => {
998
- const tabs = createMockTabs({
999
- showDivider: false
1000
- });
1001
-
1002
- const divider = tabs.element.querySelector('.mtrl-tabs__divider');
1003
- expect(divider).toBeNull();
1004
- });
1005
-
1006
- test('should be scrollable by default', () => {
1007
- const tabs = createMockTabs();
1008
-
1009
- expect(tabs.element.className).toContain('mtrl-tabs--scrollable');
1010
- expect(tabs.scrollContainer).toBeDefined();
1011
- });
1012
-
1013
- test('should not be scrollable when configured', () => {
1014
- const tabs = createMockTabs({
1015
- scrollable: false
1016
- });
1017
-
1018
- expect(tabs.element.className).not.toContain('mtrl-tabs--scrollable');
1019
- expect(tabs.scrollContainer).toBeUndefined();
1020
- });
1021
-
1022
- test('should create and add a tab', () => {
1023
- const tabs = createMockTabs();
1024
-
1025
- const tab = tabs.addTab({
1026
- text: 'Home',
1027
- value: 'home'
1028
- });
1029
-
1030
- expect(tab).toBeDefined();
1031
- expect(tab.element.tagName).toBe('BUTTON');
1032
- expect(tab.element.className).toContain('mtrl-tab');
1033
- expect(tab.getText()).toBe('Home');
1034
-
1035
- // Check that tab was added to the container
1036
- const container = tabs.element.querySelector('.mtrl-tabs__container');
1037
- expect(container?.contains(tab.element)).toBe(true);
1038
-
1039
- // Check that tab was added to the internal tabs array
1040
- expect(tabs.getTabs().length).toBe(1);
1041
- expect(tabs.getTabs()[0]).toBe(tab);
1042
- });
1043
-
1044
- test('should add a pre-created tab', () => {
1045
- const tabs = createMockTabs();
1046
- const tab = createMockTab({
1047
- text: 'Home',
1048
- value: 'home'
1049
- });
1050
-
1051
- tabs.add(tab);
1052
-
1053
- // Check that tab was added to the container
1054
- const container = tabs.element.querySelector('.mtrl-tabs__container');
1055
- expect(container?.contains(tab.element)).toBe(true);
1056
-
1057
- // Check that tab was added to the internal tabs array
1058
- expect(tabs.getTabs().length).toBe(1);
1059
- expect(tabs.getTabs()[0]).toBe(tab);
1060
- });
1061
-
1062
- test('should create tabs from config', () => {
1063
- const tabs = createMockTabs({
1064
- tabs: [
1065
- { text: 'Home', value: 'home' },
1066
- { text: 'About', value: 'about' },
1067
- { text: 'Contact', value: 'contact' }
1068
- ]
1069
- });
1070
-
1071
- expect(tabs.getTabs().length).toBe(3);
1072
-
1073
- const tabElements = tabs.element.querySelectorAll('.mtrl-tab');
1074
- expect(tabElements.length).toBe(3);
1075
-
1076
- expect(tabs.getTabs()[0].getText()).toBe('Home');
1077
- expect(tabs.getTabs()[1].getText()).toBe('About');
1078
- expect(tabs.getTabs()[2].getText()).toBe('Contact');
1079
- });
1080
-
1081
- test('should get active tab', () => {
1082
- const tabs = createMockTabs({
1083
- tabs: [
1084
- { text: 'Home', value: 'home', state: TAB_STATES.ACTIVE },
1085
- { text: 'About', value: 'about' },
1086
- { text: 'Contact', value: 'contact' }
1087
- ]
1088
- });
1089
-
1090
- const activeTab = tabs.getActiveTab();
1091
- expect(activeTab).toBeDefined();
1092
- expect(activeTab?.getText()).toBe('Home');
1093
- expect(activeTab?.getValue()).toBe('home');
1094
- });
1095
-
1096
- test('should return null for getActiveTab when no tab is active', () => {
1097
- const tabs = createMockTabs({
1098
- tabs: [
1099
- { text: 'Home', value: 'home' },
1100
- { text: 'About', value: 'about' }
1101
- ]
1102
- });
1103
-
1104
- const activeTab = tabs.getActiveTab();
1105
- expect(activeTab).toBeNull();
1106
- });
1107
-
1108
- test('should get indicator component', () => {
1109
- const tabs = createMockTabs();
1110
-
1111
- const indicator = tabs.getIndicator?.();
1112
- expect(indicator).toBeDefined();
1113
- expect(indicator?.element).toBeDefined();
1114
- expect(indicator?.element.className).toContain('mtrl-tabs__indicator');
1115
- });
1116
-
1117
- test('should set active tab by tab reference', () => {
1118
- const tabs = createMockTabs({
1119
- tabs: [
1120
- { text: 'Home', value: 'home' },
1121
- { text: 'About', value: 'about' },
1122
- { text: 'Contact', value: 'contact' }
1123
- ]
1124
- });
1125
-
1126
- const tab = tabs.getTabs()[1]; // About tab
1127
- tabs.setActiveTab(tab);
1128
-
1129
- // Check active tab
1130
- const activeTab = tabs.getActiveTab();
1131
- expect(activeTab).toBe(tab);
1132
- expect(activeTab?.getText()).toBe('About');
1133
-
1134
- // Check that only one tab is active
1135
- const activeTabs = tabs.getTabs().filter(t => t.isActive());
1136
- expect(activeTabs.length).toBe(1);
1137
- });
1138
-
1139
- test('should set active tab by value', () => {
1140
- const tabs = createMockTabs({
1141
- tabs: [
1142
- { text: 'Home', value: 'home' },
1143
- { text: 'About', value: 'about' },
1144
- { text: 'Contact', value: 'contact' }
1145
- ]
1146
- });
1147
-
1148
- tabs.setActiveTab('contact');
1149
-
1150
- // Check active tab
1151
- const activeTab = tabs.getActiveTab();
1152
- expect(activeTab).toBeDefined();
1153
- expect(activeTab?.getText()).toBe('Contact');
1154
- expect(activeTab?.getValue()).toBe('contact');
1155
- });
1156
-
1157
- test('should not activate disabled tabs', () => {
1158
- const tabs = createMockTabs({
1159
- tabs: [
1160
- { text: 'Home', value: 'home' },
1161
- { text: 'About', value: 'about', disabled: true }
1162
- ]
1163
- });
1164
-
1165
- tabs.setActiveTab('about');
1166
-
1167
- // Check that no tab is active
1168
- const activeTab = tabs.getActiveTab();
1169
- expect(activeTab).toBeNull();
1170
- });
1171
-
1172
- test('should emit change event when setting active tab', () => {
1173
- const tabs = createMockTabs({
1174
- tabs: [
1175
- { text: 'Home', value: 'home' },
1176
- { text: 'About', value: 'about' }
1177
- ]
1178
- });
1179
-
1180
- let eventFired = false;
1181
- let eventData: any = null;
1182
-
1183
- tabs.on('change', (data) => {
1184
- eventFired = true;
1185
- eventData = data;
1186
- });
1187
-
1188
- tabs.setActiveTab('about');
1189
-
1190
- expect(eventFired).toBe(true);
1191
- expect(eventData).toBeDefined();
1192
- expect(eventData.value).toBe('about');
1193
- expect(eventData.tab).toBeDefined();
1194
- expect(eventData.tab.getText()).toBe('About');
1195
- });
1196
-
1197
- test('should call change callback from config', () => {
1198
- let callbackFired = false;
1199
- let callbackData: any = null;
1200
-
1201
- const tabs = createMockTabs({
1202
- tabs: [
1203
- { text: 'Home', value: 'home' },
1204
- { text: 'About', value: 'about' }
1205
- ],
1206
- on: {
1207
- change: (data) => {
1208
- callbackFired = true;
1209
- callbackData = data;
1210
- }
1211
- }
1212
- });
1213
-
1214
- tabs.setActiveTab('about');
1215
-
1216
- expect(callbackFired).toBe(true);
1217
- expect(callbackData).toBeDefined();
1218
- expect(callbackData.value).toBe('about');
1219
- });
1220
-
1221
- test('should remove tab by reference', () => {
1222
- const tabs = createMockTabs({
1223
- tabs: [
1224
- { text: 'Home', value: 'home' },
1225
- { text: 'About', value: 'about' },
1226
- { text: 'Contact', value: 'contact' }
1227
- ]
1228
- });
1229
-
1230
- expect(tabs.getTabs().length).toBe(3);
1231
-
1232
- const tab = tabs.getTabs()[1]; // About tab
1233
- tabs.removeTab(tab);
1234
-
1235
- expect(tabs.getTabs().length).toBe(2);
1236
- expect(tabs.getTabs()[0].getText()).toBe('Home');
1237
- expect(tabs.getTabs()[1].getText()).toBe('Contact');
1238
-
1239
- // The tab element should be removed from the DOM
1240
- const container = tabs.element.querySelector('.mtrl-tabs__container');
1241
- expect(container?.contains(tab.element)).toBe(false);
1242
- });
1243
-
1244
- test('should remove tab by value', () => {
1245
- const tabs = createMockTabs({
1246
- tabs: [
1247
- { text: 'Home', value: 'home' },
1248
- { text: 'About', value: 'about' },
1249
- { text: 'Contact', value: 'contact' }
1250
- ]
1251
- });
1252
-
1253
- expect(tabs.getTabs().length).toBe(3);
1254
-
1255
- tabs.removeTab('about');
1256
-
1257
- expect(tabs.getTabs().length).toBe(2);
1258
- expect(tabs.getTabs()[0].getText()).toBe('Home');
1259
- expect(tabs.getTabs()[1].getText()).toBe('Contact');
1260
- });
1261
-
1262
- test('should activate another tab when removing the active tab', () => {
1263
- const tabs = createMockTabs({
1264
- tabs: [
1265
- { text: 'Home', value: 'home', state: TAB_STATES.ACTIVE },
1266
- { text: 'About', value: 'about' },
1267
- { text: 'Contact', value: 'contact' }
1268
- ]
1269
- });
1270
-
1271
- // Remove the active tab
1272
- tabs.removeTab('home');
1273
-
1274
- // First remaining tab should become active
1275
- const activeTab = tabs.getActiveTab();
1276
- expect(activeTab).toBeDefined();
1277
- expect(activeTab?.getText()).toBe('About');
1278
- });
1279
-
1280
- test('should handle tab click', () => {
1281
- const tabs = createMockTabs({
1282
- tabs: [
1283
- { text: 'Home', value: 'home' },
1284
- { text: 'About', value: 'about' }
1285
- ]
1286
- });
1287
-
1288
- let eventFired = false;
1289
-
1290
- tabs.on('change', () => {
1291
- eventFired = true;
1292
- });
1293
-
1294
- // Simulate click on the first tab
1295
- const firstTab = tabs.getTabs()[0];
1296
- firstTab.element.dispatchEvent(new Event('click'));
1297
-
1298
- expect(eventFired).toBe(true);
1299
- expect(firstTab.isActive()).toBe(true);
1300
- });
1301
-
1302
- test('should not respond to click on disabled tab', () => {
1303
- const tabs = createMockTabs({
1304
- tabs: [
1305
- { text: 'Home', value: 'home' },
1306
- { text: 'About', value: 'about', disabled: true }
1307
- ]
1308
- });
1309
-
1310
- let eventFired = false;
1311
-
1312
- tabs.on('change', () => {
1313
- eventFired = true;
1314
- });
1315
-
1316
- // Set first tab as active
1317
- tabs.setActiveTab('home');
1318
- eventFired = false; // Reset flag
1319
-
1320
- // Simulate click on disabled tab
1321
- const disabledTab = tabs.getTabs()[1];
1322
- disabledTab.element.dispatchEvent(new Event('click'));
1323
-
1324
- expect(eventFired).toBe(false);
1325
- expect(disabledTab.isActive()).toBe(false);
1326
- expect(tabs.getActiveTab()?.getValue()).toBe('home'); // Still active
1327
- });
1328
-
1329
- test('should add and remove event listeners', () => {
1330
- const tabs = createMockTabs();
1331
- let eventCount = 0;
1332
-
1333
- const handler = () => {
1334
- eventCount++;
1335
- };
1336
-
1337
- tabs.on('change', handler);
1338
-
1339
- // Trigger event
1340
- tabs.emit?.('change', {});
1341
- expect(eventCount).toBe(1);
1342
-
1343
- // Remove listener
1344
- tabs.off('change', handler);
1345
-
1346
- // Trigger event again
1347
- tabs.emit?.('change', {});
1348
- expect(eventCount).toBe(1); // Count should not increase
1349
- });
1350
-
1351
- test('should be properly destroyed', () => {
1352
- const tabs = createMockTabs({
1353
- tabs: [
1354
- { text: 'Home', value: 'home' },
1355
- { text: 'About', value: 'about' }
1356
- ]
1357
- });
1358
-
1359
- document.body.appendChild(tabs.element);
1360
-
1361
- expect(document.body.contains(tabs.element)).toBe(true);
1362
- expect(tabs.getTabs().length).toBe(2);
1363
-
1364
- tabs.destroy();
1365
-
1366
- expect(document.body.contains(tabs.element)).toBe(false);
1367
- expect(tabs.getTabs().length).toBe(0);
1368
- });
1369
- });