Skip to content

Commit 160f134

Browse files
committed
feat(tooltip): add class to tooltip element based on the current 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 angular#15216.
1 parent 7c49399 commit 160f134

File tree

3 files changed

+119
-2
lines changed

3 files changed

+119
-2
lines changed

src/material/tooltip/tooltip.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ the positions `before` and `after` should be used instead of `left` and `right`,
1414
| Position | Description |
1515
|-----------|--------------------------------------------------------------------------------------|
1616
| `above` | Always display above the element |
17-
| `below ` | Always display beneath the element |
17+
| `below` | Always display beneath the element |
1818
| `left` | Always display to the left of the element |
1919
| `right` | Always display to the right of the element |
2020
| `before` | Display to the left in left-to-right layout and to the right in right-to-left layout |
21-
| `after` | Display to the right in left-to-right layout and to the left in right-to-left layout|
21+
| `after` | Display to the right in left-to-right layout and to the left in right-to-left layout |
22+
23+
Based on the position in which the tooltip is shown, the `.mat-tooltip-panel` element will receive a
24+
CSS class that can be used for style (e.g. to add an arrow). The possible classes are
25+
`mat-tooltip-panel-above`, `mat-tooltip-panel-below`, `mat-tooltip-panel-left`,
26+
`mat-tooltip-panel-right`.
2227

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

src/material/tooltip/tooltip.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
TOOLTIP_PANEL_CLASS,
4040
MAT_TOOLTIP_DEFAULT_OPTIONS,
4141
TooltipTouchGestures,
42+
TooltipPosition,
4243
} from './index';
4344

4445

@@ -732,6 +733,80 @@ describe('MatTooltip', () => {
732733
expect(overlayRef.detach).not.toHaveBeenCalled();
733734
}));
734735

736+
it('should set a class on the overlay panel that reflects the position', fakeAsync(() => {
737+
// Move the element so that the primary position is always used.
738+
buttonElement.style.position = 'fixed';
739+
buttonElement.style.top = buttonElement.style.left = '200px';
740+
741+
fixture.componentInstance.message = 'hi';
742+
fixture.detectChanges();
743+
setPositionAndShow('below');
744+
745+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
746+
expect(classList).toContain('mat-tooltip-panel-below');
747+
748+
setPositionAndShow('above');
749+
expect(classList).not.toContain('mat-tooltip-panel-below');
750+
expect(classList).toContain('mat-tooltip-panel-above');
751+
752+
setPositionAndShow('left');
753+
expect(classList).not.toContain('mat-tooltip-panel-above');
754+
expect(classList).toContain('mat-tooltip-panel-left');
755+
756+
setPositionAndShow('right');
757+
expect(classList).not.toContain('mat-tooltip-panel-left');
758+
expect(classList).toContain('mat-tooltip-panel-right');
759+
760+
function setPositionAndShow(position: TooltipPosition) {
761+
tooltipDirective.hide(0);
762+
fixture.detectChanges();
763+
tick(0);
764+
tooltipDirective.position = position;
765+
tooltipDirective.show(0);
766+
fixture.detectChanges();
767+
tick(0);
768+
fixture.detectChanges();
769+
tick(500);
770+
}
771+
}));
772+
773+
it('should account for RTL when setting the tooltip position class', fakeAsync(() => {
774+
// Move the element so that the primary position is always used.
775+
buttonElement.style.position = 'fixed';
776+
buttonElement.style.top = buttonElement.style.left = '200px';
777+
fixture.componentInstance.message = 'hi';
778+
fixture.detectChanges();
779+
780+
dir.value = 'ltr';
781+
tooltipDirective.position = 'after';
782+
tooltipDirective.show(0);
783+
fixture.detectChanges();
784+
tick(0);
785+
fixture.detectChanges();
786+
tick(500);
787+
788+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
789+
expect(classList).not.toContain('mat-tooltip-panel-after');
790+
expect(classList).not.toContain('mat-tooltip-panel-before');
791+
expect(classList).not.toContain('mat-tooltip-panel-left');
792+
expect(classList).toContain('mat-tooltip-panel-right');
793+
794+
tooltipDirective.hide(0);
795+
fixture.detectChanges();
796+
tick(0);
797+
dir.value = 'rtl';
798+
tooltipDirective.show(0);
799+
fixture.detectChanges();
800+
tick(0);
801+
fixture.detectChanges();
802+
tick(500);
803+
804+
expect(classList).not.toContain('mat-tooltip-panel-after');
805+
expect(classList).not.toContain('mat-tooltip-panel-before');
806+
expect(classList).not.toContain('mat-tooltip-panel-right');
807+
expect(classList).toContain('mat-tooltip-panel-left');
808+
}));
809+
735810
});
736811

737812
describe('fallback positions', () => {

src/material/tooltip/tooltip.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
OverlayRef,
2121
ScrollStrategy,
2222
VerticalConnectionPos,
23+
ConnectionPositionPair,
2324
} from '@angular/cdk/overlay';
2425
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
2526
import {ComponentPortal} from '@angular/cdk/portal';
@@ -145,6 +146,7 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
145146
private _tooltipClass: string|string[]|Set<string>|{[key: string]: any};
146147
private _scrollStrategy: () => ScrollStrategy;
147148
private _viewInitialized = false;
149+
private _currentPosition: TooltipPosition;
148150

149151
/** Allows the user to define the position of the tooltip relative to the parent element */
150152
@Input('matTooltipPosition')
@@ -390,6 +392,8 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
390392
.withScrollableContainers(scrollableAncestors);
391393

392394
strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => {
395+
this._updateCurrentPositionClass(change.connectionPair);
396+
393397
if (this._tooltipInstance) {
394398
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
395399
// After position changes occur and the overlay is clipped by
@@ -548,6 +552,39 @@ export class MatTooltip implements OnDestroy, AfterViewInit {
548552
return {x, y};
549553
}
550554

555+
/** Updates the class on the overlay panel based on the current position of the tooltip. */
556+
private _updateCurrentPositionClass(connectionPair: ConnectionPositionPair): void {
557+
const {overlayY, originX, originY} = connectionPair;
558+
let newPosition: TooltipPosition;
559+
560+
// If the overlay is in the middle along the Y axis,
561+
// it means that it's either before or after.
562+
if (overlayY === 'center') {
563+
// Note that since this information is used for styling, we want to
564+
// resolve `start` and `end` to their real values, otherwise consumers
565+
// would have to remember to do it themselves on each consumption.
566+
if (this._dir && this._dir.value === 'rtl') {
567+
newPosition = originX === 'end' ? 'left' : 'right';
568+
} else {
569+
newPosition = originX === 'start' ? 'left' : 'right';
570+
}
571+
} else {
572+
newPosition = overlayY === 'bottom' && originY === 'top' ? 'above' : 'below';
573+
}
574+
575+
if (newPosition !== this._currentPosition) {
576+
const overlayRef = this._overlayRef;
577+
578+
if (overlayRef) {
579+
const classPrefix = 'mat-tooltip-panel-';
580+
overlayRef.removePanelClass(classPrefix + this._currentPosition);
581+
overlayRef.addPanelClass(classPrefix + newPosition);
582+
}
583+
584+
this._currentPosition = newPosition;
585+
}
586+
}
587+
551588
/** Binds the pointer events to the tooltip trigger. */
552589
private _setupPointerEvents() {
553590
// Optimization: Defer hooking up events if there's no message or the tooltip is disabled.

0 commit comments

Comments
 (0)