@vaadin/context-menu 22.0.0-alpha7
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/LICENSE +190 -0
- package/README.md +85 -0
- package/package.json +52 -0
- package/src/interfaces.d.ts +43 -0
- package/src/vaadin-context-menu-overlay.js +113 -0
- package/src/vaadin-context-menu.d.ts +281 -0
- package/src/vaadin-context-menu.js +733 -0
- package/src/vaadin-contextmenu-event.js +103 -0
- package/src/vaadin-contextmenu-items-mixin.d.ts +74 -0
- package/src/vaadin-contextmenu-items-mixin.js +397 -0
- package/src/vaadin-device-detector.js +68 -0
- package/theme/lumo/vaadin-context-menu-styles.js +127 -0
- package/theme/lumo/vaadin-context-menu.js +4 -0
- package/theme/material/vaadin-context-menu-styles.js +82 -0
- package/theme/material/vaadin-context-menu.js +4 -0
- package/vaadin-context-menu.d.ts +2 -0
- package/vaadin-context-menu.js +2 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { register, prevent } from '@polymer/polymer/lib/utils/gestures.js';
|
|
7
|
+
|
|
8
|
+
register({
|
|
9
|
+
name: 'vaadin-contextmenu',
|
|
10
|
+
deps: ['touchstart', 'touchmove', 'touchend', 'contextmenu'],
|
|
11
|
+
flow: {
|
|
12
|
+
start: ['touchstart', 'contextmenu'],
|
|
13
|
+
end: ['contextmenu']
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
emits: ['vaadin-contextmenu'],
|
|
17
|
+
|
|
18
|
+
info: {
|
|
19
|
+
sourceEvent: null,
|
|
20
|
+
_ios:
|
|
21
|
+
(/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) ||
|
|
22
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
reset: function () {
|
|
26
|
+
this.info.sourceEvent = null;
|
|
27
|
+
this._cancelTimer();
|
|
28
|
+
this.info.touchJob = null;
|
|
29
|
+
this.info.touchStartCoords = null;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
_cancelTimer: function () {
|
|
33
|
+
if (this._timerId) {
|
|
34
|
+
clearTimeout(this._timerId);
|
|
35
|
+
delete this._fired;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
touchstart: function (e) {
|
|
40
|
+
this.info.sourceEvent = e;
|
|
41
|
+
this.info.touchStartCoords = {
|
|
42
|
+
x: e.changedTouches[0].clientX,
|
|
43
|
+
y: e.changedTouches[0].clientY
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// After timeout event is already retargeted to the parent element in case there is one.
|
|
47
|
+
// So we are assigning the target synchronously on event dispatched.
|
|
48
|
+
const t = e.composedPath()[0] || e.target;
|
|
49
|
+
|
|
50
|
+
this._timerId = setTimeout(() => {
|
|
51
|
+
const ct = e.changedTouches[0];
|
|
52
|
+
if (!e.shiftKey) {
|
|
53
|
+
if (this.info._ios) {
|
|
54
|
+
this._fired = true;
|
|
55
|
+
this.fire(t, ct.clientX, ct.clientY);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// needed to prevent any 'tap' gesture events from firing
|
|
59
|
+
// which could potentially cancel/close the overlay.
|
|
60
|
+
prevent('tap');
|
|
61
|
+
}
|
|
62
|
+
}, 500); // default setting for Android and iOS.
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
touchmove: function (e) {
|
|
66
|
+
const moveThreshold = 15;
|
|
67
|
+
const touchStartCoords = this.info.touchStartCoords;
|
|
68
|
+
if (
|
|
69
|
+
Math.abs(touchStartCoords.x - e.changedTouches[0].clientX) > moveThreshold ||
|
|
70
|
+
Math.abs(touchStartCoords.y - e.changedTouches[0].clientY) > moveThreshold
|
|
71
|
+
) {
|
|
72
|
+
this._cancelTimer();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
touchend: function (e) {
|
|
77
|
+
if (this._fired) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
}
|
|
80
|
+
this._cancelTimer();
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
contextmenu: function (e) {
|
|
84
|
+
if (!e.shiftKey) {
|
|
85
|
+
this.info.sourceEvent = e;
|
|
86
|
+
this.fire(e.target, e.clientX, e.clientY);
|
|
87
|
+
prevent('tap');
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
fire: function (target, x, y) {
|
|
92
|
+
// NOTE(web-padawan): the code below is copied from `Polymer.Gestures._fire`,
|
|
93
|
+
// which is not exported from `gestures.js` module for Polymer 3.
|
|
94
|
+
const sourceEvent = this.info.sourceEvent;
|
|
95
|
+
const ev = new Event('vaadin-contextmenu', { bubbles: true, cancelable: true, composed: true });
|
|
96
|
+
ev.detail = { x, y, sourceEvent };
|
|
97
|
+
target.dispatchEvent(ev);
|
|
98
|
+
// forward `preventDefault` in a clean way
|
|
99
|
+
if (ev.defaultPrevented && sourceEvent && sourceEvent.preventDefault) {
|
|
100
|
+
sourceEvent.preventDefault();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Item } from '@vaadin/item/src/vaadin-item.js';
|
|
2
|
+
|
|
3
|
+
import { ListBox } from '@vaadin/list-box/src/vaadin-list-box.js';
|
|
4
|
+
|
|
5
|
+
import { ContextMenu } from './vaadin-context-menu.js';
|
|
6
|
+
|
|
7
|
+
import { ContextMenuItem, ContextMenuRendererContext } from './interfaces';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
|
|
11
|
+
*
|
|
12
|
+
* @protected
|
|
13
|
+
*/
|
|
14
|
+
declare class ContextMenuItemElement extends Item {}
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
interface HTMLElementTagNameMap {
|
|
18
|
+
'vaadin-context-menu-item': ContextMenuItemElement;
|
|
19
|
+
'vaadin-context-menu-list-box': ContextMenuListBox;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
|
|
25
|
+
*
|
|
26
|
+
* @protected
|
|
27
|
+
*/
|
|
28
|
+
declare class ContextMenuListBox extends ListBox {}
|
|
29
|
+
|
|
30
|
+
declare function ItemsMixin<T extends new (...args: any[]) => {}>(base: T): T & ItemsMixinConstructor;
|
|
31
|
+
|
|
32
|
+
interface ItemsMixinConstructor {
|
|
33
|
+
new (...args: any[]): ItemsMixin;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ItemsMixin {
|
|
37
|
+
readonly __isRTL: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Defines a (hierarchical) menu structure for the component.
|
|
41
|
+
* If a menu item has a non-empty `children` set, a sub-menu with the child items is opened
|
|
42
|
+
* next to the parent menu on mouseover, tap or a right arrow keypress.
|
|
43
|
+
*
|
|
44
|
+
* The items API can't be used together with a renderer!
|
|
45
|
+
*
|
|
46
|
+
* #### Example
|
|
47
|
+
*
|
|
48
|
+
* ```javascript
|
|
49
|
+
* contextMenu.items = [
|
|
50
|
+
* {text: 'Menu Item 1', theme: 'primary', children:
|
|
51
|
+
* [
|
|
52
|
+
* {text: 'Menu Item 1-1', checked: true},
|
|
53
|
+
* {text: 'Menu Item 1-2'}
|
|
54
|
+
* ]
|
|
55
|
+
* },
|
|
56
|
+
* {component: 'hr'},
|
|
57
|
+
* {text: 'Menu Item 2', children:
|
|
58
|
+
* [
|
|
59
|
+
* {text: 'Menu Item 2-1'},
|
|
60
|
+
* {text: 'Menu Item 2-2', disabled: true}
|
|
61
|
+
* ]
|
|
62
|
+
* },
|
|
63
|
+
* {text: 'Menu Item 3', disabled: true}
|
|
64
|
+
* ];
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
items: ContextMenuItem[] | undefined;
|
|
68
|
+
|
|
69
|
+
__forwardFocus(): void;
|
|
70
|
+
|
|
71
|
+
__itemsRenderer(root: HTMLElement, menu: ContextMenu, context: ContextMenuRendererContext): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { ItemsMixin, ItemsMixinConstructor };
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { flush } from '@polymer/polymer/lib/utils/flush.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
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @polymerMixin
|
|
40
|
+
*/
|
|
41
|
+
export const ItemsMixin = (superClass) =>
|
|
42
|
+
class ItemsMixin extends superClass {
|
|
43
|
+
static get properties() {
|
|
44
|
+
return {
|
|
45
|
+
/**
|
|
46
|
+
* @typedef ContextMenuItem
|
|
47
|
+
* @type {object}
|
|
48
|
+
* @property {string} text - Text to be set as the menu item component's textContent
|
|
49
|
+
* @property {union: string | object} component - The component to represent the item.
|
|
50
|
+
* Either a tagName or an element instance. Defaults to "vaadin-context-menu-item".
|
|
51
|
+
* @property {boolean} disabled - If true, the item is disabled and cannot be selected
|
|
52
|
+
* @property {boolean} checked - If true, the item shows a checkmark next to it
|
|
53
|
+
* @property {union: string | string[]} theme - If set, sets the given theme(s) as an attribute to the menu item component, overriding any theme set on the context menu.
|
|
54
|
+
* @property {MenuItem[]} children - Array of child menu items
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Defines a (hierarchical) menu structure for the component.
|
|
59
|
+
* If a menu item has a non-empty `children` set, a sub-menu with the child items is opened
|
|
60
|
+
* next to the parent menu on mouseover, tap or a right arrow keypress.
|
|
61
|
+
*
|
|
62
|
+
* The items API can't be used together with a renderer!
|
|
63
|
+
*
|
|
64
|
+
* #### Example
|
|
65
|
+
*
|
|
66
|
+
* ```javascript
|
|
67
|
+
* contextMenu.items = [
|
|
68
|
+
* {text: 'Menu Item 1', theme: 'primary', children:
|
|
69
|
+
* [
|
|
70
|
+
* {text: 'Menu Item 1-1', checked: true},
|
|
71
|
+
* {text: 'Menu Item 1-2'}
|
|
72
|
+
* ]
|
|
73
|
+
* },
|
|
74
|
+
* {component: 'hr'},
|
|
75
|
+
* {text: 'Menu Item 2', children:
|
|
76
|
+
* [
|
|
77
|
+
* {text: 'Menu Item 2-1'},
|
|
78
|
+
* {text: 'Menu Item 2-2', disabled: true}
|
|
79
|
+
* ]
|
|
80
|
+
* },
|
|
81
|
+
* {text: 'Menu Item 3', disabled: true}
|
|
82
|
+
* ];
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @type {!Array<!ContextMenuItem> | undefined}
|
|
86
|
+
*
|
|
87
|
+
*
|
|
88
|
+
* ### Styling
|
|
89
|
+
*
|
|
90
|
+
* The `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes on top of
|
|
91
|
+
* the built-in `<vaadin-item>` state attributes (see `<vaadin-item>` documentation for full listing).
|
|
92
|
+
*
|
|
93
|
+
* Part name | Attribute | Description
|
|
94
|
+
* ----------------|----------------|----------------
|
|
95
|
+
* `:host` | expanded | Expanded parent item
|
|
96
|
+
*/
|
|
97
|
+
items: Array
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @protected */
|
|
102
|
+
ready() {
|
|
103
|
+
super.ready();
|
|
104
|
+
|
|
105
|
+
// Overlay's outside click listener doesn't work with modeless
|
|
106
|
+
// overlays (submenus) so we need additional logic for it
|
|
107
|
+
this.__itemsOutsideClickListener = (e) => {
|
|
108
|
+
if (!e.composedPath().filter((el) => el.localName === 'vaadin-context-menu-overlay')[0]) {
|
|
109
|
+
this.dispatchEvent(new CustomEvent('items-outside-click'));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
this.addEventListener('items-outside-click', () => this.items && this.close());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @protected */
|
|
116
|
+
connectedCallback() {
|
|
117
|
+
super.connectedCallback();
|
|
118
|
+
// Firefox leaks click to document on contextmenu even if prevented
|
|
119
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=990614
|
|
120
|
+
document.documentElement.addEventListener('click', this.__itemsOutsideClickListener);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @protected */
|
|
124
|
+
disconnectedCallback() {
|
|
125
|
+
super.disconnectedCallback();
|
|
126
|
+
document.documentElement.removeEventListener('click', this.__itemsOutsideClickListener);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @return {boolean}
|
|
131
|
+
* @protected
|
|
132
|
+
*/
|
|
133
|
+
get __isRTL() {
|
|
134
|
+
return this.getAttribute('dir') === 'rtl';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @protected */
|
|
138
|
+
__forwardFocus() {
|
|
139
|
+
const overlay = this.$.overlay;
|
|
140
|
+
const child = overlay.getFirstChild();
|
|
141
|
+
// if parent item is not focused, do not focus submenu
|
|
142
|
+
if (overlay.parentOverlay) {
|
|
143
|
+
const parent = overlay.parentOverlay.querySelector('[expanded]');
|
|
144
|
+
if (parent && parent.hasAttribute('focused') && child) {
|
|
145
|
+
child.focus();
|
|
146
|
+
} else {
|
|
147
|
+
overlay.$.overlay.focus();
|
|
148
|
+
}
|
|
149
|
+
} else if (child) {
|
|
150
|
+
child.focus();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @private */
|
|
155
|
+
__openSubMenu(subMenu, itemElement) {
|
|
156
|
+
subMenu.items = itemElement._item.children;
|
|
157
|
+
subMenu.listenOn = itemElement;
|
|
158
|
+
|
|
159
|
+
const itemRect = itemElement.getBoundingClientRect();
|
|
160
|
+
|
|
161
|
+
const content = subMenu.$.overlay.$.content;
|
|
162
|
+
const style = getComputedStyle(content);
|
|
163
|
+
const parent = this.$.overlay;
|
|
164
|
+
const y = parent.hasAttribute('bottom-aligned')
|
|
165
|
+
? itemRect.bottom + parseFloat(style.paddingBottom)
|
|
166
|
+
: itemRect.top - parseFloat(style.paddingTop);
|
|
167
|
+
|
|
168
|
+
// Store the reference to align based on parent overlay coordinates
|
|
169
|
+
subMenu.$.overlay._setParentOverlay(parent);
|
|
170
|
+
|
|
171
|
+
// Set theme attribute from parent element
|
|
172
|
+
if (parent.theme) {
|
|
173
|
+
subMenu.setAttribute('theme', parent.theme);
|
|
174
|
+
} else {
|
|
175
|
+
subMenu.removeAttribute('theme');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let x;
|
|
179
|
+
content.style.minWidth = '';
|
|
180
|
+
if (document.documentElement.clientWidth - itemRect.right > itemRect.width) {
|
|
181
|
+
// There's room on the right side
|
|
182
|
+
x = itemRect.right;
|
|
183
|
+
} else {
|
|
184
|
+
// Open on the left side
|
|
185
|
+
x = itemRect.left - itemRect.width;
|
|
186
|
+
// Make sure there's no gaps between the menus
|
|
187
|
+
content.style.minWidth = parent.$.content.clientWidth + 'px';
|
|
188
|
+
}
|
|
189
|
+
x = Math.max(x, 0);
|
|
190
|
+
|
|
191
|
+
itemElement.dispatchEvent(
|
|
192
|
+
new CustomEvent('opensubmenu', {
|
|
193
|
+
detail: {
|
|
194
|
+
x,
|
|
195
|
+
y,
|
|
196
|
+
children: itemElement._item.children
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {!HTMLElement} root
|
|
204
|
+
* @param {!ContextMenu} menu
|
|
205
|
+
* @param {!ContextMenuRendererContext} context
|
|
206
|
+
* @protected
|
|
207
|
+
*/
|
|
208
|
+
__itemsRenderer(root, menu, context) {
|
|
209
|
+
this.__initMenu(root, menu);
|
|
210
|
+
|
|
211
|
+
const subMenu = root.querySelector(this.constructor.is);
|
|
212
|
+
subMenu.closeOn = menu.closeOn;
|
|
213
|
+
|
|
214
|
+
const listBox = root.querySelector('vaadin-context-menu-list-box');
|
|
215
|
+
|
|
216
|
+
listBox.innerHTML = '';
|
|
217
|
+
|
|
218
|
+
const items = Array.from(context.detail.children || menu.items);
|
|
219
|
+
|
|
220
|
+
items.forEach((item) => {
|
|
221
|
+
let component;
|
|
222
|
+
if (item.component instanceof HTMLElement) {
|
|
223
|
+
component = item.component;
|
|
224
|
+
} else {
|
|
225
|
+
component = document.createElement(item.component || 'vaadin-context-menu-item');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (component instanceof Item) {
|
|
229
|
+
component.setAttribute('role', 'menuitem');
|
|
230
|
+
component.classList.add('vaadin-menu-item');
|
|
231
|
+
} else if (component.localName === 'hr') {
|
|
232
|
+
component.setAttribute('role', 'separator');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this._setMenuItemTheme(component, item, this.theme);
|
|
236
|
+
|
|
237
|
+
component._item = item;
|
|
238
|
+
|
|
239
|
+
if (item.text) {
|
|
240
|
+
component.textContent = item.text;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.__toggleMenuComponentAttribute(component, 'menu-item-checked', item.checked);
|
|
244
|
+
this.__toggleMenuComponentAttribute(component, 'disabled', item.disabled);
|
|
245
|
+
|
|
246
|
+
component.setAttribute('aria-haspopup', 'false');
|
|
247
|
+
component.classList.remove('vaadin-context-menu-parent-item');
|
|
248
|
+
if (item.children && item.children.length) {
|
|
249
|
+
component.classList.add('vaadin-context-menu-parent-item');
|
|
250
|
+
component.setAttribute('aria-haspopup', 'true');
|
|
251
|
+
component.setAttribute('aria-expanded', 'false');
|
|
252
|
+
component.removeAttribute('expanded');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
listBox.appendChild(component);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @protected */
|
|
260
|
+
_setMenuItemTheme(component, item, hostTheme) {
|
|
261
|
+
let theme = hostTheme;
|
|
262
|
+
|
|
263
|
+
// item theme takes precedence over host theme even if it's empty, as long as it's not undefined or null
|
|
264
|
+
if (item.theme != null) {
|
|
265
|
+
theme = Array.isArray(item.theme) ? item.theme.join(' ') : item.theme;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (theme) {
|
|
269
|
+
component.setAttribute('theme', theme);
|
|
270
|
+
} else {
|
|
271
|
+
component.removeAttribute('theme');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @private */
|
|
276
|
+
__toggleMenuComponentAttribute(component, attribute, on) {
|
|
277
|
+
if (on) {
|
|
278
|
+
component.setAttribute(attribute, '');
|
|
279
|
+
component['__has-' + attribute] = true;
|
|
280
|
+
} else if (component['__has-' + attribute]) {
|
|
281
|
+
component.removeAttribute(attribute);
|
|
282
|
+
component['__has-' + attribute] = false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** @private */
|
|
287
|
+
__initMenu(root, menu) {
|
|
288
|
+
if (!root.firstElementChild) {
|
|
289
|
+
const is = this.constructor.is;
|
|
290
|
+
root.innerHTML = `
|
|
291
|
+
<vaadin-context-menu-list-box></vaadin-context-menu-list-box>
|
|
292
|
+
<${is} hidden></${is}>
|
|
293
|
+
`;
|
|
294
|
+
flush();
|
|
295
|
+
const listBox = root.querySelector('vaadin-context-menu-list-box');
|
|
296
|
+
this.theme && listBox.setAttribute('theme', this.theme);
|
|
297
|
+
listBox.classList.add('vaadin-menu-list-box');
|
|
298
|
+
requestAnimationFrame(() => listBox.setAttribute('role', 'menu'));
|
|
299
|
+
|
|
300
|
+
const subMenu = root.querySelector(is);
|
|
301
|
+
subMenu.$.overlay.modeless = true;
|
|
302
|
+
subMenu.openOn = 'opensubmenu';
|
|
303
|
+
|
|
304
|
+
menu.addEventListener('opened-changed', (e) => !e.detail.value && subMenu.close());
|
|
305
|
+
subMenu.addEventListener('opened-changed', (e) => {
|
|
306
|
+
if (!e.detail.value) {
|
|
307
|
+
const expandedItem = listBox.querySelector('[expanded]');
|
|
308
|
+
if (expandedItem) {
|
|
309
|
+
expandedItem.setAttribute('aria-expanded', 'false');
|
|
310
|
+
expandedItem.removeAttribute('expanded');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
listBox.addEventListener('selected-changed', (e) => {
|
|
316
|
+
if (typeof e.detail.value === 'number') {
|
|
317
|
+
const item = e.target.items[e.detail.value]._item;
|
|
318
|
+
if (!item.children) {
|
|
319
|
+
const detail = { value: item };
|
|
320
|
+
menu.dispatchEvent(new CustomEvent('item-selected', { detail }));
|
|
321
|
+
}
|
|
322
|
+
listBox.selected = null;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
subMenu.addEventListener('item-selected', (e) => {
|
|
327
|
+
menu.dispatchEvent(new CustomEvent('item-selected', { detail: e.detail }));
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
subMenu.addEventListener('close-all-menus', () => {
|
|
331
|
+
menu.dispatchEvent(new CustomEvent('close-all-menus'));
|
|
332
|
+
});
|
|
333
|
+
menu.addEventListener('close-all-menus', menu.close);
|
|
334
|
+
menu.addEventListener('item-selected', menu.close);
|
|
335
|
+
menu.$.overlay.$.backdrop.addEventListener('click', () => menu.close());
|
|
336
|
+
|
|
337
|
+
menu.$.overlay.addEventListener('keydown', (e) => {
|
|
338
|
+
const isRTL = this.__isRTL;
|
|
339
|
+
if ((!isRTL && e.keyCode === 37) || (isRTL && e.keyCode === 39)) {
|
|
340
|
+
menu.close();
|
|
341
|
+
menu.listenOn.focus();
|
|
342
|
+
} else if (e.keyCode === 27) {
|
|
343
|
+
menu.dispatchEvent(new CustomEvent('close-all-menus'));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
requestAnimationFrame(() => (this.__openListenerActive = true));
|
|
348
|
+
const openSubMenu = (
|
|
349
|
+
e,
|
|
350
|
+
itemElement = e.composedPath().filter((e) => e.localName === 'vaadin-context-menu-item')[0]
|
|
351
|
+
) => {
|
|
352
|
+
// Delay enabling the mouseover listener to avoid it from triggering on parent menu open
|
|
353
|
+
if (!this.__openListenerActive) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Don't open sub-menus while the menu is still opening
|
|
358
|
+
if (menu.$.overlay.hasAttribute('opening')) {
|
|
359
|
+
requestAnimationFrame(() => openSubMenu(e, itemElement));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (itemElement) {
|
|
364
|
+
if (subMenu.items !== itemElement._item.children) {
|
|
365
|
+
subMenu.close();
|
|
366
|
+
}
|
|
367
|
+
if (!menu.opened) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (itemElement._item.children && itemElement._item.children.length) {
|
|
371
|
+
itemElement.setAttribute('aria-expanded', 'true');
|
|
372
|
+
itemElement.setAttribute('expanded', '');
|
|
373
|
+
this.__openSubMenu(subMenu, itemElement);
|
|
374
|
+
} else {
|
|
375
|
+
subMenu.listenOn.focus();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
menu.$.overlay.addEventListener('mouseover', openSubMenu);
|
|
381
|
+
menu.$.overlay.addEventListener('keydown', (e) => {
|
|
382
|
+
const isRTL = this.__isRTL;
|
|
383
|
+
const shouldOpenSubMenu =
|
|
384
|
+
(!isRTL && e.keyCode === 39) || (isRTL && e.keyCode === 37) || e.keyCode === 13 || e.keyCode === 32;
|
|
385
|
+
|
|
386
|
+
shouldOpenSubMenu && openSubMenu(e);
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
const listBox = root.querySelector('vaadin-context-menu-list-box');
|
|
390
|
+
if (this.theme) {
|
|
391
|
+
listBox.setAttribute('theme', this.theme);
|
|
392
|
+
} else {
|
|
393
|
+
listBox.removeAttribute('theme');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
|
|
7
|
+
import '@polymer/iron-media-query/iron-media-query.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Element for internal use only.
|
|
11
|
+
*
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
class DeviceDetector extends PolymerElement {
|
|
15
|
+
static get template() {
|
|
16
|
+
return html`<iron-media-query query="min-device-width: 750px" query-matches="{{wide}}"></iron-media-query>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static get is() {
|
|
20
|
+
return 'vaadin-device-detector';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static get properties() {
|
|
24
|
+
return {
|
|
25
|
+
/**
|
|
26
|
+
* `true`, when running in a phone.
|
|
27
|
+
*/
|
|
28
|
+
phone: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
computed: '_phone(wide, touch)',
|
|
31
|
+
notify: true
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `true`, when running in a touch device.
|
|
36
|
+
* @default false
|
|
37
|
+
*/
|
|
38
|
+
touch: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
notify: true,
|
|
41
|
+
value: () => this._touch()
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `true`, when running in a tablet/desktop device.
|
|
46
|
+
*/
|
|
47
|
+
wide: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
notify: true
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static _touch() {
|
|
55
|
+
try {
|
|
56
|
+
document.createEvent('TouchEvent');
|
|
57
|
+
return true;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_phone(wide, touch) {
|
|
64
|
+
return !wide && touch;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
customElements.define(DeviceDetector.is, DeviceDetector);
|