11import { LitElement , html , nothing } from 'lit' ;
22import { property , query } from 'lit/decorators.js' ;
33import { type Ref , createRef , ref } from 'lit/directives/ref.js' ;
4+ import { EaseOut } from '../../animations/easings.js' ;
45import { addAnimationController } from '../../animations/player.js' ;
5- import { fadeIn , fadeOut } from '../../animations/presets/fade/index.js' ;
6+ import { fadeOut } from '../../animations/presets/fade/index.js' ;
7+ import { scaleInCenter } from '../../animations/presets/scale/index.js' ;
68import { watch } from '../common/decorators/watch.js' ;
79import { registerComponent } from '../common/definitions/register.js' ;
10+ import type { Constructor } from '../common/mixins/constructor.js' ;
11+ import { EventEmitterMixin } from '../common/mixins/event-emitter.js' ;
812import { getElementByIdFromRoot , isString } from '../common/util.js' ;
913import IgcPopoverComponent , { type IgcPlacement } from '../popover/popover.js' ;
1014import { styles } from './themes/tooltip.base.css.js' ;
1115import {
1216 addTooltipController ,
1317 hideOnTrigger ,
18+ showOnTrigger ,
1419} from './tooltip-event-controller.js' ;
1520import service from './tooltip-service.js' ;
1621
17- // TODO: Expose events
22+ export interface IgcTooltipComponentEventMap {
23+ igcOpening : CustomEvent < Element | null > ;
24+ igcOpened : CustomEvent < Element | null > ;
25+ igcClosing : CustomEvent < Element | null > ;
26+ igcClosed : CustomEvent < Element | null > ;
27+ }
1828
1929function parseTriggers ( string : string ) : string [ ] {
2030 return ( string ?? '' ) . split ( ',' ) . map ( ( part ) => part . trim ( ) ) ;
@@ -24,11 +34,19 @@ function parseTriggers(string: string): string[] {
2434 * @element igc-tooltip
2535 *
2636 * @slot - default slot
37+ *
38+ * @fires igcOpening - Emitted before the tooltip begins to open. Can be canceled to prevent opening.
39+ * @fires igcOpened - Emitted after the tooltip has successfully opened and is visible.
40+ * @fires igcClosing - Emitted before the tooltip begins to close. Can be canceled to prevent closing.
41+ * @fires igcClosed - Emitted after the tooltip has been fully removed from view.
2742 */
28- export default class IgcTooltipComponent extends LitElement {
43+ export default class IgcTooltipComponent extends EventEmitterMixin <
44+ IgcTooltipComponentEventMap ,
45+ Constructor < LitElement >
46+ > ( LitElement ) {
2947 public static readonly tagName = 'igc-tooltip' ;
3048
31- public static override styles = styles ;
49+ public static styles = styles ;
3250
3351 /* blazorSuppress */
3452 public static register ( ) {
@@ -40,10 +58,14 @@ export default class IgcTooltipComponent extends LitElement {
4058 private _containerRef : Ref < HTMLElement > = createRef ( ) ;
4159 private _animationPlayer = addAnimationController ( this , this . _containerRef ) ;
4260
43- private _autoHideTimeout ?: number ;
61+ private _timeoutId ?: number ;
62+ private _toBeShown = false ;
63+ private _toBeHidden = false ;
4464 private _open = false ;
4565 private _showTriggers = [ 'pointerenter' ] ;
4666 private _hideTriggers = [ 'pointerleave' ] ;
67+ private _showDelay = 500 ;
68+ private _hideDelay = 500 ;
4769
4870 @query ( '#arrow' )
4971 protected _arrowElement ! : HTMLElement ;
@@ -133,19 +155,57 @@ export default class IgcTooltipComponent extends LitElement {
133155 this . _hideTriggers = parseTriggers ( value ) ;
134156 this . _controller . set ( this . _target , {
135157 show : this . _showTriggers ,
136- hide : this . _showTriggers ,
158+ hide : this . _hideTriggers ,
137159 } ) ;
138160 }
139161
140162 public get hideTriggers ( ) : string {
141163 return this . _hideTriggers . join ( ) ;
142164 }
143165
166+ /**
167+ * Specifies the number of milliseconds that should pass before showing the tooltip.
168+ *
169+ * @attr show-delay
170+ */
171+ @property ( { attribute : 'show-delay' , type : Number } )
172+ public set showDelay ( value : number ) {
173+ this . _showDelay = Math . max ( 0 , value ) ;
174+ }
175+
176+ public get showDelay ( ) : number {
177+ return this . _showDelay ;
178+ }
179+
180+ /**
181+ * Specifies the number of milliseconds that should pass before hiding the tooltip.
182+ *
183+ * @attr hide-delay
184+ */
185+ @property ( { attribute : 'hide-delay' , type : Number } )
186+ public set hideDelay ( value : number ) {
187+ this . _hideDelay = Math . max ( 0 , value ) ;
188+ }
189+
190+ public get hideDelay ( ) : number {
191+ return this . _hideDelay ;
192+ }
193+
194+ /**
195+ * Specifies a plain text as tooltip content.
196+ *
197+ * @attr
198+ */
199+ @property ( { type : String } )
200+ public message = '' ;
201+
144202 constructor ( ) {
145203 super ( ) ;
146204
147205 this . _internals = this . attachInternals ( ) ;
148206 this . _internals . role = 'tooltip' ;
207+ this . _internals . ariaAtomic = 'true' ;
208+ this . _internals . ariaLive = 'polite' ;
149209 }
150210
151211 protected override async firstUpdated ( ) {
@@ -189,40 +249,124 @@ export default class IgcTooltipComponent extends LitElement {
189249 }
190250
191251 private async _toggleAnimation ( dir : 'open' | 'close' ) {
192- const animation = dir === 'open' ? fadeIn : fadeOut ;
193- return this . _animationPlayer . playExclusive ( animation ( ) ) ;
252+ const animation =
253+ dir === 'open'
254+ ? scaleInCenter ( { duration : 150 , easing : EaseOut . Quad } )
255+ : fadeOut ( { duration : 75 , easing : EaseOut . Sine } ) ;
256+ return this . _animationPlayer . playExclusive ( animation ) ;
257+ }
258+
259+ /**
260+ * Immediately stops any ongoing animation and resets the tooltip state.
261+ *
262+ * This method is used in edge cases when a transition needs to be forcefully interrupted,
263+ * such as when a tooltip is in the middle of showing or hiding and the user suddenly
264+ * triggers the opposite action (e.g., hovers in and out rapidly).
265+ *
266+ * It:
267+ * - Reverts `open` based on whether it was mid-hide or mid-show.
268+ * - Clears internal transition flags (`_toBeShown`, `_toBeHidden`).
269+ * - Stops any active animations, causing `_toggleAnimation()` to return false.
270+ *
271+ */
272+ private async _forceAnimationStop ( ) {
273+ this . open = this . _toBeHidden ;
274+ this . _toBeShown = false ;
275+ this . _toBeHidden = false ;
276+ this . _animationPlayer . stopAll ( ) ;
277+ }
278+
279+ private _setDelay ( ms : number ) : Promise < void > {
280+ clearTimeout ( this . _timeoutId ) ;
281+ return new Promise ( ( resolve ) => {
282+ this . _timeoutId = setTimeout ( resolve , ms ) ;
283+ } ) ;
194284 }
195285
196286 /** Shows the tooltip if not already showing. */
197- public show = async ( ) => {
198- clearTimeout ( this . _autoHideTimeout ) ;
199- if ( this . open ) {
200- return false ;
201- }
287+ public show = async ( ) : Promise < boolean > => {
288+ if ( this . open ) return false ;
289+
290+ await this . _setDelay ( this . showDelay ) ;
202291
203292 this . open = true ;
204- return await this . _toggleAnimation ( 'open' ) ;
293+ this . _toBeShown = true ;
294+ const result = await this . _toggleAnimation ( 'open' ) ;
295+ this . _toBeShown = false ;
296+
297+ return result ;
205298 } ;
206299
207300 /** Hides the tooltip if not already hidden. */
208- public hide = async ( ) => {
209- if ( ! this . open ) {
210- return false ;
301+ public hide = async ( ) : Promise < boolean > => {
302+ if ( ! this . open ) return false ;
303+
304+ await this . _setDelay ( this . hideDelay ) ;
305+
306+ this . _toBeHidden = true ;
307+ const result = await this . _toggleAnimation ( 'close' ) ;
308+ this . open = ! result ;
309+ this . _toBeHidden = false ;
310+
311+ return result ;
312+ } ;
313+
314+ /** Toggles the tooltip between shown/hidden state after the appropriate delay. */
315+ public toggle = async ( ) : Promise < boolean > => {
316+ return this . open ? this . hide ( ) : this . show ( ) ;
317+ } ;
318+
319+ public showWithEvent = async ( ) => {
320+ if ( this . _toBeHidden ) {
321+ await this . _forceAnimationStop ( ) ;
322+ return ;
323+ }
324+ if (
325+ this . open ||
326+ ! this . emitEvent ( 'igcOpening' , { cancelable : true , detail : this . _target } )
327+ ) {
328+ return ;
329+ }
330+ if ( await this . show ( ) ) {
331+ this . emitEvent ( 'igcOpened' , { detail : this . _target } ) ;
332+ }
333+ } ;
334+
335+ public hideWithEvent = async ( ) => {
336+ if ( this . _toBeShown ) {
337+ await this . _forceAnimationStop ( ) ;
338+ return ;
339+ }
340+ if (
341+ ! this . open ||
342+ ! this . emitEvent ( 'igcClosing' , { cancelable : true , detail : this . _target } )
343+ ) {
344+ return ;
345+ }
346+ if ( await this . hide ( ) ) {
347+ this . emitEvent ( 'igcClosed' , { detail : this . _target } ) ;
211348 }
349+ } ;
212350
213- await this . _toggleAnimation ( 'close' ) ;
214- this . open = false ;
215- clearTimeout ( this . _autoHideTimeout ) ;
216- return true ;
351+ protected [ showOnTrigger ] = ( ) => {
352+ clearTimeout ( this . _timeoutId ) ;
353+ this . showWithEvent ( ) ;
217354 } ;
218355
219- protected [ hideOnTrigger ] = ( ) => {
220- this . _autoHideTimeout = setTimeout ( ( ) => this . hide ( ) , 180 ) ;
356+ protected [ hideOnTrigger ] = ( ev : Event ) => {
357+ const related = ( ev as PointerEvent ) . relatedTarget as Node | null ;
358+ // If the pointer moved into the tooltip element, don't hide
359+ if ( related && ( this . contains ( related ) || this . _target ?. contains ( related ) ) )
360+ return ;
361+
362+ clearTimeout ( this . _timeoutId ) ;
363+ this . _timeoutId = setTimeout ( ( ) => this . hideWithEvent ( ) , 180 ) ;
221364 } ;
222365
223366 protected override render ( ) {
224367 return html `
225368 < igc-popover
369+ aria-hidden =${ ! this . open }
226370 .placement =${ this . placement }
227371 .offset=${ this . offset }
228372 .anchor=${ this . _target }
@@ -233,7 +377,7 @@ export default class IgcTooltipComponent extends LitElement {
233377 shift
234378 >
235379 < div ${ ref ( this . _containerRef ) } part ="base ">
236- < slot > </ slot >
380+ ${ this . message ? html ` ${ this . message } ` : html ` < slot > </ slot > ` }
237381 ${ this . disableArrow ? nothing : html `< div id ="arrow "> </ div > ` }
238382 </ div >
239383 </ igc-popover >
0 commit comments