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,718 +0,0 @@
1
- // test/components/menu.test.ts
2
- import { describe, test, expect, mock, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test';
3
- import { JSDOM } from 'jsdom';
4
- import type { MenuComponent, MenuConfig, MenuItemConfig, MenuPositionConfig } from '../../src/components/menu/types';
5
-
6
- // Import constants from utils instead of constants
7
- // The test was originally using constants but they're actually in utils
8
- import {
9
- MENU_ALIGNMENT as MENU_ALIGN,
10
- MENU_VERTICAL_ALIGNMENT as MENU_VERTICAL_ALIGN,
11
- MENU_EVENT as MENU_EVENTS,
12
- MENU_ITEM_TYPE as MENU_ITEM_TYPES
13
- } from '../../src/components/menu/utils';
14
-
15
- // IMPORTANT: Due to potential circular dependencies in the actual menu component
16
- // we are using a mock implementation for tests.
17
-
18
- // Setup jsdom environment
19
- let dom: JSDOM;
20
- let window: Window;
21
- let document: Document;
22
- let originalGlobalDocument: any;
23
- let originalGlobalWindow: any;
24
- let originalCreateElement: any;
25
- let globalEventListeners: Map<any, Map<string, Set<Function>>>;
26
-
27
- beforeAll(() => {
28
- // Create a new JSDOM instance
29
- dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
30
- url: 'http://localhost/',
31
- pretendToBeVisual: true
32
- });
33
-
34
- // Get window and document from jsdom
35
- window = dom.window;
36
- document = window.document;
37
-
38
- // Store original globals
39
- originalGlobalDocument = global.document;
40
- originalGlobalWindow = global.window;
41
-
42
- // Set globals to use jsdom
43
- global.document = document;
44
- global.window = window;
45
- global.Element = window.Element;
46
- global.HTMLElement = window.HTMLElement;
47
- global.Event = window.Event;
48
-
49
- // Store original createElement method
50
- originalCreateElement = document.createElement;
51
-
52
- // Initialize event listeners map
53
- globalEventListeners = new Map();
54
- });
55
-
56
- afterAll(() => {
57
- // Restore original globals
58
- global.document = originalGlobalDocument;
59
- global.window = originalGlobalWindow;
60
-
61
- // Clean up jsdom
62
- window.close();
63
- });
64
-
65
- // Mock DOM APIs that aren't available in the test environment
66
- beforeEach(() => {
67
- // Mock Element.prototype methods
68
- Element.prototype.getBoundingClientRect = function() {
69
- return {
70
- width: 100,
71
- height: 100,
72
- top: 0,
73
- left: 0,
74
- right: 100,
75
- bottom: 100,
76
- x: 0,
77
- y: 0,
78
- toJSON: () => ({})
79
- };
80
- };
81
-
82
- Element.prototype.closest = function(selector: string) {
83
- return null; // Simple mock that returns null by default
84
- };
85
-
86
- Element.prototype.matches = function(selector: string) {
87
- return false; // Simple mock that returns false by default
88
- };
89
-
90
- // Replace createElement to add our custom methods
91
- document.createElement = function(tag: string) {
92
- const element = originalCreateElement.call(document, tag);
93
-
94
- // Add closest method for our tests
95
- element.closest = function(selector: string) {
96
- if (selector.includes('menu-item')) {
97
- return this.classList && this.classList.contains('mtrl-menu-item') ? this : null;
98
- }
99
- return null;
100
- };
101
-
102
- // Add matches method for our tests
103
- element.matches = function(selector: string) {
104
- if (selector === ':hover') return false;
105
- return this.classList && this.classList.contains(selector.replace('.', ''));
106
- };
107
-
108
- // Mock the querySelectorAll method
109
- element.querySelectorAll = function(selector: string) {
110
- return []; // Return empty array by default
111
- };
112
-
113
- // Mock the querySelector method
114
- element.querySelector = function(selector: string) {
115
- return null; // Return null by default
116
- };
117
-
118
- return element;
119
- };
120
-
121
- // Mock window properties
122
- Object.defineProperty(global.window, 'innerWidth', { value: 1024 });
123
- Object.defineProperty(global.window, 'innerHeight', { value: 768 });
124
-
125
- // Mock event listeners
126
- globalEventListeners.clear();
127
-
128
- const originalAddEventListener = Element.prototype.addEventListener;
129
- Element.prototype.addEventListener = function(event: string, handler: Function) {
130
- if (!globalEventListeners.has(this)) {
131
- globalEventListeners.set(this, new Map());
132
- }
133
- if (!globalEventListeners.get(this)!.has(event)) {
134
- globalEventListeners.get(this)!.set(event, new Set());
135
- }
136
- globalEventListeners.get(this)!.get(event)!.add(handler);
137
-
138
- // Call original if it exists
139
- if (originalAddEventListener) {
140
- originalAddEventListener.call(this, event, handler as EventListener);
141
- }
142
- };
143
-
144
- const originalRemoveEventListener = Element.prototype.removeEventListener;
145
- Element.prototype.removeEventListener = function(event: string, handler: Function) {
146
- if (globalEventListeners.has(this) &&
147
- globalEventListeners.get(this)!.has(event)) {
148
- globalEventListeners.get(this)!.get(event)!.delete(handler);
149
- }
150
-
151
- // Call original if it exists
152
- if (originalRemoveEventListener) {
153
- originalRemoveEventListener.call(this, event, handler as EventListener);
154
- }
155
- };
156
-
157
- // Mock offsetHeight/offsetWidth
158
- Object.defineProperty(Element.prototype, 'offsetHeight', {
159
- configurable: true,
160
- get: function() { return 100; }
161
- });
162
-
163
- Object.defineProperty(Element.prototype, 'offsetWidth', {
164
- configurable: true,
165
- get: function() { return 100; }
166
- });
167
- });
168
-
169
- afterEach(() => {
170
- // Clean up our mocks and event listeners
171
- if (Element.prototype.getBoundingClientRect) {
172
- // @ts-ignore - We're deliberately cleaning up a property we added
173
- delete Element.prototype.getBoundingClientRect;
174
- }
175
-
176
- if (Element.prototype.closest) {
177
- // @ts-ignore - We're deliberately cleaning up a property we added
178
- delete Element.prototype.closest;
179
- }
180
-
181
- if (Element.prototype.matches) {
182
- // @ts-ignore - We're deliberately cleaning up a property we added
183
- delete Element.prototype.matches;
184
- }
185
-
186
- // Restore createElement
187
- document.createElement = originalCreateElement;
188
-
189
- // Clear event listeners
190
- globalEventListeners.clear();
191
- });
192
-
193
- // Mock menu component factory
194
- const createMenu = (config: MenuConfig = {}): MenuComponent => {
195
- // Create main element
196
- const element = document.createElement('div');
197
- element.className = 'mtrl-menu';
198
-
199
- if (config.class) {
200
- element.className += ` ${config.class}`;
201
- }
202
-
203
- element.setAttribute('role', 'menu');
204
-
205
- // Track visibility state
206
- let isVisibleState = false;
207
-
208
- // Create maps for items and event handlers
209
- const itemsMap = new Map<string, any>();
210
- const eventHandlers: Record<string, Function[]> = {};
211
- const submenus: MenuComponent[] = [];
212
-
213
- // Create a menu item element
214
- const createMenuItem = (item: MenuItemConfig): HTMLElement => {
215
- const itemElement = document.createElement('div');
216
-
217
- // Set up the item based on type
218
- if (item.type === MENU_ITEM_TYPES.DIVIDER) {
219
- itemElement.className = 'mtrl-menu-divider';
220
- itemElement.setAttribute('role', 'separator');
221
- } else {
222
- itemElement.className = 'mtrl-menu-item';
223
- itemElement.setAttribute('role', 'menuitem');
224
-
225
- if (item.text) {
226
- itemElement.textContent = item.text;
227
- }
228
-
229
- if (item.disabled) {
230
- itemElement.setAttribute('aria-disabled', 'true');
231
- itemElement.classList.add('mtrl-menu-item--disabled');
232
- }
233
-
234
- if (item.class) {
235
- itemElement.className += ` ${item.class}`;
236
- }
237
-
238
- // Setup click handler for normal items
239
- itemElement.addEventListener('click', () => {
240
- if (!item.disabled) {
241
- emit(MENU_EVENTS.SELECT, {
242
- name: item.name,
243
- text: item.text
244
- });
245
-
246
- // Hide menu after selection unless configured otherwise
247
- if (!config.stayOpenOnSelect) {
248
- hide();
249
- }
250
- }
251
- });
252
-
253
- // If item has children, set up submenu
254
- if (item.items && item.items.length > 0) {
255
- itemElement.classList.add('mtrl-menu-item--has-submenu');
256
-
257
- // Create submenu for this item
258
- const submenu = createMenu({
259
- items: item.items,
260
- parentItem: itemElement
261
- });
262
-
263
- submenus.push(submenu);
264
-
265
- // Add hover handler to show submenu
266
- itemElement.addEventListener('mouseenter', () => {
267
- // Position submenu next to parent item
268
- submenu.position(itemElement, {
269
- align: MENU_ALIGN.RIGHT,
270
- vAlign: MENU_VERTICAL_ALIGN.TOP
271
- });
272
-
273
- submenu.show();
274
- emit(MENU_EVENTS.SUBMENU_OPEN, { name: item.name });
275
- });
276
-
277
- itemElement.addEventListener('mouseleave', () => {
278
- submenu.hide();
279
- emit(MENU_EVENTS.SUBMENU_CLOSE, { name: item.name });
280
- });
281
- }
282
- }
283
-
284
- return itemElement;
285
- };
286
-
287
- // Create menu list
288
- const menuList = document.createElement('div');
289
- menuList.className = 'mtrl-menu-list';
290
- menuList.setAttribute('role', 'menu');
291
- element.appendChild(menuList);
292
-
293
- // Add initial items
294
- if (config.items && config.items.length > 0) {
295
- config.items.forEach(item => {
296
- // Skip items without name (like dividers)
297
- if (item.type !== MENU_ITEM_TYPES.DIVIDER) {
298
- // Add to items map
299
- itemsMap.set(item.name, {
300
- element: createMenuItem(item),
301
- config: item
302
- });
303
-
304
- menuList.appendChild(itemsMap.get(item.name).element);
305
- } else {
306
- // Just add divider directly
307
- menuList.appendChild(createMenuItem(item));
308
- }
309
- });
310
- }
311
-
312
- // Show method
313
- const show = (): MenuComponent => {
314
- element.classList.add('mtrl-menu--visible');
315
- isVisibleState = true;
316
- emit(MENU_EVENTS.OPEN, {});
317
- return menuComponent;
318
- };
319
-
320
- // Hide method
321
- const hide = (): MenuComponent => {
322
- element.classList.remove('mtrl-menu--visible');
323
- isVisibleState = false;
324
-
325
- // Hide any open submenus
326
- submenus.forEach(submenu => submenu.hide());
327
-
328
- emit(MENU_EVENTS.CLOSE, {});
329
- return menuComponent;
330
- };
331
-
332
- // Position method
333
- const position = (target: HTMLElement, options: MenuPositionConfig = {}): MenuComponent => {
334
- const {
335
- align = MENU_ALIGN.LEFT,
336
- vAlign = MENU_VERTICAL_ALIGN.TOP,
337
- offsetX = 0,
338
- offsetY = 0
339
- } = options;
340
-
341
- // Get target dimensions and position
342
- const targetRect = target.getBoundingClientRect();
343
-
344
- // Calculate position based on alignment
345
- let left = targetRect.left + offsetX;
346
- let top = targetRect.top + offsetY;
347
-
348
- // Adjust horizontal position based on alignment
349
- if (align === MENU_ALIGN.RIGHT) {
350
- left = targetRect.right - element.offsetWidth + offsetX;
351
- } else if (align === MENU_ALIGN.CENTER) {
352
- left = targetRect.left + (targetRect.width - element.offsetWidth) / 2 + offsetX;
353
- }
354
-
355
- // Adjust vertical position based on alignment
356
- if (vAlign === MENU_VERTICAL_ALIGN.BOTTOM) {
357
- top = targetRect.bottom - element.offsetHeight + offsetY;
358
- } else if (vAlign === MENU_VERTICAL_ALIGN.MIDDLE) {
359
- top = targetRect.top + (targetRect.height - element.offsetHeight) / 2 + offsetY;
360
- }
361
-
362
- // Set position
363
- element.style.left = `${left}px`;
364
- element.style.top = `${top}px`;
365
-
366
- return menuComponent;
367
- };
368
-
369
- // Add item
370
- const addItem = (item: MenuItemConfig): MenuComponent => {
371
- // Skip if no name or already exists
372
- if (!item.name || item.type === MENU_ITEM_TYPES.DIVIDER || itemsMap.has(item.name)) {
373
- return menuComponent;
374
- }
375
-
376
- const itemElement = createMenuItem(item);
377
-
378
- // Add to map and DOM
379
- itemsMap.set(item.name, {
380
- element: itemElement,
381
- config: item
382
- });
383
-
384
- menuList.appendChild(itemElement);
385
- return menuComponent;
386
- };
387
-
388
- // Remove item
389
- const removeItem = (name: string): MenuComponent => {
390
- if (itemsMap.has(name)) {
391
- const itemData = itemsMap.get(name);
392
- menuList.removeChild(itemData.element);
393
- itemsMap.delete(name);
394
- }
395
- return menuComponent;
396
- };
397
-
398
- // Event emitter
399
- const emit = (event: string, data: any): void => {
400
- if (eventHandlers[event]) {
401
- eventHandlers[event].forEach(handler => handler(data));
402
- }
403
- };
404
-
405
- // Event handlers
406
- const on = (event: string, handler: Function): MenuComponent => {
407
- if (!eventHandlers[event]) {
408
- eventHandlers[event] = [];
409
- }
410
- eventHandlers[event].push(handler);
411
- return menuComponent;
412
- };
413
-
414
- const off = (event: string, handler: Function): MenuComponent => {
415
- if (eventHandlers[event]) {
416
- eventHandlers[event] = eventHandlers[event].filter(h => h !== handler);
417
- }
418
- return menuComponent;
419
- };
420
-
421
- // Destroy
422
- const destroy = (): MenuComponent => {
423
- // Clean up event handlers
424
- Object.keys(eventHandlers).forEach(event => {
425
- eventHandlers[event] = [];
426
- });
427
-
428
- // Destroy submenus
429
- submenus.forEach(submenu => submenu.destroy());
430
-
431
- // Remove from DOM
432
- if (element.parentNode) {
433
- element.parentNode.removeChild(element);
434
- }
435
-
436
- return menuComponent;
437
- };
438
-
439
- // Component interface
440
- const menuComponent: MenuComponent = {
441
- element,
442
-
443
- show,
444
- hide,
445
- isVisible: () => isVisibleState,
446
-
447
- position,
448
-
449
- addItem,
450
- removeItem,
451
- getItems: () => itemsMap,
452
-
453
- on,
454
- off,
455
-
456
- destroy
457
- };
458
-
459
- return menuComponent;
460
- };
461
-
462
- describe('Menu Component', () => {
463
- // Sample menu items for testing
464
- const testItems: MenuItemConfig[] = [
465
- {
466
- name: 'copy',
467
- text: 'Copy'
468
- },
469
- {
470
- name: 'paste',
471
- text: 'Paste'
472
- },
473
- {
474
- type: 'divider'
475
- } as MenuItemConfig,
476
- {
477
- name: 'delete',
478
- text: 'Delete',
479
- disabled: true
480
- }
481
- ];
482
-
483
- // Sample nested menu items
484
- const nestedTestItems: MenuItemConfig[] = [
485
- {
486
- name: 'file',
487
- text: 'File',
488
- items: [
489
- {
490
- name: 'new',
491
- text: 'New'
492
- },
493
- {
494
- name: 'open',
495
- text: 'Open'
496
- }
497
- ]
498
- },
499
- {
500
- name: 'edit',
501
- text: 'Edit',
502
- items: [
503
- {
504
- name: 'copy',
505
- text: 'Copy'
506
- },
507
- {
508
- name: 'paste',
509
- text: 'Paste'
510
- }
511
- ]
512
- }
513
- ];
514
-
515
- test('should create a menu element', () => {
516
- const menu = createMenu();
517
-
518
- expect(menu.element).toBeDefined();
519
- expect(menu.element.tagName).toBe('DIV');
520
- expect(menu.element.className).toContain('mtrl-menu');
521
- expect(menu.element.getAttribute('role')).toBe('menu');
522
- });
523
-
524
- test('should apply custom class', () => {
525
- const customClass = 'custom-menu';
526
- const menu = createMenu({
527
- class: customClass
528
- });
529
-
530
- expect(menu.element.className).toContain(customClass);
531
- });
532
-
533
- test('should add initial items', () => {
534
- const menu = createMenu({
535
- items: testItems
536
- });
537
-
538
- // Check if items methods exist
539
- expect(typeof menu.getItems).toBe('function');
540
-
541
- // Get items and verify we have a Map
542
- const items = menu.getItems();
543
- expect(items instanceof Map).toBe(true);
544
-
545
- // Verify item names in map
546
- expect(items.has('copy')).toBe(true);
547
- expect(items.has('paste')).toBe(true);
548
- expect(items.has('delete')).toBe(true);
549
- });
550
-
551
- test('should have show/hide methods', () => {
552
- const menu = createMenu();
553
-
554
- // Check for API methods
555
- expect(typeof menu.show).toBe('function');
556
- expect(typeof menu.hide).toBe('function');
557
- expect(typeof menu.isVisible).toBe('function');
558
-
559
- // Test visibility state
560
- expect(menu.isVisible()).toBe(false);
561
-
562
- // Show menu
563
- menu.show();
564
- expect(menu.isVisible()).toBe(true);
565
- expect(menu.element.classList.contains('mtrl-menu--visible')).toBe(true);
566
-
567
- // Hide menu
568
- menu.hide();
569
- expect(menu.isVisible()).toBe(false);
570
- });
571
-
572
- test('should have positioning methods', () => {
573
- const menu = createMenu();
574
- const target = document.createElement('button');
575
-
576
- // Check for API method
577
- expect(typeof menu.position).toBe('function');
578
-
579
- // Test with different alignments
580
- const positionConfigs: MenuPositionConfig[] = [
581
- { align: MENU_ALIGN.LEFT, vAlign: MENU_VERTICAL_ALIGN.TOP },
582
- { align: MENU_ALIGN.RIGHT, vAlign: MENU_VERTICAL_ALIGN.BOTTOM },
583
- { align: MENU_ALIGN.CENTER, vAlign: MENU_VERTICAL_ALIGN.MIDDLE }
584
- ];
585
-
586
- positionConfigs.forEach(config => {
587
- try {
588
- menu.position(target, config);
589
- // If we reach here, no error was thrown
590
- expect(true).toBe(true);
591
- } catch (error) {
592
- // If an error occurs, the test should fail
593
- expect(error).toBeUndefined();
594
- }
595
- });
596
- });
597
-
598
- test('should add item dynamically', () => {
599
- const menu = createMenu();
600
-
601
- // Check for API method
602
- expect(typeof menu.addItem).toBe('function');
603
-
604
- // Test adding an item
605
- const newItem: MenuItemConfig = {
606
- name: 'newItem',
607
- text: 'New Item'
608
- };
609
-
610
- menu.addItem(newItem);
611
-
612
- // Verify item was added
613
- const items = menu.getItems();
614
- expect(items.has('newItem')).toBe(true);
615
- });
616
-
617
- test('should remove item dynamically', () => {
618
- const menu = createMenu({
619
- items: testItems
620
- });
621
-
622
- // Check for API method
623
- expect(typeof menu.removeItem).toBe('function');
624
-
625
- // Test removing an item
626
- menu.removeItem('copy');
627
-
628
- // Verify item was removed
629
- const items = menu.getItems();
630
- expect(items.has('copy')).toBe(false);
631
- });
632
-
633
- test('should register event handlers', () => {
634
- const menu = createMenu();
635
-
636
- // Check for API methods
637
- expect(typeof menu.on).toBe('function');
638
- expect(typeof menu.off).toBe('function');
639
-
640
- // Create a mock handler
641
- const mockHandler = mock(() => {});
642
-
643
- // Register handler
644
- menu.on(MENU_EVENTS.SELECT, mockHandler);
645
-
646
- // We can't easily test if the handler is called in this environment
647
- // But we can check that the method works without error
648
- expect(mockHandler.mock.calls.length).toBe(0);
649
-
650
- // Unregister handler
651
- menu.off(MENU_EVENTS.SELECT, mockHandler);
652
- });
653
-
654
- test('should create nested menus for items with children', () => {
655
- // This test would be more complex in a real environment
656
- // For now, just verify the basic menu creation works with nested items
657
-
658
- const menu = createMenu({
659
- items: nestedTestItems
660
- });
661
-
662
- // Verify parent items exist
663
- const items = menu.getItems();
664
- expect(items.has('file')).toBe(true);
665
- expect(items.has('edit')).toBe(true);
666
-
667
- // We can't easily test the submenu creation here
668
- // But we can check that the parent items are created without error
669
- });
670
-
671
- test('should properly clean up resources on destroy', () => {
672
- const menu = createMenu();
673
-
674
- // Check for API method
675
- expect(typeof menu.destroy).toBe('function');
676
-
677
- const parentElement = document.createElement('div');
678
- parentElement.appendChild(menu.element);
679
-
680
- // Destroy the component
681
- menu.destroy();
682
-
683
- // Check if element was removed
684
- expect(parentElement.children.length).toBe(0);
685
- });
686
-
687
- test('should support keyboard navigation', () => {
688
- // Skip detailed keyboard navigation tests due to test environment limitations
689
- // Just verify the API methods exist
690
-
691
- const menu = createMenu();
692
-
693
- // Show the menu to initialize keyboard handlers
694
- menu.show();
695
-
696
- // In a real environment, we would dispatch keydown events and check results
697
- // But here we just verify the basic setup happens without errors
698
-
699
- // Hide and clean up
700
- menu.hide();
701
- });
702
-
703
- test('should handle outside clicks', () => {
704
- // This would typically close the menu
705
- // We can't fully test this behavior in the current environment
706
-
707
- const menu = createMenu();
708
- menu.show();
709
-
710
- // In a real environment, we would:
711
- // 1. Create a click event outside the menu
712
- // 2. Dispatch it
713
- // 3. Verify menu is hidden
714
-
715
- // For now, just ensure our menu API method is called without error
716
- menu.hide();
717
- });
718
- });