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

IgniteUI / igniteui-webcomponents / 18975232228

31 Oct 2025 02:14PM UTC coverage: 98.19% (+0.06%) from 98.129%
18975232228

Pull #1935

github

web-flow
Merge 3041f990d into 6ddeda7cc
Pull Request #1935: fix(textarea): update sass themes

5319 of 5598 branches covered (95.02%)

Branch coverage included in aggregate %.

35311 of 35781 relevant lines covered (98.69%)

1648.4 hits per line

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

95.8
/src/components/tooltip/tooltip.ts
1
import { html, LitElement, nothing, type PropertyValues } 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 { addThemingController } from '../../theming/theming-controller.js';
11✔
9
import { addInternalsController } from '../common/controllers/internals.js';
11✔
10
import { addSlotController, setSlots } from '../common/controllers/slot.js';
11✔
11
import { registerComponent } from '../common/definitions/register.js';
11✔
12
import type { Constructor } from '../common/mixins/constructor.js';
11✔
13
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
11✔
14
import { partMap } from '../common/part-map.js';
11✔
15
import { asNumber, isLTR } from '../common/util.js';
11✔
16
import IgcIconComponent from '../icon/icon.js';
11✔
17
import IgcPopoverComponent, {
11✔
18
  type PopoverPlacement,
11✔
19
} from '../popover/popover.js';
11✔
20
import { addTooltipController, TooltipRegexes } from './controller.js';
11✔
21
import { styles as shared } from './themes/shared/tooltip.common.css.js';
11✔
22
import { all } from './themes/themes.js';
11✔
23
import { styles } from './themes/tooltip.base.css.js';
11✔
24

11✔
25
export interface IgcTooltipComponentEventMap {
11✔
26
  igcOpening: CustomEvent<void>;
11✔
27
  igcOpened: CustomEvent<void>;
11✔
28
  igcClosing: CustomEvent<void>;
11✔
29
  igcClosed: CustomEvent<void>;
11✔
30
}
11✔
31

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

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

11✔
62
  /* blazorSuppress */
11✔
63
  public static register(): void {
11✔
64
    registerComponent(
2✔
65
      IgcTooltipComponent,
2✔
66
      IgcPopoverComponent,
2✔
67
      IgcIconComponent
2✔
68
    );
2✔
69
  }
2✔
70

11✔
71
  private readonly _internals = addInternalsController(this, {
11✔
72
    initialARIA: {
11✔
73
      role: 'tooltip',
11✔
74
      ariaAtomic: 'true',
11✔
75
      ariaLive: 'polite',
11✔
76
    },
11✔
77
  });
11✔
78

11✔
79
  private readonly _controller = addTooltipController(this, {
11✔
80
    onShow: this._showOnInteraction,
11✔
81
    onHide: this._hideOnInteraction,
11✔
82
    onEscape: this._hideOnEscape,
11✔
83
    onClick: this._stopTimeoutAndAnimation,
11✔
84
  });
11✔
85

11✔
86
  private readonly _containerRef = createRef<HTMLElement>();
11✔
87
  private readonly _player = addAnimationController(this, this._containerRef);
11✔
88
  private readonly _slots = addSlotController(this, { slots: setSlots() });
11✔
89

11✔
90
  private readonly _showAnimation = scaleInCenter({
11✔
91
    duration: 150,
11✔
92
    easing: EaseOut.Quad,
11✔
93
  });
11✔
94

11✔
95
  private readonly _hideAnimation = fadeOut({
11✔
96
    duration: 75,
11✔
97
    easing: EaseOut.Sine,
11✔
98
  });
11✔
99

11✔
100
  private _timeoutId?: number;
11✔
101
  private _autoHideDelay = 180;
11✔
102
  private _showDelay = 200;
11✔
103
  private _hideDelay = 300;
11✔
104

11✔
105
  @query('#arrow')
11✔
106
  private _arrowElement!: HTMLElement;
11✔
107

11✔
108
  private get _arrowOffset() {
11✔
109
    if (this.placement.includes('-')) {
150!
110
      // Horizontal start | end placement
×
111

×
112
      if (TooltipRegexes.horizontalStart.test(this.placement)) {
×
113
        return -8;
×
114
      }
×
115

×
116
      if (TooltipRegexes.horizontalEnd.test(this.placement)) {
×
117
        return 8;
×
118
      }
×
119

×
120
      // Vertical start | end placement
×
121

×
122
      if (TooltipRegexes.start.test(this.placement)) {
×
123
        return isLTR(this) ? -8 : 8;
×
124
      }
×
125

×
126
      if (TooltipRegexes.end.test(this.placement)) {
×
127
        return isLTR(this) ? 8 : -8;
×
128
      }
×
129
    }
×
130

150✔
131
    return 0;
150✔
132
  }
150✔
133

11✔
134
  /**
11✔
135
   * Whether the tooltip is showing.
11✔
136
   *
11✔
137
   * @attr open
11✔
138
   * @default false
11✔
139
   */
11✔
140
  @property({ type: Boolean, reflect: true })
11✔
141
  public set open(value: boolean) {
11✔
142
    this._controller.open = value;
83✔
143
  }
83✔
144

11✔
145
  public get open(): boolean {
11✔
146
    return this._controller.open;
772✔
147
  }
772✔
148

11✔
149
  /**
11✔
150
   * Whether to disable the rendering of the arrow indicator for the tooltip.
11✔
151
   *
11✔
152
   * @deprecated since 6.1.0. Use `with-arrow` to control the behavior of the tooltip arrow.
11✔
153
   * @attr disable-arrow
11✔
154
   * @default false
11✔
155
   */
11✔
156
  @property({ type: Boolean, attribute: 'disable-arrow' })
11✔
157
  public set disableArrow(value: boolean) {
11✔
158
    this.withArrow = !value;
×
159
  }
×
160

11✔
161
  /**
11✔
162
   * @deprecated since 6.1.0. Use `with-arrow` to control the behavior of the tooltip arrow.
11✔
163
   */
11✔
164
  public get disableArrow(): boolean {
11✔
165
    return !this.withArrow;
48✔
166
  }
48✔
167

11✔
168
  /**
11✔
169
   * Whether to render an arrow indicator for the tooltip.
11✔
170
   *
11✔
171
   * @attr with-arrow
11✔
172
   * @default false
11✔
173
   */
11✔
174
  @property({ type: Boolean, reflect: true, attribute: 'with-arrow' })
11✔
175
  public withArrow = false;
11✔
176

11✔
177
  /**
11✔
178
   * The offset of the tooltip from the anchor in pixels.
11✔
179
   *
11✔
180
   * @attr offset
11✔
181
   * @default 6
11✔
182
   */
11✔
183
  @property({ type: Number })
11✔
184
  public offset = 6;
11✔
185

11✔
186
  /**
11✔
187
   * Where to place the floating element relative to the parent anchor element.
11✔
188
   *
11✔
189
   * @attr placement
11✔
190
   * @default bottom
11✔
191
   */
11✔
192
  @property()
11✔
193
  public placement: PopoverPlacement = 'bottom';
11✔
194

11✔
195
  /**
11✔
196
   * An element instance or an IDREF to use as the anchor for the tooltip.
11✔
197
   *
11✔
198
   * @remarks
11✔
199
   * Trying to bind to an IDREF that does not exist in the current DOM root at will not work.
11✔
200
   * In such scenarios, it is better to get a DOM reference and pass it to the tooltip instance.
11✔
201
   *
11✔
202
   * @attr anchor
11✔
203
   */
11✔
204
  @property()
11✔
205
  public anchor?: Element | string;
11✔
206

11✔
207
  /**
11✔
208
   * Which event triggers will show the tooltip.
11✔
209
   * Expects a comma separate string of different event triggers.
11✔
210
   *
11✔
211
   * @attr show-triggers
11✔
212
   * @default pointerenter
11✔
213
   */
11✔
214
  @property({ attribute: 'show-triggers' })
11✔
215
  public set showTriggers(value: string) {
11✔
216
    this._controller.showTriggers = value;
4✔
217
  }
4✔
218

11✔
219
  public get showTriggers(): string {
11✔
220
    return this._controller.showTriggers;
57✔
221
  }
57✔
222

11✔
223
  /**
11✔
224
   * Which event triggers will hide the tooltip.
11✔
225
   * Expects a comma separate string of different event triggers.
11✔
226
   *
11✔
227
   * @attr hide-triggers
11✔
228
   * @default pointerleave, click
11✔
229
   */
11✔
230
  @property({ attribute: 'hide-triggers' })
11✔
231
  public set hideTriggers(value: string) {
11✔
232
    this._controller.hideTriggers = value;
3✔
233
  }
3✔
234

11✔
235
  public get hideTriggers(): string {
11✔
236
    return this._controller.hideTriggers;
55✔
237
  }
55✔
238

11✔
239
  /**
11✔
240
   * Specifies the number of milliseconds that should pass before showing the tooltip.
11✔
241
   *
11✔
242
   * @attr show-delay
11✔
243
   * @default 200
11✔
244
   */
11✔
245
  @property({ attribute: 'show-delay', type: Number })
11✔
246
  public set showDelay(value: number) {
11✔
247
    this._showDelay = Math.max(0, asNumber(value));
1✔
248
  }
1✔
249

11✔
250
  public get showDelay(): number {
11✔
251
    return this._showDelay;
67✔
252
  }
67✔
253

11✔
254
  /**
11✔
255
   * Specifies the number of milliseconds that should pass before hiding the tooltip.
11✔
256
   *
11✔
257
   * @attr hide-delay
11✔
258
   * @default 300
11✔
259
   */
11✔
260
  @property({ attribute: 'hide-delay', type: Number })
11✔
261
  public set hideDelay(value: number) {
11✔
262
    this._hideDelay = Math.max(0, asNumber(value));
1✔
263
  }
1✔
264

11✔
265
  public get hideDelay(): number {
11✔
266
    return this._hideDelay;
61✔
267
  }
61✔
268

11✔
269
  /**
11✔
270
   * Specifies a plain text as tooltip content.
11✔
271
   *
11✔
272
   * @attr message
11✔
273
   */
11✔
274
  @property()
11✔
275
  public message = '';
11✔
276

11✔
277
  /**
11✔
278
   * Specifies if the tooltip remains visible until the user closes it via the close button or Esc key.
11✔
279
   *
11✔
280
   * @attr sticky
11✔
281
   * @default false
11✔
282
   */
11✔
283
  @property({ type: Boolean, reflect: true })
11✔
284
  public sticky = false;
11✔
285

11✔
286
  constructor() {
11✔
287
    super();
65✔
288
    addThemingController(this, all);
65✔
289
  }
65✔
290

11✔
291
  protected override firstUpdated(): void {
11✔
292
    if (this.open) {
47✔
293
      this.updateComplete.then(() => {
2✔
294
        this._player.playExclusive(this._showAnimation);
2✔
295
        this.requestUpdate();
2✔
296
      });
2✔
297
    }
2✔
298
  }
47✔
299

11✔
300
  protected override willUpdate(changedProperties: PropertyValues<this>): void {
11✔
301
    if (changedProperties.has('anchor')) {
150✔
302
      this._controller.resolveAnchor(this.anchor);
34✔
303
    }
34✔
304

150✔
305
    if (changedProperties.has('sticky')) {
150✔
306
      this._internals.setARIA({ role: this.sticky ? 'status' : 'tooltip' });
53✔
307
    }
53✔
308
  }
150✔
309

11✔
310
  private _emitEvent(name: keyof IgcTooltipComponentEventMap): boolean {
11✔
311
    return this.emitEvent(name, {
62✔
312
      cancelable: name === 'igcOpening' || name === 'igcClosing',
62✔
313
    });
62✔
314
  }
62✔
315

11✔
316
  private async _applyTooltipState({
11✔
317
    show,
49✔
318
    withDelay = false,
49✔
319
    withEvents = false,
49✔
320
  }: TooltipStateOptions): Promise<boolean> {
49✔
321
    if (show === this.open) {
49✔
322
      return false;
3✔
323
    }
3✔
324

46✔
325
    if (withEvents && !this._emitEvent(show ? 'igcOpening' : 'igcClosing')) {
49✔
326
      return false;
2✔
327
    }
2✔
328

44✔
329
    const commitStateChange = async () => {
44✔
330
      if (show) {
43✔
331
        this.open = true;
24✔
332
      }
24✔
333

43✔
334
      // Make the tooltip ignore most interactions while the animation
43✔
335
      // is running. In the rare case when the popover overlaps its anchor
43✔
336
      // this will prevent looping between the anchor and tooltip handlers.
43✔
337
      this.inert = true;
43✔
338

43✔
339
      const animationComplete = await this._player.playExclusive(
43✔
340
        show ? this._showAnimation : this._hideAnimation
43✔
341
      );
43✔
342

43✔
343
      this.inert = false;
43✔
344
      this.open = show;
43✔
345

43✔
346
      if (animationComplete && withEvents) {
43✔
347
        this._emitEvent(show ? 'igcOpened' : 'igcClosed');
27✔
348
      }
27✔
349

43✔
350
      return animationComplete;
43✔
351
    };
43✔
352

44✔
353
    if (withDelay) {
49✔
354
      clearTimeout(this._timeoutId);
28✔
355

28✔
356
      return new Promise(() => {
28✔
357
        this._timeoutId = setTimeout(
28✔
358
          async () => await commitStateChange(),
28✔
359
          show ? this.showDelay : this.hideDelay
28✔
360
        );
28✔
361
      });
28✔
362
    }
28✔
363

16✔
364
    return commitStateChange();
16✔
365
  }
49✔
366

11✔
367
  /**
11✔
368
   *  Shows the tooltip if not already showing.
11✔
369
   *  If a target is provided, sets it as a transient anchor.
11✔
370
   */
11✔
371
  public async show(target?: Element | string): Promise<boolean> {
11✔
372
    if (target) {
9✔
373
      this._stopTimeoutAndAnimation();
4✔
374
      this._controller.setAnchor(target, true);
4✔
375
    }
4✔
376

9✔
377
    return await this._applyTooltipState({ show: true });
9✔
378
  }
9✔
379

11✔
380
  /** Hides the tooltip if not already hidden. */
11✔
381
  public async hide(): Promise<boolean> {
11✔
382
    return await this._applyTooltipState({ show: false });
9✔
383
  }
9✔
384

11✔
385
  /** Toggles the tooltip between shown/hidden state */
11✔
386
  public async toggle(): Promise<boolean> {
11✔
387
    return await (this.open ? this.hide() : this.show());
2✔
388
  }
2✔
389

11✔
390
  protected _showWithEvent(): Promise<boolean> {
11✔
391
    return this._applyTooltipState({
19✔
392
      show: true,
19✔
393
      withDelay: true,
19✔
394
      withEvents: true,
19✔
395
    });
19✔
396
  }
19✔
397

11✔
398
  protected _hideWithEvent(): Promise<boolean> {
11✔
399
    return this._applyTooltipState({
12✔
400
      show: false,
12✔
401
      withDelay: true,
12✔
402
      withEvents: true,
12✔
403
    });
12✔
404
  }
12✔
405

11✔
406
  private _showOnInteraction(): void {
11✔
407
    this._stopTimeoutAndAnimation();
19✔
408
    this._showWithEvent();
19✔
409
  }
19✔
410

11✔
411
  private _stopTimeoutAndAnimation(): void {
11✔
412
    clearTimeout(this._timeoutId);
37✔
413
    this._player.stopAll();
37✔
414
  }
37✔
415

11✔
416
  private _setAutoHide(): void {
11✔
417
    this._stopTimeoutAndAnimation();
13✔
418

13✔
419
    this._timeoutId = setTimeout(
13✔
420
      this._hideWithEvent.bind(this),
13✔
421
      this._autoHideDelay
13✔
422
    );
13✔
423
  }
13✔
424

11✔
425
  private _hideOnInteraction(): void {
11✔
426
    if (!this.sticky) {
13✔
427
      this._setAutoHide();
12✔
428
    }
12✔
429
  }
13✔
430

11✔
431
  private async _hideOnEscape(): Promise<void> {
11✔
432
    await this.hide();
5✔
433
    this._emitEvent('igcClosed');
5✔
434
  }
5✔
435

11✔
436
  protected override render() {
11✔
437
    const parts = {
150✔
438
      base: true,
150✔
439
      'simple-text': !this._slots.hasAssignedNodes('[default]', true),
150✔
440
    };
150✔
441

150✔
442
    return html`
150✔
443
      <igc-popover
150✔
444
        .inert=${!this.open}
150✔
445
        .placement=${this.placement}
150✔
446
        .offset=${this.offset}
150✔
447
        .anchor=${this._controller.anchor ?? undefined}
150✔
448
        .arrow=${this.withArrow ? this._arrowElement : null}
150✔
449
        .arrowOffset=${this._arrowOffset}
150✔
450
        .shiftPadding=${8}
150✔
451
        ?open=${this.open}
150✔
452
        flip
150✔
453
        shift
150✔
454
      >
150✔
455
        <div ${ref(this._containerRef)} part=${partMap(parts)}>
150✔
456
          <slot>${this.message}</slot>
150✔
457
          ${this.sticky
150✔
458
            ? html`
9✔
459
                <slot name="close-button" @click=${this._setAutoHide}>
9✔
460
                  <igc-icon
141✔
461
                    name="input_clear"
141✔
462
                    collection="default"
141✔
463
                    aria-hidden="true"
141✔
464
                  ></igc-icon>
141✔
465
                </slot>
141✔
466
              `
141✔
467
            : nothing}
150✔
468
          ${this.withArrow ? html`<div id="arrow"></div>` : nothing}
150✔
469
        </div>
150✔
470
      </igc-popover>
150✔
471
    `;
150✔
472
  }
150✔
473
}
11✔
474

11✔
475
declare global {
11✔
476
  interface HTMLElementTagNameMap {
11✔
477
    'igc-tooltip': IgcTooltipComponent;
11✔
478
  }
11✔
479
}
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

© 2025 Coveralls, Inc