@vaadin/tooltip 24.2.0-dev.f254716fe → 24.3.0-alpha2

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.
@@ -5,206 +5,11 @@
5
5
  */
6
6
  import './vaadin-tooltip-overlay.js';
7
7
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
8
- import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
9
- import { microTask } from '@vaadin/component-base/src/async.js';
10
- import { Debouncer } from '@vaadin/component-base/src/debounce.js';
11
- import { addValueToAttribute, removeValueFromAttribute } from '@vaadin/component-base/src/dom-utils.js';
8
+ import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
9
+ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
12
10
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
13
- import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
14
- import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
15
11
  import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
16
-
17
- const DEFAULT_DELAY = 500;
18
-
19
- let defaultFocusDelay = DEFAULT_DELAY;
20
- let defaultHoverDelay = DEFAULT_DELAY;
21
- let defaultHideDelay = DEFAULT_DELAY;
22
-
23
- const closing = new Set();
24
-
25
- let warmedUp = false;
26
- let warmUpTimeout = null;
27
- let cooldownTimeout = null;
28
-
29
- /**
30
- * Resets the global tooltip warmup and cooldown state.
31
- * Only for internal use in tests.
32
- * @private
33
- */
34
- export function resetGlobalTooltipState() {
35
- warmedUp = false;
36
- clearTimeout(warmUpTimeout);
37
- clearTimeout(cooldownTimeout);
38
- closing.clear();
39
- }
40
-
41
- /**
42
- * Controller for handling tooltip opened state.
43
- */
44
- class TooltipStateController {
45
- constructor(host) {
46
- this.host = host;
47
- }
48
-
49
- /** @private */
50
- get openedProp() {
51
- return this.host.manual ? 'opened' : '_autoOpened';
52
- }
53
-
54
- /** @private */
55
- get focusDelay() {
56
- const tooltip = this.host;
57
- return tooltip.focusDelay != null && tooltip.focusDelay > 0 ? tooltip.focusDelay : defaultFocusDelay;
58
- }
59
-
60
- /** @private */
61
- get hoverDelay() {
62
- const tooltip = this.host;
63
- return tooltip.hoverDelay != null && tooltip.hoverDelay > 0 ? tooltip.hoverDelay : defaultHoverDelay;
64
- }
65
-
66
- /** @private */
67
- get hideDelay() {
68
- const tooltip = this.host;
69
- return tooltip.hideDelay != null && tooltip.hideDelay > 0 ? tooltip.hideDelay : defaultHideDelay;
70
- }
71
-
72
- /**
73
- * Schedule opening the tooltip.
74
- * @param {Object} options
75
- */
76
- open(options = { immediate: false }) {
77
- const { immediate, hover, focus } = options;
78
- const isHover = hover && this.hoverDelay > 0;
79
- const isFocus = focus && this.focusDelay > 0;
80
-
81
- if (!immediate && (isHover || isFocus) && !this.__closeTimeout) {
82
- this.__warmupTooltip(isFocus);
83
- } else {
84
- this.__showTooltip();
85
- }
86
- }
87
-
88
- /**
89
- * Schedule closing the tooltip.
90
- * @param {boolean} immediate
91
- */
92
- close(immediate) {
93
- if (!immediate && this.hideDelay > 0) {
94
- this.__scheduleClose();
95
- } else {
96
- this.__abortClose();
97
- this._setOpened(false);
98
- }
99
-
100
- this.__abortWarmUp();
101
-
102
- if (warmedUp) {
103
- // Re-start cooldown timer on each tooltip closing.
104
- this.__abortCooldown();
105
- this.__scheduleCooldown();
106
- }
107
- }
108
-
109
- /** @private */
110
- _isOpened() {
111
- return this.host[this.openedProp];
112
- }
113
-
114
- /** @private */
115
- _setOpened(opened) {
116
- this.host[this.openedProp] = opened;
117
- }
118
-
119
- /** @private */
120
- __flushClosingTooltips() {
121
- closing.forEach((tooltip) => {
122
- tooltip._stateController.close(true);
123
- closing.delete(tooltip);
124
- });
125
- }
126
-
127
- /** @private */
128
- __showTooltip() {
129
- this.__abortClose();
130
- this.__flushClosingTooltips();
131
-
132
- this._setOpened(true);
133
- warmedUp = true;
134
-
135
- // Abort previously scheduled timers.
136
- this.__abortWarmUp();
137
- this.__abortCooldown();
138
- }
139
-
140
- /** @private */
141
- __warmupTooltip(isFocus) {
142
- if (!this._isOpened()) {
143
- // First tooltip is opened, warm up.
144
- if (!warmedUp) {
145
- this.__scheduleWarmUp(isFocus);
146
- } else {
147
- // Warmed up, show another tooltip.
148
- this.__showTooltip();
149
- }
150
- }
151
- }
152
-
153
- /** @private */
154
- __abortClose() {
155
- if (this.__closeTimeout) {
156
- clearTimeout(this.__closeTimeout);
157
- this.__closeTimeout = null;
158
- }
159
- }
160
-
161
- /** @private */
162
- __abortCooldown() {
163
- if (cooldownTimeout) {
164
- clearTimeout(cooldownTimeout);
165
- cooldownTimeout = null;
166
- }
167
- }
168
-
169
- /** @private */
170
- __abortWarmUp() {
171
- if (warmUpTimeout) {
172
- clearTimeout(warmUpTimeout);
173
- warmUpTimeout = null;
174
- }
175
- }
176
-
177
- /** @private */
178
- __scheduleClose() {
179
- if (this._isOpened()) {
180
- closing.add(this.host);
181
-
182
- this.__closeTimeout = setTimeout(() => {
183
- closing.delete(this.host);
184
- this.__closeTimeout = null;
185
- this._setOpened(false);
186
- }, this.hideDelay);
187
- }
188
- }
189
-
190
- /** @private */
191
- __scheduleCooldown() {
192
- cooldownTimeout = setTimeout(() => {
193
- cooldownTimeout = null;
194
- warmedUp = false;
195
- }, this.hideDelay);
196
- }
197
-
198
- /** @private */
199
- __scheduleWarmUp(isFocus) {
200
- const delay = isFocus ? this.focusDelay : this.hoverDelay;
201
- warmUpTimeout = setTimeout(() => {
202
- warmUpTimeout = null;
203
- warmedUp = true;
204
- this.__showTooltip();
205
- }, delay);
206
- }
207
- }
12
+ import { TooltipMixin } from './vaadin-tooltip-mixin.js';
208
13
 
209
14
  /**
210
15
  * `<vaadin-tooltip>` is a Web Component for creating tooltips.
@@ -244,12 +49,14 @@ class TooltipStateController {
244
49
  *
245
50
  * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
246
51
  *
52
+ * @customElement
247
53
  * @extends HTMLElement
54
+ * @mixes ControllerMixin
248
55
  * @mixes ElementMixin
249
- * @mixes OverlayClassMixin
250
56
  * @mixes ThemePropertyMixin
57
+ * @mixes TooltipMixin
251
58
  */
252
- class Tooltip extends OverlayClassMixin(ThemePropertyMixin(ElementMixin(PolymerElement))) {
59
+ class Tooltip extends TooltipMixin(ThemePropertyMixin(ElementMixin(ControllerMixin(PolymerElement)))) {
253
60
  static get is() {
254
61
  return 'vaadin-tooltip';
255
62
  }
@@ -262,8 +69,6 @@ class Tooltip extends OverlayClassMixin(ThemePropertyMixin(ElementMixin(PolymerE
262
69
  }
263
70
  </style>
264
71
  <vaadin-tooltip-overlay
265
- id="[[_uniqueId]]"
266
- role="tooltip"
267
72
  renderer="[[_renderer]]"
268
73
  theme$="[[_theme]]"
269
74
  opened="[[__computeOpened(manual, opened, _autoOpened, _isConnected)]]"
@@ -273,514 +78,16 @@ class Tooltip extends OverlayClassMixin(ThemePropertyMixin(ElementMixin(PolymerE
273
78
  no-vertical-overlap$="[[__computeNoVerticalOverlap(__effectivePosition)]]"
274
79
  horizontal-align="[[__computeHorizontalAlign(__effectivePosition)]]"
275
80
  vertical-align="[[__computeVerticalAlign(__effectivePosition)]]"
81
+ on-mouseenter="__onOverlayMouseEnter"
276
82
  on-mouseleave="__onOverlayMouseLeave"
277
83
  modeless
278
84
  ></vaadin-tooltip-overlay>
279
- `;
280
- }
281
-
282
- static get properties() {
283
- return {
284
- /**
285
- * Object with properties passed to `generator` and
286
- * `shouldShow` functions for generating tooltip text
287
- * or detecting whether to show the tooltip or not.
288
- */
289
- context: {
290
- type: Object,
291
- value: () => {
292
- return {};
293
- },
294
- },
295
-
296
- /**
297
- * The delay in milliseconds before the tooltip
298
- * is opened on keyboard focus, when not in manual mode.
299
- * @attr {number} focus-delay
300
- */
301
- focusDelay: {
302
- type: Number,
303
- },
304
-
305
- /**
306
- * The id of the element used as a tooltip trigger.
307
- * The element should be in the DOM by the time when
308
- * the attribute is set, otherwise a warning is shown.
309
- */
310
- for: {
311
- type: String,
312
- observer: '__forChanged',
313
- },
314
-
315
- /**
316
- * The delay in milliseconds before the tooltip
317
- * is closed on losing hover, when not in manual mode.
318
- * On blur, the tooltip is closed immediately.
319
- * @attr {number} hide-delay
320
- */
321
- hideDelay: {
322
- type: Number,
323
- },
324
-
325
- /**
326
- * The delay in milliseconds before the tooltip
327
- * is opened on hover, when not in manual mode.
328
- * @attr {number} hover-delay
329
- */
330
- hoverDelay: {
331
- type: Number,
332
- },
333
-
334
- /**
335
- * When true, the tooltip is controlled programmatically
336
- * instead of reacting to focus and mouse events.
337
- */
338
- manual: {
339
- type: Boolean,
340
- value: false,
341
- },
342
-
343
- /**
344
- * When true, the tooltip is opened programmatically.
345
- * Only works if `manual` is set to `true`.
346
- */
347
- opened: {
348
- type: Boolean,
349
- value: false,
350
- },
351
-
352
- /**
353
- * Position of the tooltip with respect to its target.
354
- * Supported values: `top-start`, `top`, `top-end`,
355
- * `bottom-start`, `bottom`, `bottom-end`, `start-top`,
356
- * `start`, `start-bottom`, `end-top`, `end`, `end-bottom`.
357
- */
358
- position: {
359
- type: String,
360
- },
361
-
362
- /**
363
- * Function used to detect whether to show the tooltip based on a condition,
364
- * called every time the tooltip is about to be shown on hover and focus.
365
- * The function takes two parameters: `target` and `context`, which contain
366
- * values of the corresponding tooltip properties at the time of calling.
367
- * The tooltip is only shown when the function invocation returns `true`.
368
- */
369
- shouldShow: {
370
- type: Object,
371
- value: () => {
372
- return (_target, _context) => true;
373
- },
374
- },
375
-
376
- /**
377
- * Reference to the element used as a tooltip trigger.
378
- * The target must be placed in the same shadow scope.
379
- * Defaults to an element referenced with `for`.
380
- */
381
- target: {
382
- type: Object,
383
- observer: '__targetChanged',
384
- },
385
-
386
- /**
387
- * String used as a tooltip content.
388
- */
389
- text: {
390
- type: String,
391
- observer: '__textChanged',
392
- },
393
-
394
- /**
395
- * Function used to generate the tooltip content.
396
- * When provided, it overrides the `text` property.
397
- * Use the `context` property to provide argument
398
- * that can be passed to the generator function.
399
- */
400
- generator: {
401
- type: Object,
402
- },
403
-
404
- /**
405
- * Set to true when the overlay is opened using auto-added
406
- * event listeners: mouseenter and focusin (keyboard only).
407
- * @protected
408
- */
409
- _autoOpened: {
410
- type: Boolean,
411
- observer: '__autoOpenedChanged',
412
- },
413
-
414
- /**
415
- * Default value used when `position` property is not set.
416
- * @protected
417
- */
418
- _position: {
419
- type: String,
420
- value: 'bottom',
421
- },
422
-
423
- /** @private */
424
- __effectivePosition: {
425
- type: String,
426
- computed: '__computePosition(position, _position)',
427
- },
428
-
429
- /** @private */
430
- __isTargetHidden: {
431
- type: Boolean,
432
- value: false,
433
- },
434
-
435
- /** @private */
436
- _isConnected: {
437
- type: Boolean,
438
- },
439
- };
440
- }
441
-
442
- static get observers() {
443
- return ['__generatorChanged(_overlayElement, generator, context)'];
444
- }
445
-
446
- /**
447
- * Sets the default focus delay to be used by all tooltip instances,
448
- * except for those that have focus delay configured using property.
449
- *
450
- * @param {number} delay
451
- */
452
- static setDefaultFocusDelay(focusDelay) {
453
- defaultFocusDelay = focusDelay != null && focusDelay >= 0 ? focusDelay : DEFAULT_DELAY;
454
- }
455
-
456
- /**
457
- * Sets the default hide delay to be used by all tooltip instances,
458
- * except for those that have hide delay configured using property.
459
- *
460
- * @param {number} hideDelay
461
- */
462
- static setDefaultHideDelay(hideDelay) {
463
- defaultHideDelay = hideDelay != null && hideDelay >= 0 ? hideDelay : DEFAULT_DELAY;
464
- }
465
-
466
- /**
467
- * Sets the default hover delay to be used by all tooltip instances,
468
- * except for those that have hover delay configured using property.
469
- *
470
- * @param {number} delay
471
- */
472
- static setDefaultHoverDelay(hoverDelay) {
473
- defaultHoverDelay = hoverDelay != null && hoverDelay >= 0 ? hoverDelay : DEFAULT_DELAY;
474
- }
475
-
476
- constructor() {
477
- super();
478
-
479
- this._uniqueId = `vaadin-tooltip-${generateUniqueId()}`;
480
- this._renderer = this.__tooltipRenderer.bind(this);
481
-
482
- this.__onFocusin = this.__onFocusin.bind(this);
483
- this.__onFocusout = this.__onFocusout.bind(this);
484
- this.__onMouseDown = this.__onMouseDown.bind(this);
485
- this.__onMouseEnter = this.__onMouseEnter.bind(this);
486
- this.__onMouseLeave = this.__onMouseLeave.bind(this);
487
- this.__onKeyDown = this.__onKeyDown.bind(this);
488
- this.__onOverlayOpen = this.__onOverlayOpen.bind(this);
489
85
 
490
- this.__targetVisibilityObserver = new IntersectionObserver(
491
- (entries) => {
492
- entries.forEach((entry) => this.__onTargetVisibilityChange(entry.isIntersecting));
493
- },
494
- { threshold: 0 },
495
- );
496
-
497
- this._stateController = new TooltipStateController(this);
498
- }
499
-
500
- /** @protected */
501
- connectedCallback() {
502
- super.connectedCallback();
503
-
504
- this._isConnected = true;
505
-
506
- document.body.addEventListener('vaadin-overlay-open', this.__onOverlayOpen);
507
- }
508
-
509
- /** @protected */
510
- disconnectedCallback() {
511
- super.disconnectedCallback();
512
-
513
- if (this._autoOpened) {
514
- this._stateController.close(true);
515
- }
516
- this._isConnected = false;
517
-
518
- document.body.removeEventListener('vaadin-overlay-open', this.__onOverlayOpen);
519
- }
520
-
521
- /** @private */
522
- __computeHorizontalAlign(position) {
523
- return ['top-end', 'bottom-end', 'start-top', 'start', 'start-bottom'].includes(position) ? 'end' : 'start';
524
- }
525
-
526
- /** @private */
527
- __computeNoHorizontalOverlap(position) {
528
- return ['start-top', 'start', 'start-bottom', 'end-top', 'end', 'end-bottom'].includes(position);
529
- }
530
-
531
- /** @private */
532
- __computeNoVerticalOverlap(position) {
533
- return ['top-start', 'top-end', 'top', 'bottom-start', 'bottom', 'bottom-end'].includes(position);
534
- }
535
-
536
- /** @private */
537
- __computeVerticalAlign(position) {
538
- return ['top-start', 'top-end', 'top', 'start-bottom', 'end-bottom'].includes(position) ? 'bottom' : 'top';
539
- }
540
-
541
- /** @private */
542
- __computeOpened(manual, opened, autoOpened, connected) {
543
- return connected && (manual ? opened : autoOpened);
544
- }
545
-
546
- /** @private */
547
- __computePosition(position, defaultPosition) {
548
- return position || defaultPosition;
549
- }
550
-
551
- /** @private */
552
- __tooltipRenderer(root) {
553
- root.textContent = typeof this.generator === 'function' ? this.generator(this.context) : this.text;
554
- }
555
-
556
- /** @private */
557
- __autoOpenedChanged(opened, oldOpened) {
558
- if (opened) {
559
- document.addEventListener('keydown', this.__onKeyDown, true);
560
- } else if (oldOpened) {
561
- document.removeEventListener('keydown', this.__onKeyDown, true);
562
- }
563
- }
564
-
565
- /** @private */
566
- __forChanged(forId) {
567
- if (forId) {
568
- this.__setTargetByIdDebouncer = Debouncer.debounce(this.__setTargetByIdDebouncer, microTask, () =>
569
- this.__setTargetById(forId),
570
- );
571
- }
572
- }
573
-
574
- /** @private */
575
- __setTargetById(targetId) {
576
- if (!this.isConnected) {
577
- return;
578
- }
579
-
580
- const target = this.getRootNode().getElementById(targetId);
581
-
582
- if (target) {
583
- this.target = target;
584
- } else {
585
- console.warn(`No element with id="${targetId}" found to show tooltip.`);
586
- }
587
- }
588
-
589
- /** @private */
590
- __targetChanged(target, oldTarget) {
591
- if (oldTarget) {
592
- oldTarget.removeEventListener('mouseenter', this.__onMouseEnter);
593
- oldTarget.removeEventListener('mouseleave', this.__onMouseLeave);
594
- oldTarget.removeEventListener('focusin', this.__onFocusin);
595
- oldTarget.removeEventListener('focusout', this.__onFocusout);
596
- oldTarget.removeEventListener('mousedown', this.__onMouseDown);
597
-
598
- this.__targetVisibilityObserver.unobserve(oldTarget);
599
-
600
- removeValueFromAttribute(oldTarget, 'aria-describedby', this._uniqueId);
601
- }
602
-
603
- if (target) {
604
- target.addEventListener('mouseenter', this.__onMouseEnter);
605
- target.addEventListener('mouseleave', this.__onMouseLeave);
606
- target.addEventListener('focusin', this.__onFocusin);
607
- target.addEventListener('focusout', this.__onFocusout);
608
- target.addEventListener('mousedown', this.__onMouseDown);
609
-
610
- // Wait before observing to avoid Chrome issue.
611
- requestAnimationFrame(() => {
612
- this.__targetVisibilityObserver.observe(target);
613
- });
614
-
615
- addValueToAttribute(target, 'aria-describedby', this._uniqueId);
616
- }
617
- }
618
-
619
- /** @private */
620
- __onFocusin(event) {
621
- if (this.manual) {
622
- return;
623
- }
624
-
625
- // Only open on keyboard focus.
626
- if (!isKeyboardActive()) {
627
- return;
628
- }
629
-
630
- // Do not re-open while focused if closed on Esc or mousedown.
631
- if (this.target.contains(event.relatedTarget)) {
632
- return;
633
- }
634
-
635
- if (!this.__isShouldShow()) {
636
- return;
637
- }
638
-
639
- this.__focusInside = true;
640
-
641
- if (!this.__isTargetHidden && (!this.__hoverInside || !this._autoOpened)) {
642
- this._stateController.open({ focus: true });
643
- }
644
- }
645
-
646
- /** @private */
647
- __onFocusout(event) {
648
- if (this.manual) {
649
- return;
650
- }
651
-
652
- // Do not close when moving focus within a component.
653
- if (this.target.contains(event.relatedTarget)) {
654
- return;
655
- }
656
-
657
- this.__focusInside = false;
658
-
659
- if (!this.__hoverInside) {
660
- this._stateController.close(true);
661
- }
662
- }
663
-
664
- /** @private */
665
- __onKeyDown(event) {
666
- if (event.key === 'Escape') {
667
- event.stopPropagation();
668
- this._stateController.close(true);
669
- }
670
- }
671
-
672
- /** @private */
673
- __onMouseDown() {
674
- this._stateController.close(true);
675
- }
676
-
677
- /** @private */
678
- __onMouseEnter() {
679
- if (this.manual) {
680
- return;
681
- }
682
-
683
- if (!this.__isShouldShow()) {
684
- return;
685
- }
686
-
687
- if (this.__hoverInside) {
688
- // Already hovering inside the element, do nothing.
689
- return;
690
- }
691
-
692
- this.__hoverInside = true;
693
-
694
- if (!this.__isTargetHidden && (!this.__focusInside || !this._autoOpened)) {
695
- this._stateController.open({ hover: true });
696
- }
697
- }
698
-
699
- /** @private */
700
- __onMouseLeave(event) {
701
- if (event.relatedTarget !== this._overlayElement) {
702
- this.__handleMouseLeave();
703
- }
704
- }
705
-
706
- /** @private */
707
- __onOverlayMouseLeave(event) {
708
- if (event.relatedTarget !== this.target) {
709
- this.__handleMouseLeave();
710
- }
711
- }
712
-
713
- /** @private */
714
- __handleMouseLeave() {
715
- if (this.manual) {
716
- return;
717
- }
718
-
719
- this.__hoverInside = false;
720
-
721
- if (!this.__focusInside) {
722
- this._stateController.close();
723
- }
724
- }
725
-
726
- /** @private */
727
- __onOverlayOpen() {
728
- if (this.manual) {
729
- return;
730
- }
731
-
732
- // Close tooltip if another overlay is opened on top of the tooltip's overlay
733
- if (this._overlayElement.opened && !this._overlayElement._last) {
734
- this._stateController.close(true);
735
- }
736
- }
737
-
738
- /** @private */
739
- __onTargetVisibilityChange(isVisible) {
740
- const oldHidden = this.__isTargetHidden;
741
- this.__isTargetHidden = !isVisible;
742
-
743
- // Open the overlay when the target becomes visible and has focus or hover.
744
- if (oldHidden && isVisible && (this.__focusInside || this.__hoverInside)) {
745
- this._stateController.open({ immediate: true });
746
- return;
747
- }
748
-
749
- // Close the overlay when the target is no longer fully visible.
750
- if (!isVisible && this._autoOpened) {
751
- this._stateController.close(true);
752
- }
753
- }
754
-
755
- /** @private */
756
- __isShouldShow() {
757
- if (typeof this.shouldShow === 'function' && this.shouldShow(this.target, this.context) !== true) {
758
- return false;
759
- }
760
-
761
- return true;
762
- }
763
-
764
- /** @private */
765
- __textChanged(text, oldText) {
766
- if (this._overlayElement && (text || oldText)) {
767
- this._overlayElement.requestContentUpdate();
768
- }
769
- }
770
-
771
- /** @private */
772
- __generatorChanged(overlayElement, generator, context) {
773
- if (overlayElement) {
774
- if (generator !== this.__oldTextGenerator || context !== this.__oldContext) {
775
- overlayElement.requestContentUpdate();
776
- }
777
-
778
- this.__oldTextGenerator = generator;
779
- this.__oldContext = context;
780
- }
86
+ <slot name="sr-label"></slot>
87
+ `;
781
88
  }
782
89
  }
783
90
 
784
- customElements.define(Tooltip.is, Tooltip);
91
+ defineCustomElement(Tooltip);
785
92
 
786
93
  export { Tooltip };