mtrl 0.3.3 → 0.3.6
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/package.json +1 -1
- package/src/components/menu/api.ts +143 -268
- package/src/components/menu/config.ts +84 -40
- package/src/components/menu/features/anchor.ts +159 -0
- package/src/components/menu/features/controller.ts +970 -0
- package/src/components/menu/features/index.ts +4 -0
- package/src/components/menu/index.ts +31 -63
- package/src/components/menu/menu.ts +107 -97
- package/src/components/menu/types.ts +263 -447
- package/src/components/segmented-button/config.ts +59 -20
- package/src/components/segmented-button/index.ts +1 -1
- package/src/components/segmented-button/segment.ts +51 -97
- package/src/components/segmented-button/segmented-button.ts +114 -2
- package/src/components/segmented-button/types.ts +52 -0
- package/src/core/compose/features/icon.ts +15 -13
- package/src/core/dom/classes.ts +81 -9
- package/src/core/dom/create.ts +30 -19
- package/src/core/layout/README.md +531 -166
- package/src/core/layout/array.ts +3 -4
- package/src/core/layout/config.ts +193 -0
- package/src/core/layout/create.ts +1 -2
- package/src/core/layout/index.ts +12 -2
- package/src/core/layout/object.ts +2 -3
- package/src/core/layout/processor.ts +60 -12
- package/src/core/layout/result.ts +1 -2
- package/src/core/layout/types.ts +105 -50
- package/src/core/layout/utils.ts +69 -61
- package/src/index.ts +2 -1
- package/src/styles/components/_button.scss +6 -0
- package/src/styles/components/_chip.scss +4 -5
- package/src/styles/components/_menu.scss +20 -8
- package/src/styles/components/_segmented-button.scss +173 -63
- package/src/styles/main.scss +23 -23
- package/src/styles/utilities/_layout.scss +665 -0
- package/src/components/menu/features/items-manager.ts +0 -457
- package/src/components/menu/features/keyboard-navigation.ts +0 -133
- package/src/components/menu/features/positioning.ts +0 -127
- package/src/components/menu/features/visibility.ts +0 -230
- package/src/components/menu/menu-item.ts +0 -86
- package/src/components/menu/utils.ts +0 -67
- /package/src/{core/build → styles/utilities}/_ripple.scss +0 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
// src/components/menu/features/controller.ts
|
|
2
|
+
|
|
3
|
+
import { MenuConfig, MenuContent, MenuItem, MenuDivider, MenuEvent, MenuSelectEvent } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds controller functionality to the menu component
|
|
7
|
+
* Manages state, rendering, positioning, and event handling
|
|
8
|
+
*
|
|
9
|
+
* @param config - Menu configuration
|
|
10
|
+
* @returns Component enhancer with menu controller functionality
|
|
11
|
+
*/
|
|
12
|
+
export const withController = (config: MenuConfig) => component => {
|
|
13
|
+
if (!component.element) {
|
|
14
|
+
console.warn('Cannot initialize menu controller: missing element');
|
|
15
|
+
return component;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Initialize state
|
|
19
|
+
const state = {
|
|
20
|
+
visible: config.visible || false,
|
|
21
|
+
items: config.items || [],
|
|
22
|
+
placement: config.placement,
|
|
23
|
+
activeSubmenu: null,
|
|
24
|
+
activeSubmenuItem: null,
|
|
25
|
+
activeItemIndex: -1,
|
|
26
|
+
submenuTimer: null,
|
|
27
|
+
hoverIntent: {
|
|
28
|
+
timer: null,
|
|
29
|
+
activeItem: null
|
|
30
|
+
},
|
|
31
|
+
component
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Create event helpers
|
|
35
|
+
const eventHelpers = {
|
|
36
|
+
triggerEvent(eventName: string, data: any = {}, originalEvent?: Event) {
|
|
37
|
+
const eventData = {
|
|
38
|
+
menu: state.component,
|
|
39
|
+
...data,
|
|
40
|
+
originalEvent,
|
|
41
|
+
preventDefault: () => { eventData.defaultPrevented = true; },
|
|
42
|
+
defaultPrevented: false
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
component.emit(eventName, eventData);
|
|
46
|
+
return eventData;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gets the anchor element from config
|
|
52
|
+
*/
|
|
53
|
+
const getAnchorElement = (): HTMLElement => {
|
|
54
|
+
const { anchor } = config;
|
|
55
|
+
|
|
56
|
+
if (typeof anchor === 'string') {
|
|
57
|
+
const element = document.querySelector(anchor);
|
|
58
|
+
if (!element) {
|
|
59
|
+
console.warn(`Menu anchor not found: ${anchor}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return element as HTMLElement;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return anchor;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates a DOM element for a menu item
|
|
70
|
+
*/
|
|
71
|
+
const createMenuItem = (item: MenuItem, index: number): HTMLElement => {
|
|
72
|
+
const itemElement = document.createElement('li');
|
|
73
|
+
const itemClass = `${component.getClass('menu-item')}`;
|
|
74
|
+
|
|
75
|
+
itemElement.className = itemClass;
|
|
76
|
+
itemElement.setAttribute('role', 'menuitem');
|
|
77
|
+
itemElement.setAttribute('tabindex', '-1');
|
|
78
|
+
itemElement.setAttribute('data-id', item.id);
|
|
79
|
+
itemElement.setAttribute('data-index', index.toString());
|
|
80
|
+
|
|
81
|
+
if (item.disabled) {
|
|
82
|
+
itemElement.classList.add(`${itemClass}--disabled`);
|
|
83
|
+
itemElement.setAttribute('aria-disabled', 'true');
|
|
84
|
+
} else {
|
|
85
|
+
itemElement.setAttribute('aria-disabled', 'false');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (item.hasSubmenu) {
|
|
89
|
+
itemElement.classList.add(`${itemClass}--submenu`);
|
|
90
|
+
itemElement.setAttribute('aria-haspopup', 'true');
|
|
91
|
+
itemElement.setAttribute('aria-expanded', 'false');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create content container for flexible layout
|
|
95
|
+
const contentContainer = document.createElement('span');
|
|
96
|
+
contentContainer.className = `${component.getClass('menu-item-content')}`;
|
|
97
|
+
|
|
98
|
+
// Add icon if provided
|
|
99
|
+
if (item.icon) {
|
|
100
|
+
const iconElement = document.createElement('span');
|
|
101
|
+
iconElement.className = `${component.getClass('menu-item-icon')}`;
|
|
102
|
+
iconElement.innerHTML = item.icon;
|
|
103
|
+
contentContainer.appendChild(iconElement);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add text
|
|
107
|
+
const textElement = document.createElement('span');
|
|
108
|
+
textElement.className = `${component.getClass('menu-item-text')}`;
|
|
109
|
+
textElement.textContent = item.text;
|
|
110
|
+
contentContainer.appendChild(textElement);
|
|
111
|
+
|
|
112
|
+
// Add shortcut if provided
|
|
113
|
+
if (item.shortcut) {
|
|
114
|
+
const shortcutElement = document.createElement('span');
|
|
115
|
+
shortcutElement.className = `${component.getClass('menu-item-shortcut')}`;
|
|
116
|
+
shortcutElement.textContent = item.shortcut;
|
|
117
|
+
contentContainer.appendChild(shortcutElement);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
itemElement.appendChild(contentContainer);
|
|
121
|
+
|
|
122
|
+
// Add event listeners
|
|
123
|
+
if (!item.disabled) {
|
|
124
|
+
itemElement.addEventListener('click', (e) => handleItemClick(e, item, index));
|
|
125
|
+
|
|
126
|
+
if (item.hasSubmenu && config.openSubmenuOnHover) {
|
|
127
|
+
itemElement.addEventListener('mouseenter', () => handleSubmenuHover(item, index, itemElement));
|
|
128
|
+
itemElement.addEventListener('mouseleave', handleSubmenuLeave);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return itemElement;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a DOM element for a menu divider
|
|
137
|
+
*/
|
|
138
|
+
const createDivider = (divider: MenuDivider, index: number): HTMLElement => {
|
|
139
|
+
const dividerElement = document.createElement('li');
|
|
140
|
+
dividerElement.className = `${component.getClass('menu-divider')}`;
|
|
141
|
+
dividerElement.setAttribute('role', 'separator');
|
|
142
|
+
dividerElement.setAttribute('data-index', index.toString());
|
|
143
|
+
|
|
144
|
+
if (divider.id) {
|
|
145
|
+
dividerElement.setAttribute('id', divider.id);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return dividerElement;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Renders the menu items
|
|
153
|
+
*/
|
|
154
|
+
const renderMenuItems = (): void => {
|
|
155
|
+
const menuList = document.createElement('ul');
|
|
156
|
+
menuList.className = `${component.getClass('menu-list')}`;
|
|
157
|
+
menuList.setAttribute('role', 'menu');
|
|
158
|
+
|
|
159
|
+
// Create items
|
|
160
|
+
state.items.forEach((item, index) => {
|
|
161
|
+
if ('type' in item && item.type === 'divider') {
|
|
162
|
+
menuList.appendChild(createDivider(item, index));
|
|
163
|
+
} else {
|
|
164
|
+
menuList.appendChild(createMenuItem(item as MenuItem, index));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Clear and append
|
|
169
|
+
component.element.innerHTML = '';
|
|
170
|
+
component.element.appendChild(menuList);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Positions the menu relative to its anchor
|
|
175
|
+
*/
|
|
176
|
+
const positionMenu = (): void => {
|
|
177
|
+
const anchor = getAnchorElement();
|
|
178
|
+
if (!anchor) return;
|
|
179
|
+
|
|
180
|
+
const menuElement = component.element;
|
|
181
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
182
|
+
const { placement } = state;
|
|
183
|
+
const offset = config.offset || 8;
|
|
184
|
+
|
|
185
|
+
// Reset styles for measurement
|
|
186
|
+
menuElement.style.top = '0';
|
|
187
|
+
menuElement.style.left = '0';
|
|
188
|
+
menuElement.style.right = 'auto';
|
|
189
|
+
menuElement.style.bottom = 'auto';
|
|
190
|
+
menuElement.style.maxHeight = config.maxHeight || '';
|
|
191
|
+
|
|
192
|
+
// Take measurements
|
|
193
|
+
const menuRect = menuElement.getBoundingClientRect();
|
|
194
|
+
const viewportWidth = window.innerWidth;
|
|
195
|
+
const viewportHeight = window.innerHeight;
|
|
196
|
+
|
|
197
|
+
// Calculate position based on placement
|
|
198
|
+
let top = 0;
|
|
199
|
+
let left = 0;
|
|
200
|
+
|
|
201
|
+
switch (placement) {
|
|
202
|
+
case 'top-start':
|
|
203
|
+
top = anchorRect.top - menuRect.height - offset;
|
|
204
|
+
left = anchorRect.left;
|
|
205
|
+
break;
|
|
206
|
+
case 'top':
|
|
207
|
+
top = anchorRect.top - menuRect.height - offset;
|
|
208
|
+
left = anchorRect.left + (anchorRect.width / 2) - (menuRect.width / 2);
|
|
209
|
+
break;
|
|
210
|
+
case 'top-end':
|
|
211
|
+
top = anchorRect.top - menuRect.height - offset;
|
|
212
|
+
left = anchorRect.right - menuRect.width;
|
|
213
|
+
break;
|
|
214
|
+
case 'right-start':
|
|
215
|
+
top = anchorRect.top;
|
|
216
|
+
left = anchorRect.right + offset;
|
|
217
|
+
break;
|
|
218
|
+
case 'right':
|
|
219
|
+
top = anchorRect.top + (anchorRect.height / 2) - (menuRect.height / 2);
|
|
220
|
+
left = anchorRect.right + offset;
|
|
221
|
+
break;
|
|
222
|
+
case 'right-end':
|
|
223
|
+
top = anchorRect.bottom - menuRect.height;
|
|
224
|
+
left = anchorRect.right + offset;
|
|
225
|
+
break;
|
|
226
|
+
case 'bottom-start':
|
|
227
|
+
top = anchorRect.bottom + offset;
|
|
228
|
+
left = anchorRect.left;
|
|
229
|
+
break;
|
|
230
|
+
case 'bottom':
|
|
231
|
+
top = anchorRect.bottom + offset;
|
|
232
|
+
left = anchorRect.left + (anchorRect.width / 2) - (menuRect.width / 2);
|
|
233
|
+
break;
|
|
234
|
+
case 'bottom-end':
|
|
235
|
+
top = anchorRect.bottom + offset;
|
|
236
|
+
left = anchorRect.right - menuRect.width;
|
|
237
|
+
break;
|
|
238
|
+
case 'left-start':
|
|
239
|
+
top = anchorRect.top;
|
|
240
|
+
left = anchorRect.left - menuRect.width - offset;
|
|
241
|
+
break;
|
|
242
|
+
case 'left':
|
|
243
|
+
top = anchorRect.top + (anchorRect.height / 2) - (menuRect.height / 2);
|
|
244
|
+
left = anchorRect.left - menuRect.width - offset;
|
|
245
|
+
break;
|
|
246
|
+
case 'left-end':
|
|
247
|
+
top = anchorRect.bottom - menuRect.height;
|
|
248
|
+
left = anchorRect.left - menuRect.width - offset;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Auto-flip if needed to stay in viewport
|
|
253
|
+
if (config.autoFlip) {
|
|
254
|
+
// Flip vertically if needed
|
|
255
|
+
if (top < 0) {
|
|
256
|
+
if (placement.startsWith('top')) {
|
|
257
|
+
top = anchorRect.bottom + offset;
|
|
258
|
+
}
|
|
259
|
+
} else if (top + menuRect.height > viewportHeight) {
|
|
260
|
+
if (placement.startsWith('bottom')) {
|
|
261
|
+
top = anchorRect.top - menuRect.height - offset;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Flip horizontally if needed
|
|
266
|
+
if (left < 0) {
|
|
267
|
+
if (placement.startsWith('left')) {
|
|
268
|
+
left = anchorRect.right + offset;
|
|
269
|
+
} else if (placement.includes('start')) {
|
|
270
|
+
left = 0;
|
|
271
|
+
}
|
|
272
|
+
} else if (left + menuRect.width > viewportWidth) {
|
|
273
|
+
if (placement.startsWith('right')) {
|
|
274
|
+
left = anchorRect.left - menuRect.width - offset;
|
|
275
|
+
} else if (placement.includes('end')) {
|
|
276
|
+
left = viewportWidth - menuRect.width;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Apply position
|
|
282
|
+
menuElement.style.top = `${top}px`;
|
|
283
|
+
menuElement.style.left = `${left}px`;
|
|
284
|
+
|
|
285
|
+
// Ensure it's in viewport
|
|
286
|
+
const updatedRect = menuElement.getBoundingClientRect();
|
|
287
|
+
if (updatedRect.top < 0) {
|
|
288
|
+
menuElement.style.top = '0';
|
|
289
|
+
}
|
|
290
|
+
if (updatedRect.left < 0) {
|
|
291
|
+
menuElement.style.left = '0';
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Clean up hover intent timer
|
|
297
|
+
*/
|
|
298
|
+
const clearHoverIntent = () => {
|
|
299
|
+
if (state.hoverIntent.timer) {
|
|
300
|
+
clearTimeout(state.hoverIntent.timer);
|
|
301
|
+
state.hoverIntent.timer = null;
|
|
302
|
+
state.hoverIntent.activeItem = null;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Handles click on a menu item
|
|
308
|
+
*/
|
|
309
|
+
const handleItemClick = (e: MouseEvent, item: MenuItem, index: number): void => {
|
|
310
|
+
e.preventDefault();
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
|
|
313
|
+
// Don't process if disabled
|
|
314
|
+
if (item.disabled) return;
|
|
315
|
+
|
|
316
|
+
if (item.hasSubmenu) {
|
|
317
|
+
handleSubmenuClick(item, index, e.currentTarget as HTMLElement);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Trigger select event
|
|
322
|
+
const selectEvent = eventHelpers.triggerEvent('select', {
|
|
323
|
+
item,
|
|
324
|
+
itemId: item.id,
|
|
325
|
+
itemData: item.data
|
|
326
|
+
}, e) as MenuSelectEvent;
|
|
327
|
+
|
|
328
|
+
// Close menu if needed
|
|
329
|
+
if (config.closeOnSelect && !selectEvent.defaultPrevented) {
|
|
330
|
+
closeMenu(e);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handles click on a submenu item
|
|
336
|
+
*/
|
|
337
|
+
const handleSubmenuClick = (item: MenuItem, index: number, itemElement: HTMLElement, viaKeyboard = false): void => {
|
|
338
|
+
if (!item.submenu || !item.hasSubmenu) return;
|
|
339
|
+
|
|
340
|
+
const isOpen = itemElement.getAttribute('aria-expanded') === 'true';
|
|
341
|
+
|
|
342
|
+
if (isOpen) {
|
|
343
|
+
// Close submenu
|
|
344
|
+
closeSubmenu();
|
|
345
|
+
} else {
|
|
346
|
+
// Close any open submenu
|
|
347
|
+
closeSubmenu();
|
|
348
|
+
|
|
349
|
+
// Open new submenu
|
|
350
|
+
openSubmenu(item, index, itemElement, viaKeyboard);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Handles hover on a submenu item
|
|
356
|
+
*/
|
|
357
|
+
const handleSubmenuHover = (item: MenuItem, index: number, itemElement: HTMLElement): void => {
|
|
358
|
+
if (!config.openSubmenuOnHover || !item.hasSubmenu) return;
|
|
359
|
+
|
|
360
|
+
// Clear any existing timers
|
|
361
|
+
clearHoverIntent();
|
|
362
|
+
clearSubmenuTimer();
|
|
363
|
+
|
|
364
|
+
// Set hover intent
|
|
365
|
+
state.hoverIntent.activeItem = itemElement;
|
|
366
|
+
state.hoverIntent.timer = setTimeout(() => {
|
|
367
|
+
const isCurrentlyHovered = itemElement.matches(':hover');
|
|
368
|
+
if (isCurrentlyHovered) {
|
|
369
|
+
// Only close and reopen if this is a different submenu item
|
|
370
|
+
if (state.activeSubmenuItem !== itemElement) {
|
|
371
|
+
closeSubmenu();
|
|
372
|
+
openSubmenu(item, index, itemElement);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
state.hoverIntent.timer = null;
|
|
376
|
+
}, 100);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Handles mouse leave from submenu
|
|
381
|
+
*/
|
|
382
|
+
const handleSubmenuLeave = (e: MouseEvent): void => {
|
|
383
|
+
// Clear hover intent
|
|
384
|
+
clearHoverIntent();
|
|
385
|
+
|
|
386
|
+
// Don't close immediately to allow moving to submenu
|
|
387
|
+
clearSubmenuTimer();
|
|
388
|
+
|
|
389
|
+
// Set a timer to close the submenu if not re-entered
|
|
390
|
+
state.submenuTimer = setTimeout(() => {
|
|
391
|
+
// Check if mouse is over the submenu or the parent menu item
|
|
392
|
+
const submenuElement = state.activeSubmenu;
|
|
393
|
+
const menuItemElement = state.activeSubmenuItem;
|
|
394
|
+
|
|
395
|
+
if (submenuElement && menuItemElement) {
|
|
396
|
+
const overSubmenu = submenuElement.matches(':hover');
|
|
397
|
+
const overMenuItem = menuItemElement.matches(':hover');
|
|
398
|
+
|
|
399
|
+
if (!overSubmenu && !overMenuItem) {
|
|
400
|
+
closeSubmenu();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
state.submenuTimer = null;
|
|
405
|
+
}, 300);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Opens a submenu
|
|
410
|
+
*/
|
|
411
|
+
const openSubmenu = (item: MenuItem, index: number, itemElement: HTMLElement, viaKeyboard = false): void => {
|
|
412
|
+
if (!item.submenu || !item.hasSubmenu) return;
|
|
413
|
+
|
|
414
|
+
// Close any existing submenu
|
|
415
|
+
closeSubmenu();
|
|
416
|
+
|
|
417
|
+
// Set expanded state
|
|
418
|
+
itemElement.setAttribute('aria-expanded', 'true');
|
|
419
|
+
|
|
420
|
+
// Create submenu element
|
|
421
|
+
const submenuElement = document.createElement('div');
|
|
422
|
+
submenuElement.className = `${component.getClass('menu')} ${component.getClass('menu--submenu')}`;
|
|
423
|
+
submenuElement.setAttribute('role', 'menu');
|
|
424
|
+
submenuElement.setAttribute('tabindex', '-1');
|
|
425
|
+
|
|
426
|
+
// Create submenu list
|
|
427
|
+
const submenuList = document.createElement('ul');
|
|
428
|
+
submenuList.className = `${component.getClass('menu-list')}`;
|
|
429
|
+
|
|
430
|
+
// Create submenu items
|
|
431
|
+
const submenuItems = [];
|
|
432
|
+
item.submenu.forEach((subitem, subindex) => {
|
|
433
|
+
if ('type' in subitem && subitem.type === 'divider') {
|
|
434
|
+
submenuList.appendChild(createDivider(subitem, subindex));
|
|
435
|
+
} else {
|
|
436
|
+
const subitemElement = createMenuItem(subitem as MenuItem, subindex);
|
|
437
|
+
submenuList.appendChild(subitemElement);
|
|
438
|
+
if (!(subitem as MenuItem).disabled) {
|
|
439
|
+
submenuItems.push(subitemElement);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
submenuElement.appendChild(submenuList);
|
|
445
|
+
document.body.appendChild(submenuElement);
|
|
446
|
+
|
|
447
|
+
// Setup keyboard navigation for submenu
|
|
448
|
+
submenuElement.addEventListener('keydown', handleMenuKeydown);
|
|
449
|
+
|
|
450
|
+
// Add mouseenter event to prevent closing
|
|
451
|
+
submenuElement.addEventListener('mouseenter', () => {
|
|
452
|
+
clearSubmenuTimer();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Add mouseleave event to handle closing
|
|
456
|
+
submenuElement.addEventListener('mouseleave', (e) => {
|
|
457
|
+
handleSubmenuLeave(e);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Position the submenu next to its parent item
|
|
461
|
+
const itemRect = itemElement.getBoundingClientRect();
|
|
462
|
+
|
|
463
|
+
// Default position is to the right
|
|
464
|
+
submenuElement.style.top = `${itemRect.top}px`;
|
|
465
|
+
submenuElement.style.left = `${itemRect.right + 8}px`;
|
|
466
|
+
|
|
467
|
+
// Check if submenu would be outside viewport and adjust if needed
|
|
468
|
+
const submenuRect = submenuElement.getBoundingClientRect();
|
|
469
|
+
const viewportWidth = window.innerWidth;
|
|
470
|
+
|
|
471
|
+
if (submenuRect.right > viewportWidth) {
|
|
472
|
+
// Position to the left instead
|
|
473
|
+
submenuElement.style.left = 'auto';
|
|
474
|
+
submenuElement.style.right = `${window.innerWidth - itemRect.left + 8}px`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Add to state
|
|
478
|
+
state.activeSubmenu = submenuElement;
|
|
479
|
+
state.activeSubmenuItem = itemElement;
|
|
480
|
+
|
|
481
|
+
// Add submenu events
|
|
482
|
+
document.addEventListener('click', handleDocumentClickForSubmenu);
|
|
483
|
+
window.addEventListener('resize', handleWindowResizeForSubmenu);
|
|
484
|
+
window.addEventListener('scroll', handleWindowScrollForSubmenu);
|
|
485
|
+
|
|
486
|
+
// Make visible
|
|
487
|
+
setTimeout(() => {
|
|
488
|
+
submenuElement.classList.add(`${component.getClass('menu--visible')}`);
|
|
489
|
+
|
|
490
|
+
// If opened via keyboard, focus the first item in the submenu
|
|
491
|
+
if (viaKeyboard && submenuItems.length > 0) {
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
submenuItems[0].focus();
|
|
494
|
+
}, 50);
|
|
495
|
+
}
|
|
496
|
+
}, 10);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Clear submenu close timer
|
|
501
|
+
*/
|
|
502
|
+
const clearSubmenuTimer = () => {
|
|
503
|
+
if (state.submenuTimer) {
|
|
504
|
+
clearTimeout(state.submenuTimer);
|
|
505
|
+
state.submenuTimer = null;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Closes any open submenu
|
|
511
|
+
*/
|
|
512
|
+
const closeSubmenu = (): void => {
|
|
513
|
+
// Clear timers
|
|
514
|
+
clearHoverIntent();
|
|
515
|
+
clearSubmenuTimer();
|
|
516
|
+
|
|
517
|
+
if (!state.activeSubmenu) return;
|
|
518
|
+
|
|
519
|
+
// Remove expanded state from all items
|
|
520
|
+
if (state.activeSubmenuItem) {
|
|
521
|
+
state.activeSubmenuItem.setAttribute('aria-expanded', 'false');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Remove submenu element with animation
|
|
525
|
+
state.activeSubmenu.classList.remove(`${component.getClass('menu--visible')}`);
|
|
526
|
+
|
|
527
|
+
// Store reference for cleanup
|
|
528
|
+
const submenuToRemove = state.activeSubmenu;
|
|
529
|
+
|
|
530
|
+
// Remove after animation
|
|
531
|
+
setTimeout(() => {
|
|
532
|
+
if (submenuToRemove && submenuToRemove.parentNode) {
|
|
533
|
+
submenuToRemove.parentNode.removeChild(submenuToRemove);
|
|
534
|
+
}
|
|
535
|
+
}, 200);
|
|
536
|
+
|
|
537
|
+
// Clear state
|
|
538
|
+
state.activeSubmenu = null;
|
|
539
|
+
state.activeSubmenuItem = null;
|
|
540
|
+
|
|
541
|
+
// Remove document events
|
|
542
|
+
document.removeEventListener('click', handleDocumentClickForSubmenu);
|
|
543
|
+
window.removeEventListener('resize', handleWindowResizeForSubmenu);
|
|
544
|
+
window.removeEventListener('scroll', handleWindowScrollForSubmenu);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Handles document click for submenu
|
|
549
|
+
*/
|
|
550
|
+
const handleDocumentClickForSubmenu = (e: MouseEvent): void => {
|
|
551
|
+
if (!state.activeSubmenu) return;
|
|
552
|
+
|
|
553
|
+
const submenuElement = state.activeSubmenu;
|
|
554
|
+
const menuItemElement = state.activeSubmenuItem;
|
|
555
|
+
|
|
556
|
+
// Check if click was inside submenu or parent menu item
|
|
557
|
+
if (submenuElement.contains(e.target as Node) ||
|
|
558
|
+
(menuItemElement && menuItemElement.contains(e.target as Node))) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Close submenu if clicked outside
|
|
563
|
+
closeSubmenu();
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Handles window resize for submenu
|
|
568
|
+
*/
|
|
569
|
+
const handleWindowResizeForSubmenu = (): void => {
|
|
570
|
+
// Reposition or close submenu on resize
|
|
571
|
+
closeSubmenu();
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Handles window scroll for submenu
|
|
576
|
+
*/
|
|
577
|
+
const handleWindowScrollForSubmenu = (): void => {
|
|
578
|
+
// Reposition or close submenu on scroll
|
|
579
|
+
closeSubmenu();
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Opens the menu
|
|
584
|
+
*/
|
|
585
|
+
/**
|
|
586
|
+
* Opens the menu
|
|
587
|
+
*/
|
|
588
|
+
const openMenu = (event?: Event): void => {
|
|
589
|
+
if (state.visible) return;
|
|
590
|
+
|
|
591
|
+
// Update state
|
|
592
|
+
state.visible = true;
|
|
593
|
+
|
|
594
|
+
// Add the menu to the DOM if it's not already there
|
|
595
|
+
if (!component.element.parentNode) {
|
|
596
|
+
document.body.appendChild(component.element);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Set attributes
|
|
600
|
+
component.element.setAttribute('aria-hidden', 'false');
|
|
601
|
+
component.element.classList.add(`${component.getClass('menu--visible')}`);
|
|
602
|
+
|
|
603
|
+
// Position
|
|
604
|
+
positionMenu();
|
|
605
|
+
|
|
606
|
+
// Focus first item
|
|
607
|
+
setTimeout(() => {
|
|
608
|
+
focusFirstItem();
|
|
609
|
+
}, 100);
|
|
610
|
+
|
|
611
|
+
// Add document events
|
|
612
|
+
if (config.closeOnClickOutside) {
|
|
613
|
+
document.addEventListener('click', handleDocumentClick);
|
|
614
|
+
}
|
|
615
|
+
if (config.closeOnEscape) {
|
|
616
|
+
document.addEventListener('keydown', handleDocumentKeydown);
|
|
617
|
+
}
|
|
618
|
+
window.addEventListener('resize', handleWindowResize);
|
|
619
|
+
window.addEventListener('scroll', handleWindowScroll);
|
|
620
|
+
|
|
621
|
+
// Trigger event
|
|
622
|
+
eventHelpers.triggerEvent('open', {}, event);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Closes the menu
|
|
627
|
+
*/
|
|
628
|
+
const closeMenu = (event?: Event): void => {
|
|
629
|
+
if (!state.visible) return;
|
|
630
|
+
|
|
631
|
+
// Close any open submenu first
|
|
632
|
+
closeSubmenu();
|
|
633
|
+
|
|
634
|
+
// Update state
|
|
635
|
+
state.visible = false;
|
|
636
|
+
|
|
637
|
+
// Set attributes
|
|
638
|
+
component.element.setAttribute('aria-hidden', 'true');
|
|
639
|
+
component.element.classList.remove(`${component.getClass('menu--visible')}`);
|
|
640
|
+
|
|
641
|
+
// Remove document events
|
|
642
|
+
document.removeEventListener('click', handleDocumentClick);
|
|
643
|
+
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
644
|
+
window.removeEventListener('resize', handleWindowResize);
|
|
645
|
+
window.removeEventListener('scroll', handleWindowScroll);
|
|
646
|
+
|
|
647
|
+
// Trigger event
|
|
648
|
+
eventHelpers.triggerEvent('close', {}, event);
|
|
649
|
+
|
|
650
|
+
// Remove from DOM after animation completes
|
|
651
|
+
setTimeout(() => {
|
|
652
|
+
if (component.element.parentNode && !state.visible) {
|
|
653
|
+
component.element.parentNode.removeChild(component.element);
|
|
654
|
+
}
|
|
655
|
+
}, 300); // Match the animation duration in CSS
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Toggles the menu
|
|
660
|
+
*/
|
|
661
|
+
const toggleMenu = (event?: Event): void => {
|
|
662
|
+
if (state.visible) {
|
|
663
|
+
closeMenu(event);
|
|
664
|
+
} else {
|
|
665
|
+
openMenu(event);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Handles document click
|
|
671
|
+
*/
|
|
672
|
+
const handleDocumentClick = (e: MouseEvent): void => {
|
|
673
|
+
// Don't close if clicked inside menu
|
|
674
|
+
if (component.element.contains(e.target as Node)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Check if clicked on anchor element
|
|
679
|
+
const anchor = getAnchorElement();
|
|
680
|
+
if (anchor && anchor.contains(e.target as Node)) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Close menu
|
|
685
|
+
closeMenu(e);
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Handles document keydown
|
|
690
|
+
*/
|
|
691
|
+
const handleDocumentKeydown = (e: KeyboardEvent): void => {
|
|
692
|
+
if (e.key === 'Escape') {
|
|
693
|
+
closeMenu(e);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Handles window resize
|
|
699
|
+
*/
|
|
700
|
+
const handleWindowResize = (): void => {
|
|
701
|
+
if (state.visible) {
|
|
702
|
+
positionMenu();
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Handles window scroll
|
|
708
|
+
*/
|
|
709
|
+
const handleWindowScroll = (): void => {
|
|
710
|
+
if (state.visible) {
|
|
711
|
+
positionMenu();
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Sets focus to the menu itself, but doesn't auto-select the first item
|
|
717
|
+
* This improves usability by not having an item automatically selected
|
|
718
|
+
*/
|
|
719
|
+
const focusFirstItem = (): void => {
|
|
720
|
+
// Instead of focusing the first item directly, focus the menu container
|
|
721
|
+
// which will still allow keyboard navigation to work
|
|
722
|
+
component.element.setAttribute('tabindex', '0');
|
|
723
|
+
component.element.focus();
|
|
724
|
+
|
|
725
|
+
// Reset active item index
|
|
726
|
+
state.activeItemIndex = -1;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Handles keydown events on the menu or submenu
|
|
731
|
+
*/
|
|
732
|
+
const handleMenuKeydown = (e: KeyboardEvent): void => {
|
|
733
|
+
// Determine if this event is from the main menu or a submenu
|
|
734
|
+
const isSubmenu = state.activeSubmenu && state.activeSubmenu.contains(e.target as Node);
|
|
735
|
+
|
|
736
|
+
// Get the appropriate menu element
|
|
737
|
+
const menuElement = isSubmenu ? state.activeSubmenu : component.element;
|
|
738
|
+
|
|
739
|
+
// Get all non-disabled menu items from the current menu
|
|
740
|
+
const items = Array.from(menuElement.querySelectorAll(
|
|
741
|
+
`.${component.getClass('menu-item')}:not(.${component.getClass('menu-item--disabled')})`
|
|
742
|
+
)) as HTMLElement[];
|
|
743
|
+
|
|
744
|
+
if (items.length === 0) return;
|
|
745
|
+
|
|
746
|
+
// Get the currently focused item index
|
|
747
|
+
let focusedItemIndex = -1;
|
|
748
|
+
const focusedElement = menuElement.querySelector(':focus') as HTMLElement;
|
|
749
|
+
if (focusedElement && focusedElement.classList.contains(component.getClass('menu-item'))) {
|
|
750
|
+
focusedItemIndex = items.indexOf(focusedElement);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
switch (e.key) {
|
|
754
|
+
case 'ArrowDown':
|
|
755
|
+
case 'Down':
|
|
756
|
+
e.preventDefault();
|
|
757
|
+
// If no item is active, select the first one
|
|
758
|
+
if (focusedItemIndex < 0) {
|
|
759
|
+
items[0].focus();
|
|
760
|
+
} else if (focusedItemIndex < items.length - 1) {
|
|
761
|
+
items[focusedItemIndex + 1].focus();
|
|
762
|
+
} else {
|
|
763
|
+
items[0].focus();
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
|
|
767
|
+
case 'ArrowUp':
|
|
768
|
+
case 'Up':
|
|
769
|
+
e.preventDefault();
|
|
770
|
+
// If no item is active, select the last one
|
|
771
|
+
if (focusedItemIndex < 0) {
|
|
772
|
+
items[items.length - 1].focus();
|
|
773
|
+
} else if (focusedItemIndex > 0) {
|
|
774
|
+
items[focusedItemIndex - 1].focus();
|
|
775
|
+
} else {
|
|
776
|
+
items[items.length - 1].focus();
|
|
777
|
+
}
|
|
778
|
+
break;
|
|
779
|
+
|
|
780
|
+
case 'Home':
|
|
781
|
+
e.preventDefault();
|
|
782
|
+
items[0].focus();
|
|
783
|
+
break;
|
|
784
|
+
|
|
785
|
+
case 'End':
|
|
786
|
+
e.preventDefault();
|
|
787
|
+
items[items.length - 1].focus();
|
|
788
|
+
break;
|
|
789
|
+
|
|
790
|
+
case 'Enter':
|
|
791
|
+
case ' ':
|
|
792
|
+
e.preventDefault();
|
|
793
|
+
// If an item is focused, click it
|
|
794
|
+
if (focusedItemIndex >= 0) {
|
|
795
|
+
items[focusedItemIndex].click();
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
|
|
799
|
+
case 'ArrowRight':
|
|
800
|
+
case 'Right':
|
|
801
|
+
e.preventDefault();
|
|
802
|
+
// Handle right arrow in different contexts
|
|
803
|
+
if (isSubmenu) {
|
|
804
|
+
// In a submenu, right arrow opens nested submenus
|
|
805
|
+
if (focusedItemIndex >= 0 && items[focusedItemIndex].classList.contains(`${component.getClass('menu-item--submenu')}`)) {
|
|
806
|
+
items[focusedItemIndex].click();
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
// In main menu, right arrow opens a submenu
|
|
810
|
+
if (focusedItemIndex >= 0 && items[focusedItemIndex].classList.contains(`${component.getClass('menu-item--submenu')}`)) {
|
|
811
|
+
// Get the correct menu item data
|
|
812
|
+
const itemElement = items[focusedItemIndex];
|
|
813
|
+
const itemIndex = parseInt(itemElement.getAttribute('data-index'), 10);
|
|
814
|
+
const itemData = state.items[itemIndex] as MenuItem;
|
|
815
|
+
|
|
816
|
+
// Open submenu via keyboard
|
|
817
|
+
handleSubmenuClick(itemData, itemIndex, itemElement, true);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
break;
|
|
821
|
+
|
|
822
|
+
case 'ArrowLeft':
|
|
823
|
+
case 'Left':
|
|
824
|
+
e.preventDefault();
|
|
825
|
+
// Handle left arrow in different contexts
|
|
826
|
+
if (isSubmenu) {
|
|
827
|
+
// In a submenu, left arrow returns to the parent menu
|
|
828
|
+
if (state.activeSubmenuItem) {
|
|
829
|
+
// Store the reference to the parent item before closing the submenu
|
|
830
|
+
const parentItem = state.activeSubmenuItem;
|
|
831
|
+
closeSubmenu();
|
|
832
|
+
// Focus the parent item after closing
|
|
833
|
+
parentItem.focus();
|
|
834
|
+
} else {
|
|
835
|
+
closeSubmenu();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
|
|
840
|
+
case 'Escape':
|
|
841
|
+
e.preventDefault();
|
|
842
|
+
if (isSubmenu) {
|
|
843
|
+
// In a submenu, Escape closes just the submenu
|
|
844
|
+
if (state.activeSubmenuItem) {
|
|
845
|
+
// Store the reference to the parent item before closing the submenu
|
|
846
|
+
const parentItem = state.activeSubmenuItem;
|
|
847
|
+
closeSubmenu();
|
|
848
|
+
// Focus the parent item after closing
|
|
849
|
+
parentItem.focus();
|
|
850
|
+
} else {
|
|
851
|
+
closeSubmenu();
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
// In main menu, Escape closes the entire menu
|
|
855
|
+
closeMenu(e);
|
|
856
|
+
}
|
|
857
|
+
break;
|
|
858
|
+
|
|
859
|
+
case 'Tab':
|
|
860
|
+
// Close the menu when tabbing out
|
|
861
|
+
if (!isSubmenu) {
|
|
862
|
+
closeMenu();
|
|
863
|
+
}
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Sets up the menu
|
|
870
|
+
*/
|
|
871
|
+
const initMenu = () => {
|
|
872
|
+
// Set up menu structure
|
|
873
|
+
renderMenuItems();
|
|
874
|
+
|
|
875
|
+
// Set up keyboard navigation
|
|
876
|
+
component.element.addEventListener('keydown', handleMenuKeydown);
|
|
877
|
+
|
|
878
|
+
// Position if visible
|
|
879
|
+
if (state.visible) {
|
|
880
|
+
positionMenu();
|
|
881
|
+
|
|
882
|
+
// Show immediately
|
|
883
|
+
component.element.classList.add(`${component.getClass('menu--visible')}`);
|
|
884
|
+
|
|
885
|
+
// Set up document events
|
|
886
|
+
if (config.closeOnClickOutside) {
|
|
887
|
+
document.addEventListener('click', handleDocumentClick);
|
|
888
|
+
}
|
|
889
|
+
if (config.closeOnEscape) {
|
|
890
|
+
document.addEventListener('keydown', handleDocumentKeydown);
|
|
891
|
+
}
|
|
892
|
+
window.addEventListener('resize', handleWindowResize);
|
|
893
|
+
window.addEventListener('scroll', handleWindowScroll);
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// Initialize after DOM is ready
|
|
898
|
+
setTimeout(initMenu, 0);
|
|
899
|
+
|
|
900
|
+
// Register with lifecycle if available
|
|
901
|
+
if (component.lifecycle) {
|
|
902
|
+
const originalDestroy = component.lifecycle.destroy || (() => {});
|
|
903
|
+
component.lifecycle.destroy = () => {
|
|
904
|
+
// Clean up timers
|
|
905
|
+
clearHoverIntent();
|
|
906
|
+
clearSubmenuTimer();
|
|
907
|
+
|
|
908
|
+
// Clean up document events
|
|
909
|
+
document.removeEventListener('click', handleDocumentClick);
|
|
910
|
+
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
911
|
+
window.removeEventListener('resize', handleWindowResize);
|
|
912
|
+
window.removeEventListener('scroll', handleWindowScroll);
|
|
913
|
+
|
|
914
|
+
// Clean up submenu events
|
|
915
|
+
document.removeEventListener('click', handleDocumentClickForSubmenu);
|
|
916
|
+
window.removeEventListener('resize', handleWindowResizeForSubmenu);
|
|
917
|
+
window.removeEventListener('scroll', handleWindowScrollForSubmenu);
|
|
918
|
+
|
|
919
|
+
// Clean up submenu element
|
|
920
|
+
if (state.activeSubmenu && state.activeSubmenu.parentNode) {
|
|
921
|
+
state.activeSubmenu.parentNode.removeChild(state.activeSubmenu);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
originalDestroy();
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Return enhanced component
|
|
929
|
+
return {
|
|
930
|
+
...component,
|
|
931
|
+
menu: {
|
|
932
|
+
open: (event) => {
|
|
933
|
+
openMenu(event);
|
|
934
|
+
return component;
|
|
935
|
+
},
|
|
936
|
+
|
|
937
|
+
close: (event) => {
|
|
938
|
+
closeMenu(event);
|
|
939
|
+
return component;
|
|
940
|
+
},
|
|
941
|
+
|
|
942
|
+
toggle: (event) => {
|
|
943
|
+
toggleMenu(event);
|
|
944
|
+
return component;
|
|
945
|
+
},
|
|
946
|
+
|
|
947
|
+
isOpen: () => state.visible,
|
|
948
|
+
|
|
949
|
+
setItems: (items) => {
|
|
950
|
+
state.items = items;
|
|
951
|
+
renderMenuItems();
|
|
952
|
+
return component;
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
getItems: () => state.items,
|
|
956
|
+
|
|
957
|
+
setPlacement: (placement) => {
|
|
958
|
+
state.placement = placement;
|
|
959
|
+
if (state.visible) {
|
|
960
|
+
positionMenu();
|
|
961
|
+
}
|
|
962
|
+
return component;
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
getPlacement: () => state.placement
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
export default withController;
|