Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tooltip): keep tooltip content visible as user hovers #447

Merged
merged 4 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions projects/angular/clarity.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4379,7 +4379,8 @@ export class ClrTooltip {
// @public (undocumented)
export class ClrTooltipContent extends AbstractPopover implements OnInit {
// Warning: (ae-forgotten-export) The symbol "TooltipIdService" needs to be exported by the entry point index.d.ts
constructor(injector: Injector, parentHost: ElementRef, tooltipIdService: TooltipIdService);
// Warning: (ae-forgotten-export) The symbol "TooltipMouseService" needs to be exported by the entry point index.d.ts
constructor(injector: Injector, parentHost: ElementRef, tooltipIdService: TooltipIdService, tooltipMouseService: TooltipMouseService);
// (undocumented)
get id(): string;
set id(value: string);
Expand All @@ -4394,7 +4395,7 @@ export class ClrTooltipContent extends AbstractPopover implements OnInit {
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<ClrTooltipContent, "clr-tooltip-content", never, { "id": "id"; "position": "clrPosition"; "size": "clrSize"; }, {}, never, ["*"]>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<ClrTooltipContent, [null, { optional: true; }, null]>;
static ɵfac: i0.ɵɵFactoryDeclaration<ClrTooltipContent, [null, { optional: true; }, null, null]>;
}

// @public (undocumented)
Expand All @@ -4413,7 +4414,7 @@ export class ClrTooltipModule {

// @public (undocumented)
export class ClrTooltipTrigger {
constructor(toggleService: ClrPopoverToggleService, tooltipIdService: TooltipIdService);
constructor(toggleService: ClrPopoverToggleService, tooltipIdService: TooltipIdService, tooltipMouseService: TooltipMouseService);
// (undocumented)
ariaDescribedBy: string;
// (undocumented)
Expand Down
9 changes: 9 additions & 0 deletions projects/angular/src/popover/tooltip/_tooltips.clarity.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@
transition: opacity 0.3s linear;
white-space: normal;
z-index: map-get($clr-layers, tooltips);

&::after {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great solution, thanks!

position: absolute;
top: -20px;
left: -20px;
right: -20px;
bottom: -20px;
content: '';
}
}

&:hover > .tooltip-content,
Expand Down
2 changes: 2 additions & 0 deletions projects/angular/src/popover/tooltip/all.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
*/

import TooltipIdServiceSpecs from './providers/tooltip-id.service.spec';
import TooltipMouseServiceSpecs from './providers/tooltip-mouse.service.spec';
import TooltipContentSpecs from './tooltip-content.spec';
import TooltipTriggerSpecs from './tooltip-trigger.spec';
import TooltipSpecs from './tooltip.spec';

describe('Tooltip', () => {
TooltipIdServiceSpecs();
TooltipMouseServiceSpecs();
TooltipContentSpecs();
TooltipTriggerSpecs();
TooltipSpecs();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2016-2022 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { fakeAsync, tick } from '@angular/core/testing';

import { ClrPopoverToggleService } from '../../../utils/popover/providers/popover-toggle.service';
import { TooltipMouseService } from './tooltip-mouse.service';

export default function (): void {
describe('Tooltip Mouse Service', () => {
let toggleService: ClrPopoverToggleService;
let mouseService: TooltipMouseService;

beforeEach(() => {
toggleService = new ClrPopoverToggleService();
mouseService = new TooltipMouseService(toggleService);
});

it('should show the tooltip when the mouse enters the trigger', () => {
mouseService.onMouseEnterTrigger();

expect(toggleService.open).toBe(true);
});

it('should hide the tooltip if the mouse leaves the trigger and does not enter the content', fakeAsync(() => {
toggleService.open = true;

mouseService.onMouseLeaveTrigger();
tick();

expect(toggleService.open).toBe(false);
}));

it('should hide the tooltip if the mouse leaves the content and does not enter the trigger', fakeAsync(() => {
toggleService.open = true;

mouseService.onMouseLeaveContent();
tick();

expect(toggleService.open).toBe(false);
}));

it('should not hide the tooltip as the mouse moves from the trigger to the content', fakeAsync(() => {
toggleService.open = true;

mouseService.onMouseLeaveTrigger();
mouseService.onMouseEnterContent();
tick();

expect(toggleService.open).toBe(true);
}));
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2016-2022 VMware, Inc. All Rights Reserved.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { Injectable } from '@angular/core';

import { ClrPopoverToggleService } from '../../../utils/popover/providers/popover-toggle.service';

@Injectable()
export class TooltipMouseService {
private mouseOverTrigger: boolean;
private mouseOverContent: boolean;

constructor(private readonly toggleService: ClrPopoverToggleService) {}

onMouseEnterTrigger() {
this.mouseOverTrigger = true;
this.toggleService.open = true;
}

onMouseLeaveTrigger() {
this.mouseOverTrigger = false;
this.hideIfMouseOut();
}

onMouseEnterContent() {
this.mouseOverContent = true;
}

onMouseLeaveContent() {
this.mouseOverContent = false;
this.hideIfMouseOut();
}

private hideIfMouseOut() {
// A zero timeout is used so that the code has a chance to update
// the `mouseOverContent` property after the user moves the mouse from the trigger to the content.
setTimeout(() => {
if (!this.mouseOverTrigger && !this.mouseOverContent) {
this.toggleService.open = false;
}
}, 0);
}
}
16 changes: 14 additions & 2 deletions projects/angular/src/popover/tooltip/tooltip-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* The full license information can be found in LICENSE in the root directory of this project.
*/

import { Component, ElementRef, Inject, Injector, Input, OnInit, Optional } from '@angular/core';
import { Component, ElementRef, HostListener, Inject, Injector, Input, OnInit, Optional } from '@angular/core';

import { assertNever } from '../../utils/assert/assert.helpers';
import { uniqueIdFactory } from '../../utils/id-generator/id-generator.service';
import { AbstractPopover } from '../common/abstract-popover';
import { Point } from '../common/popover';
import { POPOVER_HOST_ANCHOR } from '../common/popover-host-anchor.token';
import { TooltipIdService } from './providers/tooltip-id.service';
import { TooltipMouseService } from './providers/tooltip-mouse.service';

const POSITIONS = ['bottom-left', 'bottom-right', 'top-left', 'top-right', 'right', 'left'] as const;
type Position = typeof POSITIONS[number];
Expand All @@ -37,7 +38,8 @@ export class ClrTooltipContent extends AbstractPopover implements OnInit {
@Optional()
@Inject(POPOVER_HOST_ANCHOR)
parentHost: ElementRef,
private tooltipIdService: TooltipIdService
private tooltipIdService: TooltipIdService,
private tooltipMouseService: TooltipMouseService
) {
super(injector, parentHost);

Expand Down Expand Up @@ -127,6 +129,16 @@ export class ClrTooltipContent extends AbstractPopover implements OnInit {
this.position = this.position || defaultPosition;
}

@HostListener('mouseenter')
private onMouseEnter() {
this.tooltipMouseService.onMouseEnterContent();
}

@HostListener('mouseleave')
private onMouseLeave() {
this.tooltipMouseService.onMouseLeaveContent();
}

private updateCssClass({ oldClass, newClass }: { oldClass: string; newClass: string }) {
this.renderer.removeClass(this.el.nativeElement, oldClass);
this.renderer.addClass(this.el.nativeElement, newClass);
Expand Down
3 changes: 2 additions & 1 deletion projects/angular/src/popover/tooltip/tooltip-trigger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Component } from '@angular/core';
import { ClrPopoverToggleService } from '../../utils/popover/providers/popover-toggle.service';
import { spec, TestContext } from '../../utils/testing/helpers.spec';
import { TooltipIdService } from './providers/tooltip-id.service';
import { TooltipMouseService } from './providers/tooltip-mouse.service';
import { ClrTooltipTrigger } from './tooltip-trigger';
import { ClrTooltipModule } from './tooltip.module';

Expand All @@ -27,7 +28,7 @@ interface TooltipContext extends TestContext<ClrTooltipTrigger, SimpleTest> {
export default function (): void {
describe('TooltipTrigger component', function (this: TooltipContext) {
spec(ClrTooltipTrigger, SimpleTest, ClrTooltipModule, {
providers: [ClrPopoverToggleService, TooltipIdService],
providers: [ClrPopoverToggleService, TooltipIdService, TooltipMouseService],
});

beforeEach(function () {
Expand Down
19 changes: 16 additions & 3 deletions projects/angular/src/popover/tooltip/tooltip-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Subscription } from 'rxjs';

import { ClrPopoverToggleService } from '../../utils/popover/providers/popover-toggle.service';
import { TooltipIdService } from './providers/tooltip-id.service';
import { TooltipMouseService } from './providers/tooltip-mouse.service';

@Directive({
selector: '[clrTooltipTrigger]',
Expand All @@ -23,7 +24,11 @@ export class ClrTooltipTrigger {
ariaDescribedBy: string;
private subs: Subscription[] = [];

constructor(private toggleService: ClrPopoverToggleService, private tooltipIdService: TooltipIdService) {
constructor(
private toggleService: ClrPopoverToggleService,
private tooltipIdService: TooltipIdService,
private tooltipMouseService: TooltipMouseService
) {
// The aria-described by comes from the id of content. It
this.subs.push(this.tooltipIdService.id.subscribe(tooltipId => (this.ariaDescribedBy = tooltipId)));
}
Expand All @@ -32,15 +37,23 @@ export class ClrTooltipTrigger {
this.subs.forEach(sub => sub.unsubscribe());
}

@HostListener('mouseenter')
@HostListener('focus')
showTooltip(): void {
this.toggleService.open = true;
}

@HostListener('mouseleave')
@HostListener('blur')
hideTooltip(): void {
this.toggleService.open = false;
}

@HostListener('mouseenter')
private onMouseEnter() {
this.tooltipMouseService.onMouseEnterTrigger();
}

@HostListener('mouseleave')
private onMouseLeave() {
this.tooltipMouseService.onMouseLeaveTrigger();
}
}
8 changes: 7 additions & 1 deletion projects/angular/src/popover/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import { Component, ElementRef } from '@angular/core';
import { ClrPopoverToggleService } from '../../utils/popover/providers/popover-toggle.service';
import { POPOVER_HOST_ANCHOR } from '../common/popover-host-anchor.token';
import { TooltipIdService } from './providers/tooltip-id.service';
import { TooltipMouseService } from './providers/tooltip-mouse.service';

@Component({
selector: 'clr-tooltip',
template: `<ng-content></ng-content>`,
host: {
'[class.tooltip]': 'true',
},
providers: [ClrPopoverToggleService, { provide: POPOVER_HOST_ANCHOR, useExisting: ElementRef }, TooltipIdService],
providers: [
ClrPopoverToggleService,
{ provide: POPOVER_HOST_ANCHOR, useExisting: ElementRef },
TooltipIdService,
TooltipMouseService,
],
})
export class ClrTooltip {}