Skip to content

Commit

Permalink
feat(tooltip): add class to tooltip element based on the current posi…
Browse files Browse the repository at this point in the history
…tion

Adds a class on the tooltip overlay element that indicates the current position of the tooltip. This allows for the tooltip to be customized to add position-based arrows or box shadows.

Fixes #15216.
  • Loading branch information
crisbeto committed Aug 7, 2019
1 parent dd42956 commit ece32a6
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 6 deletions.
17 changes: 11 additions & 6 deletions src/material/tooltip/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,32 @@ the positions `before` and `after` should be used instead of `left` and `right`,
| Position | Description |
|-----------|--------------------------------------------------------------------------------------|
| `above` | Always display above the element |
| `below ` | Always display beneath the element |
| `below` | Always display beneath the element |
| `left` | Always display to the left of the element |
| `right` | Always display to the right of the element |
| `before` | Display to the left in left-to-right layout and to the right in right-to-left layout |
| `after` | Display to the right in left-to-right layout and to the left in right-to-left layout|
| `after` | Display to the right in left-to-right layout and to the left in right-to-left layout |

Based on the position in which the tooltip is shown, the `.mat-tooltip-panel` element will receive a
CSS class that can be used for style (e.g. to add an arrow). The possible classes are
`mat-tooltip-panel-above`, `mat-tooltip-panel-below`, `mat-tooltip-panel-left`,
`mat-tooltip-panel-right`.

<!-- example(tooltip-position) -->

### Showing and hiding

By default, the tooltip will be immediately shown when the user's mouse hovers over the tooltip's
trigger element and immediately hides when the user's mouse leaves.
trigger element and immediately hides when the user's mouse leaves.

On mobile, the tooltip is displayed when the user longpresses the element and hides after a
delay of 1500ms. The longpress behavior requires HammerJS to be loaded on the page. To learn more
about adding HammerJS to your app, check out the Gesture Support section of the Getting Started
about adding HammerJS to your app, check out the Gesture Support section of the Getting Started
guide.

#### Show and hide delays

To add a delay before showing or hiding the tooltip, you can use the inputs `matTooltipShowDelay`
To add a delay before showing or hiding the tooltip, you can use the inputs `matTooltipShowDelay`
and `matTooltipHideDelay` to provide a delay time in milliseconds.

The following example has a tooltip that waits one second to display after the user
Expand All @@ -58,7 +63,7 @@ which both accept a number in milliseconds to delay before applying the display

#### Disabling the tooltip from showing

To completely disable a tooltip, set `matTooltipDisabled`. While disabled, a tooltip will never be
To completely disable a tooltip, set `matTooltipDisabled`. While disabled, a tooltip will never be
shown.

### Accessibility
Expand Down
75 changes: 75 additions & 0 deletions src/material/tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SCROLL_THROTTLE_MS,
TOOLTIP_PANEL_CLASS,
MAT_TOOLTIP_DEFAULT_OPTIONS,
TooltipPosition,
} from './index';


Expand Down Expand Up @@ -719,6 +720,80 @@ describe('MatTooltip', () => {
expect(overlayRef.detach).not.toHaveBeenCalled();
}));

it('should set a class on the overlay panel that reflects the position', fakeAsync(() => {
// Move the element so that the primary position is always used.
buttonElement.style.position = 'fixed';
buttonElement.style.top = buttonElement.style.left = '200px';

fixture.componentInstance.message = 'hi';
fixture.detectChanges();
setPositionAndShow('below');

const classList = tooltipDirective._overlayRef!.overlayElement.classList;
expect(classList).toContain('mat-tooltip-panel-below');

setPositionAndShow('above');
expect(classList).not.toContain('mat-tooltip-panel-below');
expect(classList).toContain('mat-tooltip-panel-above');

setPositionAndShow('left');
expect(classList).not.toContain('mat-tooltip-panel-above');
expect(classList).toContain('mat-tooltip-panel-left');

setPositionAndShow('right');
expect(classList).not.toContain('mat-tooltip-panel-left');
expect(classList).toContain('mat-tooltip-panel-right');

function setPositionAndShow(position: TooltipPosition) {
tooltipDirective.hide(0);
fixture.detectChanges();
tick(0);
tooltipDirective.position = position;
tooltipDirective.show(0);
fixture.detectChanges();
tick(0);
fixture.detectChanges();
tick(500);
}
}));

it('should account for RTL when setting the tooltip position class', fakeAsync(() => {
// Move the element so that the primary position is always used.
buttonElement.style.position = 'fixed';
buttonElement.style.top = buttonElement.style.left = '200px';
fixture.componentInstance.message = 'hi';
fixture.detectChanges();

dir.value = 'ltr';
tooltipDirective.position = 'after';
tooltipDirective.show(0);
fixture.detectChanges();
tick(0);
fixture.detectChanges();
tick(500);

const classList = tooltipDirective._overlayRef!.overlayElement.classList;
expect(classList).not.toContain('mat-tooltip-panel-after');
expect(classList).not.toContain('mat-tooltip-panel-before');
expect(classList).not.toContain('mat-tooltip-panel-left');
expect(classList).toContain('mat-tooltip-panel-right');

tooltipDirective.hide(0);
fixture.detectChanges();
tick(0);
dir.value = 'rtl';
tooltipDirective.show(0);
fixture.detectChanges();
tick(0);
fixture.detectChanges();
tick(500);

expect(classList).not.toContain('mat-tooltip-panel-after');
expect(classList).not.toContain('mat-tooltip-panel-before');
expect(classList).not.toContain('mat-tooltip-panel-right');
expect(classList).toContain('mat-tooltip-panel-left');
}));

});

describe('fallback positions', () => {
Expand Down
37 changes: 37 additions & 0 deletions src/material/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
OverlayRef,
ScrollStrategy,
VerticalConnectionPos,
ConnectionPositionPair,
} from '@angular/cdk/overlay';
import {Platform} from '@angular/cdk/platform';
import {ComponentPortal} from '@angular/cdk/portal';
Expand Down Expand Up @@ -127,6 +128,7 @@ export class MatTooltip implements OnDestroy, OnInit {
private _disabled: boolean = false;
private _tooltipClass: string|string[]|Set<string>|{[key: string]: any};
private _scrollStrategy: () => ScrollStrategy;
private _currentPosition: TooltipPosition;

/** Allows the user to define the position of the tooltip relative to the parent element */
@Input('matTooltipPosition')
Expand Down Expand Up @@ -361,6 +363,8 @@ export class MatTooltip implements OnDestroy, OnInit {
.withScrollableContainers(scrollableAncestors);

strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => {
this._updateCurrentPositionClass(change.connectionPair);

if (this._tooltipInstance) {
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
// After position changes occur and the overlay is clipped by
Expand Down Expand Up @@ -518,6 +522,39 @@ export class MatTooltip implements OnDestroy, OnInit {

return {x, y};
}

/** Updates the class on the overlay panel based on the current position of the tooltip. */
private _updateCurrentPositionClass(connectionPair: ConnectionPositionPair): void {
const {overlayY, originX, originY} = connectionPair;
let newPosition: TooltipPosition;

// If the overlay is in the middle along the Y axis,
// it means that it's either before or after.
if (overlayY === 'center') {
// Note that since this information is used for styling, we want to
// resolve `start` and `end` to their real values, otherwise consumers
// would have to remember to do it themselves on each consumption.
if (this._dir && this._dir.value === 'rtl') {
newPosition = originX === 'end' ? 'left' : 'right';
} else {
newPosition = originX === 'start' ? 'left' : 'right';
}
} else {
newPosition = overlayY === 'bottom' && originY === 'top' ? 'above' : 'below';
}

if (newPosition !== this._currentPosition) {
const overlayRef = this._overlayRef;

if (overlayRef) {
const classPrefix = 'mat-tooltip-panel-';
overlayRef.removePanelClass(classPrefix + this._currentPosition);
overlayRef.addPanelClass(classPrefix + newPosition);
}

this._currentPosition = newPosition;
}
}
}

export type TooltipVisibility = 'initial' | 'visible' | 'hidden';
Expand Down

0 comments on commit ece32a6

Please sign in to comment.