Skip to content

Commit e4a53bd

Browse files
RivaIvanovagedinakovapmoleri
authored
fix(tooltip): make touch events passive - 19.2.x (#15813)
* fix(tooltip): make touch events passive * fix(tooltip): add check for tooltipTarget * test(tooltip): should not call hideTooltip multiple times on document:touchstart * test(tooltip): add flush for document touchstart test * fix(tooltip): should not emit hide event multiple times * fix(tooltip): set tooltipTarget in showTooltip method * fix(tooltip): handle document touch event in tooltipTarget * fix(tooltip): handle documentTouch in tooltip * chore(tooltip): remove touchstart removeEventListener * fix(tooltip): remove document listener on destroy * test(tooltip): A multi target touch test --------- Co-authored-by: Galina Edinakova <[email protected]> Co-authored-by: Pablo <[email protected]>
1 parent 1044794 commit e4a53bd

File tree

4 files changed

+119
-16
lines changed

4 files changed

+119
-16
lines changed

projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
217217
return;
218218
}
219219

220+
this.target.tooltipTarget = this;
221+
220222
const showingArgs = { target: this, tooltip: this.target, cancel: false };
221223
this.tooltipShow.emit(showingArgs);
222224

@@ -258,7 +260,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
258260
/**
259261
* @hidden
260262
*/
261-
@HostListener('touchstart')
262263
public onTouchStart() {
263264
if (this.tooltipDisabled) {
264265
return;
@@ -270,7 +271,6 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
270271
/**
271272
* @hidden
272273
*/
273-
@HostListener('document:touchstart', ['$event'])
274274
public onDocumentTouchStart(event) {
275275
if (this.tooltipDisabled) {
276276
return;
@@ -301,20 +301,27 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
301301
this._overlayDefaults.closeOnEscape = true;
302302

303303
this.target.closing.pipe(takeUntil(this.destroy$)).subscribe((event) => {
304+
if (this.target.tooltipTarget !== this) {
305+
return;
306+
}
307+
304308
const hidingArgs = { target: this, tooltip: this.target, cancel: false };
305309
this.tooltipHide.emit(hidingArgs);
306310

307311
if (hidingArgs.cancel) {
308312
event.cancel = true;
309313
}
310314
});
315+
316+
this.nativeElement.addEventListener('touchstart', this.onTouchStart = this.onTouchStart.bind(this), { passive: true });
311317
}
312318

313319
/**
314320
* @hidden
315321
*/
316322
public ngOnDestroy() {
317323
this.hideTooltip();
324+
this.nativeElement.removeEventListener('touchstart', this.onTouchStart);
318325
this.destroy$.next();
319326
this.destroy$.complete();
320327
}
@@ -334,6 +341,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
334341
this.target.forceClose(this.mergedOverlaySettings);
335342
this.target.toBeHidden = false;
336343
}
344+
this.target.tooltipTarget = this;
337345

338346
const showingArgs = { target: this, tooltip: this.target, cancel: false };
339347
this.tooltipShow.emit(showingArgs);

projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.spec.ts

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { fakeAsync, TestBed, tick, flush, waitForAsync } from '@angular/core/testing';
1+
import { DebugElement } from '@angular/core';
2+
import { fakeAsync, TestBed, tick, flush, waitForAsync, ComponentFixture } from '@angular/core/testing';
23
import { By } from '@angular/platform-browser';
34
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
45
import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent } from '../../test-utils/tooltip-components.spec';
@@ -11,10 +12,10 @@ const HIDDEN_TOOLTIP_CLASS = 'igx-tooltip--hidden';
1112
const TOOLTIP_CLASS = 'igx-tooltip';
1213

1314
describe('IgxTooltip', () => {
14-
let fix;
15-
let tooltipNativeElement;
15+
let fix: ComponentFixture<any>;
16+
let tooltipNativeElement: HTMLElement;
1617
let tooltipTarget: IgxTooltipTargetDirective;
17-
let button;
18+
let button: DebugElement;
1819

1920
beforeEach(waitForAsync(() => {
2021
TestBed.configureTestingModule({
@@ -273,7 +274,7 @@ describe('IgxTooltip', () => {
273274
flush();
274275

275276
verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true);
276-
277+
277278
fix.componentInstance.showButton = false;
278279
fix.detectChanges();
279280
flush();
@@ -492,8 +493,8 @@ describe('IgxTooltip', () => {
492493
fix = TestBed.createComponent(IgxTooltipMultipleTargetsComponent);
493494
fix.detectChanges();
494495
tooltipNativeElement = fix.debugElement.query(By.directive(IgxTooltipDirective)).nativeElement;
495-
targetOne = fix.componentInstance.targetOne as IgxTooltipTargetDirective;
496-
targetTwo = fix.componentInstance.targetTwo as IgxTooltipTargetDirective;
496+
targetOne = fix.componentInstance.targetOne;
497+
targetTwo = fix.componentInstance.targetTwo;
497498
buttonOne = fix.debugElement.query(By.css('.buttonOne'));
498499
buttonTwo = fix.debugElement.query(By.css('.buttonTwo'));
499500
}));
@@ -560,6 +561,64 @@ describe('IgxTooltip', () => {
560561
// Tooltip is NOT visible and positioned relative to buttonOne
561562
verifyTooltipPosition(tooltipNativeElement, buttonOne, false);
562563
}));
564+
565+
it('Should not call `hideTooltip` multiple times on document:touchstart', fakeAsync(() => {
566+
spyOn(targetOne, 'hideTooltip').and.callThrough();
567+
spyOn(targetTwo, 'hideTooltip').and.callThrough();
568+
569+
touchElement(buttonOne);
570+
tick(500);
571+
572+
const dummyDiv = fix.debugElement.query(By.css('.dummyDiv'));
573+
touchElement(dummyDiv);
574+
flush();
575+
576+
expect(targetOne.hideTooltip).toHaveBeenCalledTimes(1);
577+
expect(targetTwo.hideTooltip).not.toHaveBeenCalled();
578+
}));
579+
580+
it('should not emit tooltipHide event multiple times', fakeAsync(() => {
581+
spyOn(targetOne.tooltipHide, 'emit');
582+
spyOn(targetTwo.tooltipHide, 'emit');
583+
584+
hoverElement(buttonOne);
585+
flush();
586+
587+
const tooltipHideArgsTargetOne = { target: targetOne, tooltip: fix.componentInstance.tooltip, cancel: false };
588+
const tooltipHideArgsTargetTwo = { target: targetTwo, tooltip: fix.componentInstance.tooltip, cancel: false };
589+
590+
unhoverElement(buttonOne);
591+
tick(500);
592+
expect(targetOne.tooltipHide.emit).toHaveBeenCalledOnceWith(tooltipHideArgsTargetOne);
593+
expect(targetTwo.tooltipHide.emit).not.toHaveBeenCalled();
594+
flush();
595+
596+
hoverElement(buttonTwo);
597+
flush();
598+
599+
unhoverElement(buttonTwo);
600+
tick(500);
601+
expect(targetOne.tooltipHide.emit).toHaveBeenCalledOnceWith(tooltipHideArgsTargetOne);
602+
expect(targetTwo.tooltipHide.emit).toHaveBeenCalledOnceWith(tooltipHideArgsTargetTwo);
603+
flush();
604+
}));
605+
606+
607+
it('IgxTooltip hides when touch one target, then another, then outside', fakeAsync(() => {
608+
touchElement(targetOne);
609+
flush();
610+
verifyTooltipVisibility(tooltipNativeElement, targetOne, true);
611+
verifyTooltipPosition(tooltipNativeElement, targetOne, true);
612+
613+
touchElement(targetTwo);
614+
flush();
615+
verifyTooltipVisibility(tooltipNativeElement, targetTwo, true);
616+
verifyTooltipPosition(tooltipNativeElement, targetTwo, true);
617+
618+
touchElement(fix.debugElement);
619+
flush();
620+
verifyTooltipVisibility(tooltipNativeElement, targetTwo, false);
621+
}));
563622
});
564623

565624
describe('Tooltip integration', () => {
@@ -593,11 +652,15 @@ describe('IgxTooltip', () => {
593652
});
594653
});
595654

596-
const hoverElement = (element) => element.nativeElement.dispatchEvent(new MouseEvent('mouseenter'));
655+
interface ElementRefLike {
656+
nativeElement: HTMLElement
657+
}
658+
659+
const hoverElement = (element: ElementRefLike) => element.nativeElement.dispatchEvent(new MouseEvent('mouseenter'));
597660

598-
const unhoverElement = (element) => element.nativeElement.dispatchEvent(new MouseEvent('mouseleave'));
661+
const unhoverElement = (element: ElementRefLike) => element.nativeElement.dispatchEvent(new MouseEvent('mouseleave'));
599662

600-
const touchElement = (element) => element.nativeElement.dispatchEvent(new TouchEvent('touchstart', { bubbles: true }));
663+
const touchElement = (element: ElementRefLike) => element.nativeElement.dispatchEvent(new TouchEvent('touchstart', { bubbles: true }));
601664

602665
const verifyTooltipVisibility = (tooltipNativeElement, tooltipTarget, shouldBeVisible: boolean) => {
603666
expect(tooltipNativeElement.classList.contains(TOOLTIP_CLASS)).toBe(shouldBeVisible);

projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {
2-
Directive, ElementRef, Input, ChangeDetectorRef, Optional, HostBinding, Inject
2+
Directive, ElementRef, Input, ChangeDetectorRef, Optional, HostBinding, Inject, OnDestroy
33
} from '@angular/core';
44
import { IgxOverlayService } from '../../services/overlay/overlay';
55
import { OverlaySettings } from '../../services/public_api';
66
import { IgxNavigationService } from '../../core/navigation';
77
import { IgxToggleDirective } from '../toggle/toggle.directive';
8+
import { IgxTooltipTargetDirective } from './tooltip-target.directive';
9+
import { Subject, takeUntil } from 'rxjs';
810

911
let NEXT_ID = 0;
1012
/**
@@ -26,7 +28,7 @@ let NEXT_ID = 0;
2628
selector: '[igxTooltip]',
2729
standalone: true
2830
})
29-
export class IgxTooltipDirective extends IgxToggleDirective {
31+
export class IgxTooltipDirective extends IgxToggleDirective implements OnDestroy {
3032
/**
3133
* @hidden
3234
*/
@@ -102,6 +104,13 @@ export class IgxTooltipDirective extends IgxToggleDirective {
102104
*/
103105
public toBeShown = false;
104106

107+
/**
108+
* @hidden
109+
*/
110+
public tooltipTarget: IgxTooltipTargetDirective;
111+
112+
private _destroy$ = new Subject<boolean>();
113+
105114
/** @hidden */
106115
constructor(
107116
elementRef: ElementRef,
@@ -110,6 +119,23 @@ export class IgxTooltipDirective extends IgxToggleDirective {
110119
@Optional() navigationService: IgxNavigationService) {
111120
// D.P. constructor duplication due to es6 compilation, might be obsolete in the future
112121
super(elementRef, cdr, overlayService, navigationService);
122+
123+
this.onDocumentTouchStart = this.onDocumentTouchStart.bind(this);
124+
this.overlayService.opening.pipe(takeUntil(this._destroy$)).subscribe(() => {
125+
document.addEventListener('touchstart', this.onDocumentTouchStart, { passive: true });
126+
});
127+
this.overlayService.closed.pipe(takeUntil(this._destroy$)).subscribe(() => {
128+
document.removeEventListener('touchstart', this.onDocumentTouchStart);
129+
});
130+
}
131+
132+
/** @hidden */
133+
public override ngOnDestroy() {
134+
super.ngOnDestroy();
135+
136+
document.removeEventListener('touchstart', this.onDocumentTouchStart);
137+
this._destroy$.next(true);
138+
this._destroy$.complete();
113139
}
114140

115141
/**
@@ -154,4 +180,8 @@ export class IgxTooltipDirective extends IgxToggleDirective {
154180
overlaySettings.positionStrategy.settings.closeAnimation = animation;
155181
}
156182
}
183+
184+
private onDocumentTouchStart(event) {
185+
this.tooltipTarget?.onDocumentTouchStart(event);
186+
}
157187
}

projects/igniteui-angular/src/lib/test-utils/tooltip-components.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IgxToggleActionDirective, IgxToggleDirective } from '../directives/togg
66
@Component({
77
template: `
88
<div class="dummyDiv">dummy div for touch tests</div>
9-
9+
1010
@if (showButton) {
1111
<button [igxTooltipTarget]="tooltipRef" [tooltip]="'Infragistics Inc. HQ'"
1212
(tooltipShow)="showing($event)" (tooltipHide)="hiding($event)"
@@ -42,6 +42,8 @@ export class IgxTooltipSingleTargetComponent {
4242

4343
@Component({
4444
template: `
45+
<div class="dummyDiv">dummy div for touch tests</div>
46+
4547
<button class="buttonOne" #targetOne="tooltipTarget" [igxTooltipTarget]="tooltipRef" style="margin: 100px">
4648
Target One
4749
</button>
@@ -57,7 +59,7 @@ export class IgxTooltipSingleTargetComponent {
5759
imports: [IgxTooltipDirective, IgxTooltipTargetDirective]
5860
})
5961
export class IgxTooltipMultipleTargetsComponent {
60-
@ViewChild('targetOne', { read: IgxTooltipTargetDirective, static: true }) public targetOne: IgxTooltipDirective;
62+
@ViewChild('targetOne', { read: IgxTooltipTargetDirective, static: true }) public targetOne: IgxTooltipTargetDirective;
6163
@ViewChild('targetTwo', { read: IgxTooltipTargetDirective, static: true }) public targetTwo: IgxTooltipTargetDirective;
6264
@ViewChild(IgxTooltipDirective, { static: true }) public tooltip: IgxTooltipDirective;
6365
}

0 commit comments

Comments
 (0)