diff --git a/src/popover/popover.spec.ts b/src/popover/popover.spec.ts
index 86a6146085..3459eec53e 100644
--- a/src/popover/popover.spec.ts
+++ b/src/popover/popover.spec.ts
@@ -633,6 +633,27 @@ describe('ngb-popover', () => {
});
describe('visibility', () => {
+ it('should stay open if the popover is hovered before the closeDelay times out', fakeAsync(() => {
+ const fixture = createTestComponent(
+ `
`,
+ );
+ const directive = fixture.debugElement.query(By.directive(NgbPopover));
+
+ triggerEvent(directive, 'mouseenter');
+ tick();
+ expect(getWindow(fixture.nativeElement)).toBeTruthy();
+
+ triggerEvent(directive, 'mouseleave');
+ tick(100);
+ triggerEvent(getWindow(fixture.nativeElement), 'mouseenter');
+ tick(300);
+ expect(getWindow(fixture.nativeElement)).toBeTruthy();
+
+ triggerEvent(getWindow(fixture.nativeElement), 'mouseleave');
+ tick(300);
+ expect(getWindow(fixture.nativeElement)).toBeFalsy();
+ }));
+
it('should emit events when showing and hiding popover', () => {
const fixture = createTestComponent(
``,
diff --git a/src/popover/popover.ts b/src/popover/popover.ts
index a8655b0b84..787440dd6e 100644
--- a/src/popover/popover.ts
+++ b/src/popover/popover.ts
@@ -6,6 +6,7 @@ import {
Directive,
ElementRef,
EventEmitter,
+ HostListener,
inject,
Input,
NgZone,
@@ -28,7 +29,7 @@ import { isString } from '../util/util';
import { NgbPopoverConfig } from './popover-config';
import { addPopperOffset } from '../util/positioning-util';
-import { Subscription } from 'rxjs';
+import { Subject, Subscription } from 'rxjs';
let nextId = 0;
@@ -67,10 +68,22 @@ export class NgbPopoverWindow {
@Input() id: string;
@Input() popoverClass: string;
@Input() context: any;
+ @Input() readonly mouseEnter: Subject;
+ @Input() readonly mouseLeave: Subject;
isTitleTemplate() {
return this.title instanceof TemplateRef;
}
+
+ @HostListener('mouseenter')
+ onMouseEnter() {
+ this.mouseEnter?.next();
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeave() {
+ this.mouseLeave?.next();
+ }
}
/**
@@ -215,6 +228,9 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
private _positioning = ngbPositioning();
private _zoneSubscription: Subscription;
+ private _mouseEnterTooltip = new Subject();
+ private _mouseLeaveTooltip = new Subject();
+
/**
* Opens the popover.
*
@@ -235,6 +251,8 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
this._windowRef.setInput('context', context ?? this.popoverContext);
this._windowRef.setInput('popoverClass', this.popoverClass);
this._windowRef.setInput('id', this._ngbPopoverWindowId);
+ this._windowRef.setInput('mouseEnter', this._mouseEnterTooltip);
+ this._windowRef.setInput('mouseLeave', this._mouseLeaveTooltip);
this._getPositionTargetElement().setAttribute('aria-describedby', this._ngbPopoverWindowId);
@@ -326,6 +344,8 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
this.close.bind(this),
+this.openDelay,
+this.closeDelay,
+ this._mouseEnterTooltip,
+ this._mouseLeaveTooltip,
);
}
@@ -341,6 +361,8 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
ngOnDestroy() {
this.close(false);
+ this._mouseEnterTooltip.complete();
+ this._mouseLeaveTooltip.complete();
// This check is needed as it might happen that ngOnDestroy is called before ngOnInit
// under certain conditions, see: https://github.com/ng-bootstrap/ng-bootstrap/issues/2199
this._unregisterListenersFn?.();
diff --git a/src/tooltip/tooltip.spec.ts b/src/tooltip/tooltip.spec.ts
index d3d46b0d0e..5d1ffd7786 100644
--- a/src/tooltip/tooltip.spec.ts
+++ b/src/tooltip/tooltip.spec.ts
@@ -646,6 +646,27 @@ describe('ngb-tooltip', () => {
});
describe('visibility', () => {
+ it('should stay open if the tooltip is hovered before the closeDelay times out', fakeAsync(() => {
+ const fixture = createTestComponent(
+ ``,
+ );
+ const directive = fixture.debugElement.query(By.directive(NgbTooltip));
+
+ triggerEvent(directive, 'mouseenter');
+ tick();
+ expect(getWindow(fixture.nativeElement)).toBeTruthy();
+
+ triggerEvent(directive, 'mouseleave');
+ tick(100);
+ triggerEvent(getWindow(fixture.nativeElement), 'mouseenter');
+ tick(300);
+ expect(getWindow(fixture.nativeElement)).toBeTruthy();
+
+ triggerEvent(getWindow(fixture.nativeElement), 'mouseleave');
+ tick(300);
+ expect(getWindow(fixture.nativeElement)).toBeFalsy();
+ }));
+
it('should emit events when showing and hiding tooltip', () => {
const fixture = createTestComponent(
``,
diff --git a/src/tooltip/tooltip.ts b/src/tooltip/tooltip.ts
index f197658b16..48867f9025 100644
--- a/src/tooltip/tooltip.ts
+++ b/src/tooltip/tooltip.ts
@@ -6,6 +6,7 @@ import {
Directive,
ElementRef,
EventEmitter,
+ HostListener,
inject,
Input,
NgZone,
@@ -26,7 +27,7 @@ import { PopupService } from '../util/popup';
import { isString } from '../util/util';
import { NgbTooltipConfig } from './tooltip-config';
-import { Subscription } from 'rxjs';
+import { Subject, Subscription } from 'rxjs';
import { addPopperOffset } from '../util/positioning-util';
let nextId = 0;
@@ -54,6 +55,18 @@ export class NgbTooltipWindow {
@Input() animation: boolean;
@Input() id: string;
@Input() tooltipClass: string;
+ @Input() readonly mouseEnter: Subject;
+ @Input() readonly mouseLeave: Subject;
+
+ @HostListener('mouseenter')
+ onMouseEnter() {
+ this.mouseEnter?.next();
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeave() {
+ this.mouseLeave?.next();
+ }
}
/**
@@ -183,6 +196,9 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
private _positioning = ngbPositioning();
private _zoneSubscription: Subscription;
+ private _mouseEnterTooltip = new Subject();
+ private _mouseLeaveTooltip = new Subject();
+
/**
* The string content or a `TemplateRef` for the content to be displayed in the tooltip.
*
@@ -217,6 +233,8 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
this._windowRef.setInput('animation', this.animation);
this._windowRef.setInput('tooltipClass', this.tooltipClass);
this._windowRef.setInput('id', this._ngbTooltipWindowId);
+ this._windowRef.setInput('mouseEnter', this._mouseEnterTooltip);
+ this._windowRef.setInput('mouseLeave', this._mouseLeaveTooltip);
this._getPositionTargetElement().setAttribute('aria-describedby', this._ngbTooltipWindowId);
@@ -314,6 +332,8 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
this.close.bind(this),
+this.openDelay,
+this.closeDelay,
+ this._mouseEnterTooltip,
+ this._mouseLeaveTooltip,
);
}
@@ -325,6 +345,8 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
ngOnDestroy() {
this.close(false);
+ this._mouseEnterTooltip.complete();
+ this._mouseLeaveTooltip.complete();
// This check is needed as it might happen that ngOnDestroy is called before ngOnInit
// under certain conditions, see: https://github.com/ng-bootstrap/ng-bootstrap/issues/2199
this._unregisterListenersFn?.();
diff --git a/src/util/triggers.ts b/src/util/triggers.ts
index 3027a14bbd..80de0cbeb9 100644
--- a/src/util/triggers.ts
+++ b/src/util/triggers.ts
@@ -1,3 +1,5 @@
+import { Observable, EMPTY } from 'rxjs';
+
const ALIASES = {
hover: ['mouseenter', 'mouseleave'],
focus: ['focusin', 'focusout'],
@@ -36,6 +38,8 @@ export function listenToTriggers(
closeFn: () => void,
openDelayMs = 0,
closeDelayMs = 0,
+ enterPopover: Observable = EMPTY,
+ leavePopover: Observable = EMPTY,
) {
const parsedTriggers = parseTriggers(triggers);
@@ -76,6 +80,17 @@ export function listenToTriggers(
withDelay(() => activeOpenTriggers.size === 0 && closeFn(), closeDelayMs);
});
}
+
+ if (openTrigger === 'mouseenter' && closeTrigger === 'mouseleave' && closeDelayMs > 0) {
+ enterPopover.subscribe(() => {
+ activeOpenTriggers.add(openTrigger);
+ clearTimeout(timeout);
+ });
+ leavePopover.subscribe(() => {
+ activeOpenTriggers.delete(openTrigger);
+ withDelay(() => activeOpenTriggers.size === 0 && closeFn(), closeDelayMs);
+ });
+ }
}
return () => cleanupFns.forEach((cleanupFn) => cleanupFn());