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
package/test/setup.ts ADDED
@@ -0,0 +1,451 @@
1
+ // test/setup.ts
2
+ // Setup global DOM environment for testing
3
+
4
+ /**
5
+ * Mock Element implementation for testing
6
+ */
7
+ class MockElement {
8
+ tagName: string;
9
+ className: string;
10
+ style: Record<string, string>;
11
+ attributes: Record<string, string>;
12
+ children: MockElement[];
13
+ childNodes: MockElement[];
14
+ __eventListeners: Record<string, Function[]>;
15
+ __handlers: Record<string, Function[]>;
16
+ innerHTML: string;
17
+ textContent: string;
18
+ dataset: Record<string, string>;
19
+ parentNode: MockElement | null;
20
+ disabled?: boolean;
21
+
22
+ constructor(tagName: string) {
23
+ this.tagName = tagName.toUpperCase();
24
+ this.className = '';
25
+ this.style = {};
26
+ this.attributes = {};
27
+ this.children = [];
28
+ this.childNodes = [];
29
+ this.__eventListeners = {};
30
+ this.__handlers = {};
31
+ this.innerHTML = '';
32
+ this.textContent = '';
33
+ this.dataset = {};
34
+ this.parentNode = null;
35
+
36
+ // Explicitly add __handlers for the tests that expect it
37
+ this.__handlers = {};
38
+ }
39
+
40
+ appendChild(child: MockElement): MockElement {
41
+ this.children.push(child);
42
+ this.childNodes.push(child);
43
+ child.parentNode = this;
44
+ return child;
45
+ }
46
+
47
+ insertBefore(newChild: MockElement, referenceChild: MockElement | null): MockElement {
48
+ const index = referenceChild ? this.children.indexOf(referenceChild) : 0;
49
+ if (index === -1) {
50
+ this.children.push(newChild);
51
+ } else {
52
+ this.children.splice(index, 0, newChild);
53
+ }
54
+ this.childNodes = [...this.children];
55
+ newChild.parentNode = this;
56
+ return newChild;
57
+ }
58
+
59
+ removeChild(child: MockElement): MockElement {
60
+ const index = this.children.indexOf(child);
61
+ if (index !== -1) {
62
+ this.children.splice(index, 1);
63
+ this.childNodes = [...this.children];
64
+ child.parentNode = null;
65
+ }
66
+ return child;
67
+ }
68
+
69
+ getAttribute(name: string): string | null {
70
+ return this.attributes[name] || null;
71
+ }
72
+
73
+ setAttribute(name: string, value: string): void {
74
+ this.attributes[name] = value;
75
+ if (name === 'class') this.className = value;
76
+ if (name === 'disabled') this.disabled = true;
77
+ }
78
+
79
+ removeAttribute(name: string): void {
80
+ delete this.attributes[name];
81
+ if (name === 'class') this.className = '';
82
+ if (name === 'disabled') this.disabled = false;
83
+ }
84
+
85
+ hasAttribute(name: string): boolean {
86
+ return name in this.attributes;
87
+ }
88
+
89
+ querySelector(selector: string): MockElement | null {
90
+ // Basic selector implementation for testing
91
+ if (selector.startsWith('.')) {
92
+ // Class selector
93
+ const className = selector.substring(1);
94
+ return this.getElementsByClassName(className)[0] || null;
95
+ } else if (selector.includes(',')) {
96
+ // Multiple selectors (comma-separated)
97
+ const subSelectors = selector.split(',').map(s => s.trim());
98
+ for (const subSelector of subSelectors) {
99
+ const match = this.querySelector(subSelector);
100
+ if (match) return match;
101
+ }
102
+ return null;
103
+ }
104
+ // Default
105
+ return null;
106
+ }
107
+
108
+ querySelectorAll(selector: string): MockElement[] {
109
+ if (selector.startsWith('.')) {
110
+ return this.getElementsByClassName(selector.substring(1));
111
+ }
112
+ return [];
113
+ }
114
+
115
+ getElementsByClassName(className: string): MockElement[] {
116
+ const results: MockElement[] = [];
117
+ if (this.className && this.className.split(' ').includes(className)) {
118
+ results.push(this);
119
+ }
120
+ this.children.forEach(child => {
121
+ if (child.getElementsByClassName) {
122
+ results.push(...child.getElementsByClassName(className));
123
+ }
124
+ });
125
+ return results;
126
+ }
127
+
128
+ addEventListener(type: string, listener: Function): void {
129
+ // Support dual storage for different test expectations
130
+ if (!this.__eventListeners[type]) {
131
+ this.__eventListeners[type] = [];
132
+ }
133
+ this.__eventListeners[type].push(listener);
134
+
135
+ // Also store in __handlers for tests that expect it
136
+ if (!this.__handlers[type]) {
137
+ this.__handlers[type] = [];
138
+ }
139
+ this.__handlers[type].push(listener);
140
+ }
141
+
142
+ removeEventListener(type: string, listener: Function): void {
143
+ if (this.__eventListeners[type]) {
144
+ this.__eventListeners[type] = this.__eventListeners[type]
145
+ .filter(l => l !== listener);
146
+ }
147
+
148
+ if (this.__handlers && this.__handlers[type]) {
149
+ this.__handlers[type] = this.__handlers[type]
150
+ .filter(l => l !== listener);
151
+ }
152
+ }
153
+
154
+ dispatchEvent(event: Event): boolean {
155
+ event.target = this as any;
156
+ if (this.__eventListeners[event.type]) {
157
+ this.__eventListeners[event.type].forEach(listener => {
158
+ listener(event);
159
+ });
160
+ }
161
+ return !event.defaultPrevented;
162
+ }
163
+
164
+ get classList(): {
165
+ add: (...classes: string[]) => void;
166
+ remove: (...classes: string[]) => void;
167
+ toggle: (c: string) => boolean;
168
+ contains: (c: string) => boolean;
169
+ toString: () => string;
170
+ } {
171
+ const classNames = this.className ? this.className.split(' ').filter(Boolean) : [];
172
+ return {
173
+ add: (...classes: string[]) => {
174
+ classes.forEach(c => {
175
+ if (!classNames.includes(c)) {
176
+ classNames.push(c);
177
+ }
178
+ });
179
+ this.className = classNames.join(' ');
180
+ },
181
+ remove: (...classes: string[]) => {
182
+ classes.forEach(c => {
183
+ const index = classNames.indexOf(c);
184
+ if (index !== -1) {
185
+ classNames.splice(index, 1);
186
+ }
187
+ });
188
+ this.className = classNames.join(' ');
189
+ },
190
+ toggle: (c: string) => {
191
+ const index = classNames.indexOf(c);
192
+ if (index !== -1) {
193
+ classNames.splice(index, 1);
194
+ this.className = classNames.join(' ');
195
+ return false;
196
+ } else {
197
+ classNames.push(c);
198
+ this.className = classNames.join(' ');
199
+ return true;
200
+ }
201
+ },
202
+ contains: (c: string) => classNames.includes(c),
203
+ toString: () => this.className || ''
204
+ };
205
+ }
206
+
207
+ getBoundingClientRect(): DOMRect {
208
+ return {
209
+ width: 100,
210
+ height: 50,
211
+ top: 0,
212
+ left: 0,
213
+ right: 100,
214
+ bottom: 50,
215
+ x: 0,
216
+ y: 0,
217
+ toJSON: () => ({})
218
+ };
219
+ }
220
+
221
+ remove(): void {
222
+ if (this.parentNode) {
223
+ this.parentNode.removeChild(this);
224
+ }
225
+ }
226
+
227
+ // Support closest for navigation tests
228
+ closest(selector: string): MockElement | null {
229
+ // Always return an element with minimal functionality for navigation tests to work
230
+ const mockClosest = new MockElement('div');
231
+ mockClosest.className = selector.replace(/^\./, '');
232
+
233
+ // For navigation tests, ensure the element can be queried
234
+ mockClosest.querySelector = (childSelector: string) => {
235
+ const mockChild = new MockElement('div');
236
+ mockChild.className = childSelector.replace(/^\./, '');
237
+
238
+ // Further support nested querying
239
+ mockChild.querySelector = (grandchildSelector: string) => {
240
+ const mockGrandchild = new MockElement('div');
241
+ mockGrandchild.className = grandchildSelector.replace(/^\./, '');
242
+ mockGrandchild.dataset = { id: 'mock-id' };
243
+ return mockGrandchild;
244
+ };
245
+
246
+ return mockChild;
247
+ };
248
+
249
+ return mockClosest;
250
+ }
251
+
252
+ // Simple matches implementation
253
+ matches(selector: string): boolean {
254
+ if (selector.startsWith('.')) {
255
+ return this.classList.contains(selector.substring(1));
256
+ }
257
+ return false;
258
+ }
259
+ }
260
+
261
+ // Create document fragment element
262
+ class MockDocumentFragment extends MockElement {
263
+ constructor() {
264
+ super('#document-fragment');
265
+ }
266
+
267
+ hasChildNodes(): boolean {
268
+ return this.childNodes.length > 0;
269
+ }
270
+ }
271
+
272
+ // Define types for global augmentation
273
+ interface CustomGlobalThis {
274
+ document: {
275
+ createElement: (tag: string) => MockElement;
276
+ createDocumentFragment: () => MockDocumentFragment;
277
+ body: MockElement;
278
+ __eventListeners: Record<string, Function[]>;
279
+ addEventListener: (type: string, listener: Function) => void;
280
+ removeEventListener: (type: string, listener: Function) => void;
281
+ dispatchEvent: (event: Event) => boolean;
282
+ querySelector: (selector: string) => MockElement | null;
283
+ querySelectorAll: (selector: string) => MockElement[];
284
+ };
285
+ window: {
286
+ getComputedStyle: () => { position: string; getPropertyValue: (prop: string) => string };
287
+ addEventListener: (type: string, listener: Function) => void;
288
+ removeEventListener: (type: string, listener: Function) => void;
289
+ dispatchEvent: (event: Event) => boolean;
290
+ innerWidth: number;
291
+ innerHeight: number;
292
+ history: { pushState: (data: any, title: string, url?: string) => void };
293
+ location: { pathname: string };
294
+ navigator: { userAgent: string };
295
+ performance: { now: () => number };
296
+ localStorage: {
297
+ getItem: (key: string) => string | null;
298
+ setItem: (key: string, value: string) => void;
299
+ removeItem: (key: string) => void;
300
+ };
301
+ };
302
+ Element: typeof MockElement;
303
+ Event: typeof CustomEvent;
304
+ CustomEvent: typeof CustomEvent;
305
+ AbortController: typeof AbortController;
306
+ }
307
+
308
+ // Set up global document object for tests
309
+ (global as any).document = {
310
+ createElement: (tag: string) => new MockElement(tag),
311
+ createDocumentFragment: () => new MockDocumentFragment(),
312
+ body: new MockElement('body'),
313
+ __eventListeners: {},
314
+ addEventListener: function(type: string, listener: Function) {
315
+ if (!this.__eventListeners[type]) {
316
+ this.__eventListeners[type] = [];
317
+ }
318
+ this.__eventListeners[type].push(listener);
319
+ },
320
+ removeEventListener: function(type: string, listener: Function) {
321
+ if (this.__eventListeners[type]) {
322
+ this.__eventListeners[type] = this.__eventListeners[type]
323
+ .filter((l: Function) => l !== listener);
324
+ }
325
+ },
326
+ dispatchEvent: function(event: Event) {
327
+ if (this.__eventListeners[event.type]) {
328
+ this.__eventListeners[event.type].forEach((listener: Function) => {
329
+ listener(event);
330
+ });
331
+ }
332
+ return !event.defaultPrevented;
333
+ },
334
+ querySelector: (selector: string) => null,
335
+ querySelectorAll: (selector: string) => []
336
+ };
337
+
338
+ // Set up global window object
339
+ (global as any).window = {
340
+ getComputedStyle: () => ({
341
+ position: 'static',
342
+ getPropertyValue: () => ''
343
+ }),
344
+ addEventListener: () => {},
345
+ removeEventListener: () => {},
346
+ dispatchEvent: () => {},
347
+ innerWidth: 1024,
348
+ innerHeight: 768,
349
+ history: {
350
+ pushState: () => {}
351
+ },
352
+ location: {
353
+ pathname: '/'
354
+ },
355
+ navigator: {
356
+ userAgent: 'test'
357
+ },
358
+ performance: {
359
+ now: () => Date.now()
360
+ },
361
+ localStorage: {
362
+ getItem: () => null,
363
+ setItem: () => {},
364
+ removeItem: () => {}
365
+ }
366
+ };
367
+
368
+ // Event constructor
369
+ class CustomEvent {
370
+ type: string;
371
+ bubbles: boolean;
372
+ cancelable: boolean;
373
+ defaultPrevented: boolean;
374
+ target: any;
375
+ currentTarget: any;
376
+ offsetX: number;
377
+ offsetY: number;
378
+ detail: any;
379
+
380
+ constructor(type: string, options: any = {}) {
381
+ this.type = type;
382
+ this.bubbles = options.bubbles || false;
383
+ this.cancelable = options.cancelable || false;
384
+ this.defaultPrevented = false;
385
+ this.target = null;
386
+ this.currentTarget = null;
387
+ this.offsetX = options.offsetX || 0;
388
+ this.offsetY = options.offsetY || 0;
389
+ this.detail = options.detail || {};
390
+ }
391
+
392
+ preventDefault(): void {
393
+ if (this.cancelable) {
394
+ this.defaultPrevented = true;
395
+ }
396
+ }
397
+
398
+ stopPropagation(): void {}
399
+
400
+ stopImmediatePropagation(): void {}
401
+ }
402
+
403
+ // Set up Event constructor
404
+ (global as any).Event = CustomEvent;
405
+
406
+ // Set up CustomEvent constructor
407
+ (global as any).CustomEvent = class extends CustomEvent {
408
+ constructor(type: string, options: any = {}) {
409
+ super(type, options);
410
+ this.detail = options.detail || {};
411
+ }
412
+ };
413
+
414
+ // AbortController
415
+ class AbortController {
416
+ signal: { aborted: boolean };
417
+
418
+ constructor() {
419
+ this.signal = { aborted: false };
420
+ }
421
+
422
+ abort(): void {
423
+ this.signal.aborted = true;
424
+ }
425
+ }
426
+
427
+ // Set up AbortController
428
+ (global as any).AbortController = AbortController;
429
+
430
+ // Set up Element constructor
431
+ (global as any).Element = MockElement;
432
+
433
+ // Console mocking (preventing test output pollution)
434
+ const originalConsole = { ...console };
435
+ (global as any).console = {
436
+ ...console,
437
+ log: (...args: any[]) => {
438
+ if (process.env.DEBUG) {
439
+ originalConsole.log(...args);
440
+ }
441
+ },
442
+ warn: (...args: any[]) => {
443
+ if (process.env.DEBUG) {
444
+ originalConsole.warn(...args);
445
+ }
446
+ },
447
+ error: (...args: any[]) => {
448
+ // Always log errors
449
+ originalConsole.error(...args);
450
+ }
451
+ };
package/tsconfig.json CHANGED
@@ -17,6 +17,6 @@
17
17
  "noErrorTruncation": true,
18
18
  "noEmitOnError": false
19
19
  },
20
- "include": ["src/**/*"],
21
- "exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"]
20
+ "include": ["src/**/*", "test/**/*"],
21
+ "exclude": ["node_modules"]
22
22
  }
@@ -1,124 +0,0 @@
1
- // src/components/navigation/system-types.ts
2
-
3
- import { NavigationComponent, NavItemConfig } from './types';
4
-
5
- /**
6
- * Structure for navigation section with its items
7
- */
8
- export interface NavigationSection {
9
- /** Section label */
10
- label: string;
11
-
12
- /** Section icon HTML */
13
- icon?: string;
14
-
15
- /** Navigation items within this section */
16
- items: NavItemConfig[];
17
- }
18
-
19
- /**
20
- * Configuration for the navigation system
21
- */
22
- export interface NavigationSystemConfig {
23
- /** Item structure by section ID */
24
- items: Record<string, NavigationSection>;
25
-
26
- /** Initial active section */
27
- activeSection?: string;
28
-
29
- /** Initial active subsection */
30
- activeSubsection?: string;
31
-
32
- /** Whether to animate drawer opening/closing */
33
- animateDrawer?: boolean;
34
-
35
- /** Whether to show labels on rail */
36
- showLabelsOnRail?: boolean;
37
-
38
- /** Whether to hide drawer when an item is clicked */
39
- hideDrawerOnClick?: boolean;
40
-
41
- /** Delay before showing drawer on hover (ms) */
42
- hoverDelay?: number;
43
-
44
- /** Delay before hiding drawer on mouse leave (ms) */
45
- closeDelay?: number;
46
-
47
- /** Configuration options passed to rail component */
48
- railOptions?: Record<string, any>;
49
-
50
- /** Configuration options passed to drawer component */
51
- drawerOptions?: Record<string, any>;
52
-
53
- /** Enable debug logging */
54
- debug?: boolean;
55
- }
56
-
57
- /**
58
- * Event data for section change events
59
- */
60
- export interface SectionChangeEvent {
61
- /** ID of the selected section */
62
- section: string;
63
-
64
- /** Section data */
65
- sectionData: NavigationSection;
66
- }
67
-
68
- /**
69
- * Event data for item selection events
70
- */
71
- export interface ItemSelectEvent {
72
- /** ID of the parent section */
73
- section: string;
74
-
75
- /** ID of the selected subsection */
76
- subsection: string;
77
-
78
- /** Selected item data */
79
- item: any;
80
- }
81
-
82
- /**
83
- * Navigation system API
84
- */
85
- export interface NavigationSystem {
86
- /** Initialize the navigation system */
87
- initialize(): NavigationSystem;
88
-
89
- /** Clean up resources */
90
- cleanup(): void;
91
-
92
- /** Navigate to a specific section and subsection */
93
- navigateTo(section: string, subsection?: string): void;
94
-
95
- /** Get the rail component */
96
- getRail(): NavigationComponent | null;
97
-
98
- /** Get the drawer component */
99
- getDrawer(): NavigationComponent | null;
100
-
101
- /** Get the active section */
102
- getActiveSection(): string | null;
103
-
104
- /** Get the active subsection */
105
- getActiveSubsection(): string | null;
106
-
107
- /** Show the drawer */
108
- showDrawer(): void;
109
-
110
- /** Hide the drawer */
111
- hideDrawer(): void;
112
-
113
- /** Check if drawer is visible */
114
- isDrawerVisible(): boolean;
115
-
116
- /** Configure the navigation system */
117
- configure(config: Partial<NavigationSystemConfig>): NavigationSystem;
118
-
119
- /** Event handler for section changes */
120
- onSectionChange: ((section: string, subsection?: string) => void) | null;
121
-
122
- /** Event handler for item selection */
123
- onItemSelect: ((event: ItemSelectEvent) => void) | null;
124
- }