From 711a22921f37f4e4af08e17059a44cee9fc91cac Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Sat, 1 Aug 2026 15:04:47 +0300 Subject: [PATCH 1/5] fix(tooltip): prevent delayed tooltip after target moves --- .../tooltip/tooltip-target.directive.ts | 51 +++++++++++++++++++ .../tooltip/tooltip.directive.spec.ts | 50 +++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts index 7c3615d58f7..88f70322b6a 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts @@ -27,6 +27,13 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs { cancel: boolean; } +const HOVER_SHOW_TRIGGERS = new Set(['mouseenter', 'mouseover', 'pointerenter', 'pointerover']); + +interface TooltipPointerPosition { + clientX: number; + clientY: number; +} + /** * **Ignite UI for Angular Tooltip Target** - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tooltip) @@ -381,9 +388,16 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen private _showTriggers = new Set(['pointerenter']); private _hideTriggers = new Set(['pointerleave', 'click']); private _pendingShowTrigger: string | null = null; + private _pointerPosition: TooltipPointerPosition | null = null; private _abortController = new AbortController(); + private _onPointerMove = (event: PointerEvent): void => { + if (this._pointerPosition) { + this._pointerPosition = { clientX: event.clientX, clientY: event.clientY }; + } + }; + /** * @hidden */ @@ -506,6 +520,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen for (const each of this._hideTriggers) { this.nativeElement.addEventListener(each, this.onHide, options); } + this.nativeElement.addEventListener('pointermove', this._onPointerMove, options); } private removeEventListeners(): void { @@ -562,14 +577,49 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen this._evaluateStickyState(); this._pendingShowTrigger = triggerEvent?.type ?? null; + this._pointerPosition = withDelay && this.showDelay > 0 + ? this._getPointerPosition(triggerEvent) + : null; this.target.timeoutId = setTimeout(() => { // Call open() of IgxTooltipDirective + const pointerPosition = this._pointerPosition; + this.target.timeoutId = null; this._pendingShowTrigger = null; + this._pointerPosition = null; + + if (pointerPosition && !this._isPointerOverTarget(pointerPosition)) { + return; + } + this.target.open(this._mergedOverlaySettings); }, withDelay ? this.showDelay : 0); } + private _getPointerPosition(event?: Event): TooltipPointerPosition | null { + if (!event || !HOVER_SHOW_TRIGGERS.has(event.type)) { + return null; + } + + const pointerEvent = event as MouseEvent; + if (typeof pointerEvent.clientX !== 'number' || typeof pointerEvent.clientY !== 'number') { + return null; + } + if (!event.isTrusted && pointerEvent.clientX === 0 && pointerEvent.clientY === 0) { + return null; + } + + return { clientX: pointerEvent.clientX, clientY: pointerEvent.clientY }; + } + + private _isPointerOverTarget(position: TooltipPointerPosition): boolean { + const root = this.nativeElement.getRootNode() as DocumentOrShadowRoot; + const hitTestRoot = typeof root.elementFromPoint === 'function' + ? root + : this.nativeElement.ownerDocument; + const element = hitTestRoot.elementFromPoint(position.clientX, position.clientY); + return !!element && this.nativeElement.contains(element); + } private _showOnInteraction(triggerEvent?: Event): void { this._stopTimeoutAndAnimation(); @@ -620,6 +670,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen clearTimeout(this.target.timeoutId); this.target.timeoutId = null; this._pendingShowTrigger = null; + this._pointerPosition = null; } /** diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts index 9fa2f14d069..e25be11e6e6 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts @@ -167,6 +167,53 @@ describe('IgxTooltip', () => { verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); })); + it('should not show a delayed tooltip after its target moves away from the pointer', fakeAsync(() => { + tooltipTarget.showDelay = 500; + const target = button.nativeElement; + target.style.position = 'fixed'; + target.style.top = '20px'; + target.style.left = '20px'; + target.style.width = '100px'; + target.style.height = '40px'; + target.style.zIndex = '9999'; + const bounds = target.getBoundingClientRect(); + const pointer = { + clientX: bounds.left + bounds.width / 2, + clientY: bounds.top + bounds.height / 2 + }; + + expect(target.contains(document.elementFromPoint(pointer.clientX, pointer.clientY))).toBeTrue(); + hoverElement(button, pointer); + target.style.transform = 'translateX(200px)'; + expect(target.contains(document.elementFromPoint(pointer.clientX, pointer.clientY))).toBeFalse(); + + tick(500); + verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); + })); + + it('should not hit-test hover interactions without a show delay', fakeAsync(() => { + tooltipTarget.showDelay = 0; + const elementFromPointSpy = spyOn(document, 'elementFromPoint'); + + hoverElement(button, { clientX: 20, clientY: 30 }); + tick(); + + expect(elementFromPointSpy).not.toHaveBeenCalled(); + verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); + })); + + it('should validate a delayed tooltip against the latest pointer position', fakeAsync(() => { + tooltipTarget.showDelay = 500; + const elementFromPointSpy = spyOn(document, 'elementFromPoint').and.returnValue(button.nativeElement); + + hoverElement(button, { clientX: 20, clientY: 30 }); + button.nativeElement.dispatchEvent(new PointerEvent('pointermove', { clientX: 40, clientY: 50 })); + tick(500); + + expect(elementFromPointSpy).toHaveBeenCalledWith(40, 50); + verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); + })); + it('IgxTooltip mouse interaction respects hideDelay', fakeAsync(() => { tooltipTarget.hideDelay = 700; fix.detectChanges(); @@ -1224,7 +1271,8 @@ interface ElementRefLike { nativeElement: HTMLElement } -const hoverElement = (element: ElementRefLike) => element.nativeElement.dispatchEvent(new MouseEvent('pointerenter')); +const hoverElement = (element: ElementRefLike, eventInit: MouseEventInit = {}) => + element.nativeElement.dispatchEvent(new MouseEvent('pointerenter', eventInit)); const unhoverElement = (element: ElementRefLike) => element.nativeElement.dispatchEvent(new MouseEvent('pointerleave')); From 81bebea752c26e21839df9de9a734ba8e4601e41 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 11 Aug 2026 14:16:46 +0300 Subject: [PATCH 2/5] refactor(tooltip): simplify delayed hover validation --- .../tooltip/tooltip-target.directive.ts | 46 ++++--------------- .../tooltip/tooltip.directive.spec.ts | 34 +++++++++----- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts index 88f70322b6a..69c51df4f22 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts @@ -29,11 +29,6 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs { const HOVER_SHOW_TRIGGERS = new Set(['mouseenter', 'mouseover', 'pointerenter', 'pointerover']); -interface TooltipPointerPosition { - clientX: number; - clientY: number; -} - /** * **Ignite UI for Angular Tooltip Target** - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tooltip) @@ -388,16 +383,9 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen private _showTriggers = new Set(['pointerenter']); private _hideTriggers = new Set(['pointerleave', 'click']); private _pendingShowTrigger: string | null = null; - private _pointerPosition: TooltipPointerPosition | null = null; private _abortController = new AbortController(); - private _onPointerMove = (event: PointerEvent): void => { - if (this._pointerPosition) { - this._pointerPosition = { clientX: event.clientX, clientY: event.clientY }; - } - }; - /** * @hidden */ @@ -520,7 +508,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen for (const each of this._hideTriggers) { this.nativeElement.addEventListener(each, this.onHide, options); } - this.nativeElement.addEventListener('pointermove', this._onPointerMove, options); } private removeEventListeners(): void { @@ -577,16 +564,19 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen this._evaluateStickyState(); this._pendingShowTrigger = triggerEvent?.type ?? null; - this._pointerPosition = withDelay && this.showDelay > 0 - ? this._getPointerPosition(triggerEvent) + const pointerEvent = triggerEvent instanceof MouseEvent ? triggerEvent : null; + // Synthetic events without coordinates report (0, 0); trusted events may legitimately originate there. + const hasPointerPosition = pointerEvent && + (pointerEvent.isTrusted || pointerEvent.clientX !== 0 || pointerEvent.clientY !== 0); + const pointerPosition = pointerEvent && withDelay && this.showDelay > 0 && hasPointerPosition && + HOVER_SHOW_TRIGGERS.has(pointerEvent.type) + ? { clientX: pointerEvent.clientX, clientY: pointerEvent.clientY } : null; this.target.timeoutId = setTimeout(() => { // Call open() of IgxTooltipDirective - const pointerPosition = this._pointerPosition; this.target.timeoutId = null; this._pendingShowTrigger = null; - this._pointerPosition = null; if (pointerPosition && !this._isPointerOverTarget(pointerPosition)) { return; @@ -596,29 +586,12 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen }, withDelay ? this.showDelay : 0); } - private _getPointerPosition(event?: Event): TooltipPointerPosition | null { - if (!event || !HOVER_SHOW_TRIGGERS.has(event.type)) { - return null; - } - - const pointerEvent = event as MouseEvent; - if (typeof pointerEvent.clientX !== 'number' || typeof pointerEvent.clientY !== 'number') { - return null; - } - if (!event.isTrusted && pointerEvent.clientX === 0 && pointerEvent.clientY === 0) { - return null; - } - - return { clientX: pointerEvent.clientX, clientY: pointerEvent.clientY }; - } - - private _isPointerOverTarget(position: TooltipPointerPosition): boolean { + private _isPointerOverTarget(position: { clientX: number; clientY: number }): boolean { const root = this.nativeElement.getRootNode() as DocumentOrShadowRoot; const hitTestRoot = typeof root.elementFromPoint === 'function' ? root : this.nativeElement.ownerDocument; - const element = hitTestRoot.elementFromPoint(position.clientX, position.clientY); - return !!element && this.nativeElement.contains(element); + return this.nativeElement.contains(hitTestRoot.elementFromPoint(position.clientX, position.clientY)); } private _showOnInteraction(triggerEvent?: Event): void { @@ -670,7 +643,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen clearTimeout(this.target.timeoutId); this.target.timeoutId = null; this._pendingShowTrigger = null; - this._pointerPosition = null; } /** diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts index e25be11e6e6..5abbc37b7db 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts @@ -191,26 +191,36 @@ describe('IgxTooltip', () => { verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); })); - it('should not hit-test hover interactions without a show delay', fakeAsync(() => { - tooltipTarget.showDelay = 0; - const elementFromPointSpy = spyOn(document, 'elementFromPoint'); + it('should show a delayed tooltip when its target remains under the pointer', fakeAsync(() => { + tooltipTarget.showDelay = 500; + const target = button.nativeElement; + target.style.position = 'fixed'; + target.style.top = '20px'; + target.style.left = '20px'; + target.style.width = '100px'; + target.style.height = '40px'; + target.style.zIndex = '9999'; + const bounds = target.getBoundingClientRect(); + const pointer = { + clientX: bounds.left + bounds.width / 2, + clientY: bounds.top + bounds.height / 2 + }; - hoverElement(button, { clientX: 20, clientY: 30 }); - tick(); + expect(target.contains(document.elementFromPoint(pointer.clientX, pointer.clientY))).toBeTrue(); + hoverElement(button, pointer); + tick(500); - expect(elementFromPointSpy).not.toHaveBeenCalled(); verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); })); - it('should validate a delayed tooltip against the latest pointer position', fakeAsync(() => { - tooltipTarget.showDelay = 500; - const elementFromPointSpy = spyOn(document, 'elementFromPoint').and.returnValue(button.nativeElement); + it('should not hit-test hover interactions without a show delay', fakeAsync(() => { + tooltipTarget.showDelay = 0; + const elementFromPointSpy = spyOn(document, 'elementFromPoint'); hoverElement(button, { clientX: 20, clientY: 30 }); - button.nativeElement.dispatchEvent(new PointerEvent('pointermove', { clientX: 40, clientY: 50 })); - tick(500); + tick(); - expect(elementFromPointSpy).toHaveBeenCalledWith(40, 50); + expect(elementFromPointSpy).not.toHaveBeenCalled(); verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); })); From 65940bcd8ce4f5842a8c15957a580885fe138345 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Wed, 12 Aug 2026 12:36:36 +0300 Subject: [PATCH 3/5] fix(tooltip): improve hover interaction handling and cleanup test cases --- .../tooltip/tooltip-target.directive.ts | 23 +---- .../tooltip/tooltip.directive.spec.ts | 87 ++++++------------- 2 files changed, 30 insertions(+), 80 deletions(-) diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts index 69c51df4f22..ad6edb46891 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts @@ -27,8 +27,6 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs { cancel: boolean; } -const HOVER_SHOW_TRIGGERS = new Set(['mouseenter', 'mouseover', 'pointerenter', 'pointerover']); - /** * **Ignite UI for Angular Tooltip Target** - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tooltip) @@ -564,21 +562,13 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen this._evaluateStickyState(); this._pendingShowTrigger = triggerEvent?.type ?? null; - const pointerEvent = triggerEvent instanceof MouseEvent ? triggerEvent : null; - // Synthetic events without coordinates report (0, 0); trusted events may legitimately originate there. - const hasPointerPosition = pointerEvent && - (pointerEvent.isTrusted || pointerEvent.clientX !== 0 || pointerEvent.clientY !== 0); - const pointerPosition = pointerEvent && withDelay && this.showDelay > 0 && hasPointerPosition && - HOVER_SHOW_TRIGGERS.has(pointerEvent.type) - ? { clientX: pointerEvent.clientX, clientY: pointerEvent.clientY } - : null; this.target.timeoutId = setTimeout(() => { - // Call open() of IgxTooltipDirective - this.target.timeoutId = null; + const isHoverTrigger = this._pendingShowTrigger === 'pointerenter' || this._pendingShowTrigger === 'mouseenter'; this._pendingShowTrigger = null; - if (pointerPosition && !this._isPointerOverTarget(pointerPosition)) { + if (isHoverTrigger && !this.nativeElement.matches(':hover')) { + this.target.timeoutId = null; return; } @@ -586,13 +576,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen }, withDelay ? this.showDelay : 0); } - private _isPointerOverTarget(position: { clientX: number; clientY: number }): boolean { - const root = this.nativeElement.getRootNode() as DocumentOrShadowRoot; - const hitTestRoot = typeof root.elementFromPoint === 'function' - ? root - : this.nativeElement.ownerDocument; - return this.nativeElement.contains(hitTestRoot.elementFromPoint(position.clientX, position.clientY)); - } private _showOnInteraction(triggerEvent?: Event): void { this._stopTimeoutAndAnimation(); diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts index 5abbc37b7db..cded98478ab 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts @@ -15,6 +15,7 @@ const SHOW_DELAY = 200; const HIDE_DELAY = 300; const AUTO_HIDE_DELAY = 180; const TOOLTIP_ARROW_SELECTOR = '[data-arrow="true"]'; +const hoveredElements = new WeakSet(); describe('IgxTooltip', () => { let fix: ComponentFixture; @@ -23,6 +24,11 @@ describe('IgxTooltip', () => { let button: DebugElement; beforeEach(waitForAsync(() => { + const matches = Element.prototype.matches; + spyOn(Element.prototype, 'matches').and.callFake(function(this: Element, selectors: string): boolean { + return selectors === ':hover' ? hoveredElements.has(this) : matches.call(this, selectors); + } as typeof Element.prototype.matches); + TestBed.configureTestingModule({ imports: [ NoopAnimationsModule, @@ -167,63 +173,6 @@ describe('IgxTooltip', () => { verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); })); - it('should not show a delayed tooltip after its target moves away from the pointer', fakeAsync(() => { - tooltipTarget.showDelay = 500; - const target = button.nativeElement; - target.style.position = 'fixed'; - target.style.top = '20px'; - target.style.left = '20px'; - target.style.width = '100px'; - target.style.height = '40px'; - target.style.zIndex = '9999'; - const bounds = target.getBoundingClientRect(); - const pointer = { - clientX: bounds.left + bounds.width / 2, - clientY: bounds.top + bounds.height / 2 - }; - - expect(target.contains(document.elementFromPoint(pointer.clientX, pointer.clientY))).toBeTrue(); - hoverElement(button, pointer); - target.style.transform = 'translateX(200px)'; - expect(target.contains(document.elementFromPoint(pointer.clientX, pointer.clientY))).toBeFalse(); - - tick(500); - verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); - })); - - it('should show a delayed tooltip when its target remains under the pointer', fakeAsync(() => { - tooltipTarget.showDelay = 500; - const target = button.nativeElement; - target.style.position = 'fixed'; - target.style.top = '20px'; - target.style.left = '20px'; - target.style.width = '100px'; - target.style.height = '40px'; - target.style.zIndex = '9999'; - const bounds = target.getBoundingClientRect(); - const pointer = { - clientX: bounds.left + bounds.width / 2, - clientY: bounds.top + bounds.height / 2 - }; - - expect(target.contains(document.elementFromPoint(pointer.clientX, pointer.clientY))).toBeTrue(); - hoverElement(button, pointer); - tick(500); - - verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); - })); - - it('should not hit-test hover interactions without a show delay', fakeAsync(() => { - tooltipTarget.showDelay = 0; - const elementFromPointSpy = spyOn(document, 'elementFromPoint'); - - hoverElement(button, { clientX: 20, clientY: 30 }); - tick(); - - expect(elementFromPointSpy).not.toHaveBeenCalled(); - verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true); - })); - it('IgxTooltip mouse interaction respects hideDelay', fakeAsync(() => { tooltipTarget.hideDelay = 700; fix.detectChanges(); @@ -589,6 +538,19 @@ describe('IgxTooltip', () => { tick(300); verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); })); + + it('should not open after the delay when the target is no longer hovered', fakeAsync(() => { + tooltipTarget.showDelay = 500; + tooltipTarget.hideTriggers = 'click'; + fix.detectChanges(); + + hoverElement(button); + tick(300); + unhoverElement(button); + tick(200); + + verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); + })); }); }); @@ -1281,10 +1243,15 @@ interface ElementRefLike { nativeElement: HTMLElement } -const hoverElement = (element: ElementRefLike, eventInit: MouseEventInit = {}) => - element.nativeElement.dispatchEvent(new MouseEvent('pointerenter', eventInit)); +const hoverElement = (element: ElementRefLike) => { + hoveredElements.add(element.nativeElement); + element.nativeElement.dispatchEvent(new MouseEvent('pointerenter')); +}; -const unhoverElement = (element: ElementRefLike) => element.nativeElement.dispatchEvent(new MouseEvent('pointerleave')); +const unhoverElement = (element: ElementRefLike) => { + hoveredElements.delete(element.nativeElement); + element.nativeElement.dispatchEvent(new MouseEvent('pointerleave')); +}; const simulateTriggerEvent = (element: ElementRefLike, event: string) => element.nativeElement.dispatchEvent(new Event(event, { bubbles: true })); From 286eeee1f55a12a972306a07fb93c533c96fbbdc Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Wed, 12 Aug 2026 12:59:48 +0300 Subject: [PATCH 4/5] fix(tooltip): unify hover trigger handling --- .../tooltip/tooltip-target.directive.ts | 6 ++-- .../tooltip/tooltip.directive.spec.ts | 31 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts index ad6edb46891..b0ddab51a45 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip-target.directive.ts @@ -27,6 +27,8 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs { cancel: boolean; } +const HOVER_SHOW_TRIGGERS = new Set(['mouseenter', 'mouseover', 'pointerenter', 'pointerover']); + /** * **Ignite UI for Angular Tooltip Target** - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tooltip) @@ -564,11 +566,11 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen this._pendingShowTrigger = triggerEvent?.type ?? null; this.target.timeoutId = setTimeout(() => { - const isHoverTrigger = this._pendingShowTrigger === 'pointerenter' || this._pendingShowTrigger === 'mouseenter'; + const isHoverTrigger = HOVER_SHOW_TRIGGERS.has(this._pendingShowTrigger); this._pendingShowTrigger = null; + this.target.timeoutId = null; if (isHoverTrigger && !this.nativeElement.matches(':hover')) { - this.target.timeoutId = null; return; } diff --git a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts index cded98478ab..d4df2525117 100644 --- a/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/tooltip/tooltip.directive.spec.ts @@ -539,18 +539,21 @@ describe('IgxTooltip', () => { verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); })); - it('should not open after the delay when the target is no longer hovered', fakeAsync(() => { - tooltipTarget.showDelay = 500; - tooltipTarget.hideTriggers = 'click'; - fix.detectChanges(); - - hoverElement(button); - tick(300); - unhoverElement(button); - tick(200); - - verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); - })); + for (const trigger of ['mouseenter', 'mouseover', 'pointerenter', 'pointerover']) { + it(`should not open after the delay when the target is no longer hovered using ${trigger}`, fakeAsync(() => { + tooltipTarget.showDelay = 500; + tooltipTarget.showTriggers = trigger; + tooltipTarget.hideTriggers = 'click'; + fix.detectChanges(); + + hoverElement(button, trigger); + tick(300); + unhoverElement(button); + tick(200); + + verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, false); + })); + } }); }); @@ -1243,9 +1246,9 @@ interface ElementRefLike { nativeElement: HTMLElement } -const hoverElement = (element: ElementRefLike) => { +const hoverElement = (element: ElementRefLike, event = 'pointerenter') => { hoveredElements.add(element.nativeElement); - element.nativeElement.dispatchEvent(new MouseEvent('pointerenter')); + element.nativeElement.dispatchEvent(new MouseEvent(event)); }; const unhoverElement = (element: ElementRefLike) => { From effdbb5d11c08a6dc4644f9f72f1aa56c16007b6 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Wed, 12 Aug 2026 18:54:57 +0300 Subject: [PATCH 5/5] fix(grid-validation): mock element.matches for tooltip hover test --- projects/igniteui-angular/grids/grid/src/grid-validation.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/igniteui-angular/grids/grid/src/grid-validation.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-validation.spec.ts index 2e6ab931322..5e5c274860e 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-validation.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-validation.spec.ts @@ -225,6 +225,7 @@ describe('IgxGrid - Validation #grid', () => { expect(cell.errorTooltip.first.collapsed).toBeTrue(); const element = fixture.debugElement.query(By.directive(IgxTooltipTargetDirective)).nativeElement; + spyOn(element, 'matches').and.returnValue(true); element.dispatchEvent(new MouseEvent('pointerenter')); flush(); fixture.detectChanges();