mtrl 0.3.1 → 0.3.2

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 (159) hide show
  1. package/.env +15 -0
  2. package/CONTRIBUTING.md +8 -8
  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/sheet/config.ts +1 -2
  83. package/src/components/sheet/features/gestures.ts +1 -1
  84. package/src/components/sheet/features/position.ts +1 -2
  85. package/src/components/sheet/features/state.ts +1 -1
  86. package/src/components/sheet/index.ts +10 -2
  87. package/src/components/sheet/sheet.ts +1 -2
  88. package/src/components/sheet/types.ts +29 -1
  89. package/src/components/slider/api.ts +1 -1
  90. package/src/components/slider/config.ts +1 -1
  91. package/src/components/slider/features/controller.ts +1 -1
  92. package/src/components/slider/features/handlers.ts +1 -1
  93. package/src/components/slider/features/states.ts +1 -1
  94. package/src/components/slider/index.ts +12 -5
  95. package/src/components/slider/schema.ts +1 -1
  96. package/src/components/slider/types.ts +31 -0
  97. package/src/components/tabs/tab-api.ts +1 -1
  98. package/src/components/tabs/types.ts +1 -1
  99. package/src/components/tooltip/api.ts +6 -2
  100. package/src/components/tooltip/config.ts +9 -28
  101. package/src/components/tooltip/index.ts +10 -1
  102. package/src/components/tooltip/types.ts +38 -3
  103. package/src/index.ts +129 -31
  104. package/src/styles/abstract/_mixins.scss +23 -9
  105. package/src/styles/abstract/_variables.scss +14 -4
  106. package/src/styles/components/_card.scss +1 -1
  107. package/src/styles/components/_chip.scss +323 -113
  108. package/src/styles/components/_tabs.scss +1 -1
  109. package/CLAUDE.md +0 -33
  110. package/src/components/checkbox/constants.ts +0 -37
  111. package/src/components/chip/chip-set.ts +0 -225
  112. package/src/components/chip/chip.ts +0 -118
  113. package/src/components/chip/constants.ts +0 -28
  114. package/src/components/chip/index.ts +0 -12
  115. package/src/components/list/constants.ts +0 -116
  116. package/src/components/sheet/constants.ts +0 -20
  117. package/src/components/slider/constants.ts +0 -32
  118. package/src/components/tooltip/constants.ts +0 -27
  119. package/test/components/badge.test.ts +0 -545
  120. package/test/components/bottom-app-bar.test.ts +0 -303
  121. package/test/components/button.test.ts +0 -233
  122. package/test/components/card.test.ts +0 -560
  123. package/test/components/carousel.test.ts +0 -951
  124. package/test/components/checkbox.test.ts +0 -462
  125. package/test/components/chip.test.ts +0 -692
  126. package/test/components/datepicker.test.ts +0 -1124
  127. package/test/components/dialog.test.ts +0 -990
  128. package/test/components/divider.test.ts +0 -412
  129. package/test/components/extended-fab.test.ts +0 -672
  130. package/test/components/fab.test.ts +0 -561
  131. package/test/components/list.test.ts +0 -365
  132. package/test/components/menu.test.ts +0 -718
  133. package/test/components/navigation.test.ts +0 -186
  134. package/test/components/progress.test.ts +0 -567
  135. package/test/components/radios.test.ts +0 -699
  136. package/test/components/search.test.ts +0 -1135
  137. package/test/components/segmented-button.test.ts +0 -732
  138. package/test/components/sheet.test.ts +0 -641
  139. package/test/components/slider.test.ts +0 -1220
  140. package/test/components/snackbar.test.ts +0 -461
  141. package/test/components/switch.test.ts +0 -452
  142. package/test/components/tabs.test.ts +0 -1369
  143. package/test/components/textfield.test.ts +0 -400
  144. package/test/components/timepicker.test.ts +0 -592
  145. package/test/components/tooltip.test.ts +0 -630
  146. package/test/components/top-app-bar.test.ts +0 -566
  147. package/test/core/dom.attributes.test.ts +0 -148
  148. package/test/core/dom.classes.test.ts +0 -152
  149. package/test/core/dom.events.test.ts +0 -243
  150. package/test/core/emitter.test.ts +0 -141
  151. package/test/core/ripple.test.ts +0 -99
  152. package/test/core/state.store.test.ts +0 -189
  153. package/test/core/utils.normalize.test.ts +0 -61
  154. package/test/core/utils.object.test.ts +0 -120
  155. package/test/setup.js +0 -371
  156. package/test/setup.ts +0 -451
  157. package/tsconfig.json +0 -22
  158. package/typedoc.json +0 -28
  159. package/typedoc.simple.json +0 -14
@@ -1,1220 +0,0 @@
1
- // test/components/slider.test.ts
2
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
3
- import { JSDOM } from 'jsdom';
4
- import {
5
- type SliderComponent,
6
- type SliderConfig,
7
- type SliderColor,
8
- type SliderSize,
9
- type SliderEventType,
10
- type SliderEvent
11
- } from '../../src/components/slider/types';
12
-
13
- // Setup jsdom environment
14
- let dom: JSDOM;
15
- let window: Window;
16
- let document: Document;
17
- let originalGlobalDocument: any;
18
- let originalGlobalWindow: any;
19
-
20
- beforeAll(() => {
21
- // Create a new JSDOM instance
22
- dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
23
- url: 'http://localhost/',
24
- pretendToBeVisual: true
25
- });
26
-
27
- // Get window and document from jsdom
28
- window = dom.window;
29
- document = window.document;
30
-
31
- // Store original globals
32
- originalGlobalDocument = global.document;
33
- originalGlobalWindow = global.window;
34
-
35
- // Set globals to use jsdom
36
- global.document = document;
37
- global.window = window;
38
- global.Element = window.Element;
39
- global.HTMLElement = window.HTMLElement;
40
- global.HTMLButtonElement = window.HTMLButtonElement;
41
- global.Event = window.Event;
42
- });
43
-
44
- afterAll(() => {
45
- // Restore original globals
46
- global.document = originalGlobalDocument;
47
- global.window = originalGlobalWindow;
48
-
49
- // Clean up jsdom
50
- window.close();
51
- });
52
-
53
- // Constants for slider colors
54
- const SLIDER_COLORS = {
55
- PRIMARY: 'primary',
56
- SECONDARY: 'secondary',
57
- TERTIARY: 'tertiary',
58
- ERROR: 'error'
59
- } as const;
60
-
61
- // Constants for slider sizes
62
- const SLIDER_SIZES = {
63
- SMALL: 'small',
64
- MEDIUM: 'medium',
65
- LARGE: 'large'
66
- } as const;
67
-
68
- // Constants for slider events
69
- const SLIDER_EVENTS = {
70
- CHANGE: 'change',
71
- INPUT: 'input',
72
- FOCUS: 'focus',
73
- BLUR: 'blur',
74
- START: 'start',
75
- END: 'end'
76
- } as const;
77
-
78
- // Mock slider implementation
79
- const createMockSlider = (config: SliderConfig = {}): SliderComponent => {
80
- // Create main container element
81
- const element = document.createElement('div');
82
- element.className = 'mtrl-slider';
83
-
84
- // Default settings
85
- const settings = {
86
- min: config.min !== undefined ? config.min : 0,
87
- max: config.max !== undefined ? config.max : 100,
88
- value: config.value !== undefined ? config.value : 0,
89
- secondValue: config.secondValue,
90
- step: config.step !== undefined ? config.step : 1,
91
- disabled: config.disabled || false,
92
- color: config.color || SLIDER_COLORS.PRIMARY,
93
- size: config.size || SLIDER_SIZES.MEDIUM,
94
- ticks: config.ticks || false,
95
- valueFormatter: config.valueFormatter || ((value: number) => value.toString()),
96
- showValue: config.showValue || false,
97
- snapToSteps: config.snapToSteps !== undefined ? config.snapToSteps : false,
98
- range: config.range || false,
99
- label: config.label || '',
100
- labelPosition: config.labelPosition || 'start',
101
- icon: config.icon || '',
102
- iconPosition: config.iconPosition || 'start'
103
- };
104
-
105
- // Apply color class
106
- element.classList.add(`mtrl-slider--${settings.color}`);
107
-
108
- // Apply size class
109
- element.classList.add(`mtrl-slider--${settings.size}`);
110
-
111
- // Apply disabled state
112
- if (settings.disabled) {
113
- element.classList.add('mtrl-slider--disabled');
114
- }
115
-
116
- // Apply ticks class
117
- if (settings.ticks) {
118
- element.classList.add('mtrl-slider--ticks');
119
- }
120
-
121
- // Apply range class
122
- if (settings.range) {
123
- element.classList.add('mtrl-slider--range');
124
- }
125
-
126
- // Apply label position class
127
- if (settings.label) {
128
- element.classList.add(`mtrl-slider--label-${settings.labelPosition}`);
129
- }
130
-
131
- // Apply additional classes
132
- if (config.class) {
133
- const classes = config.class.split(' ');
134
- classes.forEach(className => element.classList.add(className));
135
- }
136
-
137
- // Create label if provided
138
- let labelElement: HTMLElement | null = null;
139
- if (settings.label) {
140
- labelElement = document.createElement('label');
141
- labelElement.className = 'mtrl-slider__label';
142
- labelElement.textContent = settings.label;
143
- element.appendChild(labelElement);
144
- }
145
-
146
- // Create icon if provided
147
- let iconElement: HTMLElement | null = null;
148
- if (settings.icon) {
149
- iconElement = document.createElement('span');
150
- iconElement.className = `mtrl-slider__icon mtrl-slider__icon--${settings.iconPosition}`;
151
- iconElement.innerHTML = settings.icon;
152
- element.appendChild(iconElement);
153
- }
154
-
155
- // Create slider track
156
- const track = document.createElement('div');
157
- track.className = 'mtrl-slider__track';
158
-
159
- // Create slider fill (progress)
160
- const fill = document.createElement('div');
161
- fill.className = 'mtrl-slider__fill';
162
- track.appendChild(fill);
163
-
164
- // Create ticks if enabled
165
- if (settings.ticks) {
166
- const ticksContainer = document.createElement('div');
167
- ticksContainer.className = 'mtrl-slider__ticks';
168
-
169
- // Calculate number of ticks based on step
170
- const numTicks = Math.floor((settings.max - settings.min) / settings.step) + 1;
171
- for (let i = 0; i < numTicks; i++) {
172
- const tick = document.createElement('div');
173
- tick.className = 'mtrl-slider__tick';
174
- ticksContainer.appendChild(tick);
175
- }
176
-
177
- track.appendChild(ticksContainer);
178
- }
179
-
180
- // Create thumb (handle)
181
- const thumb = document.createElement('div');
182
- thumb.className = 'mtrl-slider__thumb';
183
-
184
- // Create second thumb for range slider
185
- let secondThumb: HTMLElement | null = null;
186
- if (settings.range) {
187
- secondThumb = document.createElement('div');
188
- secondThumb.className = 'mtrl-slider__thumb mtrl-slider__thumb--second';
189
- }
190
-
191
- // Create value display if enabled
192
- let valueElement: HTMLElement | null = null;
193
- if (settings.showValue) {
194
- valueElement = document.createElement('div');
195
- valueElement.className = 'mtrl-slider__value';
196
- valueElement.textContent = settings.valueFormatter(settings.value);
197
- thumb.appendChild(valueElement);
198
-
199
- if (settings.range && secondThumb) {
200
- const secondValueElement = document.createElement('div');
201
- secondValueElement.className = 'mtrl-slider__value';
202
- secondValueElement.textContent = settings.valueFormatter(settings.secondValue || settings.min);
203
- secondThumb.appendChild(secondValueElement);
204
- }
205
- }
206
-
207
- // Add thumbs to track
208
- track.appendChild(thumb);
209
- if (settings.range && secondThumb) {
210
- track.appendChild(secondThumb);
211
- }
212
-
213
- // Add track to container
214
- element.appendChild(track);
215
-
216
- // Update UI based on current values
217
- const updateSliderUI = () => {
218
- const range = settings.max - settings.min;
219
-
220
- // Calculate percentage for first thumb
221
- const percent = ((settings.value - settings.min) / range) * 100;
222
-
223
- // Set fill width and thumb position
224
- if (settings.range && settings.secondValue !== undefined) {
225
- // For range slider, fill is between the two thumbs
226
- const secondPercent = ((settings.secondValue - settings.min) / range) * 100;
227
- const startPercent = Math.min(percent, secondPercent);
228
- const endPercent = Math.max(percent, secondPercent);
229
-
230
- fill.style.left = `${startPercent}%`;
231
- fill.style.width = `${endPercent - startPercent}%`;
232
-
233
- thumb.style.left = `${percent}%`;
234
- if (secondThumb) {
235
- secondThumb.style.left = `${secondPercent}%`;
236
- }
237
- } else {
238
- // For regular slider, fill starts from beginning
239
- fill.style.width = `${percent}%`;
240
- thumb.style.left = `${percent}%`;
241
- }
242
-
243
- // Update value display if present
244
- if (valueElement) {
245
- valueElement.textContent = settings.valueFormatter(settings.value);
246
-
247
- if (settings.range && secondThumb && settings.secondValue !== undefined) {
248
- const secondValueEl = secondThumb.querySelector('.mtrl-slider__value');
249
- if (secondValueEl) {
250
- secondValueEl.textContent = settings.valueFormatter(settings.secondValue);
251
- }
252
- }
253
- }
254
- };
255
-
256
- // Initialize slider UI
257
- updateSliderUI();
258
-
259
- // Track event handlers
260
- const eventHandlers: Record<string, Function[]> = {};
261
-
262
- // Emit an event
263
- const emit = (event: SliderEventType, originalEvent?: Event | null) => {
264
- let defaultPrevented = false;
265
-
266
- const eventData: SliderEvent = {
267
- slider,
268
- value: settings.value,
269
- secondValue: settings.range ? settings.secondValue || null : null,
270
- originalEvent: originalEvent || null,
271
- preventDefault: () => {
272
- defaultPrevented = true;
273
- },
274
- defaultPrevented
275
- };
276
-
277
- // Call handlers from config.on
278
- if (config.on && config.on[event]) {
279
- config.on[event]!(eventData);
280
- }
281
-
282
- // Call registered event handlers
283
- if (eventHandlers[event]) {
284
- eventHandlers[event].forEach(handler => handler(eventData));
285
- }
286
-
287
- return defaultPrevented;
288
- };
289
-
290
- // Create the slider component
291
- const slider: SliderComponent = {
292
- element,
293
-
294
- setValue: (value: number, triggerEvent: boolean = false) => {
295
- // Ensure value is within min/max bounds
296
- value = Math.max(settings.min, Math.min(settings.max, value));
297
-
298
- // Snap to nearest step if enabled
299
- if (settings.snapToSteps && settings.step > 0) {
300
- value = Math.round((value - settings.min) / settings.step) * settings.step + settings.min;
301
- }
302
-
303
- // Update value
304
- settings.value = value;
305
-
306
- // Update UI
307
- updateSliderUI();
308
-
309
- // Emit events if requested
310
- if (triggerEvent) {
311
- emit(SLIDER_EVENTS.INPUT);
312
- emit(SLIDER_EVENTS.CHANGE);
313
- }
314
-
315
- return slider;
316
- },
317
-
318
- getValue: () => settings.value,
319
-
320
- setSecondValue: (value: number, triggerEvent: boolean = false) => {
321
- if (!settings.range) return slider;
322
-
323
- // Ensure value is within min/max bounds
324
- value = Math.max(settings.min, Math.min(settings.max, value));
325
-
326
- // Snap to nearest step if enabled
327
- if (settings.snapToSteps && settings.step > 0) {
328
- value = Math.round((value - settings.min) / settings.step) * settings.step + settings.min;
329
- }
330
-
331
- // Update value
332
- settings.secondValue = value;
333
-
334
- // Update UI
335
- updateSliderUI();
336
-
337
- // Emit events if requested
338
- if (triggerEvent) {
339
- emit(SLIDER_EVENTS.INPUT);
340
- emit(SLIDER_EVENTS.CHANGE);
341
- }
342
-
343
- return slider;
344
- },
345
-
346
- getSecondValue: () => {
347
- return settings.range ? settings.secondValue || null : null;
348
- },
349
-
350
- setMin: (min: number) => {
351
- settings.min = min;
352
-
353
- // Adjust values if they're now outside bounds
354
- if (settings.value < min) {
355
- settings.value = min;
356
- }
357
-
358
- if (settings.range && settings.secondValue !== undefined && settings.secondValue < min) {
359
- settings.secondValue = min;
360
- }
361
-
362
- // Update ticks if enabled
363
- if (settings.ticks) {
364
- const ticksContainer = element.querySelector('.mtrl-slider__ticks');
365
- if (ticksContainer) {
366
- ticksContainer.innerHTML = '';
367
-
368
- const numTicks = Math.floor((settings.max - settings.min) / settings.step) + 1;
369
- for (let i = 0; i < numTicks; i++) {
370
- const tick = document.createElement('div');
371
- tick.className = 'mtrl-slider__tick';
372
- ticksContainer.appendChild(tick);
373
- }
374
- }
375
- }
376
-
377
- // Update UI
378
- updateSliderUI();
379
-
380
- return slider;
381
- },
382
-
383
- getMin: () => settings.min,
384
-
385
- setMax: (max: number) => {
386
- settings.max = max;
387
-
388
- // Adjust values if they're now outside bounds
389
- if (settings.value > max) {
390
- settings.value = max;
391
- }
392
-
393
- if (settings.range && settings.secondValue !== undefined && settings.secondValue > max) {
394
- settings.secondValue = max;
395
- }
396
-
397
- // Update ticks if enabled
398
- if (settings.ticks) {
399
- const ticksContainer = element.querySelector('.mtrl-slider__ticks');
400
- if (ticksContainer) {
401
- ticksContainer.innerHTML = '';
402
-
403
- const numTicks = Math.floor((settings.max - settings.min) / settings.step) + 1;
404
- for (let i = 0; i < numTicks; i++) {
405
- const tick = document.createElement('div');
406
- tick.className = 'mtrl-slider__tick';
407
- ticksContainer.appendChild(tick);
408
- }
409
- }
410
- }
411
-
412
- // Update UI
413
- updateSliderUI();
414
-
415
- return slider;
416
- },
417
-
418
- getMax: () => settings.max,
419
-
420
- setStep: (step: number) => {
421
- settings.step = step;
422
-
423
- // Update ticks if enabled
424
- if (settings.ticks) {
425
- const ticksContainer = element.querySelector('.mtrl-slider__ticks');
426
- if (ticksContainer) {
427
- ticksContainer.innerHTML = '';
428
-
429
- const numTicks = Math.floor((settings.max - settings.min) / settings.step) + 1;
430
- for (let i = 0; i < numTicks; i++) {
431
- const tick = document.createElement('div');
432
- tick.className = 'mtrl-slider__tick';
433
- ticksContainer.appendChild(tick);
434
- }
435
- }
436
- }
437
-
438
- // Snap current values to new step if enabled
439
- if (settings.snapToSteps) {
440
- settings.value = Math.round((settings.value - settings.min) / step) * step + settings.min;
441
-
442
- if (settings.range && settings.secondValue !== undefined) {
443
- settings.secondValue = Math.round((settings.secondValue - settings.min) / step) * step + settings.min;
444
- }
445
-
446
- // Update UI
447
- updateSliderUI();
448
- }
449
-
450
- return slider;
451
- },
452
-
453
- getStep: () => settings.step,
454
-
455
- enable: () => {
456
- settings.disabled = false;
457
- element.classList.remove('mtrl-slider--disabled');
458
- return slider;
459
- },
460
-
461
- disable: () => {
462
- settings.disabled = true;
463
- element.classList.add('mtrl-slider--disabled');
464
- return slider;
465
- },
466
-
467
- isDisabled: () => settings.disabled,
468
-
469
- setColor: (color: SliderColor) => {
470
- // Remove existing color class
471
- Object.values(SLIDER_COLORS).forEach(c => {
472
- element.classList.remove(`mtrl-slider--${c}`);
473
- });
474
-
475
- // Add new color class
476
- element.classList.add(`mtrl-slider--${color}`);
477
- settings.color = color;
478
-
479
- return slider;
480
- },
481
-
482
- getColor: () => settings.color,
483
-
484
- setSize: (size: SliderSize) => {
485
- // Remove existing size class
486
- Object.values(SLIDER_SIZES).forEach(s => {
487
- element.classList.remove(`mtrl-slider--${s}`);
488
- });
489
-
490
- // Add new size class
491
- element.classList.add(`mtrl-slider--${size}`);
492
- settings.size = size;
493
-
494
- return slider;
495
- },
496
-
497
- getSize: () => settings.size,
498
-
499
- showTicks: (show: boolean) => {
500
- settings.ticks = show;
501
-
502
- if (show) {
503
- element.classList.add('mtrl-slider--ticks');
504
-
505
- // Create ticks if they don't exist
506
- let ticksContainer = element.querySelector('.mtrl-slider__ticks');
507
- if (!ticksContainer) {
508
- ticksContainer = document.createElement('div');
509
- ticksContainer.className = 'mtrl-slider__ticks';
510
-
511
- const numTicks = Math.floor((settings.max - settings.min) / settings.step) + 1;
512
- for (let i = 0; i < numTicks; i++) {
513
- const tick = document.createElement('div');
514
- tick.className = 'mtrl-slider__tick';
515
- ticksContainer.appendChild(tick);
516
- }
517
-
518
- const track = element.querySelector('.mtrl-slider__track');
519
- if (track) {
520
- track.appendChild(ticksContainer);
521
- }
522
- }
523
- } else {
524
- element.classList.remove('mtrl-slider--ticks');
525
-
526
- // Remove ticks if they exist
527
- const ticksContainer = element.querySelector('.mtrl-slider__ticks');
528
- if (ticksContainer) {
529
- ticksContainer.remove();
530
- }
531
- }
532
-
533
- return slider;
534
- },
535
-
536
- showCurrentValue: (show: boolean) => {
537
- settings.showValue = show;
538
-
539
- if (show) {
540
- // Create value display for main thumb if it doesn't exist
541
- let valueEl = thumb.querySelector('.mtrl-slider__value');
542
- if (!valueEl) {
543
- valueEl = document.createElement('div');
544
- valueEl.className = 'mtrl-slider__value';
545
- valueEl.textContent = settings.valueFormatter(settings.value);
546
- thumb.appendChild(valueEl);
547
- }
548
-
549
- // Create value display for second thumb if range slider
550
- if (settings.range && secondThumb) {
551
- let secondValueEl = secondThumb.querySelector('.mtrl-slider__value');
552
- if (!secondValueEl) {
553
- secondValueEl = document.createElement('div');
554
- secondValueEl.className = 'mtrl-slider__value';
555
- secondValueEl.textContent = settings.valueFormatter(settings.secondValue || settings.min);
556
- secondThumb.appendChild(secondValueEl);
557
- }
558
- }
559
- } else {
560
- // Remove value display from main thumb
561
- const valueEl = thumb.querySelector('.mtrl-slider__value');
562
- if (valueEl) {
563
- valueEl.remove();
564
- }
565
-
566
- // Remove value display from second thumb
567
- if (settings.range && secondThumb) {
568
- const secondValueEl = secondThumb.querySelector('.mtrl-slider__value');
569
- if (secondValueEl) {
570
- secondValueEl.remove();
571
- }
572
- }
573
- }
574
-
575
- return slider;
576
- },
577
-
578
- setLabel: (text: string) => {
579
- settings.label = text;
580
-
581
- if (text) {
582
- if (!labelElement) {
583
- labelElement = document.createElement('label');
584
- labelElement.className = 'mtrl-slider__label';
585
- element.insertBefore(labelElement, element.firstChild);
586
-
587
- // Apply label position class
588
- element.classList.add(`mtrl-slider--label-${settings.labelPosition}`);
589
- }
590
-
591
- labelElement.textContent = text;
592
- } else if (labelElement) {
593
- labelElement.remove();
594
- labelElement = null;
595
-
596
- // Remove label position class
597
- element.classList.remove(`mtrl-slider--label-${settings.labelPosition}`);
598
- }
599
-
600
- return slider;
601
- },
602
-
603
- getLabel: () => settings.label,
604
-
605
- setIcon: (iconHtml: string) => {
606
- settings.icon = iconHtml;
607
-
608
- if (iconHtml) {
609
- if (!iconElement) {
610
- iconElement = document.createElement('span');
611
- iconElement.className = `mtrl-slider__icon mtrl-slider__icon--${settings.iconPosition}`;
612
-
613
- if (settings.iconPosition === 'start') {
614
- element.insertBefore(iconElement, element.firstChild);
615
- } else {
616
- element.appendChild(iconElement);
617
- }
618
- }
619
-
620
- iconElement.innerHTML = iconHtml;
621
- } else if (iconElement) {
622
- iconElement.remove();
623
- iconElement = null;
624
- }
625
-
626
- return slider;
627
- },
628
-
629
- getIcon: () => settings.icon,
630
-
631
- on: (event: SliderEventType, handler: (event: SliderEvent) => void) => {
632
- if (!eventHandlers[event]) {
633
- eventHandlers[event] = [];
634
- }
635
-
636
- eventHandlers[event].push(handler);
637
- return slider;
638
- },
639
-
640
- off: (event: SliderEventType, handler: (event: SliderEvent) => void) => {
641
- if (eventHandlers[event]) {
642
- eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
643
- }
644
-
645
- return slider;
646
- },
647
-
648
- destroy: () => {
649
- // Remove element from DOM if it has a parent
650
- if (element.parentNode) {
651
- element.parentNode.removeChild(element);
652
- }
653
-
654
- // Clear event handlers
655
- for (const event in eventHandlers) {
656
- eventHandlers[event] = [];
657
- }
658
- }
659
- };
660
-
661
- return slider;
662
- };
663
-
664
- describe('Slider Component', () => {
665
- test('should create a slider element', () => {
666
- const slider = createMockSlider();
667
-
668
- expect(slider.element).toBeDefined();
669
- expect(slider.element.tagName).toBe('DIV');
670
- expect(slider.element.className).toContain('mtrl-slider');
671
-
672
- const track = slider.element.querySelector('.mtrl-slider__track');
673
- expect(track).toBeDefined();
674
-
675
- const fill = slider.element.querySelector('.mtrl-slider__fill');
676
- expect(fill).toBeDefined();
677
-
678
- const thumb = slider.element.querySelector('.mtrl-slider__thumb');
679
- expect(thumb).toBeDefined();
680
- });
681
-
682
- test('should apply primary color by default', () => {
683
- const slider = createMockSlider();
684
- expect(slider.element.className).toContain('mtrl-slider--primary');
685
- });
686
-
687
- test('should apply different colors', () => {
688
- const colors: SliderColor[] = [
689
- SLIDER_COLORS.PRIMARY,
690
- SLIDER_COLORS.SECONDARY,
691
- SLIDER_COLORS.TERTIARY,
692
- SLIDER_COLORS.ERROR
693
- ];
694
-
695
- colors.forEach(color => {
696
- const slider = createMockSlider({ color });
697
- expect(slider.element.className).toContain(`mtrl-slider--${color}`);
698
- });
699
- });
700
-
701
- test('should apply medium size by default', () => {
702
- const slider = createMockSlider();
703
- expect(slider.element.className).toContain('mtrl-slider--medium');
704
- });
705
-
706
- test('should apply different sizes', () => {
707
- const sizes: SliderSize[] = [
708
- SLIDER_SIZES.SMALL,
709
- SLIDER_SIZES.MEDIUM,
710
- SLIDER_SIZES.LARGE
711
- ];
712
-
713
- sizes.forEach(size => {
714
- const slider = createMockSlider({ size });
715
- expect(slider.element.className).toContain(`mtrl-slider--${size}`);
716
- });
717
- });
718
-
719
- test('should set initial value', () => {
720
- const slider = createMockSlider({
721
- min: 0,
722
- max: 100,
723
- value: 50
724
- });
725
-
726
- expect(slider.getValue()).toBe(50);
727
-
728
- const fill = slider.element.querySelector('.mtrl-slider__fill');
729
- expect(fill?.style.width).toBe('50%');
730
-
731
- const thumb = slider.element.querySelector('.mtrl-slider__thumb');
732
- expect(thumb?.style.left).toBe('50%');
733
- });
734
-
735
- test('should support range slider with two thumbs', () => {
736
- const slider = createMockSlider({
737
- range: true,
738
- value: 25,
739
- secondValue: 75
740
- });
741
-
742
- expect(slider.element.className).toContain('mtrl-slider--range');
743
- expect(slider.getValue()).toBe(25);
744
- expect(slider.getSecondValue()).toBe(75);
745
-
746
- const thumbs = slider.element.querySelectorAll('.mtrl-slider__thumb');
747
- expect(thumbs.length).toBe(2);
748
- expect(thumbs[0].style.left).toBe('25%');
749
- expect(thumbs[1].style.left).toBe('75%');
750
-
751
- const fill = slider.element.querySelector('.mtrl-slider__fill');
752
- expect(fill?.style.left).toBe('25%');
753
- expect(fill?.style.width).toBe('50%');
754
- });
755
-
756
- test('should display tick marks when configured', () => {
757
- const slider = createMockSlider({
758
- min: 0,
759
- max: 10,
760
- step: 2,
761
- ticks: true
762
- });
763
-
764
- expect(slider.element.className).toContain('mtrl-slider--ticks');
765
-
766
- const ticksContainer = slider.element.querySelector('.mtrl-slider__ticks');
767
- expect(ticksContainer).toBeDefined();
768
-
769
- const ticks = slider.element.querySelectorAll('.mtrl-slider__tick');
770
- // Should have 6 ticks for values 0, 2, 4, 6, 8, 10
771
- expect(ticks.length).toBe(6);
772
- });
773
-
774
- test('should display current value when configured', () => {
775
- const slider = createMockSlider({
776
- value: 50,
777
- showValue: true
778
- });
779
-
780
- const valueElement = slider.element.querySelector('.mtrl-slider__value');
781
- expect(valueElement).toBeDefined();
782
- expect(valueElement?.textContent).toBe('50');
783
- });
784
-
785
- test('should apply custom value formatter', () => {
786
- const formatter = (value: number) => `$${value}`;
787
-
788
- const slider = createMockSlider({
789
- value: 50,
790
- showValue: true,
791
- valueFormatter: formatter
792
- });
793
-
794
- const valueElement = slider.element.querySelector('.mtrl-slider__value');
795
- expect(valueElement?.textContent).toBe('$50');
796
- });
797
-
798
- test('should apply disabled state', () => {
799
- const slider = createMockSlider({
800
- disabled: true
801
- });
802
-
803
- expect(slider.element.className).toContain('mtrl-slider--disabled');
804
- expect(slider.isDisabled()).toBe(true);
805
- });
806
-
807
- test('should set label', () => {
808
- const slider = createMockSlider({
809
- label: 'Volume'
810
- });
811
-
812
- const label = slider.element.querySelector('.mtrl-slider__label');
813
- expect(label).toBeDefined();
814
- expect(label?.textContent).toBe('Volume');
815
- expect(slider.getLabel()).toBe('Volume');
816
- });
817
-
818
- test('should set label position', () => {
819
- const slider = createMockSlider({
820
- label: 'Volume',
821
- labelPosition: 'end'
822
- });
823
-
824
- expect(slider.element.className).toContain('mtrl-slider--label-end');
825
- });
826
-
827
- test('should set icon', () => {
828
- const iconHtml = '<svg>volume</svg>';
829
-
830
- const slider = createMockSlider({
831
- icon: iconHtml
832
- });
833
-
834
- const icon = slider.element.querySelector('.mtrl-slider__icon');
835
- expect(icon).toBeDefined();
836
- expect(icon?.innerHTML).toBe(iconHtml);
837
- expect(slider.getIcon()).toBe(iconHtml);
838
- });
839
-
840
- test('should set icon position', () => {
841
- const slider = createMockSlider({
842
- icon: '<svg>volume</svg>',
843
- iconPosition: 'end'
844
- });
845
-
846
- const icon = slider.element.querySelector('.mtrl-slider__icon');
847
- expect(icon?.className).toContain('mtrl-slider__icon--end');
848
- });
849
-
850
- test('should update value', () => {
851
- const slider = createMockSlider({
852
- min: 0,
853
- max: 100,
854
- value: 0
855
- });
856
-
857
- expect(slider.getValue()).toBe(0);
858
-
859
- slider.setValue(75);
860
-
861
- expect(slider.getValue()).toBe(75);
862
-
863
- const fill = slider.element.querySelector('.mtrl-slider__fill');
864
- expect(fill?.style.width).toBe('75%');
865
-
866
- const thumb = slider.element.querySelector('.mtrl-slider__thumb');
867
- expect(thumb?.style.left).toBe('75%');
868
- });
869
-
870
- test('should constrain value to min/max range', () => {
871
- const slider = createMockSlider({
872
- min: 0,
873
- max: 100,
874
- value: 50
875
- });
876
-
877
- slider.setValue(-10);
878
- expect(slider.getValue()).toBe(0);
879
-
880
- slider.setValue(150);
881
- expect(slider.getValue()).toBe(100);
882
- });
883
-
884
- test('should snap to steps when configured', () => {
885
- const slider = createMockSlider({
886
- min: 0,
887
- max: 10,
888
- step: 2,
889
- snapToSteps: true
890
- });
891
-
892
- slider.setValue(3);
893
- // Rounding to nearest step, 3 is closer to 4 than 2
894
- expect(slider.getValue()).toBe(4); // Snaps to nearest step (4)
895
-
896
- slider.setValue(5.1);
897
- // Rounding to nearest step, 5.1 is closer to 6 than 4
898
- expect(slider.getValue()).toBe(6); // Snaps to nearest step (6)
899
- });
900
-
901
- test('should update second value for range slider', () => {
902
- const slider = createMockSlider({
903
- range: true,
904
- value: 25,
905
- secondValue: 75
906
- });
907
-
908
- slider.setSecondValue(60);
909
-
910
- expect(slider.getSecondValue()).toBe(60);
911
-
912
- const secondThumb = slider.element.querySelector('.mtrl-slider__thumb--second');
913
- expect(secondThumb?.style.left).toBe('60%');
914
-
915
- const fill = slider.element.querySelector('.mtrl-slider__fill');
916
- expect(fill?.style.left).toBe('25%');
917
- expect(fill?.style.width).toBe('35%');
918
- });
919
-
920
- test('should update min value', () => {
921
- const slider = createMockSlider({
922
- min: 0,
923
- max: 100,
924
- value: 50
925
- });
926
-
927
- slider.setMin(20);
928
-
929
- expect(slider.getMin()).toBe(20);
930
-
931
- // Value percentage should now be (50-20)/(100-20) = 30/80 = 37.5%
932
- const fill = slider.element.querySelector('.mtrl-slider__fill');
933
- expect(fill?.style.width).toBe('37.5%');
934
- });
935
-
936
- test('should update max value', () => {
937
- const slider = createMockSlider({
938
- min: 0,
939
- max: 100,
940
- value: 50
941
- });
942
-
943
- slider.setMax(200);
944
-
945
- expect(slider.getMax()).toBe(200);
946
-
947
- // Value percentage should now be (50-0)/(200-0) = 50/200 = 25%
948
- const fill = slider.element.querySelector('.mtrl-slider__fill');
949
- expect(fill?.style.width).toBe('25%');
950
- });
951
-
952
- test('should adjust value when min/max changes', () => {
953
- const slider = createMockSlider({
954
- min: 0,
955
- max: 100,
956
- value: 10
957
- });
958
-
959
- // Value should be adjusted to new minimum
960
- slider.setMin(20);
961
- expect(slider.getValue()).toBe(20);
962
-
963
- // Set value to test max adjustment
964
- slider.setValue(90);
965
-
966
- // Value should be adjusted to new maximum
967
- slider.setMax(80);
968
- expect(slider.getValue()).toBe(80);
969
- });
970
-
971
- test('should update step size', () => {
972
- const slider = createMockSlider({
973
- min: 0,
974
- max: 10,
975
- step: 1,
976
- ticks: true
977
- });
978
-
979
- // Initially should have 11 ticks (0 to 10 in steps of 1)
980
- let ticks = slider.element.querySelectorAll('.mtrl-slider__tick');
981
- expect(ticks.length).toBe(11);
982
-
983
- slider.setStep(2);
984
-
985
- expect(slider.getStep()).toBe(2);
986
-
987
- // After updating step to 2, should have 6 ticks (0, 2, 4, 6, 8, 10)
988
- ticks = slider.element.querySelectorAll('.mtrl-slider__tick');
989
- expect(ticks.length).toBe(6);
990
- });
991
-
992
- test('should adjust value to new step', () => {
993
- const slider = createMockSlider({
994
- min: 0,
995
- max: 10,
996
- value: 3,
997
- step: 1,
998
- snapToSteps: true
999
- });
1000
-
1001
- slider.setStep(5);
1002
-
1003
- // Value should be adjusted to nearest step of 5
1004
- expect(slider.getValue()).toBe(5);
1005
- });
1006
-
1007
- test('should enable and disable slider', () => {
1008
- const slider = createMockSlider();
1009
-
1010
- expect(slider.isDisabled()).toBe(false);
1011
-
1012
- slider.disable();
1013
-
1014
- expect(slider.isDisabled()).toBe(true);
1015
- expect(slider.element.className).toContain('mtrl-slider--disabled');
1016
-
1017
- slider.enable();
1018
-
1019
- expect(slider.isDisabled()).toBe(false);
1020
- expect(slider.element.className).not.toContain('mtrl-slider--disabled');
1021
- });
1022
-
1023
- test('should change color', () => {
1024
- const slider = createMockSlider({
1025
- color: SLIDER_COLORS.PRIMARY
1026
- });
1027
-
1028
- expect(slider.getColor()).toBe(SLIDER_COLORS.PRIMARY);
1029
- expect(slider.element.className).toContain('mtrl-slider--primary');
1030
-
1031
- slider.setColor(SLIDER_COLORS.SECONDARY);
1032
-
1033
- expect(slider.getColor()).toBe(SLIDER_COLORS.SECONDARY);
1034
- expect(slider.element.className).toContain('mtrl-slider--secondary');
1035
- expect(slider.element.className).not.toContain('mtrl-slider--primary');
1036
- });
1037
-
1038
- test('should change size', () => {
1039
- const slider = createMockSlider({
1040
- size: SLIDER_SIZES.MEDIUM
1041
- });
1042
-
1043
- expect(slider.getSize()).toBe(SLIDER_SIZES.MEDIUM);
1044
- expect(slider.element.className).toContain('mtrl-slider--medium');
1045
-
1046
- slider.setSize(SLIDER_SIZES.LARGE);
1047
-
1048
- expect(slider.getSize()).toBe(SLIDER_SIZES.LARGE);
1049
- expect(slider.element.className).toContain('mtrl-slider--large');
1050
- expect(slider.element.className).not.toContain('mtrl-slider--medium');
1051
- });
1052
-
1053
- test('should toggle tick marks', () => {
1054
- const slider = createMockSlider({
1055
- ticks: false
1056
- });
1057
-
1058
- expect(slider.element.className).not.toContain('mtrl-slider--ticks');
1059
- expect(slider.element.querySelector('.mtrl-slider__ticks')).toBeNull();
1060
-
1061
- slider.showTicks(true);
1062
-
1063
- expect(slider.element.className).toContain('mtrl-slider--ticks');
1064
- expect(slider.element.querySelector('.mtrl-slider__ticks')).not.toBeNull();
1065
-
1066
- slider.showTicks(false);
1067
-
1068
- expect(slider.element.className).not.toContain('mtrl-slider--ticks');
1069
- expect(slider.element.querySelector('.mtrl-slider__ticks')).toBeNull();
1070
- });
1071
-
1072
- test('should toggle value display', () => {
1073
- const slider = createMockSlider({
1074
- showValue: false,
1075
- value: 50
1076
- });
1077
-
1078
- expect(slider.element.querySelector('.mtrl-slider__value')).toBeNull();
1079
-
1080
- slider.showCurrentValue(true);
1081
-
1082
- const valueElement = slider.element.querySelector('.mtrl-slider__value');
1083
- expect(valueElement).not.toBeNull();
1084
- expect(valueElement?.textContent).toBe('50');
1085
-
1086
- slider.showCurrentValue(false);
1087
-
1088
- expect(slider.element.querySelector('.mtrl-slider__value')).toBeNull();
1089
- });
1090
-
1091
- test('should change label', () => {
1092
- const slider = createMockSlider();
1093
-
1094
- expect(slider.getLabel()).toBe('');
1095
- expect(slider.element.querySelector('.mtrl-slider__label')).toBeNull();
1096
-
1097
- slider.setLabel('Volume');
1098
-
1099
- expect(slider.getLabel()).toBe('Volume');
1100
- const label = slider.element.querySelector('.mtrl-slider__label');
1101
- expect(label).not.toBeNull();
1102
- expect(label?.textContent).toBe('Volume');
1103
-
1104
- // Change existing label
1105
- slider.setLabel('Brightness');
1106
- expect(label?.textContent).toBe('Brightness');
1107
-
1108
- // Remove label
1109
- slider.setLabel('');
1110
- expect(slider.element.querySelector('.mtrl-slider__label')).toBeNull();
1111
- });
1112
-
1113
- test('should change icon', () => {
1114
- const slider = createMockSlider();
1115
-
1116
- expect(slider.getIcon()).toBe('');
1117
- expect(slider.element.querySelector('.mtrl-slider__icon')).toBeNull();
1118
-
1119
- const iconHtml = '<svg>volume</svg>';
1120
- slider.setIcon(iconHtml);
1121
-
1122
- expect(slider.getIcon()).toBe(iconHtml);
1123
- const icon = slider.element.querySelector('.mtrl-slider__icon');
1124
- expect(icon).not.toBeNull();
1125
- expect(icon?.innerHTML).toBe(iconHtml);
1126
-
1127
- // Change existing icon
1128
- const newIconHtml = '<svg>brightness</svg>';
1129
- slider.setIcon(newIconHtml);
1130
- expect(icon?.innerHTML).toBe(newIconHtml);
1131
-
1132
- // Remove icon
1133
- slider.setIcon('');
1134
- expect(slider.element.querySelector('.mtrl-slider__icon')).toBeNull();
1135
- });
1136
-
1137
- test('should emit change events', () => {
1138
- const slider = createMockSlider({
1139
- value: 0
1140
- });
1141
-
1142
- let changeEventFired = false;
1143
- let eventValue: number | null = null;
1144
-
1145
- slider.on(SLIDER_EVENTS.CHANGE, (event) => {
1146
- changeEventFired = true;
1147
- eventValue = event.value;
1148
- });
1149
-
1150
- slider.setValue(50, true);
1151
-
1152
- expect(changeEventFired).toBe(true);
1153
- expect(eventValue).toBe(50);
1154
- });
1155
-
1156
- test('should emit input events', () => {
1157
- const slider = createMockSlider({
1158
- value: 0
1159
- });
1160
-
1161
- let inputEventFired = false;
1162
-
1163
- slider.on(SLIDER_EVENTS.INPUT, () => {
1164
- inputEventFired = true;
1165
- });
1166
-
1167
- slider.setValue(50, true);
1168
-
1169
- expect(inputEventFired).toBe(true);
1170
- });
1171
-
1172
- test('should include secondValue in events for range slider', () => {
1173
- const slider = createMockSlider({
1174
- range: true,
1175
- value: 25,
1176
- secondValue: 75
1177
- });
1178
-
1179
- let secondValue: number | null = null;
1180
-
1181
- slider.on(SLIDER_EVENTS.CHANGE, (event) => {
1182
- secondValue = event.secondValue;
1183
- });
1184
-
1185
- slider.setValue(30, true);
1186
-
1187
- expect(secondValue).toBe(75);
1188
- });
1189
-
1190
- test('should remove event listeners', () => {
1191
- const slider = createMockSlider();
1192
-
1193
- let eventCount = 0;
1194
-
1195
- const handler = () => {
1196
- eventCount++;
1197
- };
1198
-
1199
- slider.on(SLIDER_EVENTS.CHANGE, handler);
1200
-
1201
- slider.setValue(10, true);
1202
- expect(eventCount).toBe(1);
1203
-
1204
- slider.off(SLIDER_EVENTS.CHANGE, handler);
1205
-
1206
- slider.setValue(20, true);
1207
- expect(eventCount).toBe(1); // Count should not increase
1208
- });
1209
-
1210
- test('should be properly destroyed', () => {
1211
- const slider = createMockSlider();
1212
- document.body.appendChild(slider.element);
1213
-
1214
- expect(document.body.contains(slider.element)).toBe(true);
1215
-
1216
- slider.destroy();
1217
-
1218
- expect(document.body.contains(slider.element)).toBe(false);
1219
- });
1220
- });