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 Jul 26, 2020
1 parent 89b5fa8 commit ead811b
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 2 deletions.
9 changes: 7 additions & 2 deletions src/material/tooltip/tooltip.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ 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) -->

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 @@ -39,6 +39,7 @@ import {
TOOLTIP_PANEL_CLASS,
MAT_TOOLTIP_DEFAULT_OPTIONS,
TooltipTouchGestures,
TooltipPosition,
} from './index';


Expand Down Expand Up @@ -731,6 +732,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, normalizePassiveListenerOptions} from '@angular/cdk/platform';
import {ComponentPortal} from '@angular/cdk/portal';
Expand Down Expand Up @@ -145,6 +146,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
private _tooltipClass: string|string[]|Set<string>|{[key: string]: any};
private _scrollStrategy: () => ScrollStrategy;
private _viewInitialized = false;
private _currentPosition: TooltipPosition;

/** Allows the user to define the position of the tooltip relative to the parent element */
@Input('matTooltipPosition')
Expand Down Expand Up @@ -390,6 +392,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
.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 @@ -548,6 +552,39 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
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;
}
}

/** Binds the pointer events to the tooltip trigger. */
private _setupPointerEvents() {
// Optimization: Defer hooking up events if there's no message or the tooltip is disabled.
Expand Down

0 comments on commit ead811b

Please sign in to comment.