mtrl 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +15 -0
- package/CONTRIBUTING.md +62 -23
- package/DOCS.md +3 -3
- package/README.md +43 -20
- package/TESTING.md +128 -18
- package/dist/index.js +14865 -0
- package/git-user-stats.js +545 -0
- package/index.ts +9 -67
- package/package.json +8 -3
- package/src/components/badge/api.ts +15 -1
- package/src/components/badge/badge.ts +43 -4
- package/src/components/badge/config.ts +40 -8
- package/src/components/badge/index.ts +64 -3
- package/src/components/badge/types.ts +175 -33
- package/src/components/button/api.ts +63 -1
- package/src/components/button/button.ts +39 -3
- package/src/components/button/config.ts +21 -4
- package/src/components/button/index.ts +26 -1
- package/src/components/button/types.ts +7 -1
- package/src/components/card/api.ts +78 -9
- package/src/components/card/card.ts +58 -3
- package/src/components/card/config.ts +41 -11
- package/src/components/card/features.ts +39 -12
- package/src/components/card/index.ts +84 -19
- package/src/components/card/types.ts +218 -29
- package/src/components/carousel/carousel.ts +92 -28
- package/src/components/carousel/constants.ts +107 -21
- package/src/components/carousel/index.ts +31 -13
- package/src/components/checkbox/checkbox.ts +83 -16
- package/src/components/checkbox/index.ts +43 -1
- package/src/components/checkbox/types.ts +219 -32
- package/src/components/chips/api.ts +194 -0
- package/src/components/{chip → chips/chip}/api.ts +42 -2
- package/src/components/chips/chip/chip.ts +131 -0
- package/src/components/{chip → chips/chip}/config.ts +3 -3
- package/src/components/chips/chip/index.ts +3 -0
- package/src/components/chips/chips.md +481 -0
- package/src/components/chips/chips.ts +75 -0
- package/src/components/chips/config.ts +109 -0
- package/src/components/chips/constants.ts +61 -0
- package/src/components/chips/features/chip-items.ts +33 -0
- package/src/components/chips/features/container.ts +77 -0
- package/src/components/chips/features/controller.ts +448 -0
- package/src/components/chips/features/index.ts +5 -0
- package/src/components/chips/features/label.ts +108 -0
- package/src/components/chips/index.ts +11 -0
- package/src/components/chips/schema.ts +61 -0
- package/src/components/{chip → chips}/types.ts +203 -92
- package/src/components/dialog/dialog.ts +99 -16
- package/src/components/dialog/index.ts +97 -1
- package/src/components/dialog/types.ts +375 -69
- package/src/components/divider/config.ts +90 -6
- package/src/components/divider/divider.ts +32 -2
- package/src/components/divider/features.ts +26 -0
- package/src/components/divider/index.ts +30 -0
- package/src/components/divider/types.ts +86 -9
- package/src/components/extended-fab/api.ts +53 -1
- package/src/components/extended-fab/config.ts +29 -1
- package/src/components/extended-fab/extended-fab.ts +28 -0
- package/src/components/extended-fab/index.ts +36 -0
- package/src/components/extended-fab/types.ts +458 -13
- package/src/components/fab/api.ts +42 -2
- package/src/components/fab/config.ts +29 -1
- package/src/components/fab/fab.ts +16 -2
- package/src/components/fab/index.ts +35 -0
- package/src/components/fab/types.ts +374 -10
- package/src/components/list/api.ts +12 -2
- package/src/components/list/config.ts +21 -0
- package/src/components/list/features.ts +6 -0
- package/src/components/list/index.ts +56 -1
- package/src/components/list/list-item.ts +46 -2
- package/src/components/list/list.ts +73 -2
- package/src/components/list/types.ts +172 -0
- package/src/components/list/utils.ts +26 -2
- package/src/components/menu/api.ts +217 -20
- package/src/components/menu/config.ts +27 -0
- package/src/components/menu/features/visibility.ts +55 -6
- package/src/components/menu/index.ts +64 -0
- package/src/components/menu/menu-item.ts +46 -3
- package/src/components/menu/menu.ts +77 -1
- package/src/components/menu/types.ts +404 -39
- package/src/components/navigation/nav-item.ts +13 -2
- package/src/components/sheet/config.ts +1 -2
- package/src/components/sheet/features/gestures.ts +1 -1
- package/src/components/sheet/features/position.ts +1 -2
- package/src/components/sheet/features/state.ts +1 -1
- package/src/components/sheet/index.ts +10 -2
- package/src/components/sheet/sheet.ts +1 -2
- package/src/components/sheet/types.ts +29 -1
- package/src/components/slider/api.ts +1 -1
- package/src/components/slider/config.ts +1 -1
- package/src/components/slider/features/controller.ts +1 -1
- package/src/components/slider/features/handlers.ts +1 -1
- package/src/components/slider/features/states.ts +1 -1
- package/src/components/slider/index.ts +12 -5
- package/src/components/slider/schema.ts +1 -1
- package/src/components/slider/types.ts +31 -0
- package/src/components/tabs/tab-api.ts +1 -1
- package/src/components/tabs/types.ts +1 -1
- package/src/components/tooltip/api.ts +6 -2
- package/src/components/tooltip/config.ts +9 -28
- package/src/components/tooltip/index.ts +10 -1
- package/src/components/tooltip/types.ts +38 -3
- package/src/core/dom/create.ts +57 -51
- package/src/index.ts +129 -31
- package/src/styles/abstract/_mixins.scss +23 -9
- package/src/styles/abstract/_variables.scss +14 -4
- package/src/styles/components/_card.scss +1 -1
- package/src/styles/components/_chip.scss +323 -113
- package/src/styles/components/_tabs.scss +1 -1
- package/src/styles/themes/_autumn.scss +3 -0
- package/CLAUDE.md +0 -33
- package/src/components/checkbox/constants.ts +0 -37
- package/src/components/chip/chip-set.ts +0 -225
- package/src/components/chip/chip.ts +0 -118
- package/src/components/chip/constants.ts +0 -28
- package/src/components/chip/index.ts +0 -12
- package/src/components/list/constants.ts +0 -116
- package/src/components/sheet/constants.ts +0 -20
- package/src/components/slider/constants.ts +0 -32
- package/src/components/tooltip/constants.ts +0 -27
- package/test/components/badge.test.ts +0 -545
- package/test/components/bottom-app-bar.test.ts +0 -303
- package/test/components/button.test.ts +0 -233
- package/test/components/card.test.ts +0 -560
- package/test/components/carousel.test.ts +0 -951
- package/test/components/checkbox.test.ts +0 -462
- package/test/components/chip.test.ts +0 -692
- package/test/components/datepicker.test.ts +0 -1124
- package/test/components/dialog.test.ts +0 -990
- package/test/components/divider.test.ts +0 -412
- package/test/components/extended-fab.test.ts +0 -672
- package/test/components/fab.test.ts +0 -561
- package/test/components/list.test.ts +0 -365
- package/test/components/menu.test.ts +0 -718
- package/test/components/navigation.test.ts +0 -186
- package/test/components/progress.test.ts +0 -567
- package/test/components/radios.test.ts +0 -699
- package/test/components/search.test.ts +0 -1135
- package/test/components/segmented-button.test.ts +0 -732
- package/test/components/sheet.test.ts +0 -641
- package/test/components/slider.test.ts +0 -1220
- package/test/components/snackbar.test.ts +0 -461
- package/test/components/switch.test.ts +0 -452
- package/test/components/tabs.test.ts +0 -1369
- package/test/components/textfield.test.ts +0 -400
- package/test/components/timepicker.test.ts +0 -592
- package/test/components/tooltip.test.ts +0 -630
- package/test/components/top-app-bar.test.ts +0 -566
- package/test/core/dom.attributes.test.ts +0 -148
- package/test/core/dom.classes.test.ts +0 -152
- package/test/core/dom.events.test.ts +0 -243
- package/test/core/emitter.test.ts +0 -141
- package/test/core/ripple.test.ts +0 -99
- package/test/core/state.store.test.ts +0 -189
- package/test/core/utils.normalize.test.ts +0 -61
- package/test/core/utils.object.test.ts +0 -120
- package/test/setup.js +0 -371
- package/test/setup.ts +0 -451
- package/tsconfig.json +0 -22
- package/typedoc.json +0 -28
- 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
|
-
});
|