@vaadin/context-menu 25.3.0-alpha1 → 25.3.0-alpha10

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.
@@ -13,11 +13,43 @@ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mix
13
13
  import { contextMenuItemStyles } from './styles/vaadin-context-menu-item-base-styles.js';
14
14
 
15
15
  /**
16
- * An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
16
+ * `<vaadin-context-menu-item>` is a Web Component for creating `<vaadin-context-menu>` items.
17
17
  *
18
+ * ### Styling
19
+ *
20
+ * The following shadow DOM parts are available for styling:
21
+ *
22
+ * Part name | Description
23
+ * -------------|----------------
24
+ * `checkmark` | The graphical checkmark shown for a checked item
25
+ * `content` | The element that wraps the slot
26
+ *
27
+ * The following state attributes are available for styling:
28
+ *
29
+ * Attribute | Description
30
+ * -------------|-------------
31
+ * `active` | Set when the item is pressed down, either with mouse, touch or the keyboard.
32
+ * `disabled` | Set when the item is disabled.
33
+ * `focus-ring` | Set when the item is focused using the keyboard.
34
+ * `focused` | Set when the item is focused.
35
+ * `expanded` | Set when the item has a sub-menu and it is opened.
36
+ *
37
+ * The following custom CSS properties are available for styling:
38
+ *
39
+ * Custom CSS property |
40
+ * :----------------------------------|
41
+ * | `--vaadin-item-border-radius` |
42
+ * | `--vaadin-item-checkmark-color` |
43
+ * | `--vaadin-item-gap` |
44
+ * | `--vaadin-item-height` |
45
+ * | `--vaadin-item-padding` |
46
+ * | `--vaadin-item-text-align` |
47
+ *
48
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
49
+ *
50
+ * @attr {string} theme - The theme variants to apply to the component.
18
51
  * @customElement vaadin-context-menu-item
19
52
  * @extends HTMLElement
20
- * @protected
21
53
  */
22
54
  class ContextMenuItem extends ItemMixin(ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMixin(LitElement))))) {
23
55
  static get is() {
@@ -8,7 +8,26 @@ import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
8
8
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
9
9
 
10
10
  /**
11
- * An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
11
+ * `<vaadin-context-menu-list-box>` is a Web Component for wrapping `<vaadin-context-menu>` items.
12
+ *
13
+ * ```html
14
+ * <vaadin-context-menu>
15
+ * <vaadin-context-menu-list-box slot="overlay">
16
+ * <vaadin-context-menu-item>Edit</vaadin-context-menu-item>
17
+ * <vaadin-context-menu-item>Delete</vaadin-context-menu-item>
18
+ * </vaadin-context-menu-list-box>
19
+ * </vaadin-context-menu>
20
+ * ```
21
+ *
22
+ * ### Styling
23
+ *
24
+ * The following shadow DOM parts are available for styling:
25
+ *
26
+ * Part name | Description
27
+ * ------------------|------------------------
28
+ * `items` | The items container
29
+ *
30
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
12
31
  */
13
32
  declare class ContextMenuListBox extends ListMixin(DirMixin(ThemableMixin(HTMLElement))) {}
14
33
 
@@ -13,11 +13,30 @@ import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection
13
13
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
14
14
 
15
15
  /**
16
- * An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
16
+ * `<vaadin-context-menu-list-box>` is a Web Component for wrapping `<vaadin-context-menu>` items.
17
17
  *
18
+ * ```html
19
+ * <vaadin-context-menu>
20
+ * <vaadin-context-menu-list-box slot="overlay">
21
+ * <vaadin-context-menu-item>Edit</vaadin-context-menu-item>
22
+ * <vaadin-context-menu-item>Delete</vaadin-context-menu-item>
23
+ * </vaadin-context-menu-list-box>
24
+ * </vaadin-context-menu>
25
+ * ```
26
+ *
27
+ * ### Styling
28
+ *
29
+ * The following shadow DOM parts are available for styling:
30
+ *
31
+ * Part name | Description
32
+ * ------------------|------------------------
33
+ * `items` | The items container
34
+ *
35
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
36
+ *
37
+ * @attr {string} theme - The theme variants to apply to the component.
18
38
  * @customElement vaadin-context-menu-list-box
19
39
  * @extends HTMLElement
20
- * @protected
21
40
  */
22
41
  class ContextMenuListBox extends ListMixin(ThemableMixin(DirMixin(PolylitMixin(LumoInjectionMixin(LitElement))))) {
23
42
  static get is() {
@@ -5,11 +5,11 @@
5
5
  */
6
6
  import type { Constructor } from '@open-wc/dedupe-mixin';
7
7
  import type { ContextMenuRenderer } from './vaadin-context-menu.js';
8
- import type { ContextMenuItem, ItemsMixinClass } from './vaadin-contextmenu-items-mixin.js';
8
+ import type { ContextMenuItemData, ItemsMixinClass } from './vaadin-contextmenu-items-mixin.js';
9
9
 
10
10
  export declare function ContextMenuMixin<
11
11
  T extends Constructor<HTMLElement>,
12
- TItem extends ContextMenuItem = ContextMenuItem,
12
+ TItem extends ContextMenuItemData = ContextMenuItemData,
13
13
  >(base: T): Constructor<ContextMenuMixinClass> & Constructor<ItemsMixinClass<TItem>> & T;
14
14
 
15
15
  export declare class ContextMenuMixinClass {
@@ -52,6 +52,8 @@ export declare class ContextMenuMixinClass {
52
52
  * - `context` The object with the menu context, contains:
53
53
  * - `context.target` the target of the menu opening event,
54
54
  * - `context.detail` the menu opening event detail.
55
+ *
56
+ * @deprecated Use a slotted `<vaadin-context-menu-list-box>` or the `items` property instead
55
57
  */
56
58
  renderer: ContextMenuRenderer | null | undefined;
57
59
 
@@ -83,5 +85,5 @@ export declare class ContextMenuMixinClass {
83
85
 
84
86
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
85
87
  export declare interface ContextMenuMixinClass<
86
- TItem extends ContextMenuItem = ContextMenuItem,
88
+ TItem extends ContextMenuItemData = ContextMenuItemData,
87
89
  > extends ItemsMixinClass<TItem> {}
@@ -81,12 +81,22 @@ export const ContextMenuMixin = (superClass) =>
81
81
  * - `context.target` the target of the menu opening event,
82
82
  * - `context.detail` the menu opening event detail.
83
83
  * @type {ContextMenuRenderer | undefined}
84
+ * @deprecated Use a slotted `<vaadin-context-menu-list-box>` or the `items` property instead
84
85
  */
85
86
  renderer: {
86
87
  type: Function,
87
88
  sync: true,
88
89
  },
89
90
 
91
+ /**
92
+ * The `<vaadin-context-menu-list-box>` slotted directly into the overlay slot.
93
+ * @private
94
+ */
95
+ __slottedListBox: {
96
+ type: Object,
97
+ sync: true,
98
+ },
99
+
90
100
  /**
91
101
  * When true, the menu overlay is modeless.
92
102
  * @protected
@@ -119,11 +129,7 @@ export const ContextMenuMixin = (superClass) =>
119
129
  }
120
130
 
121
131
  static get observers() {
122
- return [
123
- '_targetOrOpenOnChanged(listenOn, openOn)',
124
- '_rendererChanged(renderer, items)',
125
- '_fullscreenChanged(_fullscreen)',
126
- ];
132
+ return ['_targetOrOpenOnChanged(listenOn, openOn)', '_fullscreenChanged(_fullscreen)'];
127
133
  }
128
134
 
129
135
  constructor() {
@@ -188,6 +194,26 @@ export const ContextMenuMixin = (superClass) =>
188
194
  }
189
195
  }
190
196
 
197
+ /** @protected */
198
+ updated(props) {
199
+ super.updated(props);
200
+
201
+ if (props.has('renderer') || props.has('items') || props.has('__slottedListBox')) {
202
+ const contentSources = [this.items, this.renderer, this.__slottedListBox].filter(Boolean);
203
+ if (contentSources.length > 1) {
204
+ throw new Error(
205
+ 'The "items", "renderer", and slotted "<vaadin-context-menu-list-box>" cannot be used together.',
206
+ );
207
+ }
208
+
209
+ // Clicks on items bubble to the overlay and must not close the menu
210
+ // (items with `keepOpen` or with children), so disable `closeOn`.
211
+ if (this.items && this.closeOn === 'click') {
212
+ this.closeOn = '';
213
+ }
214
+ }
215
+ }
216
+
191
217
  /**
192
218
  * Runs before overlay is fully rendered
193
219
  * @private
@@ -199,9 +225,12 @@ export const ContextMenuMixin = (superClass) =>
199
225
  }
200
226
 
201
227
  const opened = event.detail.value;
202
- this._setOpened(opened);
203
228
  if (opened) {
229
+ this._setOpened(true);
204
230
  this.__alignOverlayPosition();
231
+ } else if (this.opened) {
232
+ // Use `close()` to also reset menu-bar button on outside click.
233
+ this.close();
205
234
  }
206
235
  }
207
236
 
@@ -290,7 +319,11 @@ export const ContextMenuMixin = (superClass) =>
290
319
 
291
320
  /** @private */
292
321
  _preventDefault(e) {
293
- e.preventDefault();
322
+ // With items, `closeOn` is cleared to not close the menu on clicks bubbling
323
+ // from the items to the overlay, and the menu still closes on outside click.
324
+ if (!this.items) {
325
+ e.preventDefault();
326
+ }
294
327
  }
295
328
 
296
329
  /** @private */
@@ -319,19 +352,6 @@ export const ContextMenuMixin = (superClass) =>
319
352
  this.__restoreMenuState();
320
353
  }
321
354
 
322
- /** @private */
323
- _rendererChanged(renderer, items) {
324
- if (items) {
325
- if (renderer) {
326
- throw new Error('The items API cannot be used together with a renderer');
327
- }
328
-
329
- if (this.closeOn === 'click') {
330
- this.closeOn = '';
331
- }
332
- }
333
- }
334
-
335
355
  /**
336
356
  * Closes the overlay.
337
357
  */
@@ -341,13 +361,22 @@ export const ContextMenuMixin = (superClass) =>
341
361
  this._setOpened(false);
342
362
  }
343
363
 
364
+ /**
365
+ * Returns the composed path of the event. By default, it uses the path stored
366
+ * at dispatch time (needed for Flow connector that opens menu asynchronously).
367
+ * @private
368
+ */
369
+ __getComposedPath(e) {
370
+ return e.__composedPath ?? e.detail?.sourceEvent?.__composedPath ?? e.composedPath();
371
+ }
372
+
344
373
  /** @private */
345
374
  _contextTarget(e) {
346
375
  if (this.selector) {
347
376
  const targets = this.listenOn.querySelectorAll(this.selector);
348
377
 
349
378
  return Array.prototype.filter.call(targets, (el) => {
350
- return e.composedPath().indexOf(el) > -1;
379
+ return this.__getComposedPath(e).indexOf(el) > -1;
351
380
  })[0];
352
381
  } else if (this.listenOn && this.listenOn !== this && this.position) {
353
382
  // If listenOn has been set on a different element than the context menu root, then use listenOn as the target.
@@ -362,7 +391,7 @@ export const ContextMenuMixin = (superClass) =>
362
391
  */
363
392
  open(e) {
364
393
  // Ignore events from the overlay
365
- if (this._overlayElement && e.composedPath().includes(this._overlayElement)) {
394
+ if (this._overlayElement && this.__getComposedPath(e).includes(this._overlayElement)) {
366
395
  return;
367
396
  }
368
397
 
@@ -392,7 +421,7 @@ export const ContextMenuMixin = (superClass) =>
392
421
 
393
422
  /** @private */
394
423
  __preserveMenuState() {
395
- const listBox = this.__getListBox();
424
+ const listBox = this._menuListBox;
396
425
  if (listBox) {
397
426
  this.__focusedIndex = listBox.items.indexOf(listBox.focused);
398
427
 
@@ -408,11 +437,11 @@ export const ContextMenuMixin = (superClass) =>
408
437
  const subMenuIndex = this.__subMenuIndex;
409
438
  const selectedIndex = this.__selectedIndex;
410
439
 
411
- const listBox = this.__getListBox();
440
+ const listBox = this._menuListBox;
412
441
 
413
442
  if (listBox) {
414
443
  // Initialize menu items synchronously
415
- listBox._observer.flush();
444
+ listBox._observer?.flush();
416
445
 
417
446
  if (subMenuIndex > -1) {
418
447
  const itemToOpen = listBox.items[subMenuIndex];
@@ -449,6 +478,17 @@ export const ContextMenuMixin = (superClass) =>
449
478
  }
450
479
  }
451
480
 
481
+ /**
482
+ * Detects a `<vaadin-context-menu-list-box>` slotted directly into the overlay
483
+ * slot (instead of the default one rendered into slotted `div` element).
484
+ *
485
+ * @param {!Event} e
486
+ * @private
487
+ */
488
+ __onOverlaySlotChange(e) {
489
+ this.__slottedListBox = e.target.assignedElements().find((el) => el._hasVaadinListMixin);
490
+ }
491
+
452
492
  /** @private */
453
493
  __onScroll() {
454
494
  if (!this.opened || this.position) {
@@ -559,7 +599,8 @@ export const ContextMenuMixin = (superClass) =>
559
599
 
560
600
  if (position === 0) {
561
601
  // Native keyboard event
562
- const rect = event.target.getBoundingClientRect();
602
+ const target = this.__getComposedPath(event)[0] || event.target;
603
+ const rect = target.getBoundingClientRect();
563
604
  return coord === 'x' ? rect.left : rect.top + rect.height;
564
605
  }
565
606
  // Native mouse or touch event
@@ -596,7 +637,12 @@ export const ContextMenuMixin = (superClass) =>
596
637
  });
597
638
  }
598
639
 
599
- /** @private */
640
+ /**
641
+ * Mimics the native focus on mousedown: walks up the flat tree from the
642
+ * target and focuses the closest focusable element, including elements
643
+ * with `tabindex="-1"` which are focusable by mouse (e.g. grid cells).
644
+ * @private
645
+ */
600
646
  __focusClosestFocusable(target) {
601
647
  let currentElement = target;
602
648
  while (currentElement) {
@@ -604,7 +650,8 @@ export const ContextMenuMixin = (superClass) =>
604
650
  currentElement.focus();
605
651
  return;
606
652
  }
607
- currentElement = currentElement.parentNode || currentElement.host;
653
+ // Use assignedSlot to cross shadow DOM boundaries for slotted content
654
+ currentElement = currentElement.assignedSlot || currentElement.parentNode || currentElement.host;
608
655
  }
609
656
  }
610
657
 
@@ -618,10 +665,8 @@ export const ContextMenuMixin = (superClass) =>
618
665
  if (target) {
619
666
  // Need to run asynchronously to avoid timing issues with the Lit-based context menu
620
667
  queueMicrotask(() => {
621
- // Dispatch mousedown and mouseup to the target (grid cell focus depends on it)
622
- target.dispatchEvent(this.__createMouseEvent('mousedown', x, y));
623
- target.dispatchEvent(this.__createMouseEvent('mouseup', x, y));
624
- // Manually try to focus the closest focusable of the target
668
+ // Focus the target as the native mousedown preceding a real
669
+ // contextmenu event would do
625
670
  this.__focusClosestFocusable(target);
626
671
  // Dispatch a contextmenu event to the target
627
672
  target.dispatchEvent(this.__createMouseEvent('contextmenu', x, y));
@@ -16,11 +16,12 @@ import { MenuOverlayMixin } from './vaadin-menu-overlay-mixin.js';
16
16
  /**
17
17
  * An element used internally by `<vaadin-context-menu>`. Not intended to be used separately.
18
18
  *
19
+ * @attr {string} theme - The theme variants to apply to the component.
19
20
  * @customElement vaadin-context-menu-overlay
20
21
  * @extends HTMLElement
21
22
  * @protected
22
23
  */
23
- export class ContextMenuOverlay extends MenuOverlayMixin(
24
+ class ContextMenuOverlay extends MenuOverlayMixin(
24
25
  OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))),
25
26
  ) {
26
27
  static get is() {
@@ -104,3 +105,5 @@ export class ContextMenuOverlay extends MenuOverlayMixin(
104
105
  }
105
106
 
106
107
  defineCustomElement(ContextMenuOverlay);
108
+
109
+ export { ContextMenuOverlay };
@@ -6,9 +6,9 @@
6
6
  import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js';
7
7
  import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
8
8
  import type { ContextMenuMixinClass } from './vaadin-context-menu-mixin.js';
9
- import type { ContextMenuItem } from './vaadin-contextmenu-items-mixin.js';
9
+ import type { ContextMenuItem, ContextMenuItemData } from './vaadin-contextmenu-items-mixin.js';
10
10
 
11
- export { ContextMenuItem };
11
+ export { ContextMenuItem, ContextMenuItemData };
12
12
 
13
13
  export type ContextMenuPosition =
14
14
  | 'bottom-end'
@@ -43,7 +43,7 @@ export type ContextMenuOpenedChangedEvent = CustomEvent<{ value: boolean }>;
43
43
  /**
44
44
  * Fired when an item is selected when the context menu is populated using the `items` API.
45
45
  */
46
- export type ContextMenuItemSelectedEvent<TItem extends ContextMenuItem = ContextMenuItem> = CustomEvent<{
46
+ export type ContextMenuItemSelectedEvent<TItem extends ContextMenuItemData = ContextMenuItemData> = CustomEvent<{
47
47
  value: TItem;
48
48
  }>;
49
49
 
@@ -52,19 +52,25 @@ export type ContextMenuItemSelectedEvent<TItem extends ContextMenuItem = Context
52
52
  */
53
53
  export type ContextMenuClosedEvent = CustomEvent;
54
54
 
55
- export interface ContextMenuCustomEventMap<TItem extends ContextMenuItem = ContextMenuItem> {
55
+ export interface ContextMenuCustomEventMap<TItem extends ContextMenuItemData = ContextMenuItemData> {
56
56
  'opened-changed': ContextMenuOpenedChangedEvent;
57
57
 
58
58
  'item-selected': ContextMenuItemSelectedEvent<TItem>;
59
59
 
60
+ /**
61
+ * @deprecated This event is deprecated and will be removed in Vaadin 26. Use `closed` instead.
62
+ */
60
63
  'close-all-menus': Event;
61
64
 
65
+ /**
66
+ * @deprecated This event is deprecated and will be removed in Vaadin 26. Use `closed` instead.
67
+ */
62
68
  'items-outside-click': Event;
63
69
 
64
70
  closed: ContextMenuClosedEvent;
65
71
  }
66
72
 
67
- export interface ContextMenuEventMap<TItem extends ContextMenuItem = ContextMenuItem>
73
+ export interface ContextMenuEventMap<TItem extends ContextMenuItemData = ContextMenuItemData>
68
74
  extends HTMLElementEventMap, ContextMenuCustomEventMap<TItem> {}
69
75
 
70
76
  /**
@@ -134,6 +140,23 @@ export interface ContextMenuEventMap<TItem extends ContextMenuItem = ContextMenu
134
140
  * </vaadin-context-menu>
135
141
  * ```
136
142
  *
143
+ * ### Slotted list-box
144
+ *
145
+ * The content of the menu can also be populated by providing a custom
146
+ * `<vaadin-context-menu-list-box>` with the `overlay` slot:
147
+ *
148
+ * ```html
149
+ * <vaadin-context-menu>
150
+ * <vaadin-context-menu-list-box slot="overlay">
151
+ * <vaadin-context-menu-item>Edit</vaadin-context-menu-item>
152
+ * <vaadin-context-menu-item>Delete</vaadin-context-menu-item>
153
+ * </vaadin-context-menu-list-box>
154
+ * </vaadin-context-menu>
155
+ * ```
156
+ *
157
+ * **Note:** slotted list-box supports a single root-level menu only and
158
+ * cannot be combined with the `items` or `renderer` API.
159
+ *
137
160
  * ### Rendering
138
161
  *
139
162
  * The content of the menu can be populated by using the renderer callback function.
@@ -276,27 +299,20 @@ export interface ContextMenuEventMap<TItem extends ContextMenuItem = ContextMenu
276
299
  *
277
300
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
278
301
  *
279
- * ### Internal components
280
- *
281
- * When using `items` API the following internal components are themable:
282
- *
283
- * - `<vaadin-context-menu-item>` - has the same API as [`<vaadin-item>`](#/elements/vaadin-item).
284
- * - `<vaadin-context-menu-list-box>` - has the same API as [`<vaadin-list-box>`](#/elements/vaadin-list-box).
302
+ * ### Related components
285
303
  *
286
- * The `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes
287
- * on top of the built-in `<vaadin-item>` state attributes:
304
+ * In addition to `<vaadin-context-menu>` itself, the following components are themable:
288
305
  *
289
- * Attribute | Description
290
- * ---------- |-------------
291
- * `expanded` | Expanded parent item.
306
+ * - [`<vaadin-context-menu-item>`](#/elements/vaadin-context-menu-item) - an item element.
307
+ * - [`<vaadin-context-menu-list-box>`](#/elements/vaadin-context-menu-list-box) - a list-box element.
292
308
  *
293
309
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
294
310
  * @fires {CustomEvent} item-selected - Fired when an item is selected when the context menu is populated using the `items` API.
295
311
  * @fires {CustomEvent} closed - Fired when the context menu is closed.
296
- * @fires {CustomEvent} close-all-menus - Fired when all menus should close, e.g., after pressing Tab or on submenu close.
297
- * @fires {CustomEvent} items-outside-click - Fired when a click happens outside any open sub-menus.
312
+ * @fires {CustomEvent} close-all-menus - Fired when all menus should close, e.g., after pressing Tab or on submenu close. This event is deprecated and will be removed in Vaadin 26. Use `closed` instead.
313
+ * @fires {CustomEvent} items-outside-click - Fired when a click happens outside any open sub-menus. This event is deprecated and will be removed in Vaadin 26. Use `closed` instead.
298
314
  */
299
- declare class ContextMenu<TItem extends ContextMenuItem = ContextMenuItem> extends HTMLElement {
315
+ declare class ContextMenu<TItem extends ContextMenuItemData = ContextMenuItemData> extends HTMLElement {
300
316
  /**
301
317
  * Position of the overlay with respect to the target.
302
318
  * Supported values: null, `top-start`, `top`, `top-end`,
@@ -318,7 +334,7 @@ declare class ContextMenu<TItem extends ContextMenuItem = ContextMenuItem> exten
318
334
  ): void;
319
335
  }
320
336
 
321
- interface ContextMenu<TItem extends ContextMenuItem = ContextMenuItem>
337
+ interface ContextMenu<TItem extends ContextMenuItemData = ContextMenuItemData>
322
338
  extends ContextMenuMixinClass<TItem>, ElementMixinClass, ThemePropertyMixinClass {}
323
339
 
324
340
  declare global {
@@ -82,6 +82,23 @@ import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
82
82
  * </vaadin-context-menu>
83
83
  * ```
84
84
  *
85
+ * ### Slotted list-box
86
+ *
87
+ * The content of the menu can also be populated by providing a custom
88
+ * `<vaadin-context-menu-list-box>` with the `overlay` slot:
89
+ *
90
+ * ```html
91
+ * <vaadin-context-menu>
92
+ * <vaadin-context-menu-list-box slot="overlay">
93
+ * <vaadin-context-menu-item>Edit</vaadin-context-menu-item>
94
+ * <vaadin-context-menu-item>Delete</vaadin-context-menu-item>
95
+ * </vaadin-context-menu-list-box>
96
+ * </vaadin-context-menu>
97
+ * ```
98
+ *
99
+ * **Note:** slotted list-box supports a single root-level menu only and
100
+ * cannot be combined with the `items` or `renderer` API.
101
+ *
85
102
  * ### Rendering
86
103
  *
87
104
  * The content of the menu can be populated by using the renderer callback function.
@@ -224,26 +241,20 @@ import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
224
241
  *
225
242
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
226
243
  *
227
- * ### Internal components
228
- *
229
- * When using `items` API the following internal components are themable:
244
+ * ### Related components
230
245
  *
231
- * - `<vaadin-context-menu-item>` - has the same API as [`<vaadin-item>`](#/elements/vaadin-item).
232
- * - `<vaadin-context-menu-list-box>` - has the same API as [`<vaadin-list-box>`](#/elements/vaadin-list-box).
246
+ * In addition to `<vaadin-context-menu>` itself, the following components are themable:
233
247
  *
234
- * The `<vaadin-context-menu-item>` sub-menu elements have the following additional state attributes
235
- * on top of the built-in `<vaadin-item>` state attributes:
236
- *
237
- * Attribute | Description
238
- * ---------- |-------------
239
- * `expanded` | Expanded parent item.
248
+ * - [`<vaadin-context-menu-item>`](#/elements/vaadin-context-menu-item) - an item element.
249
+ * - [`<vaadin-context-menu-list-box>`](#/elements/vaadin-context-menu-list-box) - a list-box element.
240
250
  *
241
251
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
242
252
  * @fires {CustomEvent} item-selected - Fired when an item is selected when the context menu is populated using the `items` API.
243
253
  * @fires {CustomEvent} closed - Fired when the context menu is closed.
244
- * @fires {CustomEvent} close-all-menus - Fired when all menus should close, e.g., after pressing Tab or on submenu close.
245
- * @fires {CustomEvent} items-outside-click - Fired when a click happens outside any open sub-menus.
254
+ * @fires {CustomEvent} close-all-menus - Fired when all menus should close, e.g., after pressing Tab or on submenu close. This event is deprecated and will be removed in Vaadin 26. Use `closed` instead.
255
+ * @fires {CustomEvent} items-outside-click - Fired when a click happens outside any open sub-menus. This event is deprecated and will be removed in Vaadin 26. Use `closed` instead.
246
256
  *
257
+ * @attr {string} theme - The theme variants to apply to the component.
247
258
  * @customElement vaadin-context-menu
248
259
  * @extends HTMLElement
249
260
  */
@@ -290,7 +301,7 @@ class ContextMenu extends ContextMenuMixin(ElementMixin(ThemePropertyMixin(Polyl
290
301
  .opened="${this.opened}"
291
302
  .model="${context}"
292
303
  .modeless="${this._modeless}"
293
- .renderer="${this.items ? this.__itemsRenderer : this.renderer}"
304
+ .renderer="${this.__slottedListBox ? undefined : this.items ? this.__itemsRenderer : this.renderer}"
294
305
  .position="${position}"
295
306
  .positionTarget="${position ? context?.target : this._positionTarget}"
296
307
  .horizontalAlign="${this.__computeHorizontalAlign(position)}"
@@ -305,7 +316,7 @@ class ContextMenu extends ContextMenuMixin(ElementMixin(ThemePropertyMixin(Polyl
305
316
  @vaadin-overlay-open="${this._onVaadinOverlayOpen}"
306
317
  @vaadin-overlay-closed="${this._onVaadinOverlayClosed}"
307
318
  >
308
- <slot name="overlay"></slot>
319
+ <slot name="overlay" @slotchange="${this.__onOverlaySlotChange}"></slot>
309
320
  <slot name="submenu" slot="submenu"></slot>
310
321
  </vaadin-context-menu-overlay>
311
322
 
@@ -351,4 +362,5 @@ class ContextMenu extends ContextMenuMixin(ElementMixin(ThemePropertyMixin(Polyl
351
362
  }
352
363
 
353
364
  defineCustomElement(ContextMenu);
365
+
354
366
  export { ContextMenu };
@@ -7,6 +7,11 @@ import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
7
7
  import { isFirefox, isIOS } from '@vaadin/component-base/src/browser-utils.js';
8
8
  import { prevent, register } from '@vaadin/component-base/src/gestures.js';
9
9
 
10
+ // A `contextmenu` caused by a selection change on touch focus (e.g. with `autoselect`) is
11
+ // dispatched in the same input turn as the tap, measured at ~80 ms after `touchstart`. A long
12
+ // press needs the finger held for 500 ms or more, which platform settings can shorten.
13
+ const SELECTION_CONTEXTMENU_MAX_DELAY = 200;
14
+
10
15
  register({
11
16
  name: 'vaadin-contextmenu',
12
17
  deps: ['touchstart', 'touchmove', 'touchend', 'contextmenu'],
@@ -21,6 +26,10 @@ register({
21
26
  sourceEvent: null,
22
27
  },
23
28
 
29
+ // Deliberately not cleared in `reset()`: it runs when the `contextmenu` handling starts,
30
+ // which is exactly when this value is needed.
31
+ _touchStartTime: null,
32
+
24
33
  reset() {
25
34
  this.info.sourceEvent = null;
26
35
  this._cancelTimer();
@@ -49,6 +58,8 @@ register({
49
58
  touchstart(e) {
50
59
  this._setSourceEvent(e);
51
60
 
61
+ this._touchStartTime = performance.now();
62
+
52
63
  this.info.touchStartCoords = {
53
64
  x: e.changedTouches[0].clientX,
54
65
  y: e.changedTouches[0].clientY,
@@ -92,6 +103,18 @@ register({
92
103
  },
93
104
 
94
105
  contextmenu(e) {
106
+ // Ignore a `contextmenu` that fires too soon after `touchstart` to be a long press. On
107
+ // touch, changing the text selection on focus (e.g. with `autoselect`) makes the browser
108
+ // fire `contextmenu` as a side effect of the tap itself. Mouse and keyboard events do
109
+ // not follow a touch this closely in practice.
110
+ //
111
+ // Uses `performance.now()` rather than `e.timeStamp`: on Android, the `contextmenu` of a
112
+ // long press carries the timestamp of the touch that started it, so `e.timeStamp` would
113
+ // report no elapsed time and suppress every long press.
114
+ if (this._touchStartTime !== null && performance.now() - this._touchStartTime < SELECTION_CONTEXTMENU_MAX_DELAY) {
115
+ return;
116
+ }
117
+
95
118
  if (!e.shiftKey) {
96
119
  this._setSourceEvent(e);
97
120
  if (isFirefox && isKeyboardActive()) {
@@ -7,7 +7,7 @@ import './vaadin-context-menu-item.js';
7
7
  import './vaadin-context-menu-list-box.js';
8
8
  import type { Constructor } from '@open-wc/dedupe-mixin';
9
9
 
10
- export type ContextMenuItem<TItemData extends object = object> = {
10
+ export type ContextMenuItemData<TItemData extends object = object> = {
11
11
  text?: string;
12
12
  /**
13
13
  * Text to be set as the menu item's tooltip.
@@ -28,12 +28,17 @@ export type ContextMenuItem<TItemData extends object = object> = {
28
28
  keepOpen?: boolean;
29
29
  theme?: string[] | string;
30
30
  className?: string;
31
- children?: Array<ContextMenuItem<TItemData>>;
31
+ children?: Array<ContextMenuItemData<TItemData>>;
32
32
  } & TItemData;
33
33
 
34
+ /**
35
+ * @deprecated Use `ContextMenuItemData` instead.
36
+ */
37
+ export type ContextMenuItem<TItemData extends object = object> = ContextMenuItemData<TItemData>;
38
+
34
39
  export declare function ItemsMixin<T extends Constructor<HTMLElement>>(base: T): Constructor<ItemsMixinClass> & T;
35
40
 
36
- export declare class ItemsMixinClass<TItem extends ContextMenuItem = ContextMenuItem> {
41
+ export declare class ItemsMixinClass<TItem extends ContextMenuItemData = ContextMenuItemData> {
37
42
  /**
38
43
  * Defines a (hierarchical) menu structure for the component.
39
44
  * If a menu item has a non-empty `children` set, a sub-menu with the child items is opened