Skip to content

Commit

Permalink
fix(tooltip): align closing behavior with Bootstrap
Browse files Browse the repository at this point in the history
If several triggers are set, ex. 'hover focus', make sure that all triggers are taken into account for closure and not only the first one.
Ex: mouseout on the triggering element, should not close the tooltip while the element is focused; and focus leaving the element should not close the tooltip while the mouse is still hovering it.

Fixes ng-bootstrap#3889
  • Loading branch information
maxokorokov committed Sep 12, 2023
1 parent 4f334ee commit 10bdf81
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 277 deletions.
2 changes: 2 additions & 0 deletions e2e-app/src/app/app.routes.ts
Expand Up @@ -27,6 +27,7 @@ import { OffcanvasFocusComponent } from './offcanvas/focus/offcanvas-focus.compo
import { OffcanvasNestingComponent } from './offcanvas/nesting/offcanvas-nesting.component';
import { OffcanvasStackConfirmationComponent } from './offcanvas/stack-confirmation/offcanvas-stack-confirmation.component';
import { DropdownShadowComponent } from './dropdown/shadow/dropdown-shadow.component';
import { TooltipTriggersComponent } from './tooltip/triggers/tooltip-triggers.component';

export const ROUTES: Routes = [
{
Expand Down Expand Up @@ -74,6 +75,7 @@ export const ROUTES: Routes = [
{ path: 'autoclose', component: TooltipAutocloseComponent },
{ path: 'focus', component: TooltipFocusComponent },
{ path: 'position', component: TooltipPositionComponent },
{ path: 'triggers', component: TooltipTriggersComponent },
],
},
{
Expand Down
28 changes: 28 additions & 0 deletions e2e-app/src/app/tooltip/triggers/tooltip-triggers.component.html
@@ -0,0 +1,28 @@
<h3>
Tooltip triggers
<span class="ms-1 badge {{ d.isOpen() ? 'bg-success' : 'bg-danger' }}" id="open-status">{{
d.isOpen() ? 'open' : 'closed'
}}</span>
</h3>

<form id="default">
<div class="mb-3 row row-cols-lg-auto">
<div class="col-12">
<button class="btn btn-outline-light" id="before">Before</button>
<button
id="trigger"
#d="ngbTooltip"
ngbTooltip="Tooltip here"
type="button"
class="btn btn-outline-secondary ms-2"
>
Tooltip
</button>
<button class="btn btn-outline-light ms-2" id="after">After</button>
</div>

<div class="col-12">
<button class="btn btn-outline-secondary ms-1" id="outside-button">Outside button</button>
</div>
</div>
</form>
13 changes: 13 additions & 0 deletions e2e-app/src/app/tooltip/triggers/tooltip-triggers.component.ts
@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

@Component({
standalone: true,
imports: [FormsModule, NgbModule],
templateUrl: './tooltip-triggers.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TooltipTriggersComponent {
autoClose: boolean | 'inside' | 'outside' = true;
}
67 changes: 67 additions & 0 deletions e2e-app/src/app/tooltip/triggers/tooltip-triggers.e2e-spec.ts
@@ -0,0 +1,67 @@
import { getPage, setPage, test } from '../../../../baseTest';
import { expectTooltipToBeClosed, expectTooltipToBeOpen } from '../tooltip.po';
import { sendKey } from '../../tools.po';

const hoverOutside = async () => await getPage().hover('#outside-button');
const clickOutside = async () => await getPage().click('#outside-button');
const clickTriggeringElement = async () => await getPage().click('#trigger');

const openTooltip = async (message: string) => {
await getPage().hover('button[ngbTooltip]');
await expectTooltipToBeOpen(message);
};

test.use({ testURL: 'tooltip/triggers', testSelector: 'h3:text("Tooltip triggers")' });
test.beforeEach(async ({ page }) => setPage(page));

test.describe('Tooltip Triggers', () => {
test(`open close tooltip with hover`, async () => {
await openTooltip(`Opening tooltip with hover`);

await hoverOutside();
await expectTooltipToBeClosed(`Tooltip close when hover outside`);
});

test(`should not close tooltip on mouseout after triggering element click`, async () => {
await openTooltip(`Opening tooltip with hover`);

await clickTriggeringElement();
await expectTooltipToBeOpen(`Tooltip should stay visible when clicking on the triggering element`);

await hoverOutside();
await expectTooltipToBeOpen(`Tooltip should not close after hovering the outside element`);

await clickOutside();
await expectTooltipToBeClosed(`Tooltip should close after clicking the outside element`);
});

test(`should close tooltip on blur`, async ({ browserName }) => {
test.skip(browserName === 'webkit');

await getPage().click('#before');

await sendKey('Tab');
await expectTooltipToBeOpen(`Tooltip should have been opened with focus`);

await sendKey('Tab');
await expectTooltipToBeClosed(`Tooltip should close after focusing outside the element`);
});

test(`should not close tooltip on blur after triggering element click`, async ({ browserName }) => {
test.skip(browserName === 'webkit');

await getPage().click('#before');

await sendKey('Tab');
await expectTooltipToBeOpen(`Tooltip should have been opened with focus`);

await clickTriggeringElement();
await expectTooltipToBeOpen(`Tooltip should stay visible when clicking on the triggering element`);

await sendKey('Tab');
await expectTooltipToBeOpen(`Tooltip should not close after focusing outside the element`);

await hoverOutside();
await expectTooltipToBeClosed(`Tooltip should close after hovering the outside element`);
});
});
1 change: 0 additions & 1 deletion src/popover/popover.ts
Expand Up @@ -353,7 +353,6 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {

ngOnInit() {
this._unregisterListenersFn = listenToTriggers(
this._renderer,
this._elementRef.nativeElement,
this.triggers,
this.isOpen.bind(this),
Expand Down
13 changes: 9 additions & 4 deletions src/tooltip/tooltip.ts
Expand Up @@ -279,9 +279,15 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
});
});

ngbAutoClose(this._ngZone, this._document, this.autoClose, () => this.close(), this.hidden, [
this._windowRef.location.nativeElement,
]);
ngbAutoClose(
this._ngZone,
this._document,
this.autoClose,
() => this.close(),
this.hidden,
[this._windowRef.location.nativeElement],
[this._elementRef.nativeElement],
);

transition$.subscribe(() => this.shown.emit());
}
Expand Down Expand Up @@ -327,7 +333,6 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {

ngOnInit() {
this._unregisterListenersFn = listenToTriggers(
this._renderer,
this._elementRef.nativeElement,
this.triggers,
this.isOpen.bind(this),
Expand Down

0 comments on commit 10bdf81

Please sign in to comment.