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

IgniteUI / igniteui-webcomponents / 15601242557

12 Jun 2025 03:56AM UTC coverage: 98.242% (-0.07%) from 98.309%
15601242557

push

github

web-flow
refactor(tooltip): Align API and behavior to Angular tooltip (#1740)

* refactor(tooltip): Align API and behavior to Angular tooltip
docs: Updated CHANGELOG with release notes.

4889 of 5133 branches covered (95.25%)

Branch coverage included in aggregate %.

39 of 41 new or added lines in 1 file covered. (95.12%)

19 existing lines in 1 file now uncovered.

31314 of 31718 relevant lines covered (98.73%)

1754.03 hits per line

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

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

10✔
22
export interface IgcTooltipComponentEventMap {
10✔
23
  igcOpening: CustomEvent<void>;
10✔
24
  igcOpened: CustomEvent<void>;
10✔
25
  igcClosing: CustomEvent<void>;
10✔
26
  igcClosed: CustomEvent<void>;
10✔
27
}
10✔
28

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

10✔
35
/**
10✔
36
 * Provides a way to display supplementary information related to an element when a user interacts with it (e.g., hover, focus).
10✔
37
 * It offers features such as placement customization, delays, sticky mode, and animations.
10✔
38
 *
10✔
39
 * @element igc-tooltip
10✔
40
 *
10✔
41
 * @slot - Default slot of the tooltip component.
10✔
42
 * @slot close-button - Slot for custom sticky-mode close action (e.g., an icon/button).
10✔
43
 *
10✔
44
 * @csspart base - The wrapping container of the tooltip content.
10✔
45
 *
10✔
46
 * @fires igcOpening - Emitted before the tooltip begins to open. Can be canceled to prevent opening.
10✔
47
 * @fires igcOpened - Emitted after the tooltip has successfully opened and is visible.
10✔
48
 * @fires igcClosing - Emitted before the tooltip begins to close. Can be canceled to prevent closing.
10✔
49
 * @fires igcClosed - Emitted after the tooltip has been fully removed from view.
10✔
50
 */
10✔
51
@themes(all)
10✔
52
export default class IgcTooltipComponent extends EventEmitterMixin<
10✔
53
  IgcTooltipComponentEventMap,
10✔
54
  Constructor<LitElement>
10✔
55
>(LitElement) {
10✔
56
  public static readonly tagName = 'igc-tooltip';
10✔
57
  public static styles = [styles, shared];
10✔
58

10✔
59
  /* blazorSuppress */
10✔
60
  public static register(): void {
10✔
61
    registerComponent(
10✔
62
      IgcTooltipComponent,
10✔
63
      IgcPopoverComponent,
10✔
64
      IgcIconComponent
10✔
65
    );
10✔
66
  }
10✔
67

10✔
68
  private readonly _internals: ElementInternals;
10✔
69

10✔
70
  private readonly _controller = addTooltipController(this, {
10✔
71
    onShow: this._showOnInteraction,
10✔
72
    onHide: this._hideOnInteraction,
10✔
73
    onEscape: this._hideOnEscape,
10✔
74
  });
10✔
75

10✔
76
  private readonly _containerRef = createRef<HTMLElement>();
10✔
77
  private readonly _player = addAnimationController(this, this._containerRef);
10✔
78

10✔
79
  private readonly _showAnimation = scaleInCenter({
10✔
80
    duration: 150,
10✔
81
    easing: EaseOut.Quad,
10✔
82
  });
10✔
83

10✔
84
  private readonly _hideAnimation = fadeOut({
10✔
85
    duration: 75,
10✔
86
    easing: EaseOut.Sine,
10✔
87
  });
10✔
88

10✔
89
  private _timeoutId?: number;
10✔
90
  private _autoHideDelay = 180;
10✔
91
  private _showDelay = 200;
10✔
92
  private _hideDelay = 300;
10✔
93

10✔
94
  @query('#arrow')
10✔
95
  private _arrowElement!: HTMLElement;
10✔
96

10✔
97
  private get _arrowOffset() {
10✔
98
    if (this.placement.includes('-')) {
99!
99
      // Horizontal start | end placement
×
100

×
101
      if (TooltipRegexes.horizontalStart.test(this.placement)) {
×
102
        return -8;
×
103
      }
×
104

×
105
      if (TooltipRegexes.horizontalEnd.test(this.placement)) {
×
106
        return 8;
×
107
      }
×
108

×
109
      // Vertical start | end placement
×
110

×
111
      if (TooltipRegexes.start.test(this.placement)) {
×
112
        return isLTR(this) ? -8 : 8;
×
113
      }
×
114

×
115
      if (TooltipRegexes.end.test(this.placement)) {
×
116
        return isLTR(this) ? 8 : -8;
×
117
      }
×
118
    }
×
119

99✔
120
    return 0;
99✔
121
  }
99✔
122

10✔
123
  /**
10✔
124
   * Whether the tooltip is showing.
10✔
125
   *
10✔
126
   * @attr open
10✔
127
   * @default false
10✔
128
   */
10✔
129
  @property({ type: Boolean, reflect: true })
10✔
130
  public set open(value: boolean) {
10✔
131
    this._controller.open = value;
83✔
132
  }
83✔
133

10✔
134
  public get open(): boolean {
10✔
135
    return this._controller.open;
648✔
136
  }
648✔
137

10✔
138
  /**
10✔
139
   * Whether to disable the rendering of the arrow indicator for the tooltip.
10✔
140
   *
10✔
141
   * @deprecated since 6.1.0. Use `with-arrow` to control the behavior of the tooltip arrow.
10✔
142
   * @attr disable-arrow
10✔
143
   * @default false
10✔
144
   */
10✔
145
  @property({ type: Boolean, attribute: 'disable-arrow' })
10✔
146
  public set disableArrow(value: boolean) {
10✔
NEW
147
    this.withArrow = !value;
×
NEW
148
  }
×
149

10✔
150
  /**
10✔
151
   * @deprecated since 6.1.0. Use `with-arrow` to control the behavior of the tooltip arrow.
10✔
152
   */
10✔
153
  public get disableArrow(): boolean {
10✔
154
    return !this.withArrow;
43✔
155
  }
43✔
156

10✔
157
  /**
10✔
158
   * Whether to render an arrow indicator for the tooltip.
10✔
159
   *
10✔
160
   * @attr with-arrow
10✔
161
   * @default false
10✔
162
   */
10✔
163
  @property({ type: Boolean, reflect: true, attribute: 'with-arrow' })
10✔
164
  public withArrow = false;
10✔
165

10✔
166
  /**
10✔
167
   * The offset of the tooltip from the anchor in pixels.
10✔
168
   *
10✔
169
   * @attr offset
10✔
170
   * @default 6
10✔
171
   */
10✔
172
  @property({ type: Number })
10✔
173
  public offset = 6;
10✔
174

10✔
175
  /**
10✔
176
   * Where to place the floating element relative to the parent anchor element.
10✔
177
   *
10✔
178
   * @attr placement
10✔
179
   * @default bottom
10✔
180
   */
10✔
181
  @property()
10✔
182
  public placement: PopoverPlacement = 'bottom';
10✔
183

10✔
184
  /**
10✔
185
   * An element instance or an IDREF to use as the anchor for the tooltip.
10✔
186
   *
10✔
187
   * @remarks
10✔
188
   * Trying to bind to an IDREF that does not exist in the current DOM root at will not work.
10✔
189
   * In such scenarios, it is better to get a DOM reference and pass it to the tooltip instance.
10✔
190
   *
10✔
191
   * @attr anchor
10✔
192
   */
10✔
193
  @property()
10✔
194
  public anchor?: Element | string;
10✔
195

10✔
196
  /**
10✔
197
   * Which event triggers will show the tooltip.
10✔
198
   * Expects a comma separate string of different event triggers.
10✔
199
   *
10✔
200
   * @attr show-triggers
10✔
201
   * @default pointerenter
10✔
202
   */
10✔
203
  @property({ attribute: 'show-triggers' })
10✔
204
  public set showTriggers(value: string) {
10✔
205
    this._controller.showTriggers = value;
2✔
206
  }
2✔
207

10✔
208
  public get showTriggers(): string {
10✔
209
    return this._controller.showTriggers;
48✔
210
  }
48✔
211

10✔
212
  /**
10✔
213
   * Which event triggers will hide the tooltip.
10✔
214
   * Expects a comma separate string of different event triggers.
10✔
215
   *
10✔
216
   * @attr hide-triggers
10✔
217
   * @default pointerleave, click
10✔
218
   */
10✔
219
  @property({ attribute: 'hide-triggers' })
10✔
220
  public set hideTriggers(value: string) {
10✔
221
    this._controller.hideTriggers = value;
2✔
222
  }
2✔
223

10✔
224
  public get hideTriggers(): string {
10✔
225
    return this._controller.hideTriggers;
48✔
226
  }
48✔
227

10✔
228
  /**
10✔
229
   * Specifies the number of milliseconds that should pass before showing the tooltip.
10✔
230
   *
10✔
231
   * @attr show-delay
10✔
232
   * @default 200
10✔
233
   */
10✔
234
  @property({ attribute: 'show-delay', type: Number })
10✔
235
  public set showDelay(value: number) {
10✔
236
    this._showDelay = Math.max(0, asNumber(value));
1✔
237
  }
1✔
238

10✔
239
  public get showDelay(): number {
10✔
240
    return this._showDelay;
61✔
241
  }
61✔
242

10✔
243
  /**
10✔
244
   * Specifies the number of milliseconds that should pass before hiding the tooltip.
10✔
245
   *
10✔
246
   * @attr hide-delay
10✔
247
   * @default 300
10✔
248
   */
10✔
249
  @property({ attribute: 'hide-delay', type: Number })
10✔
250
  public set hideDelay(value: number) {
10✔
251
    this._hideDelay = Math.max(0, asNumber(value));
1✔
252
  }
1✔
253

10✔
254
  public get hideDelay(): number {
10✔
255
    return this._hideDelay;
56✔
256
  }
56✔
257

10✔
258
  /**
10✔
259
   * Specifies a plain text as tooltip content.
10✔
260
   *
10✔
261
   * @attr message
10✔
262
   */
10✔
263
  @property()
10✔
264
  public message = '';
10✔
265

10✔
266
  /**
10✔
267
   * Specifies if the tooltip remains visible until the user closes it via the close button or Esc key.
10✔
268
   *
10✔
269
   * @attr sticky
10✔
270
   * @default false
10✔
271
   */
10✔
272
  @property({ type: Boolean, reflect: true })
10✔
273
  public sticky = false;
10✔
274

10✔
275
  constructor() {
10✔
276
    super();
60✔
277

60✔
278
    this._internals = this.attachInternals();
60✔
279
    this._internals.role = 'tooltip';
60✔
280
    this._internals.ariaAtomic = 'true';
60✔
281
    this._internals.ariaLive = 'polite';
60✔
282
  }
60✔
283

10✔
284
  protected override firstUpdated(): void {
10✔
285
    if (this.open) {
42✔
286
      this.updateComplete.then(() => {
2✔
287
        this._player.playExclusive(this._showAnimation);
2✔
288
        this.requestUpdate();
2✔
289
      });
2✔
290
    }
2✔
291
  }
42✔
292

10✔
293
  protected override willUpdate(changedProperties: PropertyValues<this>): void {
10✔
294
    if (changedProperties.has('anchor')) {
99✔
295
      this._controller.resolveAnchor(this.anchor);
31✔
296
    }
31✔
297

99✔
298
    if (changedProperties.has('sticky')) {
99✔
299
      this._internals.role = this.sticky ? 'status' : 'tooltip';
48✔
300
    }
48✔
301
  }
99✔
302

10✔
303
  private _emitEvent(name: keyof IgcTooltipComponentEventMap): boolean {
10✔
304
    return this.emitEvent(name, {
59✔
305
      cancelable: name === 'igcOpening' || name === 'igcClosing',
59✔
306
    });
59✔
307
  }
59✔
308

10✔
309
  private async _applyTooltipState({
10✔
310
    show,
48✔
311
    withDelay = false,
48✔
312
    withEvents = false,
48✔
313
  }: TooltipStateOptions): Promise<boolean> {
48✔
314
    if (show === this.open) {
48✔
315
      return false;
3✔
316
    }
3✔
317

45✔
318
    if (withEvents && !this._emitEvent(show ? 'igcOpening' : 'igcClosing')) {
48✔
319
      return false;
2✔
320
    }
2✔
321

43✔
322
    const commitStateChange = async () => {
43✔
323
      if (show) {
43✔
324
        this.open = true;
24✔
325
      }
24✔
326

43✔
327
      // Make the tooltip ignore most interactions while the animation
43✔
328
      // is running. In the rare case when the popover overlaps its anchor
43✔
329
      // this will prevent looping between the anchor and tooltip handlers.
43✔
330
      this.inert = true;
43✔
331

43✔
332
      const animationComplete = await this._player.playExclusive(
43✔
333
        show ? this._showAnimation : this._hideAnimation
43✔
334
      );
43✔
335

43✔
336
      this.inert = false;
43✔
337
      this.open = show;
43✔
338

43✔
339
      if (animationComplete && withEvents) {
43✔
340
        this._emitEvent(show ? 'igcOpened' : 'igcClosed');
25✔
341
      }
25✔
342

43✔
343
      return animationComplete;
43✔
344
    };
43✔
345

43✔
346
    if (withDelay) {
48✔
347
      clearTimeout(this._timeoutId);
27✔
348

27✔
349
      return new Promise(() => {
27✔
350
        this._timeoutId = setTimeout(
27✔
351
          async () => await commitStateChange(),
27✔
352
          show ? this.showDelay : this.hideDelay
27✔
353
        );
27✔
354
      });
27✔
355
    }
27✔
356

16✔
357
    return commitStateChange();
16✔
358
  }
48✔
359

10✔
360
  /**
10✔
361
   *  Shows the tooltip if not already showing.
10✔
362
   *  If a target is provided, sets it as a transient anchor.
10✔
363
   */
10✔
364
  public async show(target?: Element | string): Promise<boolean> {
10✔
365
    if (target) {
9✔
366
      this._stopTimeoutAndAnimation();
4✔
367
      this._controller.setAnchor(target, true);
4✔
368
    }
4✔
369

9✔
370
    return await this._applyTooltipState({ show: true });
9✔
371
  }
9✔
372

10✔
373
  /** Hides the tooltip if not already hidden. */
10✔
374
  public async hide(): Promise<boolean> {
10✔
375
    return await this._applyTooltipState({ show: false });
9✔
376
  }
9✔
377

10✔
378
  /** Toggles the tooltip between shown/hidden state */
10✔
379
  public async toggle(): Promise<boolean> {
10✔
380
    return await (this.open ? this.hide() : this.show());
2✔
381
  }
2✔
382

10✔
383
  protected _showWithEvent(): Promise<boolean> {
10✔
384
    return this._applyTooltipState({
18✔
385
      show: true,
18✔
386
      withDelay: true,
18✔
387
      withEvents: true,
18✔
388
    });
18✔
389
  }
18✔
390

10✔
391
  protected _hideWithEvent(): Promise<boolean> {
10✔
392
    return this._applyTooltipState({
12✔
393
      show: false,
12✔
394
      withDelay: true,
12✔
395
      withEvents: true,
12✔
396
    });
12✔
397
  }
12✔
398

10✔
399
  private _showOnInteraction(): void {
10✔
400
    this._stopTimeoutAndAnimation();
18✔
401
    this._showWithEvent();
18✔
402
  }
18✔
403

10✔
404
  private _stopTimeoutAndAnimation(): void {
10✔
405
    clearTimeout(this._timeoutId);
34✔
406
    this._player.stopAll();
34✔
407
  }
34✔
408

10✔
409
  private _setAutoHide(): void {
10✔
410
    this._stopTimeoutAndAnimation();
12✔
411

12✔
412
    this._timeoutId = setTimeout(
12✔
413
      this._hideWithEvent.bind(this),
12✔
414
      this._autoHideDelay
12✔
415
    );
12✔
416
  }
12✔
417

10✔
418
  private _hideOnInteraction(): void {
10✔
419
    if (!this.sticky) {
12✔
420
      this._setAutoHide();
11✔
421
    }
11✔
422
  }
12✔
423

10✔
424
  private async _hideOnEscape(): Promise<void> {
10✔
425
    await this.hide();
5✔
426
    this._emitEvent('igcClosed');
5✔
427
  }
5✔
428

10✔
429
  protected override render() {
10✔
430
    return html`
99✔
431
      <igc-popover
99✔
432
        .inert=${!this.open}
99✔
433
        .placement=${this.placement}
99✔
434
        .offset=${this.offset}
99✔
435
        .anchor=${this._controller.anchor ?? undefined}
99✔
436
        .arrow=${this.withArrow ? this._arrowElement : null}
99✔
437
        .arrowOffset=${this._arrowOffset}
99✔
438
        .shiftPadding=${8}
99✔
439
        ?open=${this.open}
99✔
440
        flip
99✔
441
        shift
99✔
442
      >
99✔
443
        <div ${ref(this._containerRef)} part="base">
99✔
444
          <slot>${this.message}</slot>
99✔
445
          ${this.sticky
99✔
446
            ? html`
9✔
447
                <slot name="close-button" @click=${this._setAutoHide}>
9✔
448
                  <igc-icon
90✔
449
                    name="input_clear"
90✔
450
                    collection="default"
90✔
451
                    aria-hidden="true"
90✔
452
                  ></igc-icon>
90✔
453
                </slot>
90✔
454
              `
90✔
455
            : nothing}
99✔
456
          ${this.withArrow ? html`<div id="arrow"></div>` : nothing}
99✔
457
        </div>
99✔
458
      </igc-popover>
99✔
459
    `;
99✔
460
  }
99✔
461
}
10✔
462

10✔
463
declare global {
10✔
464
  interface HTMLElementTagNameMap {
10✔
465
    'igc-tooltip': IgcTooltipComponent;
10✔
466
  }
10✔
467
}
10✔
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