Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -620,6 +670,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
clearTimeout(this.target.timeoutId);
this.target.timeoutId = null;
this._pendingShowTrigger = null;
this._pointerPosition = null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1169,7 +1216,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'));

Expand Down
Loading