• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 14510568203

17 Apr 2025 07:36AM UTC coverage: 98.265% (-0.05%) from 98.318%
14510568203

Pull #1621

github

web-flow
Merge f053392fd into d3cc33c87
Pull Request #1621: Add Tooltip component

4509 of 4740 branches covered (95.13%)

Branch coverage included in aggregate %.

856 of 883 new or added lines in 8 files covered. (96.94%)

2 existing lines in 1 file now uncovered.

29019 of 29380 relevant lines covered (98.77%)

442.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.79
/src/components/tooltip/tooltip.ts
1
import { LitElement, html, nothing } from 'lit';
11✔
2
import { property, query } from 'lit/decorators.js';
11✔
3
import { createRef, ref } from 'lit/directives/ref.js';
11✔
4
import { EaseOut } from '../../animations/easings.js';
11✔
5
import { addAnimationController } from '../../animations/player.js';
11✔
6
import { fadeOut } from '../../animations/presets/fade/index.js';
11✔
7
import { scaleInCenter } from '../../animations/presets/scale/index.js';
11✔
8
import { themes } from '../../theming/theming-decorator.js';
11✔
9
import { watch } from '../common/decorators/watch.js';
11✔
10
import { registerComponent } from '../common/definitions/register.js';
11✔
11
import type { Constructor } from '../common/mixins/constructor.js';
11✔
12
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
11✔
13
import { asNumber, isLTR } from '../common/util.js';
11✔
14
import IgcIconComponent from '../icon/icon.js';
11✔
15
import IgcPopoverComponent, {
11✔
16
  type PopoverPlacement,
11✔
17
} from '../popover/popover.js';
11✔
18
import { styles as shared } from './themes/shared/tooltip.common.css';
11✔
19
import { all } from './themes/themes.js';
11✔
20
import { styles } from './themes/tooltip.base.css.js';
11✔
21
import { addTooltipController } from './tooltip-event-controller.js';
11✔
22

11✔
23
export interface IgcTooltipComponentEventMap {
11✔
24
  igcOpening: CustomEvent<Element | null>;
11✔
25
  igcOpened: CustomEvent<Element | null>;
11✔
26
  igcClosing: CustomEvent<Element | null>;
11✔
27
  igcClosed: CustomEvent<Element | null>;
11✔
28
}
11✔
29

11✔
30
type TooltipStateOptions = {
11✔
31
  show: boolean;
11✔
32
  withDelay?: boolean;
11✔
33
  withEvents?: boolean;
11✔
34
};
11✔
35

11✔
36
/**
11✔
37
 * @element igc-tooltip
11✔
38
 *
11✔
39
 * @slot - default slot
11✔
40
 * @slot close-button - Slot for custom sticky-mode close action (e.g., an icon/button).
11✔
41
 *
11✔
42
 * @fires igcOpening - Emitted before the tooltip begins to open. Can be canceled to prevent opening.
11✔
43
 * @fires igcOpened - Emitted after the tooltip has successfully opened and is visible.
11✔
44
 * @fires igcClosing - Emitted before the tooltip begins to close. Can be canceled to prevent closing.
11✔
45
 * @fires igcClosed - Emitted after the tooltip has been fully removed from view.
11✔
46
 */
11✔
47
@themes(all)
11✔
48
export default class IgcTooltipComponent extends EventEmitterMixin<
11✔
49
  IgcTooltipComponentEventMap,
11✔
50
  Constructor<LitElement>
11✔
51
>(LitElement) {
11✔
52
  public static readonly tagName = 'igc-tooltip';
11✔
53
  public static styles = [styles, shared];
11✔
54

11✔
55
  /* blazorSuppress */
11✔
56
  public static register(): void {
11✔
57
    registerComponent(
11✔
58
      IgcTooltipComponent,
11✔
59
      IgcPopoverComponent,
11✔
60
      IgcIconComponent
11✔
61
    );
11✔
62
  }
11✔
63

11✔
64
  private readonly _internals: ElementInternals;
11✔
65

11✔
66
  private readonly _controller = addTooltipController(this, {
11✔
67
    onShow: this._showOnInteraction,
11✔
68
    onHide: this._hideOnInteraction,
11✔
69
    onEscape: this._hideOnEscape,
11✔
70
  });
11✔
71

11✔
72
  private readonly _containerRef = createRef<HTMLElement>();
11✔
73
  private readonly _player = addAnimationController(this, this._containerRef);
11✔
74

11✔
75
  private readonly _showAnimation = scaleInCenter({
11✔
76
    duration: 150,
11✔
77
    easing: EaseOut.Quad,
11✔
78
  });
11✔
79

11✔
80
  private readonly _hideAnimation = fadeOut({
11✔
81
    duration: 75,
11✔
82
    easing: EaseOut.Sine,
11✔
83
  });
11✔
84

11✔
85
  private _timeoutId?: number;
11✔
86
  private _autoHideDelay = 180;
11✔
87
  private _showDelay = 200;
11✔
88
  private _hideDelay = 300;
11✔
89

11✔
90
  @query('#arrow')
11✔
91
  private _arrowElement!: HTMLElement;
11✔
92

11✔
93
  private get _arrowOffset() {
11✔
94
    if (/-/.test(this.placement)) {
95!
NEW
95
      // Horizontal start | end placement
×
NEW
96

×
NEW
97
      if (/^(left|right)-start$/.test(this.placement)) {
×
NEW
98
        return -8;
×
NEW
99
      }
×
NEW
100

×
NEW
101
      if (/^(left|right)-end$/.test(this.placement)) {
×
NEW
102
        return 8;
×
NEW
103
      }
×
NEW
104

×
NEW
105
      // Vertical start | end placement
×
NEW
106

×
NEW
107
      if (/start$/.test(this.placement)) {
×
NEW
108
        return isLTR(this) ? -8 : 8;
×
NEW
109
      }
×
NEW
110

×
NEW
111
      if (/end$/.test(this.placement)) {
×
NEW
112
        return isLTR(this) ? 8 : -8;
×
NEW
113
      }
×
NEW
114
    }
×
115

95✔
116
    return 0;
95✔
117
  }
95✔
118

11✔
119
  /**
11✔
120
   * Whether the tooltip is showing.
11✔
121
   *
11✔
122
   * @attr open
11✔
123
   * @default false
11✔
124
   */
11✔
125
  @property({ type: Boolean, reflect: true })
11✔
126
  public set open(value: boolean) {
11✔
127
    this._controller.open = value;
76✔
128
  }
76✔
129

11✔
130
  public get open(): boolean {
11✔
131
    return this._controller.open;
612✔
132
  }
612✔
133

11✔
134
  /**
11✔
135
   * Whether to disable the rendering of the arrow indicator for the tooltip.
11✔
136
   *
11✔
137
   * @attr disable-arrow
11✔
138
   * @default false
11✔
139
   */
11✔
140
  @property({ attribute: 'disable-arrow', type: Boolean, reflect: true })
11✔
141
  public disableArrow = false;
11✔
142

11✔
143
  /**
11✔
144
   * Improves positioning for inline based elements, such as links.
11✔
145
   *
11✔
146
   * @attr inline
11✔
147
   * @default false
11✔
148
   */
11✔
149
  @property({ type: Boolean, reflect: true })
11✔
150
  public inline = false;
11✔
151

11✔
152
  /**
11✔
153
   * The offset of the tooltip from the anchor in pixels.
11✔
154
   *
11✔
155
   * @attr offset
11✔
156
   * @default 6
11✔
157
   */
11✔
158
  @property({ type: Number })
11✔
159
  public offset = 6;
11✔
160

11✔
161
  /**
11✔
162
   * Where to place the floating element relative to the parent anchor element.
11✔
163
   *
11✔
164
   * @attr placement
11✔
165
   * @default top
11✔
166
   */
11✔
167
  @property()
11✔
168
  public placement: PopoverPlacement = 'top';
11✔
169

11✔
170
  /**
11✔
171
   * An element instance or an IDREF to use as the anchor for the tooltip.
11✔
172
   *
11✔
173
   * @attr anchor
11✔
174
   */
11✔
175
  @property()
11✔
176
  public anchor?: Element | string;
11✔
177

11✔
178
  /**
11✔
179
   * Which event triggers will show the tooltip.
11✔
180
   * Expects a comma separate string of different event triggers.
11✔
181
   *
11✔
182
   * @attr show-triggers
11✔
183
   * @default pointerenter
11✔
184
   */
11✔
185
  @property({ attribute: 'show-triggers' })
11✔
186
  public set showTriggers(value: string) {
11✔
187
    this._controller.showTriggers = value;
2✔
188
  }
2✔
189

11✔
190
  public get showTriggers(): string {
11✔
191
    return this._controller.showTriggers;
47✔
192
  }
47✔
193

11✔
194
  /**
11✔
195
   * Which event triggers will hide the tooltip.
11✔
196
   * Expects a comma separate string of different event triggers.
11✔
197
   *
11✔
198
   * @attr hide-triggers
11✔
199
   * @default pointerleave, click
11✔
200
   */
11✔
201
  @property({ attribute: 'hide-triggers' })
11✔
202
  public set hideTriggers(value: string) {
11✔
203
    this._controller.hideTriggers = value;
2✔
204
  }
2✔
205

11✔
206
  public get hideTriggers(): string {
11✔
207
    return this._controller.hideTriggers;
47✔
208
  }
47✔
209

11✔
210
  /**
11✔
211
   * Specifies the number of milliseconds that should pass before showing the tooltip.
11✔
212
   *
11✔
213
   * @attr show-delay
11✔
214
   * @default 200
11✔
215
   */
11✔
216
  @property({ attribute: 'show-delay', type: Number })
11✔
217
  public set showDelay(value: number) {
11✔
218
    this._showDelay = Math.max(0, asNumber(value));
1✔
219
  }
1✔
220

11✔
221
  public get showDelay(): number {
11✔
222
    return this._showDelay;
58✔
223
  }
58✔
224

11✔
225
  /**
11✔
226
   * Specifies the number of milliseconds that should pass before hiding the tooltip.
11✔
227
   *
11✔
228
   * @attr hide-delay
11✔
229
   * @default 300
11✔
230
   */
11✔
231
  @property({ attribute: 'hide-delay', type: Number })
11✔
232
  public set hideDelay(value: number) {
11✔
233
    this._hideDelay = Math.max(0, asNumber(value));
1✔
234
  }
1✔
235

11✔
236
  public get hideDelay(): number {
11✔
237
    return this._hideDelay;
55✔
238
  }
55✔
239

11✔
240
  /**
11✔
241
   * Specifies a plain text as tooltip content.
11✔
242
   *
11✔
243
   * @attr message
11✔
244
   */
11✔
245
  @property()
11✔
246
  public message = '';
11✔
247

11✔
248
  /**
11✔
249
   * Specifies if the tooltip remains visible until the user closes it via the close button or Esc key.
11✔
250
   *
11✔
251
   * @attr sticky
11✔
252
   * @default false
11✔
253
   */
11✔
254
  @property({ type: Boolean, reflect: true })
11✔
255
  public sticky = false;
11✔
256

11✔
257
  constructor() {
11✔
258
    super();
59✔
259

59✔
260
    this._internals = this.attachInternals();
59✔
261
    this._internals.role = this.sticky ? 'status' : 'tooltip';
59!
262
    this._internals.ariaAtomic = 'true';
59✔
263
    this._internals.ariaLive = 'polite';
59✔
264
  }
59✔
265

11✔
266
  protected override firstUpdated(): void {
11✔
267
    if (this.open) {
41✔
268
      this.updateComplete.then(() => {
2✔
269
        this._player.playExclusive(this._showAnimation);
2✔
270
        this.requestUpdate();
2✔
271
      });
2✔
272
    }
2✔
273
  }
41✔
274

11✔
275
  @watch('anchor')
11✔
276
  protected _onAnchorChange() {
11✔
277
    this._controller.resolveAnchor(this.anchor);
29✔
278
  }
29✔
279

11✔
280
  @watch('sticky')
11✔
281
  protected _onStickyChange(): void {
11✔
282
    this._internals.role = this.sticky ? 'status' : 'tooltip';
47✔
283
  }
47✔
284

11✔
285
  private _emitEvent(name: keyof IgcTooltipComponentEventMap): boolean {
11✔
286
    return this.emitEvent(name, {
56✔
287
      cancelable: name === 'igcOpening' || name === 'igcClosing',
56✔
288
      detail: this._controller.anchor,
56✔
289
    });
56✔
290
  }
56✔
291

11✔
292
  private async _applyTooltipState({
11✔
293
    show,
44✔
294
    withDelay = false,
44✔
295
    withEvents = false,
44✔
296
  }: TooltipStateOptions): Promise<boolean> {
44✔
297
    if (show === this.open) {
44✔
298
      return false;
3✔
299
    }
3✔
300

41✔
301
    if (withEvents && !this._emitEvent(show ? 'igcOpening' : 'igcClosing')) {
44✔
302
      return false;
2✔
303
    }
2✔
304

39✔
305
    const commitStateChange = async () => {
39✔
306
      if (show) {
39✔
307
        this.open = true;
20✔
308
      }
20✔
309

39✔
310
      // Make the tooltip ignore most interactions while the animation
39✔
311
      // is running. In the rare case when the popover overlaps its anchor
39✔
312
      // this will prevent looping between the anchor and tooltip handlers.
39✔
313
      this.inert = true;
39✔
314

39✔
315
      const animationComplete = await this._player.playExclusive(
39✔
316
        show ? this._showAnimation : this._hideAnimation
39✔
317
      );
39✔
318

39✔
319
      this.inert = false;
39✔
320
      this.open = show;
39✔
321

39✔
322
      if (animationComplete && withEvents) {
39✔
323
        this._emitEvent(show ? 'igcOpened' : 'igcClosed');
24✔
324
      }
24✔
325

39✔
326
      return animationComplete;
39✔
327
    };
39✔
328

39✔
329
    if (withDelay) {
44✔
330
      clearTimeout(this._timeoutId);
25✔
331
      return new Promise(() => {
25✔
332
        this._timeoutId = setTimeout(
25✔
333
          async () => await commitStateChange(),
25✔
334
          show ? this.showDelay : this.hideDelay
25✔
335
        );
25✔
336
      });
25✔
337
    }
25✔
338

14✔
339
    return commitStateChange();
14✔
340
  }
44✔
341

11✔
342
  /**
11✔
343
   *  Shows the tooltip if not already showing.
11✔
344
   *  If a target is provided, sets it as a transient anchor.
11✔
345
   */
11✔
346
  public show(target?: Element): Promise<boolean> {
11✔
347
    if (target) {
7✔
348
      this._stopTimeoutAndAnimation();
2✔
349

2✔
350
      if (this._controller.anchor !== target) {
2✔
351
        this.open = false;
1✔
352
      }
1✔
353
      this._controller.setAnchor(target, true);
2✔
354
    }
2✔
355

7✔
356
    return this._applyTooltipState({ show: true });
7✔
357
  }
7✔
358

11✔
359
  /** Hides the tooltip if not already hidden. */
11✔
360
  public hide(): Promise<boolean> {
11✔
361
    return this._applyTooltipState({ show: false });
9✔
362
  }
9✔
363

11✔
364
  /** Toggles the tooltip between shown/hidden state */
11✔
365
  public toggle(): Promise<boolean> {
11✔
366
    return this.open ? this.hide() : this.show();
2✔
367
  }
2✔
368

11✔
369
  protected _showWithEvent(): Promise<boolean> {
11✔
370
    return this._applyTooltipState({
16✔
371
      show: true,
16✔
372
      withDelay: true,
16✔
373
      withEvents: true,
16✔
374
    });
16✔
375
  }
16✔
376

11✔
377
  protected _hideWithEvent(): Promise<boolean> {
11✔
378
    return this._applyTooltipState({
12✔
379
      show: false,
12✔
380
      withDelay: true,
12✔
381
      withEvents: true,
12✔
382
    });
12✔
383
  }
12✔
384

11✔
385
  private _showOnInteraction(): void {
11✔
386
    this._stopTimeoutAndAnimation();
16✔
387
    this._showWithEvent();
16✔
388
  }
16✔
389

11✔
390
  private _stopTimeoutAndAnimation(): void {
11✔
391
    clearTimeout(this._timeoutId);
30✔
392
    this._player.stopAll();
30✔
393
  }
30✔
394

11✔
395
  private _setAutoHide(): void {
11✔
396
    this._stopTimeoutAndAnimation();
12✔
397

12✔
398
    this._timeoutId = setTimeout(
12✔
399
      () => this._hideWithEvent(),
12✔
400
      this._autoHideDelay
12✔
401
    );
12✔
402
  }
12✔
403

11✔
404
  private _hideOnInteraction(): void {
11✔
405
    if (!this.sticky) {
12✔
406
      this._setAutoHide();
11✔
407
    }
11✔
408
  }
12✔
409

11✔
410
  private async _hideOnEscape(): Promise<void> {
11✔
411
    await this.hide();
5✔
412
    this._emitEvent('igcClosed');
5✔
413
  }
5✔
414

11✔
415
  protected override render() {
11✔
416
    return html`
95✔
417
      <igc-popover
95✔
418
        .inert=${!this.open}
95✔
419
        .placement=${this.placement}
95✔
420
        .offset=${this.offset}
95✔
421
        .anchor=${this._controller.anchor ?? undefined}
95✔
422
        .arrow=${this.disableArrow ? null : this._arrowElement}
95✔
423
        .arrowOffset=${this._arrowOffset}
95✔
424
        .shiftPadding=${8}
95✔
425
        ?open=${this.open}
95✔
426
        ?inline=${this.inline}
95✔
427
        flip
95✔
428
        shift
95✔
429
      >
95✔
430
        <div ${ref(this._containerRef)} part="base">
95✔
431
          <slot>${this.message}</slot>
95✔
432
          ${this.sticky
95✔
433
            ? html`
9✔
434
                <slot name="close-button" @click=${this._setAutoHide}>
9✔
435
                  <igc-icon
86✔
436
                    name="input_clear"
86✔
437
                    collection="default"
86✔
438
                    aria-hidden="true"
86✔
439
                  ></igc-icon>
86✔
440
                </slot>
86✔
441
              `
86✔
442
            : nothing}
95✔
443
          ${this.disableArrow ? nothing : html`<div id="arrow"></div>`}
95✔
444
        </div>
95✔
445
      </igc-popover>
95✔
446
    `;
95✔
447
  }
95✔
448
}
11✔
449

11✔
450
declare global {
11✔
451
  interface HTMLElementTagNameMap {
11✔
452
    'igc-tooltip': IgcTooltipComponent;
11✔
453
  }
11✔
454
}
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc