Skip to content

Commit

Permalink
feat(material/tooltip): add class to tooltip element based on the cur…
Browse files Browse the repository at this point in the history
…rent position

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 Feb 9, 2021
1 parent 0930dc3 commit 325c4b2
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 2 deletions.
75 changes: 75 additions & 0 deletions src/material-experimental/mdc-tooltip/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
TOOLTIP_PANEL_CLASS,
MAT_TOOLTIP_DEFAULT_OPTIONS,
TooltipTouchGestures,
TooltipPosition,
} from './index';


Expand Down Expand Up @@ -750,6 +751,80 @@ describe('MDC-based 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
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 @@ -40,6 +40,7 @@ import {
TOOLTIP_PANEL_CLASS,
MAT_TOOLTIP_DEFAULT_OPTIONS,
TooltipTouchGestures,
TooltipPosition,
} from './index';


Expand Down Expand Up @@ -749,6 +750,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 @@ -21,6 +21,7 @@ import {
OverlayRef,
ScrollStrategy,
VerticalConnectionPos,
ConnectionPositionPair,
} from '@angular/cdk/overlay';
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
Expand Down Expand Up @@ -141,6 +142,7 @@ export abstract class _MatTooltipBase<T extends _TooltipComponentBase> implement
protected abstract readonly _tooltipComponent: ComponentType<T>;
protected abstract readonly _transformOriginSelector: string;
protected _viewportMargin = 8;
private _currentPosition: TooltipPosition;

/** Allows the user to define the position of the tooltip relative to the parent element */
@Input('matTooltipPosition')
Expand Down Expand Up @@ -396,6 +398,8 @@ export abstract class _MatTooltipBase<T extends _TooltipComponentBase> implement
.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 @@ -559,6 +563,39 @@ export abstract class _MatTooltipBase<T extends _TooltipComponentBase> implement
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 _setupPointerEnterEventsIfNeeded() {
// Optimization: Defer hooking up events if there's no message or the tooltip is disabled.
Expand Down

0 comments on commit 325c4b2

Please sign in to comment.