mtrl 0.2.9 → 0.3.1

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 (99) hide show
  1. package/CLAUDE.md +33 -0
  2. package/package.json +3 -1
  3. package/src/components/button/button.ts +34 -5
  4. package/src/components/navigation/index.ts +4 -1
  5. package/src/components/navigation/system/core.ts +302 -0
  6. package/src/components/navigation/system/events.ts +240 -0
  7. package/src/components/navigation/system/index.ts +184 -0
  8. package/src/components/navigation/system/mobile.ts +278 -0
  9. package/src/components/navigation/system/state.ts +77 -0
  10. package/src/components/navigation/system/types.ts +364 -0
  11. package/src/components/navigation/types.ts +33 -0
  12. package/src/components/slider/config.ts +2 -2
  13. package/src/components/slider/features/controller.ts +1 -25
  14. package/src/components/slider/features/handlers.ts +0 -1
  15. package/src/components/slider/features/range.ts +7 -7
  16. package/src/components/slider/{structure.ts → schema.ts} +2 -13
  17. package/src/components/slider/slider.ts +3 -2
  18. package/src/components/snackbar/index.ts +7 -1
  19. package/src/components/snackbar/types.ts +25 -0
  20. package/src/components/switch/api.ts +16 -0
  21. package/src/components/switch/config.ts +1 -18
  22. package/src/components/switch/features.ts +198 -0
  23. package/src/components/switch/index.ts +6 -1
  24. package/src/components/switch/switch.ts +3 -3
  25. package/src/components/switch/types.ts +27 -2
  26. package/src/components/textfield/index.ts +7 -1
  27. package/src/components/textfield/types.ts +36 -0
  28. package/src/core/composition/features/dom.ts +26 -14
  29. package/src/core/composition/features/icon.ts +18 -18
  30. package/src/core/composition/features/index.ts +3 -2
  31. package/src/core/composition/features/label.ts +16 -17
  32. package/src/core/composition/features/layout.ts +47 -0
  33. package/src/core/composition/index.ts +4 -4
  34. package/src/core/layout/README.md +350 -0
  35. package/src/core/layout/array.ts +181 -0
  36. package/src/core/layout/create.ts +55 -0
  37. package/src/core/layout/index.ts +26 -0
  38. package/src/core/layout/object.ts +124 -0
  39. package/src/core/layout/processor.ts +58 -0
  40. package/src/core/layout/result.ts +85 -0
  41. package/src/core/layout/types.ts +125 -0
  42. package/src/core/layout/utils.ts +136 -0
  43. package/src/styles/abstract/_variables.scss +28 -0
  44. package/src/styles/components/_switch.scss +133 -69
  45. package/src/styles/components/_textfield.scss +9 -16
  46. package/test/components/badge.test.ts +545 -0
  47. package/test/components/bottom-app-bar.test.ts +303 -0
  48. package/test/components/button.test.ts +233 -0
  49. package/test/components/card.test.ts +560 -0
  50. package/test/components/carousel.test.ts +951 -0
  51. package/test/components/checkbox.test.ts +462 -0
  52. package/test/components/chip.test.ts +692 -0
  53. package/test/components/datepicker.test.ts +1124 -0
  54. package/test/components/dialog.test.ts +990 -0
  55. package/test/components/divider.test.ts +412 -0
  56. package/test/components/extended-fab.test.ts +672 -0
  57. package/test/components/fab.test.ts +561 -0
  58. package/test/components/list.test.ts +365 -0
  59. package/test/components/menu.test.ts +718 -0
  60. package/test/components/navigation.test.ts +186 -0
  61. package/test/components/progress.test.ts +567 -0
  62. package/test/components/radios.test.ts +699 -0
  63. package/test/components/search.test.ts +1135 -0
  64. package/test/components/segmented-button.test.ts +732 -0
  65. package/test/components/sheet.test.ts +641 -0
  66. package/test/components/slider.test.ts +1220 -0
  67. package/test/components/snackbar.test.ts +461 -0
  68. package/test/components/switch.test.ts +452 -0
  69. package/test/components/tabs.test.ts +1369 -0
  70. package/test/components/textfield.test.ts +400 -0
  71. package/test/components/timepicker.test.ts +592 -0
  72. package/test/components/tooltip.test.ts +630 -0
  73. package/test/components/top-app-bar.test.ts +566 -0
  74. package/test/core/dom.attributes.test.ts +148 -0
  75. package/test/core/dom.classes.test.ts +152 -0
  76. package/test/core/dom.events.test.ts +243 -0
  77. package/test/core/emitter.test.ts +141 -0
  78. package/test/core/ripple.test.ts +99 -0
  79. package/test/core/state.store.test.ts +189 -0
  80. package/test/core/utils.normalize.test.ts +61 -0
  81. package/test/core/utils.object.test.ts +120 -0
  82. package/test/setup.ts +451 -0
  83. package/tsconfig.json +2 -2
  84. package/src/components/navigation/system-types.ts +0 -124
  85. package/src/components/navigation/system.ts +0 -776
  86. package/src/components/snackbar/constants.ts +0 -26
  87. package/src/core/composition/features/structure.ts +0 -22
  88. package/src/core/layout/index.js +0 -95
  89. package/src/core/structure.ts +0 -288
  90. package/test/components/button.test.js +0 -170
  91. package/test/components/checkbox.test.js +0 -238
  92. package/test/components/list.test.js +0 -105
  93. package/test/components/menu.test.js +0 -385
  94. package/test/components/navigation.test.js +0 -227
  95. package/test/components/snackbar.test.js +0 -234
  96. package/test/components/switch.test.js +0 -186
  97. package/test/components/textfield.test.js +0 -314
  98. package/test/core/emitter.test.js +0 -141
  99. package/test/core/ripple.test.js +0 -66
@@ -0,0 +1,567 @@
1
+ // test/components/progress.test.ts
2
+ import { describe, test, expect } from 'bun:test';
3
+ import {
4
+ type ProgressComponent,
5
+ type ProgressConfig,
6
+ type ProgressVariant,
7
+ type ProgressEvent
8
+ } from '../../src/components/progress/types';
9
+
10
+ // Constants for progress variants
11
+ const PROGRESS_VARIANTS = {
12
+ LINEAR: 'linear',
13
+ CIRCULAR: 'circular'
14
+ } as const;
15
+
16
+ // Constants for progress events
17
+ const PROGRESS_EVENTS = {
18
+ CHANGE: 'change',
19
+ COMPLETE: 'complete'
20
+ } as const;
21
+
22
+ // Mock progress component implementation
23
+ const createMockProgress = (config: ProgressConfig = {}): ProgressComponent => {
24
+ // Create main elements
25
+ const element = document.createElement('div');
26
+ element.className = 'mtrl-progress';
27
+
28
+ const trackElement = document.createElement('div');
29
+ trackElement.className = 'mtrl-progress-track';
30
+
31
+ const indicatorElement = document.createElement('div');
32
+ indicatorElement.className = 'mtrl-progress-indicator';
33
+
34
+ // Default settings
35
+ const settings = {
36
+ variant: config.variant || PROGRESS_VARIANTS.LINEAR,
37
+ value: config.value !== undefined ? config.value : 0,
38
+ max: config.max !== undefined ? config.max : 100,
39
+ buffer: config.buffer !== undefined ? config.buffer : 0,
40
+ disabled: config.disabled || false,
41
+ indeterminate: config.indeterminate || false,
42
+ showLabel: config.showLabel || false,
43
+ labelFormatter: config.labelFormatter || ((value, max) => `${Math.round((value/max) * 100)}%`),
44
+ componentName: 'progress',
45
+ prefix: config.prefix || 'mtrl'
46
+ };
47
+
48
+ // Apply variant class
49
+ element.classList.add(`mtrl-progress--${settings.variant}`);
50
+
51
+ // Apply disabled state
52
+ if (settings.disabled) {
53
+ element.classList.add('mtrl-progress--disabled');
54
+ }
55
+
56
+ // Apply indeterminate state
57
+ if (settings.indeterminate) {
58
+ element.classList.add('mtrl-progress--indeterminate');
59
+ }
60
+
61
+ // Apply additional classes
62
+ if (config.class) {
63
+ const classes = config.class.split(' ');
64
+ classes.forEach(className => element.classList.add(className));
65
+ }
66
+
67
+ // Create buffer element for linear variant
68
+ let bufferElement: HTMLElement | undefined;
69
+ if (settings.variant === PROGRESS_VARIANTS.LINEAR) {
70
+ bufferElement = document.createElement('div');
71
+ bufferElement.className = 'mtrl-progress-buffer';
72
+ trackElement.appendChild(bufferElement);
73
+ }
74
+
75
+ // Create label element if enabled
76
+ let labelElement: HTMLElement | undefined;
77
+ if (settings.showLabel) {
78
+ labelElement = document.createElement('div');
79
+ labelElement.className = 'mtrl-progress-label';
80
+ labelElement.textContent = settings.labelFormatter(settings.value, settings.max);
81
+ }
82
+
83
+ // Assemble elements
84
+ trackElement.appendChild(indicatorElement);
85
+ element.appendChild(trackElement);
86
+
87
+ if (labelElement) {
88
+ element.appendChild(labelElement);
89
+ }
90
+
91
+ // Track event handlers
92
+ const eventHandlers: Record<string, Function[]> = {};
93
+
94
+ // Update visuals based on current value
95
+ const updateVisuals = () => {
96
+ if (!settings.indeterminate) {
97
+ const percentage = (settings.value / settings.max) * 100;
98
+
99
+ if (settings.variant === PROGRESS_VARIANTS.LINEAR) {
100
+ indicatorElement.style.width = `${percentage}%`;
101
+
102
+ if (bufferElement) {
103
+ const bufferPercentage = (settings.buffer / settings.max) * 100;
104
+ bufferElement.style.width = `${bufferPercentage}%`;
105
+ }
106
+ } else if (settings.variant === PROGRESS_VARIANTS.CIRCULAR) {
107
+ // For circular progress, update stroke-dashoffset or similar property
108
+ const circumference = 100; // Simplified for testing
109
+ const offset = circumference - (percentage / 100 * circumference);
110
+ indicatorElement.style.setProperty('--progress-offset', `${offset}`);
111
+ }
112
+
113
+ // Update label if present
114
+ if (labelElement) {
115
+ labelElement.textContent = settings.labelFormatter(settings.value, settings.max);
116
+ }
117
+ }
118
+ };
119
+
120
+ // Initialize visuals
121
+ updateVisuals();
122
+
123
+ // Emit an event
124
+ const emit = (event: string, data?: any) => {
125
+ if (eventHandlers[event]) {
126
+ eventHandlers[event].forEach(handler => handler(data));
127
+ }
128
+ };
129
+
130
+ // Create the progress component
131
+ const progress: ProgressComponent = {
132
+ element,
133
+ trackElement,
134
+ indicatorElement,
135
+ bufferElement,
136
+ labelElement,
137
+
138
+ getClass: (name: string) => {
139
+ const prefix = settings.prefix;
140
+ return name ? `${prefix}-${name}` : `${prefix}-progress`;
141
+ },
142
+
143
+ setValue: (value: number) => {
144
+ // Ensure value is within bounds
145
+ value = Math.max(0, Math.min(value, settings.max));
146
+
147
+ const oldValue = settings.value;
148
+ settings.value = value;
149
+
150
+ // Update visuals
151
+ updateVisuals();
152
+
153
+ // Emit change event
154
+ emit(PROGRESS_EVENTS.CHANGE, { value, oldValue });
155
+
156
+ // Emit complete event if reached 100%
157
+ if (value === settings.max && oldValue !== settings.max) {
158
+ emit(PROGRESS_EVENTS.COMPLETE, { value });
159
+ }
160
+
161
+ return progress;
162
+ },
163
+
164
+ getValue: () => settings.value,
165
+
166
+ setBuffer: (value: number) => {
167
+ // Ensure buffer is within bounds
168
+ value = Math.max(0, Math.min(value, settings.max));
169
+ settings.buffer = value;
170
+
171
+ // Update visuals
172
+ updateVisuals();
173
+
174
+ return progress;
175
+ },
176
+
177
+ getBuffer: () => settings.buffer,
178
+
179
+ enable: () => {
180
+ progress.disabled.enable();
181
+ return progress;
182
+ },
183
+
184
+ disable: () => {
185
+ progress.disabled.disable();
186
+ return progress;
187
+ },
188
+
189
+ isDisabled: () => progress.disabled.isDisabled(),
190
+
191
+ showLabel: () => {
192
+ settings.showLabel = true;
193
+
194
+ if (!labelElement) {
195
+ labelElement = document.createElement('div');
196
+ labelElement.className = 'mtrl-progress-label';
197
+ labelElement.textContent = settings.labelFormatter(settings.value, settings.max);
198
+ element.appendChild(labelElement);
199
+ progress.labelElement = labelElement;
200
+ }
201
+
202
+ return progress;
203
+ },
204
+
205
+ hideLabel: () => {
206
+ settings.showLabel = false;
207
+
208
+ if (labelElement) {
209
+ element.removeChild(labelElement);
210
+ labelElement = undefined;
211
+ progress.labelElement = undefined;
212
+ }
213
+
214
+ return progress;
215
+ },
216
+
217
+ setLabelFormatter: (formatter: (value: number, max: number) => string) => {
218
+ settings.labelFormatter = formatter;
219
+
220
+ if (labelElement) {
221
+ labelElement.textContent = formatter(settings.value, settings.max);
222
+ }
223
+
224
+ return progress;
225
+ },
226
+
227
+ setIndeterminate: (indeterminate: boolean) => {
228
+ settings.indeterminate = indeterminate;
229
+
230
+ if (indeterminate) {
231
+ element.classList.add('mtrl-progress--indeterminate');
232
+ } else {
233
+ element.classList.remove('mtrl-progress--indeterminate');
234
+ updateVisuals();
235
+ }
236
+
237
+ return progress;
238
+ },
239
+
240
+ isIndeterminate: () => settings.indeterminate,
241
+
242
+ on: (event: string, handler: Function) => {
243
+ if (!eventHandlers[event]) {
244
+ eventHandlers[event] = [];
245
+ }
246
+
247
+ eventHandlers[event].push(handler);
248
+ return progress;
249
+ },
250
+
251
+ off: (event: string, handler: Function) => {
252
+ if (eventHandlers[event]) {
253
+ eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
254
+ }
255
+
256
+ return progress;
257
+ },
258
+
259
+ destroy: () => {
260
+ // Remove element from DOM if it has a parent
261
+ if (element.parentNode) {
262
+ element.parentNode.removeChild(element);
263
+ }
264
+
265
+ // Clear event handlers
266
+ for (const event in eventHandlers) {
267
+ eventHandlers[event] = [];
268
+ }
269
+ },
270
+
271
+ addClass: (...classes: string[]) => {
272
+ classes.forEach(className => element.classList.add(className));
273
+ return progress;
274
+ },
275
+
276
+ disabled: {
277
+ enable: () => {
278
+ settings.disabled = false;
279
+ element.classList.remove('mtrl-progress--disabled');
280
+ },
281
+
282
+ disable: () => {
283
+ settings.disabled = true;
284
+ element.classList.add('mtrl-progress--disabled');
285
+ },
286
+
287
+ isDisabled: () => settings.disabled
288
+ },
289
+
290
+ lifecycle: {
291
+ destroy: () => {
292
+ progress.destroy();
293
+ }
294
+ }
295
+ };
296
+
297
+ return progress;
298
+ };
299
+
300
+ describe('Progress Component', () => {
301
+ test('should create a progress element', () => {
302
+ const progress = createMockProgress();
303
+
304
+ expect(progress.element).toBeDefined();
305
+ expect(progress.element.tagName).toBe('DIV');
306
+ expect(progress.element.className).toContain('mtrl-progress');
307
+
308
+ expect(progress.trackElement).toBeDefined();
309
+ expect(progress.trackElement.className).toContain('mtrl-progress-track');
310
+
311
+ expect(progress.indicatorElement).toBeDefined();
312
+ expect(progress.indicatorElement.className).toContain('mtrl-progress-indicator');
313
+ });
314
+
315
+ test('should apply linear variant by default', () => {
316
+ const progress = createMockProgress();
317
+
318
+ expect(progress.element.className).toContain('mtrl-progress--linear');
319
+ expect(progress.bufferElement).toBeDefined();
320
+ });
321
+
322
+ test('should apply circular variant when specified', () => {
323
+ const progress = createMockProgress({
324
+ variant: PROGRESS_VARIANTS.CIRCULAR
325
+ });
326
+
327
+ expect(progress.element.className).toContain('mtrl-progress--circular');
328
+ expect(progress.bufferElement).toBeUndefined();
329
+ });
330
+
331
+ test('should set initial value', () => {
332
+ const progress = createMockProgress({
333
+ value: 25
334
+ });
335
+
336
+ expect(progress.getValue()).toBe(25);
337
+ expect(progress.indicatorElement.style.width).toBe('25%');
338
+ });
339
+
340
+ test('should set initial buffer value', () => {
341
+ const progress = createMockProgress({
342
+ buffer: 50
343
+ });
344
+
345
+ expect(progress.getBuffer()).toBe(50);
346
+ expect(progress.bufferElement?.style.width).toBe('50%');
347
+ });
348
+
349
+ test('should apply disabled state', () => {
350
+ const progress = createMockProgress({
351
+ disabled: true
352
+ });
353
+
354
+ expect(progress.element.className).toContain('mtrl-progress--disabled');
355
+ expect(progress.isDisabled()).toBe(true);
356
+ });
357
+
358
+ test('should apply indeterminate state', () => {
359
+ const progress = createMockProgress({
360
+ indeterminate: true
361
+ });
362
+
363
+ expect(progress.element.className).toContain('mtrl-progress--indeterminate');
364
+ expect(progress.isIndeterminate()).toBe(true);
365
+ });
366
+
367
+ test('should show label when configured', () => {
368
+ const progress = createMockProgress({
369
+ showLabel: true,
370
+ value: 75
371
+ });
372
+
373
+ expect(progress.labelElement).toBeDefined();
374
+ expect(progress.labelElement?.textContent).toBe('75%');
375
+ });
376
+
377
+ test('should use custom label formatter', () => {
378
+ const formatter = (value: number, max: number) => `${value} of ${max}`;
379
+
380
+ const progress = createMockProgress({
381
+ showLabel: true,
382
+ value: 50,
383
+ max: 200,
384
+ labelFormatter: formatter
385
+ });
386
+
387
+ expect(progress.labelElement?.textContent).toBe('50 of 200');
388
+ });
389
+
390
+ test('should update value and appearance', () => {
391
+ const progress = createMockProgress({
392
+ value: 0
393
+ });
394
+
395
+ expect(progress.getValue()).toBe(0);
396
+ expect(progress.indicatorElement.style.width).toBe('0%');
397
+
398
+ progress.setValue(60);
399
+
400
+ expect(progress.getValue()).toBe(60);
401
+ expect(progress.indicatorElement.style.width).toBe('60%');
402
+ });
403
+
404
+ test('should update buffer value', () => {
405
+ const progress = createMockProgress();
406
+
407
+ expect(progress.getBuffer()).toBe(0);
408
+
409
+ progress.setBuffer(75);
410
+
411
+ expect(progress.getBuffer()).toBe(75);
412
+ expect(progress.bufferElement?.style.width).toBe('75%');
413
+ });
414
+
415
+ test('should constrain values within bounds', () => {
416
+ const progress = createMockProgress({
417
+ max: 100
418
+ });
419
+
420
+ progress.setValue(-10);
421
+ expect(progress.getValue()).toBe(0);
422
+
423
+ progress.setValue(150);
424
+ expect(progress.getValue()).toBe(100);
425
+
426
+ progress.setBuffer(-20);
427
+ expect(progress.getBuffer()).toBe(0);
428
+
429
+ progress.setBuffer(200);
430
+ expect(progress.getBuffer()).toBe(100);
431
+ });
432
+
433
+ test('should toggle disabled state', () => {
434
+ const progress = createMockProgress();
435
+
436
+ expect(progress.isDisabled()).toBe(false);
437
+
438
+ progress.disable();
439
+
440
+ expect(progress.isDisabled()).toBe(true);
441
+ expect(progress.element.className).toContain('mtrl-progress--disabled');
442
+
443
+ progress.enable();
444
+
445
+ expect(progress.isDisabled()).toBe(false);
446
+ expect(progress.element.className).not.toContain('mtrl-progress--disabled');
447
+ });
448
+
449
+ test('should toggle indeterminate state', () => {
450
+ const progress = createMockProgress();
451
+
452
+ expect(progress.isIndeterminate()).toBe(false);
453
+
454
+ progress.setIndeterminate(true);
455
+
456
+ expect(progress.isIndeterminate()).toBe(true);
457
+ expect(progress.element.className).toContain('mtrl-progress--indeterminate');
458
+
459
+ progress.setIndeterminate(false);
460
+
461
+ expect(progress.isIndeterminate()).toBe(false);
462
+ expect(progress.element.className).not.toContain('mtrl-progress--indeterminate');
463
+ });
464
+
465
+ test('should show and hide label', () => {
466
+ const progress = createMockProgress();
467
+
468
+ expect(progress.labelElement).toBeUndefined();
469
+
470
+ progress.showLabel();
471
+
472
+ expect(progress.labelElement).toBeDefined();
473
+ expect(progress.element.contains(progress.labelElement)).toBe(true);
474
+
475
+ progress.hideLabel();
476
+
477
+ expect(progress.labelElement).toBeUndefined();
478
+ expect(progress.element.querySelector('.mtrl-progress-label')).toBeNull();
479
+ });
480
+
481
+ test('should update label formatter', () => {
482
+ const progress = createMockProgress({
483
+ showLabel: true,
484
+ value: 50
485
+ });
486
+
487
+ expect(progress.labelElement?.textContent).toBe('50%');
488
+
489
+ progress.setLabelFormatter((value) => `${value} units`);
490
+
491
+ expect(progress.labelElement?.textContent).toBe('50 units');
492
+ });
493
+
494
+ test('should emit change event when value changes', () => {
495
+ const progress = createMockProgress();
496
+ let changeEventFired = false;
497
+ let eventData = null;
498
+
499
+ progress.on(PROGRESS_EVENTS.CHANGE, (data) => {
500
+ changeEventFired = true;
501
+ eventData = data;
502
+ });
503
+
504
+ progress.setValue(50);
505
+
506
+ expect(changeEventFired).toBe(true);
507
+ expect(eventData).toEqual({ value: 50, oldValue: 0 });
508
+ });
509
+
510
+ test('should emit complete event when reaching max', () => {
511
+ const progress = createMockProgress({
512
+ value: 50,
513
+ max: 100
514
+ });
515
+
516
+ let completeEventFired = false;
517
+
518
+ progress.on(PROGRESS_EVENTS.COMPLETE, () => {
519
+ completeEventFired = true;
520
+ });
521
+
522
+ progress.setValue(99);
523
+ expect(completeEventFired).toBe(false);
524
+
525
+ progress.setValue(100);
526
+ expect(completeEventFired).toBe(true);
527
+ });
528
+
529
+ test('should remove event listeners', () => {
530
+ const progress = createMockProgress();
531
+ let eventCount = 0;
532
+
533
+ const handler = () => {
534
+ eventCount++;
535
+ };
536
+
537
+ progress.on(PROGRESS_EVENTS.CHANGE, handler);
538
+
539
+ progress.setValue(10);
540
+ expect(eventCount).toBe(1);
541
+
542
+ progress.off(PROGRESS_EVENTS.CHANGE, handler);
543
+
544
+ progress.setValue(20);
545
+ expect(eventCount).toBe(1); // Count should not increase
546
+ });
547
+
548
+ test('should add CSS classes', () => {
549
+ const progress = createMockProgress();
550
+
551
+ progress.addClass('custom-class', 'special-progress');
552
+
553
+ expect(progress.element.className).toContain('custom-class');
554
+ expect(progress.element.className).toContain('special-progress');
555
+ });
556
+
557
+ test('should be properly destroyed', () => {
558
+ const progress = createMockProgress();
559
+ document.body.appendChild(progress.element);
560
+
561
+ expect(document.body.contains(progress.element)).toBe(true);
562
+
563
+ progress.destroy();
564
+
565
+ expect(document.body.contains(progress.element)).toBe(false);
566
+ });
567
+ });