@vaadin/context-menu 24.0.0-alpha9 → 24.0.0-beta2
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 +11 -11
- package/src/vaadin-context-menu-item.d.ts +21 -0
- package/src/vaadin-context-menu-item.js +53 -0
- package/src/vaadin-context-menu-list-box.d.ts +22 -0
- package/src/vaadin-context-menu-list-box.js +81 -0
- package/src/vaadin-context-menu-overlay.d.ts +20 -0
- package/src/vaadin-context-menu-overlay.js +6 -117
- package/src/vaadin-context-menu.d.ts +14 -9
- package/src/vaadin-context-menu.js +19 -11
- package/src/vaadin-contextmenu-items-mixin.d.ts +15 -31
- package/src/vaadin-contextmenu-items-mixin.js +236 -181
- package/src/vaadin-menu-overlay-mixin.d.ts +25 -0
- package/src/vaadin-menu-overlay-mixin.js +119 -0
- package/src/vaadin-menu-overlay-styles.d.ts +8 -0
- package/src/vaadin-menu-overlay-styles.js +26 -0
- package/theme/lumo/vaadin-context-menu-item-styles.js +46 -0
- package/theme/lumo/vaadin-context-menu-list-box-styles.js +47 -0
- package/theme/lumo/vaadin-context-menu-overlay-styles.js +33 -0
- package/theme/lumo/vaadin-context-menu.js +3 -3
- package/theme/material/vaadin-context-menu-item-styles.js +36 -0
- package/theme/material/vaadin-context-menu-list-box-styles.js +38 -0
- package/theme/material/vaadin-context-menu-overlay-styles.js +15 -0
- package/theme/material/vaadin-context-menu.js +3 -3
- package/web-types.json +3 -3
- package/web-types.lit.json +3 -3
- package/theme/lumo/vaadin-context-menu-styles.js +0 -115
- package/theme/material/vaadin-context-menu-styles.js +0 -79
|
@@ -3,37 +3,9 @@
|
|
|
3
3
|
* Copyright (c) 2016 - 2023 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
+
import './vaadin-context-menu-item.js';
|
|
7
|
+
import './vaadin-context-menu-list-box.js';
|
|
6
8
|
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
|
|
7
|
-
import { Item } from '@vaadin/item/src/vaadin-item.js';
|
|
8
|
-
import { ListBox } from '@vaadin/list-box/src/vaadin-list-box.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
|
|
12
|
-
*
|
|
13
|
-
* @extends Item
|
|
14
|
-
* @protected
|
|
15
|
-
*/
|
|
16
|
-
class ContextMenuItemElement extends Item {
|
|
17
|
-
static get is() {
|
|
18
|
-
return 'vaadin-context-menu-item';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
customElements.define(ContextMenuItemElement.is, ContextMenuItemElement);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
|
|
26
|
-
*
|
|
27
|
-
* @extends ListBox
|
|
28
|
-
* @protected
|
|
29
|
-
*/
|
|
30
|
-
class ContextMenuListBox extends ListBox {
|
|
31
|
-
static get is() {
|
|
32
|
-
return 'vaadin-context-menu-list-box';
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
customElements.define(ContextMenuListBox.is, ContextMenuListBox);
|
|
37
9
|
|
|
38
10
|
/**
|
|
39
11
|
* @polymerMixin
|
|
@@ -65,20 +37,20 @@ export const ItemsMixin = (superClass) =>
|
|
|
65
37
|
*
|
|
66
38
|
* ```javascript
|
|
67
39
|
* contextMenu.items = [
|
|
68
|
-
* {text: 'Menu Item 1', theme: 'primary', children:
|
|
40
|
+
* { text: 'Menu Item 1', theme: 'primary', children:
|
|
69
41
|
* [
|
|
70
|
-
* {text: 'Menu Item 1-1', checked: true},
|
|
71
|
-
* {text: 'Menu Item 1-2'}
|
|
42
|
+
* { text: 'Menu Item 1-1', checked: true },
|
|
43
|
+
* { text: 'Menu Item 1-2' }
|
|
72
44
|
* ]
|
|
73
45
|
* },
|
|
74
|
-
* {component: 'hr'},
|
|
75
|
-
* {text: 'Menu Item 2', children:
|
|
46
|
+
* { component: 'hr' },
|
|
47
|
+
* { text: 'Menu Item 2', children:
|
|
76
48
|
* [
|
|
77
|
-
* {text: 'Menu Item 2-1'},
|
|
78
|
-
* {text: 'Menu Item 2-2', disabled: true}
|
|
49
|
+
* { text: 'Menu Item 2-1' },
|
|
50
|
+
* { text: 'Menu Item 2-2', disabled: true }
|
|
79
51
|
* ]
|
|
80
52
|
* },
|
|
81
|
-
* {text: 'Menu Item 3', disabled: true}
|
|
53
|
+
* { text: 'Menu Item 3', disabled: true }
|
|
82
54
|
* ];
|
|
83
55
|
* ```
|
|
84
56
|
*
|
|
@@ -98,6 +70,15 @@ export const ItemsMixin = (superClass) =>
|
|
|
98
70
|
};
|
|
99
71
|
}
|
|
100
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Tag name prefix used by overlay, list-box and items.
|
|
75
|
+
* @protected
|
|
76
|
+
* @return {string}
|
|
77
|
+
*/
|
|
78
|
+
get _tagNamePrefix() {
|
|
79
|
+
return 'vaadin-context-menu';
|
|
80
|
+
}
|
|
81
|
+
|
|
101
82
|
/** @protected */
|
|
102
83
|
ready() {
|
|
103
84
|
super.ready();
|
|
@@ -105,7 +86,7 @@ export const ItemsMixin = (superClass) =>
|
|
|
105
86
|
// Overlay's outside click listener doesn't work with modeless
|
|
106
87
|
// overlays (submenus) so we need additional logic for it
|
|
107
88
|
this.__itemsOutsideClickListener = (e) => {
|
|
108
|
-
if (!e.composedPath().some((el) => el.localName ===
|
|
89
|
+
if (!e.composedPath().some((el) => el.localName === `${this._tagNamePrefix}-overlay`)) {
|
|
109
90
|
this.dispatchEvent(new CustomEvent('items-outside-click'));
|
|
110
91
|
}
|
|
111
92
|
};
|
|
@@ -177,55 +158,216 @@ export const ItemsMixin = (superClass) =>
|
|
|
177
158
|
}
|
|
178
159
|
|
|
179
160
|
/**
|
|
180
|
-
* @param {!
|
|
181
|
-
* @
|
|
182
|
-
* @
|
|
183
|
-
* @protected
|
|
161
|
+
* @param {!ContextMenuItem} item
|
|
162
|
+
* @return {HTMLElement}
|
|
163
|
+
* @private
|
|
184
164
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
165
|
+
__createComponent(item) {
|
|
166
|
+
let component;
|
|
187
167
|
|
|
188
|
-
|
|
189
|
-
|
|
168
|
+
if (item.component instanceof HTMLElement) {
|
|
169
|
+
component = item.component;
|
|
170
|
+
} else {
|
|
171
|
+
component = document.createElement(item.component || `${this._tagNamePrefix}-item`);
|
|
172
|
+
}
|
|
190
173
|
|
|
191
|
-
|
|
174
|
+
// Support menu-bar / context-menu item
|
|
175
|
+
if (component._hasVaadinItemMixin) {
|
|
176
|
+
component.setAttribute('role', 'menuitem');
|
|
177
|
+
}
|
|
192
178
|
|
|
193
|
-
|
|
179
|
+
if (component.localName === 'hr') {
|
|
180
|
+
component.setAttribute('role', 'separator');
|
|
181
|
+
} else {
|
|
182
|
+
// Accept not `menuitem` elements e.g. `<button>`
|
|
183
|
+
component.setAttribute('aria-haspopup', 'false');
|
|
184
|
+
}
|
|
194
185
|
|
|
195
|
-
|
|
186
|
+
this._setMenuItemTheme(component, item, this._theme);
|
|
196
187
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
188
|
+
component._item = item;
|
|
189
|
+
|
|
190
|
+
if (item.text) {
|
|
191
|
+
component.textContent = item.text;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.__toggleMenuComponentAttribute(component, 'menu-item-checked', item.checked);
|
|
195
|
+
this.__toggleMenuComponentAttribute(component, 'disabled', item.disabled);
|
|
196
|
+
|
|
197
|
+
if (item.children && item.children.length) {
|
|
198
|
+
this.__updateExpanded(component, false);
|
|
199
|
+
component.setAttribute('aria-haspopup', 'true');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return component;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @private */
|
|
206
|
+
__initListBox() {
|
|
207
|
+
const listBox = document.createElement(`${this._tagNamePrefix}-list-box`);
|
|
208
|
+
|
|
209
|
+
if (this._theme) {
|
|
210
|
+
listBox.setAttribute('theme', this._theme);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
listBox.addEventListener('selected-changed', (event) => {
|
|
214
|
+
const { value } = event.detail;
|
|
215
|
+
if (typeof value === 'number') {
|
|
216
|
+
const item = listBox.items[value]._item;
|
|
217
|
+
if (!item.children) {
|
|
218
|
+
this.dispatchEvent(new CustomEvent('item-selected', { detail: { value: item } }));
|
|
219
|
+
}
|
|
220
|
+
listBox.selected = null;
|
|
203
221
|
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return listBox;
|
|
225
|
+
}
|
|
204
226
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
/** @private */
|
|
228
|
+
__initOverlay() {
|
|
229
|
+
const overlay = this.$.overlay;
|
|
230
|
+
|
|
231
|
+
overlay.$.backdrop.addEventListener('click', () => {
|
|
232
|
+
this.close();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Open a submenu on click event when a touch device is used.
|
|
236
|
+
// On desktop, a submenu opens on hover.
|
|
237
|
+
overlay.addEventListener(isTouch ? 'click' : 'mouseover', (event) => {
|
|
238
|
+
this.__showSubMenu(event);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
overlay.addEventListener('keydown', (event) => {
|
|
242
|
+
const { key } = event;
|
|
243
|
+
const isRTL = this.__isRTL;
|
|
244
|
+
|
|
245
|
+
const isArrowRight = key === 'ArrowRight';
|
|
246
|
+
const isArrowLeft = key === 'ArrowLeft';
|
|
247
|
+
|
|
248
|
+
if ((!isRTL && isArrowRight) || (isRTL && isArrowLeft) || key === 'Enter' || key === ' ') {
|
|
249
|
+
// Open a sub-menu
|
|
250
|
+
this.__showSubMenu(event);
|
|
251
|
+
} else if ((!isRTL && isArrowLeft) || (isRTL && isArrowRight)) {
|
|
252
|
+
// Close the menu
|
|
253
|
+
this.close();
|
|
254
|
+
this.listenOn.focus();
|
|
255
|
+
} else if (key === 'Escape' || key === 'Tab') {
|
|
256
|
+
// Close all menus
|
|
257
|
+
this.dispatchEvent(new CustomEvent('close-all-menus'));
|
|
209
258
|
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** @private */
|
|
263
|
+
__initSubMenu() {
|
|
264
|
+
const subMenu = document.createElement(this.constructor.is);
|
|
210
265
|
|
|
211
|
-
|
|
266
|
+
subMenu._modeless = true;
|
|
267
|
+
subMenu.openOn = 'opensubmenu';
|
|
212
268
|
|
|
213
|
-
|
|
269
|
+
// Sub-menu doesn't have a target to wrap,
|
|
270
|
+
// so there is no need to keep it visible.
|
|
271
|
+
subMenu.setAttribute('hidden', '');
|
|
214
272
|
|
|
215
|
-
|
|
216
|
-
|
|
273
|
+
// Close sub-menu when the parent menu closes.
|
|
274
|
+
this.addEventListener('opened-changed', (event) => {
|
|
275
|
+
if (!event.detail.value) {
|
|
276
|
+
this._subMenu.close();
|
|
217
277
|
}
|
|
278
|
+
});
|
|
218
279
|
|
|
219
|
-
|
|
220
|
-
|
|
280
|
+
// Forward event to the parent menu element.
|
|
281
|
+
subMenu.addEventListener('close-all-menus', () => {
|
|
282
|
+
this.dispatchEvent(new CustomEvent('close-all-menus'));
|
|
283
|
+
});
|
|
221
284
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
285
|
+
// Forward event to the parent menu element.
|
|
286
|
+
subMenu.addEventListener('item-selected', (event) => {
|
|
287
|
+
const { detail } = event;
|
|
288
|
+
this.dispatchEvent(new CustomEvent('item-selected', { detail }));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Listen to the forwarded event from sub-menu.
|
|
292
|
+
this.addEventListener('close-all-menus', () => {
|
|
293
|
+
this.close();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Listen to the forwarded event from sub-menu.
|
|
297
|
+
this.addEventListener('item-selected', () => {
|
|
298
|
+
this.close();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Mark parent item as collapsed when closing.
|
|
302
|
+
subMenu.addEventListener('opened-changed', (event) => {
|
|
303
|
+
if (!event.detail.value) {
|
|
304
|
+
const expandedItem = this._listBox.querySelector('[expanded]');
|
|
305
|
+
if (expandedItem) {
|
|
306
|
+
this.__updateExpanded(expandedItem, false);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return subMenu;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** @private */
|
|
315
|
+
__showSubMenu(event, item = event.composedPath().find((node) => node.localName === `${this._tagNamePrefix}-item`)) {
|
|
316
|
+
// Delay enabling the mouseover listener to avoid it from triggering on parent menu open
|
|
317
|
+
if (!this.__openListenerActive) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Don't open sub-menus while the menu is still opening
|
|
322
|
+
if (this.$.overlay.hasAttribute('opening')) {
|
|
323
|
+
requestAnimationFrame(() => {
|
|
324
|
+
this.__showSubMenu(event, item);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const subMenu = this._subMenu;
|
|
331
|
+
|
|
332
|
+
if (item) {
|
|
333
|
+
const { children } = item._item;
|
|
334
|
+
|
|
335
|
+
if (subMenu.items !== children) {
|
|
336
|
+
subMenu.close();
|
|
337
|
+
}
|
|
338
|
+
if (!this.opened) {
|
|
339
|
+
return;
|
|
227
340
|
}
|
|
228
341
|
|
|
342
|
+
if (children && children.length) {
|
|
343
|
+
this.__updateExpanded(item, true);
|
|
344
|
+
|
|
345
|
+
// Forward parent overlay class
|
|
346
|
+
const { overlayClass } = this;
|
|
347
|
+
this.__openSubMenu(subMenu, item, overlayClass);
|
|
348
|
+
} else {
|
|
349
|
+
subMenu.listenOn.focus();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {!HTMLElement} root
|
|
356
|
+
* @param {!ContextMenu} menu
|
|
357
|
+
* @param {!ContextMenuRendererContext} context
|
|
358
|
+
* @protected
|
|
359
|
+
*/
|
|
360
|
+
__itemsRenderer(root, menu, { detail }) {
|
|
361
|
+
this.__initMenu(root, menu);
|
|
362
|
+
|
|
363
|
+
const subMenu = root.querySelector(this.constructor.is);
|
|
364
|
+
subMenu.closeOn = menu.closeOn;
|
|
365
|
+
|
|
366
|
+
const listBox = root.querySelector(`${this._tagNamePrefix}-list-box`);
|
|
367
|
+
listBox.innerHTML = '';
|
|
368
|
+
|
|
369
|
+
[...(detail.children || menu.items)].forEach((item) => {
|
|
370
|
+
const component = this.__createComponent(item);
|
|
229
371
|
listBox.appendChild(component);
|
|
230
372
|
});
|
|
231
373
|
}
|
|
@@ -241,11 +383,7 @@ export const ItemsMixin = (superClass) =>
|
|
|
241
383
|
theme = Array.isArray(item.theme) ? item.theme.join(' ') : item.theme;
|
|
242
384
|
}
|
|
243
385
|
|
|
244
|
-
|
|
245
|
-
component.setAttribute('theme', theme);
|
|
246
|
-
} else {
|
|
247
|
-
component.removeAttribute('theme');
|
|
248
|
-
}
|
|
386
|
+
this.__updateTheme(component, theme);
|
|
249
387
|
}
|
|
250
388
|
|
|
251
389
|
/** @private */
|
|
@@ -261,122 +399,39 @@ export const ItemsMixin = (superClass) =>
|
|
|
261
399
|
|
|
262
400
|
/** @private */
|
|
263
401
|
__initMenu(root, menu) {
|
|
402
|
+
// NOTE: in this method, `menu` and `this` reference the same element,
|
|
403
|
+
// so we can use either of those. Original implementation used `menu`.
|
|
264
404
|
if (!root.firstElementChild) {
|
|
265
|
-
|
|
266
|
-
root.appendChild(listBox);
|
|
405
|
+
this.__initOverlay();
|
|
267
406
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
requestAnimationFrame(() => listBox.setAttribute('role', 'menu'));
|
|
407
|
+
const listBox = this.__initListBox();
|
|
408
|
+
this._listBox = listBox;
|
|
409
|
+
root.appendChild(listBox);
|
|
272
410
|
|
|
273
|
-
const subMenu =
|
|
274
|
-
|
|
411
|
+
const subMenu = this.__initSubMenu();
|
|
412
|
+
this._subMenu = subMenu;
|
|
275
413
|
root.appendChild(subMenu);
|
|
276
|
-
subMenu.$.overlay.modeless = true;
|
|
277
|
-
subMenu.openOn = 'opensubmenu';
|
|
278
|
-
|
|
279
|
-
menu.addEventListener('opened-changed', (e) => !e.detail.value && subMenu.close());
|
|
280
|
-
subMenu.addEventListener('opened-changed', (e) => {
|
|
281
|
-
if (!e.detail.value) {
|
|
282
|
-
const expandedItem = listBox.querySelector('[expanded]');
|
|
283
|
-
if (expandedItem) {
|
|
284
|
-
expandedItem.setAttribute('aria-expanded', 'false');
|
|
285
|
-
expandedItem.removeAttribute('expanded');
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
listBox.addEventListener('selected-changed', (e) => {
|
|
291
|
-
if (typeof e.detail.value === 'number') {
|
|
292
|
-
const item = e.target.items[e.detail.value]._item;
|
|
293
|
-
if (!item.children) {
|
|
294
|
-
const detail = { value: item };
|
|
295
|
-
menu.dispatchEvent(new CustomEvent('item-selected', { detail }));
|
|
296
|
-
}
|
|
297
|
-
listBox.selected = null;
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
subMenu.addEventListener('item-selected', (e) => {
|
|
302
|
-
menu.dispatchEvent(new CustomEvent('item-selected', { detail: e.detail }));
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
subMenu.addEventListener('close-all-menus', () => {
|
|
306
|
-
menu.dispatchEvent(new CustomEvent('close-all-menus'));
|
|
307
|
-
});
|
|
308
|
-
menu.addEventListener('close-all-menus', menu.close);
|
|
309
|
-
menu.addEventListener('item-selected', menu.close);
|
|
310
|
-
menu.$.overlay.$.backdrop.addEventListener('click', () => menu.close());
|
|
311
|
-
|
|
312
|
-
menu.$.overlay.addEventListener('keydown', (e) => {
|
|
313
|
-
const isRTL = this.__isRTL;
|
|
314
|
-
if ((!isRTL && e.keyCode === 37) || (isRTL && e.keyCode === 39)) {
|
|
315
|
-
menu.close();
|
|
316
|
-
menu.listenOn.focus();
|
|
317
|
-
} else if (e.key === 'Escape' || e.key === 'Tab') {
|
|
318
|
-
menu.dispatchEvent(new CustomEvent('close-all-menus'));
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
414
|
|
|
322
415
|
requestAnimationFrame(() => {
|
|
323
416
|
this.__openListenerActive = true;
|
|
324
417
|
});
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// Delay enabling the mouseover listener to avoid it from triggering on parent menu open
|
|
330
|
-
if (!this.__openListenerActive) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Don't open sub-menus while the menu is still opening
|
|
335
|
-
if (menu.$.overlay.hasAttribute('opening')) {
|
|
336
|
-
requestAnimationFrame(() => openSubMenu(e, itemElement));
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (itemElement) {
|
|
341
|
-
if (subMenu.items !== itemElement._item.children) {
|
|
342
|
-
subMenu.close();
|
|
343
|
-
}
|
|
344
|
-
if (!menu.opened) {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (itemElement._item.children && itemElement._item.children.length) {
|
|
348
|
-
itemElement.setAttribute('aria-expanded', 'true');
|
|
349
|
-
itemElement.setAttribute('expanded', '');
|
|
350
|
-
|
|
351
|
-
// Forward parent overlay class
|
|
352
|
-
const { overlayClass } = menu;
|
|
353
|
-
this.__openSubMenu(subMenu, itemElement, overlayClass);
|
|
354
|
-
} else {
|
|
355
|
-
subMenu.listenOn.focus();
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// Open a submenu on click event when a touch device is used.
|
|
361
|
-
// On desktop, a submenu opens on hover.
|
|
362
|
-
menu.$.overlay.addEventListener(isTouch ? 'click' : 'mouseover', openSubMenu);
|
|
418
|
+
} else {
|
|
419
|
+
this.__updateTheme(this._listBox, this._theme);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
363
422
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
423
|
+
/** @private */
|
|
424
|
+
__updateExpanded(component, expanded) {
|
|
425
|
+
component.setAttribute('aria-expanded', expanded.toString());
|
|
426
|
+
component.toggleAttribute('expanded', expanded);
|
|
427
|
+
}
|
|
368
428
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
429
|
+
/** @private */
|
|
430
|
+
__updateTheme(component, theme) {
|
|
431
|
+
if (theme) {
|
|
432
|
+
component.setAttribute('theme', theme);
|
|
373
433
|
} else {
|
|
374
|
-
|
|
375
|
-
if (this._theme) {
|
|
376
|
-
listBox.setAttribute('theme', this._theme);
|
|
377
|
-
} else {
|
|
378
|
-
listBox.removeAttribute('theme');
|
|
379
|
-
}
|
|
434
|
+
component.removeAttribute('theme');
|
|
380
435
|
}
|
|
381
436
|
}
|
|
382
437
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2016 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import type { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import type { PositionMixinClass } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
|
|
8
|
+
|
|
9
|
+
export declare function MenuOverlayMixin<T extends Constructor<HTMLElement>>(
|
|
10
|
+
base: T,
|
|
11
|
+
): Constructor<MenuOverlayMixinClass> & Constructor<PositionMixinClass> & T;
|
|
12
|
+
|
|
13
|
+
export declare class MenuOverlayMixinClass {
|
|
14
|
+
protected readonly parentOverlay: HTMLElement | undefined;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns the adjusted boundaries of the overlay.
|
|
18
|
+
*/
|
|
19
|
+
getBoundaries(): { xMax: number; xMin: number; yMax: number };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns the first element in the overlay content.
|
|
23
|
+
*/
|
|
24
|
+
getFirstChild(): HTMLElement;
|
|
25
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2016 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @polymerMixin
|
|
10
|
+
*/
|
|
11
|
+
export const MenuOverlayMixin = (superClass) =>
|
|
12
|
+
class MenuOverlayMixin extends PositionMixin(superClass) {
|
|
13
|
+
static get properties() {
|
|
14
|
+
return {
|
|
15
|
+
/**
|
|
16
|
+
* @protected
|
|
17
|
+
*/
|
|
18
|
+
parentOverlay: {
|
|
19
|
+
type: Object,
|
|
20
|
+
readOnly: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static get observers() {
|
|
26
|
+
return ['_themeChanged(_theme)'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @protected */
|
|
30
|
+
ready() {
|
|
31
|
+
super.ready();
|
|
32
|
+
|
|
33
|
+
this.addEventListener('keydown', (e) => {
|
|
34
|
+
if (!e.defaultPrevented && e.composedPath()[0] === this.$.overlay && [38, 40].indexOf(e.keyCode) > -1) {
|
|
35
|
+
const child = this.getFirstChild();
|
|
36
|
+
if (child && Array.isArray(child.items) && child.items.length) {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
if (e.keyCode === 38) {
|
|
39
|
+
child.items[child.items.length - 1].focus();
|
|
40
|
+
} else {
|
|
41
|
+
child.focus();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the first element in the overlay content.
|
|
50
|
+
*
|
|
51
|
+
* @returns {HTMLElement}
|
|
52
|
+
*/
|
|
53
|
+
getFirstChild() {
|
|
54
|
+
return this.querySelector(':not(style):not(slot)');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @private */
|
|
58
|
+
_themeChanged() {
|
|
59
|
+
this.close();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns the adjusted boundaries of the overlay.
|
|
64
|
+
*
|
|
65
|
+
* @returns {object}
|
|
66
|
+
*/
|
|
67
|
+
getBoundaries() {
|
|
68
|
+
// Measure actual overlay and content sizes
|
|
69
|
+
const overlayRect = this.getBoundingClientRect();
|
|
70
|
+
const contentRect = this.$.overlay.getBoundingClientRect();
|
|
71
|
+
|
|
72
|
+
// Maximum x and y values are imposed by content size and overlay limits.
|
|
73
|
+
let yMax = overlayRect.bottom - contentRect.height;
|
|
74
|
+
|
|
75
|
+
// Adjust constraints to ensure bottom-aligned applies to sub-menu.
|
|
76
|
+
const parent = this.parentOverlay;
|
|
77
|
+
if (parent && parent.hasAttribute('bottom-aligned')) {
|
|
78
|
+
const parentStyle = getComputedStyle(parent);
|
|
79
|
+
yMax = yMax - parseFloat(parentStyle.bottom) - parseFloat(parentStyle.height);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
xMax: overlayRect.right - contentRect.width,
|
|
84
|
+
xMin: overlayRect.left + contentRect.width,
|
|
85
|
+
yMax,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @protected
|
|
91
|
+
* @override
|
|
92
|
+
*/
|
|
93
|
+
_updatePosition() {
|
|
94
|
+
super._updatePosition();
|
|
95
|
+
|
|
96
|
+
if (this.positionTarget && this.parentOverlay) {
|
|
97
|
+
// This overlay is positioned by a parent menu item,
|
|
98
|
+
// adjust the position by the overlay content paddings
|
|
99
|
+
const content = this.$.content;
|
|
100
|
+
const style = getComputedStyle(content);
|
|
101
|
+
|
|
102
|
+
// Horizontal adjustment
|
|
103
|
+
const isLeftAligned = !!this.style.left;
|
|
104
|
+
if (isLeftAligned) {
|
|
105
|
+
this.style.left = `${parseFloat(this.style.left) + parseFloat(style.paddingLeft)}px`;
|
|
106
|
+
} else {
|
|
107
|
+
this.style.right = `${parseFloat(this.style.right) + parseFloat(style.paddingRight)}px`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Vertical adjustment
|
|
111
|
+
const isBottomAligned = !!this.style.bottom;
|
|
112
|
+
if (isBottomAligned) {
|
|
113
|
+
this.style.bottom = `${parseFloat(this.style.bottom) - parseFloat(style.paddingBottom)}px`;
|
|
114
|
+
} else {
|
|
115
|
+
this.style.top = `${parseFloat(this.style.top) - parseFloat(style.paddingTop)}px`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2016 - 2023 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { css } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
7
|
+
|
|
8
|
+
export const styles = css`
|
|
9
|
+
:host {
|
|
10
|
+
align-items: flex-start;
|
|
11
|
+
justify-content: flex-start;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:host([right-aligned]),
|
|
15
|
+
:host([end-aligned]) {
|
|
16
|
+
align-items: flex-end;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:host([bottom-aligned]) {
|
|
20
|
+
justify-content: flex-end;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[part='overlay'] {
|
|
24
|
+
background-color: #fff;
|
|
25
|
+
}
|
|
26
|
+
`;
|