@vaadin/context-menu 25.0.0-alpha9 → 25.0.0-beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/package.json +13 -16
  2. package/src/styles/vaadin-context-menu-item-base-styles.js +3 -3
  3. package/src/styles/vaadin-context-menu-overlay-base-styles.js +28 -1
  4. package/src/vaadin-context-menu-item.js +1 -1
  5. package/src/vaadin-context-menu-list-box.js +1 -1
  6. package/src/vaadin-context-menu-mixin.js +19 -6
  7. package/src/vaadin-context-menu-overlay.js +55 -1
  8. package/src/vaadin-context-menu.d.ts +41 -5
  9. package/src/vaadin-context-menu.js +73 -9
  10. package/src/vaadin-contextmenu-items-mixin.js +25 -15
  11. package/src/vaadin-menu-overlay-mixin.js +1 -1
  12. package/vaadin-context-menu.js +1 -1
  13. package/web-types.json +10 -6
  14. package/web-types.lit.json +11 -4
  15. package/src/styles/vaadin-context-menu-item-core-styles.d.ts +0 -8
  16. package/src/styles/vaadin-context-menu-item-core-styles.js +0 -8
  17. package/src/styles/vaadin-context-menu-overlay-core-styles.d.ts +0 -8
  18. package/src/styles/vaadin-context-menu-overlay-core-styles.js +0 -9
  19. package/src/styles/vaadin-menu-overlay-core-styles.d.ts +0 -8
  20. package/src/styles/vaadin-menu-overlay-core-styles.js +0 -32
  21. package/theme/lumo/vaadin-context-menu-item-styles.d.ts +0 -6
  22. package/theme/lumo/vaadin-context-menu-item-styles.js +0 -45
  23. package/theme/lumo/vaadin-context-menu-list-box-styles.d.ts +0 -5
  24. package/theme/lumo/vaadin-context-menu-list-box-styles.js +0 -47
  25. package/theme/lumo/vaadin-context-menu-overlay-styles.d.ts +0 -4
  26. package/theme/lumo/vaadin-context-menu-overlay-styles.js +0 -35
  27. package/theme/lumo/vaadin-context-menu.d.ts +0 -4
  28. package/theme/lumo/vaadin-context-menu.js +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/context-menu",
3
- "version": "25.0.0-alpha9",
3
+ "version": "25.0.0-beta1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -23,9 +23,6 @@
23
23
  "lit.d.ts",
24
24
  "lit.js",
25
25
  "src",
26
- "!src/styles/*-base-styles.d.ts",
27
- "!src/styles/*-base-styles.js",
28
- "theme",
29
26
  "vaadin-*.d.ts",
30
27
  "vaadin-*.js",
31
28
  "web-types.json",
@@ -39,25 +36,25 @@
39
36
  ],
40
37
  "dependencies": {
41
38
  "@open-wc/dedupe-mixin": "^1.3.0",
42
- "@vaadin/a11y-base": "25.0.0-alpha9",
43
- "@vaadin/component-base": "25.0.0-alpha9",
44
- "@vaadin/item": "25.0.0-alpha9",
45
- "@vaadin/list-box": "25.0.0-alpha9",
46
- "@vaadin/lit-renderer": "25.0.0-alpha9",
47
- "@vaadin/overlay": "25.0.0-alpha9",
48
- "@vaadin/vaadin-lumo-styles": "25.0.0-alpha9",
49
- "@vaadin/vaadin-themable-mixin": "25.0.0-alpha9",
39
+ "@vaadin/a11y-base": "25.0.0-beta1",
40
+ "@vaadin/component-base": "25.0.0-beta1",
41
+ "@vaadin/item": "25.0.0-beta1",
42
+ "@vaadin/list-box": "25.0.0-beta1",
43
+ "@vaadin/lit-renderer": "25.0.0-beta1",
44
+ "@vaadin/overlay": "25.0.0-beta1",
45
+ "@vaadin/vaadin-themable-mixin": "25.0.0-beta1",
50
46
  "lit": "^3.0.0"
51
47
  },
52
48
  "devDependencies": {
53
- "@vaadin/chai-plugins": "25.0.0-alpha9",
54
- "@vaadin/test-runner-commands": "25.0.0-alpha9",
49
+ "@vaadin/chai-plugins": "25.0.0-beta1",
50
+ "@vaadin/test-runner-commands": "25.0.0-beta1",
55
51
  "@vaadin/testing-helpers": "^2.0.0",
56
- "sinon": "^18.0.0"
52
+ "@vaadin/vaadin-lumo-styles": "25.0.0-beta1",
53
+ "sinon": "^21.0.0"
57
54
  },
58
55
  "web-types": [
59
56
  "web-types.json",
60
57
  "web-types.lit.json"
61
58
  ],
62
- "gitHead": "bbe4720721e0955ffc87a79b412bee38b1f0eb1e"
59
+ "gitHead": "1d20cf54e582d1f2e209126d4586f8b4c01c50e0"
63
60
  }
@@ -3,17 +3,17 @@
3
3
  * Copyright (c) 2016 - 2025 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/component-base/src/style-props.js';
6
+ import '@vaadin/component-base/src/styles/style-props.js';
7
7
  import { css } from 'lit';
8
8
  import { itemStyles } from '@vaadin/item/src/styles/vaadin-item-base-styles.js';
9
9
 
10
10
  const menuItemStyles = css`
11
11
  :host::after {
12
- background: var(--vaadin-color-subtle);
12
+ background: var(--vaadin-text-color-secondary);
13
13
  content: '';
14
14
  display: block;
15
15
  height: var(--vaadin-icon-size, 1lh);
16
- mask-image: var(--_vaadin-icon-chevron-down);
16
+ mask: var(--_vaadin-icon-chevron-down) 50% / var(--vaadin-icon-visual-size, 100%) no-repeat;
17
17
  rotate: -90deg;
18
18
  visibility: hidden;
19
19
  width: var(--vaadin-icon-size, 1lh);
@@ -3,7 +3,34 @@
3
3
  * Copyright (c) 2016 - 2025 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
+ import { css } from 'lit';
6
7
  import { overlayStyles } from '@vaadin/overlay/src/styles/vaadin-overlay-base-styles.js';
7
8
  import { menuOverlayStyles } from './vaadin-menu-overlay-base-styles.js';
8
9
 
9
- export const contextMenuOverlayStyles = [overlayStyles, menuOverlayStyles];
10
+ const contextMenuOverlay = css`
11
+ :host {
12
+ --_default-offset: 4px;
13
+ }
14
+
15
+ :host([position^='top'][top-aligned]) [part='overlay'],
16
+ :host([position^='bottom'][top-aligned]) [part='overlay'] {
17
+ margin-top: var(--vaadin-context-menu-offset-top, var(--_default-offset));
18
+ }
19
+
20
+ :host([position^='top'][bottom-aligned]) [part='overlay'],
21
+ :host([position^='bottom'][bottom-aligned]) [part='overlay'] {
22
+ margin-bottom: var(--vaadin-context-menu-offset-bottom, var(--_default-offset));
23
+ }
24
+
25
+ :host([position^='start'][start-aligned]) [part='overlay'],
26
+ :host([position^='end'][start-aligned]) [part='overlay'] {
27
+ margin-inline-start: var(--vaadin-context-menu-offset-start, var(--_default-offset));
28
+ }
29
+
30
+ :host([position^='start'][end-aligned]) [part='overlay'],
31
+ :host([position^='end'][end-aligned]) [part='overlay'] {
32
+ margin-inline-end: var(--vaadin-context-menu-offset-end, var(--_default-offset));
33
+ }
34
+ `;
35
+
36
+ export const contextMenuOverlayStyles = [overlayStyles, menuOverlayStyles, contextMenuOverlay];
@@ -10,7 +10,7 @@ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
10
10
  import { ItemMixin } from '@vaadin/item/src/vaadin-item-mixin.js';
11
11
  import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
12
12
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
13
- import { contextMenuItemStyles } from './styles/vaadin-context-menu-item-core-styles.js';
13
+ import { contextMenuItemStyles } from './styles/vaadin-context-menu-item-base-styles.js';
14
14
 
15
15
  /**
16
16
  * An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
@@ -8,7 +8,7 @@ import { ListMixin } from '@vaadin/a11y-base/src/list-mixin.js';
8
8
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
9
9
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
10
10
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
11
- import { listBoxStyles } from '@vaadin/list-box/src/styles/vaadin-list-box-core-styles.js';
11
+ import { listBoxStyles } from '@vaadin/list-box/src/styles/vaadin-list-box-base-styles.js';
12
12
  import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
13
13
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
14
14
 
@@ -209,6 +209,14 @@ export const ContextMenuMixin = (superClass) =>
209
209
  this.__forwardFocus();
210
210
  }
211
211
 
212
+ /**
213
+ * Runs after overlay's closing animation is finished
214
+ * @private
215
+ */
216
+ _onVaadinOverlayClosed() {
217
+ this.dispatchEvent(new CustomEvent('closed'));
218
+ }
219
+
212
220
  /** @private */
213
221
  _targetOrOpenOnChanged(listenOn, openOn) {
214
222
  if (this._oldListenOn && this._oldOpenOn) {
@@ -334,6 +342,9 @@ export const ContextMenuMixin = (superClass) =>
334
342
  return Array.prototype.filter.call(targets, (el) => {
335
343
  return e.composedPath().indexOf(el) > -1;
336
344
  })[0];
345
+ } else if (this.listenOn && this.listenOn !== this && this.position) {
346
+ // If listenOn has been set on a different element than the context menu root, then use listenOn as the target.
347
+ return this.listenOn;
337
348
  }
338
349
  return e.target;
339
350
  }
@@ -427,17 +438,13 @@ export const ContextMenuMixin = (superClass) =>
427
438
  /** @private */
428
439
  __focusItem(item) {
429
440
  if (item) {
430
- item.focus();
431
-
432
- if (isKeyboardActive()) {
433
- item.setAttribute('focus-ring', '');
434
- }
441
+ item.focus({ focusVisible: isKeyboardActive() });
435
442
  }
436
443
  }
437
444
 
438
445
  /** @private */
439
446
  __onScroll() {
440
- if (!this.opened) {
447
+ if (!this.opened || this.position) {
441
448
  return;
442
449
  }
443
450
 
@@ -641,4 +648,10 @@ export const ContextMenuMixin = (superClass) =>
641
648
  this.close();
642
649
  }
643
650
  }
651
+
652
+ /**
653
+ * Fired when the context menu is closed.
654
+ *
655
+ * @event closed
656
+ */
644
657
  };
@@ -10,7 +10,7 @@ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
10
10
  import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
11
11
  import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
12
12
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
13
- import { contextMenuOverlayStyles } from './styles/vaadin-context-menu-overlay-core-styles.js';
13
+ import { contextMenuOverlayStyles } from './styles/vaadin-context-menu-overlay-base-styles.js';
14
14
  import { MenuOverlayMixin } from './vaadin-menu-overlay-mixin.js';
15
15
 
16
16
  /**
@@ -31,10 +31,64 @@ export class ContextMenuOverlay extends MenuOverlayMixin(
31
31
  return 'vaadin-context-menu-overlay';
32
32
  }
33
33
 
34
+ static get properties() {
35
+ return {
36
+ /**
37
+ * Position of the overlay with respect to the target.
38
+ * Supported values: null, `top-start`, `top`, `top-end`,
39
+ * `bottom-start`, `bottom`, `bottom-end`, `start-top`,
40
+ * `start`, `start-bottom`, `end-top`, `end`, `end-bottom`.
41
+ */
42
+ position: {
43
+ type: String,
44
+ reflectToAttribute: true,
45
+ },
46
+ };
47
+ }
48
+
34
49
  static get styles() {
35
50
  return contextMenuOverlayStyles;
36
51
  }
37
52
 
53
+ /**
54
+ * @protected
55
+ * @override
56
+ */
57
+ _updatePosition() {
58
+ super._updatePosition();
59
+
60
+ if (this.parentOverlay == null && this.positionTarget && this.position && this.opened) {
61
+ if (this.position === 'bottom' || this.position === 'top') {
62
+ const targetRect = this.positionTarget.getBoundingClientRect();
63
+ const overlayRect = this.$.overlay.getBoundingClientRect();
64
+
65
+ const offset = targetRect.width / 2 - overlayRect.width / 2;
66
+
67
+ if (this.style.left) {
68
+ const left = overlayRect.left + offset;
69
+ if (left > 0) {
70
+ this.style.left = `${left}px`;
71
+ }
72
+ }
73
+
74
+ if (this.style.right) {
75
+ const right = parseFloat(this.style.right) + offset;
76
+ if (right > 0) {
77
+ this.style.right = `${right}px`;
78
+ }
79
+ }
80
+ }
81
+
82
+ if (this.position === 'start' || this.position === 'end') {
83
+ const targetRect = this.positionTarget.getBoundingClientRect();
84
+ const overlayRect = this.$.overlay.getBoundingClientRect();
85
+
86
+ const offset = targetRect.height / 2 - overlayRect.height / 2;
87
+ this.style.top = `${overlayRect.top + offset}px`;
88
+ }
89
+ }
90
+ }
91
+
38
92
  /** @protected */
39
93
  render() {
40
94
  return html`
@@ -4,13 +4,26 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
7
- import type { OverlayClassMixinClass } from '@vaadin/component-base/src/overlay-class-mixin.js';
8
7
  import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
9
8
  import type { ContextMenuMixinClass } from './vaadin-context-menu-mixin.js';
10
9
  import type { ContextMenuItem } from './vaadin-contextmenu-items-mixin.js';
11
10
 
12
11
  export { ContextMenuItem };
13
12
 
13
+ export type ContextMenuPosition =
14
+ | 'bottom-end'
15
+ | 'bottom-start'
16
+ | 'bottom'
17
+ | 'end-bottom'
18
+ | 'end-top'
19
+ | 'end'
20
+ | 'start-bottom'
21
+ | 'start-top'
22
+ | 'start'
23
+ | 'top-end'
24
+ | 'top-start'
25
+ | 'top';
26
+
14
27
  export interface ContextMenuRendererContext {
15
28
  target: HTMLElement;
16
29
  detail?: { sourceEvent: Event };
@@ -34,6 +47,11 @@ export type ContextMenuItemSelectedEvent<TItem extends ContextMenuItem = Context
34
47
  value: TItem;
35
48
  }>;
36
49
 
50
+ /**
51
+ * Fired when the context menu is closed.
52
+ */
53
+ export type ContextMenuClosedEvent = CustomEvent;
54
+
37
55
  export interface ContextMenuCustomEventMap<TItem extends ContextMenuItem = ContextMenuItem> {
38
56
  'opened-changed': ContextMenuOpenedChangedEvent;
39
57
 
@@ -42,6 +60,8 @@ export interface ContextMenuCustomEventMap<TItem extends ContextMenuItem = Conte
42
60
  'close-all-menus': Event;
43
61
 
44
62
  'items-outside-click': Event;
63
+
64
+ closed: ContextMenuClosedEvent;
45
65
  }
46
66
 
47
67
  export interface ContextMenuEventMap<TItem extends ContextMenuItem = ContextMenuItem>
@@ -216,6 +236,17 @@ export interface ContextMenuEventMap<TItem extends ContextMenuItem = ContextMenu
216
236
  * `overlay` | The overlay container
217
237
  * `content` | The overlay content
218
238
  *
239
+ * ### Custom CSS Properties
240
+ *
241
+ * The following custom CSS properties are available for styling:
242
+ *
243
+ * Custom CSS property | Description
244
+ * --------------------------------------|-------------
245
+ * `--vaadin-context-menu-offset-top` | Used as an offset when using `position` and the context menu is aligned vertically below the target
246
+ * `--vaadin-context-menu-offset-bottom` | Used as an offset when using `position` and the context menu is aligned vertically above the target
247
+ * `--vaadin-context-menu-offset-start` | Used as an offset when using `position` and the context menu is aligned horizontally after the target
248
+ * `--vaadin-context-menu-offset-end` | Used as an offset when using `position` and the context menu is aligned horizontally before the target
249
+ *
219
250
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
220
251
  *
221
252
  * ### Internal components
@@ -232,13 +263,19 @@ export interface ContextMenuEventMap<TItem extends ContextMenuItem = ContextMenu
232
263
  * ---------- |-------------
233
264
  * `expanded` | Expanded parent item.
234
265
  *
235
- * Note: the `theme` attribute value set on `<vaadin-context-menu>` is
236
- * propagated to the internal components listed above.
237
- *
238
266
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
239
267
  * @fires {CustomEvent} item-selected - Fired when an item is selected when the context menu is populated using the `items` API.
268
+ * @fires {CustomEvent} closed - Fired when the context menu is closed.
240
269
  */
241
270
  declare class ContextMenu<TItem extends ContextMenuItem = ContextMenuItem> extends HTMLElement {
271
+ /**
272
+ * Position of the overlay with respect to the target.
273
+ * Supported values: null, `top-start`, `top`, `top-end`,
274
+ * `bottom-start`, `bottom`, `bottom-end`, `start-top`,
275
+ * `start`, `start-bottom`, `end-top`, `end`, `end-bottom`.
276
+ */
277
+ position: ContextMenuPosition | null | undefined;
278
+
242
279
  addEventListener<K extends keyof ContextMenuEventMap>(
243
280
  type: K,
244
281
  listener: (this: ContextMenu<TItem>, ev: ContextMenuEventMap<TItem>[K]) => void,
@@ -254,7 +291,6 @@ declare class ContextMenu<TItem extends ContextMenuItem = ContextMenuItem> exten
254
291
 
255
292
  interface ContextMenu<TItem extends ContextMenuItem = ContextMenuItem>
256
293
  extends ContextMenuMixinClass<TItem>,
257
- OverlayClassMixinClass,
258
294
  ElementMixinClass,
259
295
  ThemePropertyMixinClass {}
260
296
 
@@ -11,7 +11,6 @@ import { css, html, LitElement } from 'lit';
11
11
  import { ifDefined } from 'lit/directives/if-defined.js';
12
12
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
13
13
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
14
- import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
15
14
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
16
15
  import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
17
16
  import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
@@ -184,6 +183,17 @@ import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
184
183
  * `overlay` | The overlay container
185
184
  * `content` | The overlay content
186
185
  *
186
+ * ### Custom CSS Properties
187
+ *
188
+ * The following custom CSS properties are available for styling:
189
+ *
190
+ * Custom CSS property | Description
191
+ * --------------------------------------|-------------
192
+ * `--vaadin-context-menu-offset-top` | Used as an offset when using `position` and the context menu is aligned vertically below the target
193
+ * `--vaadin-context-menu-offset-bottom` | Used as an offset when using `position` and the context menu is aligned vertically above the target
194
+ * `--vaadin-context-menu-offset-start` | Used as an offset when using `position` and the context menu is aligned horizontally after the target
195
+ * `--vaadin-context-menu-offset-end` | Used as an offset when using `position` and the context menu is aligned horizontally before the target
196
+ *
187
197
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
188
198
  *
189
199
  * ### Internal components
@@ -200,22 +210,17 @@ import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
200
210
  * ---------- |-------------
201
211
  * `expanded` | Expanded parent item.
202
212
  *
203
- * Note: the `theme` attribute value set on `<vaadin-context-menu>` is
204
- * propagated to the internal components listed above.
205
- *
206
213
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
207
214
  * @fires {CustomEvent} item-selected - Fired when an item is selected when the context menu is populated using the `items` API.
215
+ * @fires {CustomEvent} closed - Fired when the context menu is closed.
208
216
  *
209
217
  * @customElement
210
218
  * @extends HTMLElement
211
219
  * @mixes ElementMixin
212
220
  * @mixes ContextMenuMixin
213
- * @mixes OverlayClassMixin
214
221
  * @mixes ThemePropertyMixin
215
222
  */
216
- class ContextMenu extends ContextMenuMixin(
217
- OverlayClassMixin(ElementMixin(ThemePropertyMixin(PolylitMixin(LitElement)))),
218
- ) {
223
+ class ContextMenu extends ContextMenuMixin(ElementMixin(ThemePropertyMixin(PolylitMixin(LitElement)))) {
219
224
  static get is() {
220
225
  return 'vaadin-context-menu';
221
226
  }
@@ -232,23 +237,46 @@ class ContextMenu extends ContextMenuMixin(
232
237
  `;
233
238
  }
234
239
 
240
+ static get properties() {
241
+ return {
242
+ /**
243
+ * Position of the overlay with respect to the target.
244
+ * Supported values: null, `top-start`, `top`, `top-end`,
245
+ * `bottom-start`, `bottom`, `bottom-end`, `start-top`,
246
+ * `start`, `start-bottom`, `end-top`, `end`, `end-bottom`.
247
+ */
248
+ position: {
249
+ type: String,
250
+ },
251
+ };
252
+ }
253
+
235
254
  /** @protected */
236
255
  render() {
256
+ const { _context: context, position } = this;
257
+
237
258
  return html`
238
259
  <slot id="slot"></slot>
239
260
  <vaadin-context-menu-overlay
240
261
  id="overlay"
241
262
  .owner="${this}"
242
263
  .opened="${this.opened}"
243
- .model="${this._context}"
264
+ .model="${context}"
244
265
  .modeless="${this._modeless}"
245
266
  .renderer="${this.items ? this.__itemsRenderer : this.renderer}"
267
+ .position="${position}"
268
+ .positionTarget="${position ? context && context.target : this._positionTarget}"
269
+ .horizontalAlign="${this.__computeHorizontalAlign(position)}"
270
+ .verticalAlign="${this.__computeVerticalAlign(position)}"
271
+ ?no-horizontal-overlap="${this.__computeNoHorizontalOverlap(position)}"
272
+ ?no-vertical-overlap="${this.__computeNoVerticalOverlap(position)}"
246
273
  .withBackdrop="${this._phone}"
247
274
  ?phone="${this._phone}"
248
275
  theme="${ifDefined(this._theme)}"
249
276
  exportparts="backdrop, overlay, content"
250
277
  @opened-changed="${this._onOverlayOpened}"
251
278
  @vaadin-overlay-open="${this._onVaadinOverlayOpen}"
279
+ @vaadin-overlay-closed="${this._onVaadinOverlayClosed}"
252
280
  >
253
281
  <slot name="overlay"></slot>
254
282
  <slot name="submenu" slot="submenu"></slot>
@@ -256,6 +284,42 @@ class ContextMenu extends ContextMenuMixin(
256
284
  `;
257
285
  }
258
286
 
287
+ /** @private */
288
+ __computeHorizontalAlign(position) {
289
+ if (!position) {
290
+ return 'start';
291
+ }
292
+
293
+ return ['top-end', 'bottom-end', 'start-top', 'start', 'start-bottom'].includes(position) ? 'end' : 'start';
294
+ }
295
+
296
+ /** @private */
297
+ __computeNoHorizontalOverlap(position) {
298
+ if (!position) {
299
+ return !!this._positionTarget;
300
+ }
301
+
302
+ return ['start-top', 'start', 'start-bottom', 'end-top', 'end', 'end-bottom'].includes(position);
303
+ }
304
+
305
+ /** @private */
306
+ __computeNoVerticalOverlap(position) {
307
+ if (!position) {
308
+ return false;
309
+ }
310
+
311
+ return ['top-start', 'top-end', 'top', 'bottom-start', 'bottom', 'bottom-end'].includes(position);
312
+ }
313
+
314
+ /** @private */
315
+ __computeVerticalAlign(position) {
316
+ if (!position) {
317
+ return 'top';
318
+ }
319
+
320
+ return ['top-start', 'top-end', 'top', 'start-bottom', 'end-bottom'].includes(position) ? 'bottom' : 'top';
321
+ }
322
+
259
323
  /**
260
324
  * Fired when an item is selected when the context menu is populated using the `items` API.
261
325
  *
@@ -16,14 +16,14 @@ export const ItemsMixin = (superClass) =>
16
16
  * @typedef ContextMenuItem
17
17
  * @type {object}
18
18
  * @property {string} text - Text to be set as the menu item component's textContent
19
- * @property {union: string | object} component - The component to represent the item.
19
+ * @property {string | HTMLElement} component - The component to represent the item.
20
20
  * Either a tagName or an element instance. Defaults to "vaadin-context-menu-item".
21
21
  * @property {boolean} disabled - If true, the item is disabled and cannot be selected
22
22
  * @property {boolean} checked - If true, the item shows a checkmark next to it
23
23
  * @property {boolean} keepOpen - If true, the menu will not be closed on item selection
24
24
  * @property {string} className - A space-delimited list of CSS class names to be set on the menu item component.
25
- * @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.
26
- * @property {MenuItem[]} children - Array of child menu items
25
+ * @property {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.
26
+ * @property {ContextMenuItem[]} children - Array of child menu items
27
27
  */
28
28
 
29
29
  /**
@@ -60,6 +60,12 @@ export const ItemsMixin = (superClass) =>
60
60
  type: Array,
61
61
  sync: true,
62
62
  },
63
+
64
+ /** @protected */
65
+ _positionTarget: {
66
+ type: Object,
67
+ sync: true,
68
+ },
63
69
  };
64
70
  }
65
71
 
@@ -131,14 +137,12 @@ export const ItemsMixin = (superClass) =>
131
137
  }
132
138
 
133
139
  /** @private */
134
- __openSubMenu(subMenu, itemElement, overlayClass) {
140
+ __openSubMenu(subMenu, itemElement) {
141
+ // Update sub-menu items and position target
135
142
  this.__updateSubMenuForItem(subMenu, itemElement);
136
- subMenu.overlayClass = overlayClass;
137
143
 
138
144
  const parent = this._overlayElement;
139
-
140
145
  const subMenuOverlay = subMenu._overlayElement;
141
- subMenuOverlay.noHorizontalOverlap = true;
142
146
  // Store the reference parent overlay
143
147
  subMenuOverlay._setParentOverlay(parent);
144
148
 
@@ -165,7 +169,8 @@ export const ItemsMixin = (superClass) =>
165
169
  __updateSubMenuForItem(subMenu, itemElement) {
166
170
  subMenu.items = itemElement._item.children;
167
171
  subMenu.listenOn = itemElement;
168
- subMenu._overlayElement.positionTarget = itemElement;
172
+ subMenu._positionTarget = itemElement;
173
+ subMenu._overlayElement.requestContentUpdate();
169
174
  }
170
175
 
171
176
  /**
@@ -367,27 +372,33 @@ export const ItemsMixin = (superClass) =>
367
372
  }
368
373
 
369
374
  const subMenu = this._subMenu;
375
+ const expandedItem = this._listBox.querySelector('[expanded]');
370
376
 
371
- if (item) {
377
+ if (item && item !== expandedItem) {
372
378
  const { children } = item._item;
373
379
 
374
380
  // Check if the sub-menu was focused before closing it.
375
381
  const child = subMenu._overlayElement._contentRoot.firstElementChild;
376
382
  const isSubmenuFocused = child && child.focused;
377
383
 
378
- if (subMenu.items !== children) {
384
+ // Mark previously expanded item as collapsed
385
+ if (expandedItem) {
386
+ this.__updateExpanded(expandedItem, false);
387
+ }
388
+
389
+ // Close sub-menu if there are no children for the new item
390
+ if (!children || !children.length) {
379
391
  subMenu.close();
380
392
  }
393
+
381
394
  if (!this.opened) {
382
395
  return;
383
396
  }
384
397
 
385
398
  if (children && children.length) {
399
+ // Open or update the submenu if the new item has children
386
400
  this.__updateExpanded(item, true);
387
-
388
- // Forward parent overlay class
389
- const { overlayClass } = this;
390
- this.__openSubMenu(subMenu, item, overlayClass);
401
+ this.__openSubMenu(subMenu, item);
391
402
  } else if (isSubmenuFocused) {
392
403
  // If the sub-menu item was focused, focus its parent item.
393
404
  subMenu.listenOn.focus();
@@ -406,7 +417,6 @@ export const ItemsMixin = (superClass) =>
406
417
  /**
407
418
  * @param {!HTMLElement} root
408
419
  * @param {!ContextMenu} menu
409
- * @param {!ContextMenuRendererContext} context
410
420
  * @protected
411
421
  */
412
422
  __itemsRenderer(root, menu) {
@@ -113,7 +113,7 @@ export const MenuOverlayMixin = (superClass) =>
113
113
  _updatePosition() {
114
114
  super._updatePosition();
115
115
 
116
- if (this.positionTarget && this.parentOverlay) {
116
+ if (this.positionTarget && this.parentOverlay && this.opened) {
117
117
  // This overlay is positioned by a parent menu item,
118
118
  // adjust the position by the overlay content paddings
119
119
  const content = this.$.content;
@@ -1,2 +1,2 @@
1
- import './theme/lumo/vaadin-context-menu.js';
1
+ import './src/vaadin-context-menu.js';
2
2
  export * from './src/vaadin-context-menu.js';
package/web-types.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/context-menu",
4
- "version": "25.0.0-alpha9",
4
+ "version": "25.0.0-beta1",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
8
8
  "elements": [
9
9
  {
10
10
  "name": "vaadin-context-menu",
11
- "description": "`<vaadin-context-menu>` is a Web Component for creating context menus.\n\n### Items\n\nItems is a higher level convenience API for defining a (hierarchical) menu structure for the component.\nIf a menu item has a non-empty `children` set, a sub-menu with the child items is opened\nnext to the parent menu on mouseover, tap or a right arrow keypress.\n\nWhen an item is selected, `<vaadin-context-menu>` dispatches an \"item-selected\" event\nwith the selected item as `event.detail.value` property.\nIf item does not have `keepOpen` property the menu will be closed.\n\n```javascript\ncontextMenu.items = [\n { text: 'Menu Item 1', theme: 'primary', className: 'first', children:\n [\n { text: 'Menu Item 1-1', checked: true, keepOpen: true },\n { text: 'Menu Item 1-2' }\n ]\n },\n { component: 'hr' },\n { text: 'Menu Item 2', children:\n [\n { text: 'Menu Item 2-1' },\n { text: 'Menu Item 2-2', disabled: true }\n ]\n },\n { text: 'Menu Item 3', disabled: true, className: 'last' }\n];\n\ncontextMenu.addEventListener('item-selected', e => {\n const item = e.detail.value;\n console.log(`${item.text} selected`);\n});\n```\n\n**NOTE:** when the `items` array is defined, the renderer cannot be used.\n\n### Rendering\n\nThe content of the menu can be populated by using the renderer callback function.\n\nThe renderer function provides `root`, `contextMenu`, `model` arguments when applicable.\nGenerate DOM content by using `model` object properties if needed, append it to the `root`\nelement and control the state of the host element by accessing `contextMenu`. Before generating\nnew content, the renderer function should check if there is already content in `root` for reusing it.\n\n```html\n<vaadin-context-menu id=\"contextMenu\">\n <p>This paragraph has a context menu.</p>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'Content of the selector: ' + context.target.textContent;\n};\n```\n\nYou can access the menu context inside the renderer using\n`context.target` and `context.detail`.\n\nRenderer is called on the opening of the context-menu and each time the related context is updated.\nDOM generated during the renderer call can be reused\nin the next renderer call and will be provided with the `root` argument.\nOn first call it will be empty.\n\n### `vaadin-contextmenu` Gesture Event\n\n`vaadin-contextmenu` is a gesture event (a custom event),\nwhich is dispatched after either `contextmenu` or long touch events.\nThis enables support for both mouse and touch environments in a uniform way.\n\n`<vaadin-context-menu>` opens the menu overlay on the `vaadin-contextmenu`\nevent by default.\n\n### Menu Listener\n\nBy default, the `<vaadin-context-menu>` element listens for the menu opening\nevent on itself. In case if you do not want to wrap the target, you can listen for\nevents on an element outside the `<vaadin-context-menu>` by setting the\n`listenOn` property:\n\n```html\n<vaadin-context-menu id=\"contextMenu\"></vaadin-context-menu>\n\n<div id=\"menuListener\">The element that listens for the contextmenu event.</div>\n```\n```javascript\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.listenOn = document.querySelector('#menuListener');\n```\n\n### Filtering Menu Targets\n\nBy default, the listener element and all its descendants open the context\nmenu. You can filter the menu targets to a smaller set of elements inside\nthe listener element by setting the `selector` property.\n\nIn the following example, only the elements matching `.has-menu` will open the context menu:\n\n```html\n<vaadin-context-menu selector=\".has-menu\">\n <p class=\"has-menu\">This paragraph opens the context menu</p>\n <p>This paragraph does not open the context menu</p>\n</vaadin-context-menu>\n```\n\n### Menu Context\n\nThe following properties are available in the `context` argument:\n\n- `target` is the menu opening event target, which is the element that\nthe user has called the context menu for\n- `detail` is the menu opening event detail\n\nIn the following example, the menu item text is composed with the contents\nof the element that opened the menu:\n\n```html\n<vaadin-context-menu selector=\"li\" id=\"contextMenu\">\n <ul>\n <li>Foo</li>\n <li>Bar</li>\n <li>Baz</li>\n </ul>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'The menu target: ' + context.target.textContent;\n};\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`backdrop` | Backdrop of the overlay\n`overlay` | The overlay container\n`content` | The overlay content\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.\n\n### Internal components\n\nWhen using `items` API the following internal components are themable:\n\n- `<vaadin-context-menu-item>` - has the same API as [`<vaadin-item>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-alpha9/#/elements/vaadin-item).\n- `<vaadin-context-menu-list-box>` - has the same API as [`<vaadin-list-box>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-alpha9/#/elements/vaadin-list-box).\n\nThe `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes\non top of the built-in `<vaadin-item>` state attributes:\n\nAttribute | Description\n---------- |-------------\n`expanded` | Expanded parent item.\n\nNote: the `theme` attribute value set on `<vaadin-context-menu>` is\npropagated to the internal components listed above.",
11
+ "description": "`<vaadin-context-menu>` is a Web Component for creating context menus.\n\n### Items\n\nItems is a higher level convenience API for defining a (hierarchical) menu structure for the component.\nIf a menu item has a non-empty `children` set, a sub-menu with the child items is opened\nnext to the parent menu on mouseover, tap or a right arrow keypress.\n\nWhen an item is selected, `<vaadin-context-menu>` dispatches an \"item-selected\" event\nwith the selected item as `event.detail.value` property.\nIf item does not have `keepOpen` property the menu will be closed.\n\n```javascript\ncontextMenu.items = [\n { text: 'Menu Item 1', theme: 'primary', className: 'first', children:\n [\n { text: 'Menu Item 1-1', checked: true, keepOpen: true },\n { text: 'Menu Item 1-2' }\n ]\n },\n { component: 'hr' },\n { text: 'Menu Item 2', children:\n [\n { text: 'Menu Item 2-1' },\n { text: 'Menu Item 2-2', disabled: true }\n ]\n },\n { text: 'Menu Item 3', disabled: true, className: 'last' }\n];\n\ncontextMenu.addEventListener('item-selected', e => {\n const item = e.detail.value;\n console.log(`${item.text} selected`);\n});\n```\n\n**NOTE:** when the `items` array is defined, the renderer cannot be used.\n\n### Rendering\n\nThe content of the menu can be populated by using the renderer callback function.\n\nThe renderer function provides `root`, `contextMenu`, `model` arguments when applicable.\nGenerate DOM content by using `model` object properties if needed, append it to the `root`\nelement and control the state of the host element by accessing `contextMenu`. Before generating\nnew content, the renderer function should check if there is already content in `root` for reusing it.\n\n```html\n<vaadin-context-menu id=\"contextMenu\">\n <p>This paragraph has a context menu.</p>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'Content of the selector: ' + context.target.textContent;\n};\n```\n\nYou can access the menu context inside the renderer using\n`context.target` and `context.detail`.\n\nRenderer is called on the opening of the context-menu and each time the related context is updated.\nDOM generated during the renderer call can be reused\nin the next renderer call and will be provided with the `root` argument.\nOn first call it will be empty.\n\n### `vaadin-contextmenu` Gesture Event\n\n`vaadin-contextmenu` is a gesture event (a custom event),\nwhich is dispatched after either `contextmenu` or long touch events.\nThis enables support for both mouse and touch environments in a uniform way.\n\n`<vaadin-context-menu>` opens the menu overlay on the `vaadin-contextmenu`\nevent by default.\n\n### Menu Listener\n\nBy default, the `<vaadin-context-menu>` element listens for the menu opening\nevent on itself. In case if you do not want to wrap the target, you can listen for\nevents on an element outside the `<vaadin-context-menu>` by setting the\n`listenOn` property:\n\n```html\n<vaadin-context-menu id=\"contextMenu\"></vaadin-context-menu>\n\n<div id=\"menuListener\">The element that listens for the contextmenu event.</div>\n```\n```javascript\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.listenOn = document.querySelector('#menuListener');\n```\n\n### Filtering Menu Targets\n\nBy default, the listener element and all its descendants open the context\nmenu. You can filter the menu targets to a smaller set of elements inside\nthe listener element by setting the `selector` property.\n\nIn the following example, only the elements matching `.has-menu` will open the context menu:\n\n```html\n<vaadin-context-menu selector=\".has-menu\">\n <p class=\"has-menu\">This paragraph opens the context menu</p>\n <p>This paragraph does not open the context menu</p>\n</vaadin-context-menu>\n```\n\n### Menu Context\n\nThe following properties are available in the `context` argument:\n\n- `target` is the menu opening event target, which is the element that\nthe user has called the context menu for\n- `detail` is the menu opening event detail\n\nIn the following example, the menu item text is composed with the contents\nof the element that opened the menu:\n\n```html\n<vaadin-context-menu selector=\"li\" id=\"contextMenu\">\n <ul>\n <li>Foo</li>\n <li>Bar</li>\n <li>Baz</li>\n </ul>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'The menu target: ' + context.target.textContent;\n};\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`backdrop` | Backdrop of the overlay\n`overlay` | The overlay container\n`content` | The overlay content\n\n### Custom CSS Properties\n\nThe following custom CSS properties are available for styling:\n\nCustom CSS property | Description\n--------------------------------------|-------------\n`--vaadin-context-menu-offset-top` | Used as an offset when using `position` and the context menu is aligned vertically below the target\n`--vaadin-context-menu-offset-bottom` | Used as an offset when using `position` and the context menu is aligned vertically above the target\n`--vaadin-context-menu-offset-start` | Used as an offset when using `position` and the context menu is aligned horizontally after the target\n`--vaadin-context-menu-offset-end` | Used as an offset when using `position` and the context menu is aligned horizontally before the target\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.\n\n### Internal components\n\nWhen using `items` API the following internal components are themable:\n\n- `<vaadin-context-menu-item>` - has the same API as [`<vaadin-item>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-beta1/#/elements/vaadin-item).\n- `<vaadin-context-menu-list-box>` - has the same API as [`<vaadin-list-box>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-beta1/#/elements/vaadin-list-box).\n\nThe `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes\non top of the built-in `<vaadin-item>` state attributes:\n\nAttribute | Description\n---------- |-------------\n`expanded` | Expanded parent item.",
12
12
  "attributes": [
13
13
  {
14
14
  "name": "selector",
@@ -40,8 +40,8 @@
40
40
  }
41
41
  },
42
42
  {
43
- "name": "overlay-class",
44
- "description": "A space-delimited list of CSS class names to set on the overlay element.\nThis property does not affect other CSS class names set manually via JS.\n\nNote, if the CSS class name was set with this property, clearing it will\nremove it from the overlay, even if the same class name was also added\nmanually, e.g. by using `classList.add()` in the `renderer` function.",
43
+ "name": "position",
44
+ "description": "Position of the overlay with respect to the target.\nSupported values: null, `top-start`, `top`, `top-end`,\n`bottom-start`, `bottom`, `bottom-end`, `start-top`,\n`start`, `start-bottom`, `end-top`, `end`, `end-bottom`.",
45
45
  "value": {
46
46
  "type": [
47
47
  "string",
@@ -123,8 +123,8 @@
123
123
  }
124
124
  },
125
125
  {
126
- "name": "overlayClass",
127
- "description": "A space-delimited list of CSS class names to set on the overlay element.\nThis property does not affect other CSS class names set manually via JS.\n\nNote, if the CSS class name was set with this property, clearing it will\nremove it from the overlay, even if the same class name was also added\nmanually, e.g. by using `classList.add()` in the `renderer` function.",
126
+ "name": "position",
127
+ "description": "Position of the overlay with respect to the target.\nSupported values: null, `top-start`, `top`, `top-end`,\n`bottom-start`, `bottom`, `bottom-end`, `start-top`,\n`start`, `start-bottom`, `end-top`, `end`, `end-bottom`.",
128
128
  "value": {
129
129
  "type": [
130
130
  "string",
@@ -135,6 +135,10 @@
135
135
  }
136
136
  ],
137
137
  "events": [
138
+ {
139
+ "name": "closed",
140
+ "description": "Fired when the context menu is closed."
141
+ },
138
142
  {
139
143
  "name": "item-selected",
140
144
  "description": "Fired when an item is selected when the context menu is populated using the `items` API."
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/web-types",
3
3
  "name": "@vaadin/context-menu",
4
- "version": "25.0.0-alpha9",
4
+ "version": "25.0.0-beta1",
5
5
  "description-markup": "markdown",
6
6
  "framework": "lit",
7
7
  "framework-config": {
@@ -16,7 +16,7 @@
16
16
  "elements": [
17
17
  {
18
18
  "name": "vaadin-context-menu",
19
- "description": "`<vaadin-context-menu>` is a Web Component for creating context menus.\n\n### Items\n\nItems is a higher level convenience API for defining a (hierarchical) menu structure for the component.\nIf a menu item has a non-empty `children` set, a sub-menu with the child items is opened\nnext to the parent menu on mouseover, tap or a right arrow keypress.\n\nWhen an item is selected, `<vaadin-context-menu>` dispatches an \"item-selected\" event\nwith the selected item as `event.detail.value` property.\nIf item does not have `keepOpen` property the menu will be closed.\n\n```javascript\ncontextMenu.items = [\n { text: 'Menu Item 1', theme: 'primary', className: 'first', children:\n [\n { text: 'Menu Item 1-1', checked: true, keepOpen: true },\n { text: 'Menu Item 1-2' }\n ]\n },\n { component: 'hr' },\n { text: 'Menu Item 2', children:\n [\n { text: 'Menu Item 2-1' },\n { text: 'Menu Item 2-2', disabled: true }\n ]\n },\n { text: 'Menu Item 3', disabled: true, className: 'last' }\n];\n\ncontextMenu.addEventListener('item-selected', e => {\n const item = e.detail.value;\n console.log(`${item.text} selected`);\n});\n```\n\n**NOTE:** when the `items` array is defined, the renderer cannot be used.\n\n### Rendering\n\nThe content of the menu can be populated by using the renderer callback function.\n\nThe renderer function provides `root`, `contextMenu`, `model` arguments when applicable.\nGenerate DOM content by using `model` object properties if needed, append it to the `root`\nelement and control the state of the host element by accessing `contextMenu`. Before generating\nnew content, the renderer function should check if there is already content in `root` for reusing it.\n\n```html\n<vaadin-context-menu id=\"contextMenu\">\n <p>This paragraph has a context menu.</p>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'Content of the selector: ' + context.target.textContent;\n};\n```\n\nYou can access the menu context inside the renderer using\n`context.target` and `context.detail`.\n\nRenderer is called on the opening of the context-menu and each time the related context is updated.\nDOM generated during the renderer call can be reused\nin the next renderer call and will be provided with the `root` argument.\nOn first call it will be empty.\n\n### `vaadin-contextmenu` Gesture Event\n\n`vaadin-contextmenu` is a gesture event (a custom event),\nwhich is dispatched after either `contextmenu` or long touch events.\nThis enables support for both mouse and touch environments in a uniform way.\n\n`<vaadin-context-menu>` opens the menu overlay on the `vaadin-contextmenu`\nevent by default.\n\n### Menu Listener\n\nBy default, the `<vaadin-context-menu>` element listens for the menu opening\nevent on itself. In case if you do not want to wrap the target, you can listen for\nevents on an element outside the `<vaadin-context-menu>` by setting the\n`listenOn` property:\n\n```html\n<vaadin-context-menu id=\"contextMenu\"></vaadin-context-menu>\n\n<div id=\"menuListener\">The element that listens for the contextmenu event.</div>\n```\n```javascript\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.listenOn = document.querySelector('#menuListener');\n```\n\n### Filtering Menu Targets\n\nBy default, the listener element and all its descendants open the context\nmenu. You can filter the menu targets to a smaller set of elements inside\nthe listener element by setting the `selector` property.\n\nIn the following example, only the elements matching `.has-menu` will open the context menu:\n\n```html\n<vaadin-context-menu selector=\".has-menu\">\n <p class=\"has-menu\">This paragraph opens the context menu</p>\n <p>This paragraph does not open the context menu</p>\n</vaadin-context-menu>\n```\n\n### Menu Context\n\nThe following properties are available in the `context` argument:\n\n- `target` is the menu opening event target, which is the element that\nthe user has called the context menu for\n- `detail` is the menu opening event detail\n\nIn the following example, the menu item text is composed with the contents\nof the element that opened the menu:\n\n```html\n<vaadin-context-menu selector=\"li\" id=\"contextMenu\">\n <ul>\n <li>Foo</li>\n <li>Bar</li>\n <li>Baz</li>\n </ul>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'The menu target: ' + context.target.textContent;\n};\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`backdrop` | Backdrop of the overlay\n`overlay` | The overlay container\n`content` | The overlay content\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.\n\n### Internal components\n\nWhen using `items` API the following internal components are themable:\n\n- `<vaadin-context-menu-item>` - has the same API as [`<vaadin-item>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-alpha9/#/elements/vaadin-item).\n- `<vaadin-context-menu-list-box>` - has the same API as [`<vaadin-list-box>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-alpha9/#/elements/vaadin-list-box).\n\nThe `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes\non top of the built-in `<vaadin-item>` state attributes:\n\nAttribute | Description\n---------- |-------------\n`expanded` | Expanded parent item.\n\nNote: the `theme` attribute value set on `<vaadin-context-menu>` is\npropagated to the internal components listed above.",
19
+ "description": "`<vaadin-context-menu>` is a Web Component for creating context menus.\n\n### Items\n\nItems is a higher level convenience API for defining a (hierarchical) menu structure for the component.\nIf a menu item has a non-empty `children` set, a sub-menu with the child items is opened\nnext to the parent menu on mouseover, tap or a right arrow keypress.\n\nWhen an item is selected, `<vaadin-context-menu>` dispatches an \"item-selected\" event\nwith the selected item as `event.detail.value` property.\nIf item does not have `keepOpen` property the menu will be closed.\n\n```javascript\ncontextMenu.items = [\n { text: 'Menu Item 1', theme: 'primary', className: 'first', children:\n [\n { text: 'Menu Item 1-1', checked: true, keepOpen: true },\n { text: 'Menu Item 1-2' }\n ]\n },\n { component: 'hr' },\n { text: 'Menu Item 2', children:\n [\n { text: 'Menu Item 2-1' },\n { text: 'Menu Item 2-2', disabled: true }\n ]\n },\n { text: 'Menu Item 3', disabled: true, className: 'last' }\n];\n\ncontextMenu.addEventListener('item-selected', e => {\n const item = e.detail.value;\n console.log(`${item.text} selected`);\n});\n```\n\n**NOTE:** when the `items` array is defined, the renderer cannot be used.\n\n### Rendering\n\nThe content of the menu can be populated by using the renderer callback function.\n\nThe renderer function provides `root`, `contextMenu`, `model` arguments when applicable.\nGenerate DOM content by using `model` object properties if needed, append it to the `root`\nelement and control the state of the host element by accessing `contextMenu`. Before generating\nnew content, the renderer function should check if there is already content in `root` for reusing it.\n\n```html\n<vaadin-context-menu id=\"contextMenu\">\n <p>This paragraph has a context menu.</p>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'Content of the selector: ' + context.target.textContent;\n};\n```\n\nYou can access the menu context inside the renderer using\n`context.target` and `context.detail`.\n\nRenderer is called on the opening of the context-menu and each time the related context is updated.\nDOM generated during the renderer call can be reused\nin the next renderer call and will be provided with the `root` argument.\nOn first call it will be empty.\n\n### `vaadin-contextmenu` Gesture Event\n\n`vaadin-contextmenu` is a gesture event (a custom event),\nwhich is dispatched after either `contextmenu` or long touch events.\nThis enables support for both mouse and touch environments in a uniform way.\n\n`<vaadin-context-menu>` opens the menu overlay on the `vaadin-contextmenu`\nevent by default.\n\n### Menu Listener\n\nBy default, the `<vaadin-context-menu>` element listens for the menu opening\nevent on itself. In case if you do not want to wrap the target, you can listen for\nevents on an element outside the `<vaadin-context-menu>` by setting the\n`listenOn` property:\n\n```html\n<vaadin-context-menu id=\"contextMenu\"></vaadin-context-menu>\n\n<div id=\"menuListener\">The element that listens for the contextmenu event.</div>\n```\n```javascript\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.listenOn = document.querySelector('#menuListener');\n```\n\n### Filtering Menu Targets\n\nBy default, the listener element and all its descendants open the context\nmenu. You can filter the menu targets to a smaller set of elements inside\nthe listener element by setting the `selector` property.\n\nIn the following example, only the elements matching `.has-menu` will open the context menu:\n\n```html\n<vaadin-context-menu selector=\".has-menu\">\n <p class=\"has-menu\">This paragraph opens the context menu</p>\n <p>This paragraph does not open the context menu</p>\n</vaadin-context-menu>\n```\n\n### Menu Context\n\nThe following properties are available in the `context` argument:\n\n- `target` is the menu opening event target, which is the element that\nthe user has called the context menu for\n- `detail` is the menu opening event detail\n\nIn the following example, the menu item text is composed with the contents\nof the element that opened the menu:\n\n```html\n<vaadin-context-menu selector=\"li\" id=\"contextMenu\">\n <ul>\n <li>Foo</li>\n <li>Bar</li>\n <li>Baz</li>\n </ul>\n</vaadin-context-menu>\n```\n```js\nconst contextMenu = document.querySelector('#contextMenu');\ncontextMenu.renderer = (root, contextMenu, context) => {\n let listBox = root.firstElementChild;\n if (!listBox) {\n listBox = document.createElement('vaadin-list-box');\n root.appendChild(listBox);\n }\n\n let item = listBox.querySelector('vaadin-item');\n if (!item) {\n item = document.createElement('vaadin-item');\n listBox.appendChild(item);\n }\n item.textContent = 'The menu target: ' + context.target.textContent;\n};\n```\n\n### Styling\n\nThe following shadow DOM parts are available for styling:\n\nPart name | Description\n-----------------|-------------------------------------------\n`backdrop` | Backdrop of the overlay\n`overlay` | The overlay container\n`content` | The overlay content\n\n### Custom CSS Properties\n\nThe following custom CSS properties are available for styling:\n\nCustom CSS property | Description\n--------------------------------------|-------------\n`--vaadin-context-menu-offset-top` | Used as an offset when using `position` and the context menu is aligned vertically below the target\n`--vaadin-context-menu-offset-bottom` | Used as an offset when using `position` and the context menu is aligned vertically above the target\n`--vaadin-context-menu-offset-start` | Used as an offset when using `position` and the context menu is aligned horizontally after the target\n`--vaadin-context-menu-offset-end` | Used as an offset when using `position` and the context menu is aligned horizontally before the target\n\nSee [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.\n\n### Internal components\n\nWhen using `items` API the following internal components are themable:\n\n- `<vaadin-context-menu-item>` - has the same API as [`<vaadin-item>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-beta1/#/elements/vaadin-item).\n- `<vaadin-context-menu-list-box>` - has the same API as [`<vaadin-list-box>`](https://cdn.vaadin.com/vaadin-web-components/25.0.0-beta1/#/elements/vaadin-list-box).\n\nThe `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes\non top of the built-in `<vaadin-item>` state attributes:\n\nAttribute | Description\n---------- |-------------\n`expanded` | Expanded parent item.",
20
20
  "extension": true,
21
21
  "attributes": [
22
22
  {
@@ -62,8 +62,15 @@
62
62
  }
63
63
  },
64
64
  {
65
- "name": ".overlayClass",
66
- "description": "A space-delimited list of CSS class names to set on the overlay element.\nThis property does not affect other CSS class names set manually via JS.\n\nNote, if the CSS class name was set with this property, clearing it will\nremove it from the overlay, even if the same class name was also added\nmanually, e.g. by using `classList.add()` in the `renderer` function.",
65
+ "name": ".position",
66
+ "description": "Position of the overlay with respect to the target.\nSupported values: null, `top-start`, `top`, `top-end`,\n`bottom-start`, `bottom`, `bottom-end`, `start-top`,\n`start`, `start-bottom`, `end-top`, `end`, `end-bottom`.",
67
+ "value": {
68
+ "kind": "expression"
69
+ }
70
+ },
71
+ {
72
+ "name": "@closed",
73
+ "description": "Fired when the context menu is closed.",
67
74
  "value": {
68
75
  "kind": "expression"
69
76
  }
@@ -1,8 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2016 - 2025 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import type { CSSResult } from 'lit';
7
-
8
- export const contextMenuItemStyles: CSSResult;
@@ -1,8 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2016 - 2025 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { itemStyles } from '@vaadin/item/src/styles/vaadin-item-core-styles.js';
7
-
8
- export const contextMenuItemStyles = [itemStyles];
@@ -1,8 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2016 - 2025 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import type { CSSResult } from 'lit';
7
-
8
- export const contextMenuOverlayStyles: CSSResult;
@@ -1,9 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2016 - 2025 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { overlayStyles } from '@vaadin/overlay/src/styles/vaadin-overlay-core-styles.js';
7
- import { menuOverlayStyles } from './vaadin-menu-overlay-core-styles.js';
8
-
9
- export const contextMenuOverlayStyles = [overlayStyles, menuOverlayStyles];
@@ -1,8 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2016 - 2025 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import type { CSSResult } from 'lit';
7
-
8
- export const menuOverlayStyles: CSSResult;
@@ -1,32 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2016 - 2025 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 menuOverlayStyles = 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
-
27
- @media (forced-colors: active) {
28
- [part='overlay'] {
29
- outline: 3px solid !important;
30
- }
31
- }
32
- `;
@@ -1,6 +0,0 @@
1
- import '@vaadin/vaadin-lumo-styles/color.js';
2
- import '@vaadin/vaadin-lumo-styles/font-icons.js';
3
- import '@vaadin/vaadin-lumo-styles/sizing.js';
4
- import '@vaadin/vaadin-lumo-styles/spacing.js';
5
- declare const contextMenuItem: import("lit").CSSResult;
6
- export { contextMenuItem };
@@ -1,45 +0,0 @@
1
- import '@vaadin/vaadin-lumo-styles/color.js';
2
- import '@vaadin/vaadin-lumo-styles/font-icons.js';
3
- import '@vaadin/vaadin-lumo-styles/sizing.js';
4
- import '@vaadin/vaadin-lumo-styles/spacing.js';
5
- import { item } from '@vaadin/item/theme/lumo/vaadin-item-styles.js';
6
- import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
7
-
8
- const contextMenuItem = css`
9
- /* :hover needed to workaround https://github.com/vaadin/web-components/issues/3133 */
10
- :host(:hover) {
11
- user-select: none;
12
- -webkit-user-select: none;
13
- }
14
-
15
- :host([role='menuitem'][menu-item-checked]) [part='checkmark']::before {
16
- opacity: 1;
17
- }
18
-
19
- :host([aria-haspopup='true'])::after {
20
- font-family: lumo-icons;
21
- font-size: var(--lumo-icon-size-xs);
22
- content: var(--lumo-icons-angle-right);
23
- color: var(--lumo-tertiary-text-color);
24
- }
25
-
26
- :host(:not([dir='rtl'])[aria-haspopup='true'])::after {
27
- margin-right: calc(var(--lumo-space-m) * -1);
28
- padding-left: var(--lumo-space-m);
29
- }
30
-
31
- :host([expanded]) {
32
- background-color: var(--lumo-primary-color-10pct);
33
- }
34
-
35
- /* RTL styles */
36
- :host([dir='rtl'][aria-haspopup='true'])::after {
37
- content: var(--lumo-icons-angle-left);
38
- margin-left: calc(var(--lumo-space-m) * -1);
39
- padding-right: var(--lumo-space-m);
40
- }
41
- `;
42
-
43
- registerStyles('vaadin-context-menu-item', [item, contextMenuItem], { moduleId: 'lumo-context-menu-item' });
44
-
45
- export { contextMenuItem };
@@ -1,5 +0,0 @@
1
- import '@vaadin/vaadin-lumo-styles/color.js';
2
- import '@vaadin/vaadin-lumo-styles/spacing.js';
3
- import '@vaadin/vaadin-lumo-styles/style.js';
4
- declare const contextMenuListBox: import("lit").CSSResult;
5
- export { contextMenuListBox };
@@ -1,47 +0,0 @@
1
- import '@vaadin/vaadin-lumo-styles/color.js';
2
- import '@vaadin/vaadin-lumo-styles/spacing.js';
3
- import '@vaadin/vaadin-lumo-styles/style.js';
4
- import { listBox } from '@vaadin/list-box/theme/lumo/vaadin-list-box-styles.js';
5
- import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
6
-
7
- const contextMenuListBox = css`
8
- :host {
9
- --_lumo-list-box-item-selected-icon-display: block;
10
- }
11
-
12
- /* Normal item */
13
- [part='items'] ::slotted([role='menuitem']) {
14
- -webkit-tap-highlight-color: var(--lumo-primary-color-10pct);
15
- cursor: default;
16
- outline: none;
17
- border-radius: var(--lumo-border-radius-m);
18
- padding-left: calc(var(--lumo-border-radius-m) / 4);
19
- padding-right: calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4);
20
- }
21
-
22
- /* Hovered item */
23
- /* TODO a workaround until we have "focus-follows-mouse". After that, use the hover style for focus-ring as well */
24
- [part='items'] ::slotted([role='menuitem']:hover:not([disabled])),
25
- [part='items'] ::slotted([role='menuitem'][expanded]:not([disabled])) {
26
- background-color: var(--lumo-primary-color-10pct);
27
- }
28
-
29
- /* RTL styles */
30
- :host([dir='rtl']) [part='items'] ::slotted([role='menuitem']) {
31
- padding-left: calc(var(--lumo-space-l) + var(--lumo-border-radius-m) / 4);
32
- padding-right: calc(var(--lumo-border-radius-m) / 4);
33
- }
34
-
35
- /* Focused item */
36
- @media (pointer: coarse) {
37
- [part='items'] ::slotted([role='menuitem']:hover:not([expanded]):not([disabled])) {
38
- background-color: transparent;
39
- }
40
- }
41
- `;
42
-
43
- registerStyles('vaadin-context-menu-list-box', [listBox, contextMenuListBox], {
44
- moduleId: 'lumo-context-menu-list-box',
45
- });
46
-
47
- export { contextMenuListBox };
@@ -1,4 +0,0 @@
1
- import '@vaadin/vaadin-lumo-styles/spacing.js';
2
- import '@vaadin/vaadin-lumo-styles/style.js';
3
- declare const contextMenuOverlay: import("lit").CSSResult;
4
- export { contextMenuOverlay };
@@ -1,35 +0,0 @@
1
- import '@vaadin/vaadin-lumo-styles/spacing.js';
2
- import '@vaadin/vaadin-lumo-styles/style.js';
3
- import { menuOverlay } from '@vaadin/vaadin-lumo-styles/mixins/menu-overlay.js';
4
- import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
5
-
6
- const contextMenuOverlay = css`
7
- :host([phone]) {
8
- /* stylelint-disable declaration-block-no-redundant-longhand-properties */
9
- top: 0 !important;
10
- right: 0 !important;
11
- bottom: var(--vaadin-overlay-viewport-bottom) !important;
12
- left: 0 !important;
13
- /* stylelint-enable declaration-block-no-redundant-longhand-properties */
14
- align-items: stretch;
15
- justify-content: flex-end;
16
- }
17
-
18
- /* TODO These style overrides should not be needed.
19
- We should instead offer a way to have non-selectable items inside the context menu. */
20
-
21
- :host {
22
- --_lumo-list-box-item-selected-icon-display: none;
23
- --_lumo-list-box-item-padding-left: calc(var(--lumo-space-m) + var(--lumo-border-radius-m) / 4);
24
- }
25
-
26
- [part='overlay'] {
27
- outline: none;
28
- }
29
- `;
30
-
31
- registerStyles('vaadin-context-menu-overlay', [menuOverlay, contextMenuOverlay], {
32
- moduleId: 'lumo-context-menu-overlay',
33
- });
34
-
35
- export { contextMenuOverlay };
@@ -1,4 +0,0 @@
1
- import './vaadin-context-menu-item-styles.js';
2
- import './vaadin-context-menu-list-box-styles.js';
3
- import './vaadin-context-menu-overlay-styles.js';
4
- import '../../src/vaadin-context-menu.js';
@@ -1,4 +0,0 @@
1
- import './vaadin-context-menu-item-styles.js';
2
- import './vaadin-context-menu-list-box-styles.js';
3
- import './vaadin-context-menu-overlay-styles.js';
4
- import '../../src/vaadin-context-menu.js';