@vaadin/context-menu 24.3.0-alpha1 → 24.3.0-alpha3

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.
@@ -6,7 +6,8 @@
6
6
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
7
7
  import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
8
8
  import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
9
- import { ContextMenuItem, ItemsMixin } from './vaadin-contextmenu-items-mixin.js';
9
+ import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
10
+ import { ContextMenuItem } from './vaadin-contextmenu-items-mixin.js';
10
11
 
11
12
  export { ContextMenuItem };
12
13
 
@@ -58,7 +59,7 @@ export interface ContextMenuEventMap extends HTMLElementEventMap, ContextMenuCus
58
59
  *
59
60
  * ```javascript
60
61
  * contextMenu.items = [
61
- * { text: 'Menu Item 1', theme: 'primary', children:
62
+ * { text: 'Menu Item 1', theme: 'primary', className: 'first', children:
62
63
  * [
63
64
  * { text: 'Menu Item 1-1', checked: true, keepOpen: true },
64
65
  * { text: 'Menu Item 1-2' }
@@ -71,7 +72,7 @@ export interface ContextMenuEventMap extends HTMLElementEventMap, ContextMenuCus
71
72
  * { text: 'Menu Item 2-2', disabled: true }
72
73
  * ]
73
74
  * },
74
- * { text: 'Menu Item 3', disabled: true }
75
+ * { text: 'Menu Item 3', disabled: true, className: 'last' }
75
76
  * ];
76
77
  *
77
78
  * contextMenu.addEventListener('item-selected', e => {
@@ -225,74 +226,7 @@ export interface ContextMenuEventMap extends HTMLElementEventMap, ContextMenuCus
225
226
  * @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
226
227
  * @fires {CustomEvent} item-selected - Fired when an item is selected when the context menu is populated using the `items` API.
227
228
  */
228
- declare class ContextMenu extends OverlayClassMixin(ElementMixin(ThemePropertyMixin(ItemsMixin(HTMLElement)))) {
229
- /**
230
- * CSS selector that can be used to target any child element
231
- * of the context menu to listen for `openOn` events.
232
- */
233
- selector: string | null | undefined;
234
-
235
- /**
236
- * True if the overlay is currently displayed.
237
- */
238
- readonly opened: boolean;
239
-
240
- /**
241
- * Event name to listen for opening the context menu.
242
- * @attr {string} open-on
243
- */
244
- openOn: string;
245
-
246
- /**
247
- * The target element that's listened to for context menu opening events.
248
- * By default the vaadin-context-menu listens to the target's `vaadin-contextmenu`
249
- * events.
250
- */
251
- listenOn: HTMLElement;
252
-
253
- /**
254
- * Event name to listen for closing the context menu.
255
- * @attr {string} close-on
256
- */
257
- closeOn: string;
258
-
259
- /**
260
- * Custom function for rendering the content of the menu overlay.
261
- * Receives three arguments:
262
- *
263
- * - `root` The root container DOM element. Append your content to it.
264
- * - `contextMenu` The reference to the `<vaadin-context-menu>` element.
265
- * - `context` The object with the menu context, contains:
266
- * - `context.target` the target of the menu opening event,
267
- * - `context.detail` the menu opening event detail.
268
- */
269
- renderer: ContextMenuRenderer | null | undefined;
270
-
271
- /**
272
- * When true, the menu overlay is modeless.
273
- */
274
- protected _modeless: boolean;
275
-
276
- /**
277
- * Requests an update for the content of the menu overlay.
278
- * While performing the update, it invokes the renderer passed in the `renderer` property.
279
- *
280
- * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
281
- */
282
- requestContentUpdate(): void;
283
-
284
- /**
285
- * Closes the overlay.
286
- */
287
- close(): void;
288
-
289
- /**
290
- * Opens the overlay.
291
- *
292
- * @param e used as the context for the menu. Overlay coordinates are taken from this event.
293
- */
294
- open(e: Event | undefined): void;
295
-
229
+ declare class ContextMenu extends ContextMenuMixin(OverlayClassMixin(ElementMixin(ThemePropertyMixin(HTMLElement)))) {
296
230
  addEventListener<K extends keyof ContextMenuEventMap>(
297
231
  type: K,
298
232
  listener: (this: ContextMenu, ev: ContextMenuEventMap[K]) => void,
@@ -4,18 +4,17 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import './vaadin-contextmenu-event.js';
7
+ import './vaadin-context-menu-item.js';
8
+ import './vaadin-context-menu-list-box.js';
7
9
  import './vaadin-context-menu-overlay.js';
8
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
9
- import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
10
11
  import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
11
12
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
12
13
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
13
- import { addListener, gestures, removeListener } from '@vaadin/component-base/src/gestures.js';
14
- import { MediaQueryController } from '@vaadin/component-base/src/media-query-controller.js';
15
14
  import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
16
15
  import { processTemplates } from '@vaadin/component-base/src/templates.js';
17
16
  import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
18
- import { ItemsMixin } from './vaadin-contextmenu-items-mixin.js';
17
+ import { ContextMenuMixin } from './vaadin-context-menu-mixin.js';
19
18
 
20
19
  /**
21
20
  * `<vaadin-context-menu>` is a Web Component for creating context menus.
@@ -32,7 +31,7 @@ import { ItemsMixin } from './vaadin-contextmenu-items-mixin.js';
32
31
  *
33
32
  * ```javascript
34
33
  * contextMenu.items = [
35
- * { text: 'Menu Item 1', theme: 'primary', children:
34
+ * { text: 'Menu Item 1', theme: 'primary', className: 'first', children:
36
35
  * [
37
36
  * { text: 'Menu Item 1-1', checked: true, keepOpen: true },
38
37
  * { text: 'Menu Item 1-2' }
@@ -45,7 +44,7 @@ import { ItemsMixin } from './vaadin-contextmenu-items-mixin.js';
45
44
  * { text: 'Menu Item 2-2', disabled: true }
46
45
  * ]
47
46
  * },
48
- * { text: 'Menu Item 3', disabled: true }
47
+ * { text: 'Menu Item 3', disabled: true, className: 'last' }
49
48
  * ];
50
49
  *
51
50
  * contextMenu.addEventListener('item-selected', e => {
@@ -202,13 +201,13 @@ import { ItemsMixin } from './vaadin-contextmenu-items-mixin.js';
202
201
  * @customElement
203
202
  * @extends HTMLElement
204
203
  * @mixes ElementMixin
204
+ * @mixes ContextMenuMixin
205
205
  * @mixes ControllerMixin
206
206
  * @mixes OverlayClassMixin
207
207
  * @mixes ThemePropertyMixin
208
- * @mixes ItemsMixin
209
208
  */
210
- class ContextMenu extends OverlayClassMixin(
211
- ControllerMixin(ElementMixin(ThemePropertyMixin(ItemsMixin(PolymerElement)))),
209
+ class ContextMenu extends ContextMenuMixin(
210
+ OverlayClassMixin(ControllerMixin(ElementMixin(ThemePropertyMixin(PolymerElement)))),
212
211
  ) {
213
212
  static get template() {
214
213
  return html`
@@ -241,485 +240,13 @@ class ContextMenu extends OverlayClassMixin(
241
240
  return 'vaadin-context-menu';
242
241
  }
243
242
 
244
- static get properties() {
245
- return {
246
- /**
247
- * CSS selector that can be used to target any child element
248
- * of the context menu to listen for `openOn` events.
249
- */
250
- selector: {
251
- type: String,
252
- },
253
-
254
- /**
255
- * True if the overlay is currently displayed.
256
- * @type {boolean}
257
- */
258
- opened: {
259
- type: Boolean,
260
- value: false,
261
- notify: true,
262
- readOnly: true,
263
- },
264
-
265
- /**
266
- * Event name to listen for opening the context menu.
267
- * @attr {string} open-on
268
- * @type {string}
269
- */
270
- openOn: {
271
- type: String,
272
- value: 'vaadin-contextmenu',
273
- },
274
-
275
- /**
276
- * The target element that's listened to for context menu opening events.
277
- * By default the vaadin-context-menu listens to the target's `vaadin-contextmenu`
278
- * events.
279
- * @type {!HTMLElement}
280
- * @default self
281
- */
282
- listenOn: {
283
- type: Object,
284
- value() {
285
- return this;
286
- },
287
- },
288
-
289
- /**
290
- * Event name to listen for closing the context menu.
291
- * @attr {string} close-on
292
- * @type {string}
293
- */
294
- closeOn: {
295
- type: String,
296
- value: 'click',
297
- observer: '_closeOnChanged',
298
- },
299
-
300
- /**
301
- * Custom function for rendering the content of the menu overlay.
302
- * Receives three arguments:
303
- *
304
- * - `root` The root container DOM element. Append your content to it.
305
- * - `contextMenu` The reference to the `<vaadin-context-menu>` element.
306
- * - `context` The object with the menu context, contains:
307
- * - `context.target` the target of the menu opening event,
308
- * - `context.detail` the menu opening event detail.
309
- * @type {ContextMenuRenderer | undefined}
310
- */
311
- renderer: {
312
- type: Function,
313
- },
314
-
315
- /**
316
- * When true, the menu overlay is modeless.
317
- * @protected
318
- */
319
- _modeless: {
320
- type: Boolean,
321
- },
322
-
323
- /** @private */
324
- _context: Object,
325
-
326
- /** @private */
327
- _phone: {
328
- type: Boolean,
329
- },
330
-
331
- /** @private */
332
- _touch: {
333
- type: Boolean,
334
- value: isTouch,
335
- },
336
-
337
- /** @private */
338
- _wide: {
339
- type: Boolean,
340
- },
341
-
342
- /** @private */
343
- _wideMediaQuery: {
344
- type: String,
345
- value: '(min-device-width: 750px)',
346
- },
347
- };
348
- }
349
-
350
- static get observers() {
351
- return [
352
- '_openedChanged(opened)',
353
- '_targetOrOpenOnChanged(listenOn, openOn)',
354
- '_rendererChanged(renderer, items)',
355
- '_touchOrWideChanged(_touch, _wide)',
356
- ];
357
- }
358
-
359
- constructor() {
360
- super();
361
- this._boundOpen = this.open.bind(this);
362
- this._boundClose = this.close.bind(this);
363
- this._boundPreventDefault = this._preventDefault.bind(this);
364
- this._boundOnGlobalContextMenu = this._onGlobalContextMenu.bind(this);
365
- }
366
-
367
- /** @protected */
368
- connectedCallback() {
369
- super.connectedCallback();
370
-
371
- this.__boundOnScroll = this.__onScroll.bind(this);
372
- window.addEventListener('scroll', this.__boundOnScroll, true);
373
-
374
- // Restore opened state if overlay was opened when disconnecting
375
- if (this.__restoreOpened) {
376
- this._setOpened(true);
377
- }
378
- }
379
-
380
- /** @protected */
381
- disconnectedCallback() {
382
- super.disconnectedCallback();
383
-
384
- window.removeEventListener('scroll', this.__boundOnScroll, true);
385
-
386
- // Close overlay and memorize opened state
387
- this.__restoreOpened = this.opened;
388
- this.close();
389
- }
390
-
391
243
  /** @protected */
392
244
  ready() {
393
245
  super.ready();
394
246
 
395
- this._overlayElement = this.$.overlay;
396
-
397
- this.addController(
398
- new MediaQueryController(this._wideMediaQuery, (matches) => {
399
- this._wide = matches;
400
- }),
401
- );
402
-
403
247
  processTemplates(this);
404
248
  }
405
249
 
406
- /**
407
- * Runs before overlay is fully rendered
408
- * @private
409
- */
410
- _onOverlayOpened(e) {
411
- this._setOpened(e.detail.value);
412
- this.__alignOverlayPosition();
413
- }
414
-
415
- /**
416
- * Runs after overlay is fully rendered
417
- * @private
418
- */
419
- _onVaadinOverlayOpen() {
420
- this.__alignOverlayPosition();
421
- this.$.overlay.style.opacity = '';
422
- this.__forwardFocus();
423
- }
424
-
425
- /** @private */
426
- _targetOrOpenOnChanged(listenOn, openOn) {
427
- if (this._oldListenOn && this._oldOpenOn) {
428
- this._unlisten(this._oldListenOn, this._oldOpenOn, this._boundOpen);
429
-
430
- this._oldListenOn.style.webkitTouchCallout = '';
431
- this._oldListenOn.style.webkitUserSelect = '';
432
- this._oldListenOn.style.userSelect = '';
433
-
434
- this._oldListenOn = null;
435
- this._oldOpenOn = null;
436
- }
437
-
438
- if (listenOn && openOn) {
439
- this._listen(listenOn, openOn, this._boundOpen);
440
-
441
- this._oldListenOn = listenOn;
442
- this._oldOpenOn = openOn;
443
- }
444
- }
445
-
446
- /** @private */
447
- _touchOrWideChanged(touch, wide) {
448
- this._phone = !wide && touch;
449
- }
450
-
451
- /** @private */
452
- _setListenOnUserSelect(value) {
453
- // Note: these styles don't seem to work in Firefox on iOS.
454
- this.listenOn.style.webkitTouchCallout = value;
455
- this.listenOn.style.webkitUserSelect = value; // Chrome, Safari, Firefox
456
- this.listenOn.style.userSelect = value;
457
-
458
- // Note: because user-selection is disabled on the overlay
459
- // before opening the menu the text could be already selected
460
- // so we need to clear that selection
461
- document.getSelection().removeAllRanges();
462
- }
463
-
464
- /** @private */
465
- _closeOnChanged(closeOn, oldCloseOn) {
466
- // Outside click event from overlay
467
- const evtOverlay = 'vaadin-overlay-outside-click';
468
-
469
- const overlay = this.$.overlay;
470
-
471
- if (oldCloseOn) {
472
- this._unlisten(overlay, oldCloseOn, this._boundClose);
473
- }
474
- if (closeOn) {
475
- this._listen(overlay, closeOn, this._boundClose);
476
- overlay.removeEventListener(evtOverlay, this._boundPreventDefault);
477
- } else {
478
- overlay.addEventListener(evtOverlay, this._boundPreventDefault);
479
- }
480
- }
481
-
482
- /** @private */
483
- _preventDefault(e) {
484
- e.preventDefault();
485
- }
486
-
487
- /** @private */
488
- _openedChanged(opened) {
489
- if (opened) {
490
- document.documentElement.addEventListener('contextmenu', this._boundOnGlobalContextMenu, true);
491
- this._setListenOnUserSelect('none');
492
- } else {
493
- document.documentElement.removeEventListener('contextmenu', this._boundOnGlobalContextMenu, true);
494
- this._setListenOnUserSelect('');
495
- }
496
-
497
- // Has to be set after instance has been created
498
- this.$.overlay.opened = opened;
499
- }
500
-
501
- /**
502
- * Requests an update for the content of the menu overlay.
503
- * While performing the update, it invokes the renderer passed in the `renderer` property.
504
- *
505
- * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
506
- */
507
- requestContentUpdate() {
508
- if (!this._overlayElement || !this.renderer) {
509
- return;
510
- }
511
-
512
- this._overlayElement.requestContentUpdate();
513
- }
514
-
515
- /** @private */
516
- _rendererChanged(renderer, items) {
517
- if (items) {
518
- if (renderer) {
519
- throw new Error('The items API cannot be used together with a renderer');
520
- }
521
-
522
- if (this.closeOn === 'click') {
523
- this.closeOn = '';
524
- }
525
-
526
- renderer = this.__itemsRenderer;
527
- }
528
-
529
- this.$.overlay.setProperties({ owner: this, renderer });
530
- }
531
-
532
- /**
533
- * Closes the overlay.
534
- */
535
- close() {
536
- this._setOpened(false);
537
- }
538
-
539
- /** @private */
540
- _contextTarget(e) {
541
- if (this.selector) {
542
- const targets = this.listenOn.querySelectorAll(this.selector);
543
-
544
- return Array.prototype.filter.call(targets, (el) => {
545
- return e.composedPath().indexOf(el) > -1;
546
- })[0];
547
- }
548
- return e.target;
549
- }
550
-
551
- /**
552
- * Opens the overlay.
553
- * @param {!Event | undefined} e used as the context for the menu. Overlay coordinates are taken from this event.
554
- */
555
- open(e) {
556
- if (e && !this.opened) {
557
- this._context = {
558
- detail: e.detail,
559
- target: this._contextTarget(e),
560
- };
561
-
562
- if (this._context.target) {
563
- e.preventDefault();
564
- e.stopPropagation();
565
-
566
- // Used in alignment which is delayed until overlay is rendered
567
- this.__x = this._getEventCoordinate(e, 'x');
568
- this.__pageXOffset = window.pageXOffset;
569
-
570
- this.__y = this._getEventCoordinate(e, 'y');
571
- this.__pageYOffset = window.pageYOffset;
572
-
573
- this.$.overlay.style.opacity = '0';
574
- this._setOpened(true);
575
- }
576
- }
577
- }
578
-
579
- /** @private */
580
- __onScroll() {
581
- if (!this.opened) {
582
- return;
583
- }
584
-
585
- const yDiff = window.pageYOffset - this.__pageYOffset;
586
- const xDiff = window.pageXOffset - this.__pageXOffset;
587
-
588
- this.__adjustPosition('left', -xDiff);
589
- this.__adjustPosition('right', xDiff);
590
-
591
- this.__adjustPosition('top', -yDiff);
592
- this.__adjustPosition('bottom', yDiff);
593
-
594
- this.__pageYOffset += yDiff;
595
- this.__pageXOffset += xDiff;
596
- }
597
-
598
- /** @private */
599
- __adjustPosition(coord, diff) {
600
- const overlay = this.$.overlay;
601
- const style = overlay.style;
602
-
603
- style[coord] = `${(parseInt(style[coord]) || 0) + diff}px`;
604
- }
605
-
606
- /** @private */
607
- __alignOverlayPosition() {
608
- const overlay = this.$.overlay;
609
-
610
- if (overlay.positionTarget) {
611
- // The overlay is positioned relative to another node, for example, a
612
- // menu item in a nested submenu structure where this overlay lists
613
- // the items for another submenu.
614
- // It means that the overlay positioning is controlled by
615
- // vaadin-overlay-position-mixin so no manual alignment is needed.
616
- return;
617
- }
618
-
619
- const style = overlay.style;
620
-
621
- // Reset all properties before measuring
622
- ['top', 'right', 'bottom', 'left'].forEach((prop) => style.removeProperty(prop));
623
- ['right-aligned', 'end-aligned', 'bottom-aligned'].forEach((attr) => overlay.removeAttribute(attr));
624
-
625
- // Maximum x and y values are imposed by content size and overlay limits.
626
- const { xMax, xMin, yMax } = overlay.getBoundaries();
627
- // Reuse saved x and y event values, in order to this method be used async
628
- // in the `vaadin-overlay-change` which guarantees that overlay is ready.
629
- // The valus represent an anchor position on the page where the contextmenu
630
- // event took place.
631
- const x = this.__x;
632
- const y = this.__y;
633
-
634
- // Select one overlay corner and move to the event x/y position.
635
- // Then set styling attrs for flex-aligning the content appropriately.
636
- const wdthVport = document.documentElement.clientWidth;
637
- const hghtVport = document.documentElement.clientHeight;
638
-
639
- if (!this.__isRTL) {
640
- if (x < wdthVport / 2 || x < xMax) {
641
- // Menu is displayed in the right side of the anchor
642
- style.left = `${x}px`;
643
- } else {
644
- // Menu is displayed in the left side of the anchor
645
- style.right = `${Math.max(0, wdthVport - x)}px`;
646
- this._setEndAligned(overlay);
647
- }
648
- } else if (x > wdthVport / 2 || x > xMin) {
649
- // Menu is displayed in the right side of the anchor
650
- style.right = `${Math.max(0, wdthVport - x)}px`;
651
- } else {
652
- // Menu is displayed in the left side of the anchor
653
- style.left = `${x}px`;
654
- this._setEndAligned(overlay);
655
- }
656
-
657
- if (y < hghtVport / 2 || y < yMax) {
658
- style.top = `${y}px`;
659
- } else {
660
- style.bottom = `${Math.max(0, hghtVport - y)}px`;
661
- overlay.setAttribute('bottom-aligned', '');
662
- }
663
- }
664
-
665
- /** @private */
666
- _setEndAligned(element) {
667
- element.setAttribute('end-aligned', '');
668
- if (!this.__isRTL) {
669
- element.setAttribute('right-aligned', '');
670
- }
671
- }
672
-
673
- /** @private */
674
- _getEventCoordinate(event, coord) {
675
- if (event.detail instanceof Object) {
676
- if (event.detail[coord]) {
677
- // Polymer gesture events, get coordinate from detail
678
- return event.detail[coord];
679
- } else if (event.detail.sourceEvent) {
680
- // Unwrap detailed event
681
- return this._getEventCoordinate(event.detail.sourceEvent, coord);
682
- }
683
- } else {
684
- const prop = `client${coord.toUpperCase()}`;
685
- const position = event.changedTouches ? event.changedTouches[0][prop] : event[prop];
686
-
687
- if (position === 0) {
688
- // Native keyboard event
689
- const rect = event.target.getBoundingClientRect();
690
- return coord === 'x' ? rect.left : rect.top + rect.height;
691
- }
692
- // Native mouse or touch event
693
- return position;
694
- }
695
- }
696
-
697
- /** @private */
698
- _listen(node, evType, handler) {
699
- if (gestures[evType]) {
700
- addListener(node, evType, handler);
701
- } else {
702
- node.addEventListener(evType, handler);
703
- }
704
- }
705
-
706
- /** @private */
707
- _unlisten(node, evType, handler) {
708
- if (gestures[evType]) {
709
- removeListener(node, evType, handler);
710
- } else {
711
- node.removeEventListener(evType, handler);
712
- }
713
- }
714
-
715
- /** @private */
716
- _onGlobalContextMenu(e) {
717
- if (!e.shiftKey) {
718
- e.preventDefault();
719
- this.close();
720
- }
721
- }
722
-
723
250
  /**
724
251
  * Fired when an item is selected when the context menu is populated using the `items` API.
725
252
  *
@@ -14,6 +14,7 @@ export interface ContextMenuItem {
14
14
  checked?: boolean;
15
15
  keepOpen?: boolean;
16
16
  theme?: string[] | string;
17
+ className?: string;
17
18
  children?: ContextMenuItem[];
18
19
  }
19
20
 
@@ -31,7 +32,7 @@ export declare class ItemsMixinClass {
31
32
  *
32
33
  * ```javascript
33
34
  * contextMenu.items = [
34
- * { text: 'Menu Item 1', theme: 'primary', children:
35
+ * { text: 'Menu Item 1', theme: 'primary', className: 'first', children:
35
36
  * [
36
37
  * { text: 'Menu Item 1-1', checked: true, keepOpen: true },
37
38
  * { text: 'Menu Item 1-2' }
@@ -44,7 +45,7 @@ export declare class ItemsMixinClass {
44
45
  * { text: 'Menu Item 2-2', disabled: true }
45
46
  * ]
46
47
  * },
47
- * { text: 'Menu Item 3', disabled: true }
48
+ * { text: 'Menu Item 3', disabled: true, className: 'last' }
48
49
  * ];
49
50
  * ```
50
51
  */