Skip to content

Commit 68ab64b

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 #15216.
1 parent 3b0f7fc commit 68ab64b

File tree

3 files changed

+123
-6
lines changed

3 files changed

+123
-6
lines changed

src/lib/tooltip/tooltip.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,32 @@ 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

2530
### Showing and hiding
2631

2732
By default, the tooltip will be immediately shown when the user's mouse hovers over the tooltip's
28-
trigger element and immediately hides when the user's mouse leaves.
33+
trigger element and immediately hides when the user's mouse leaves.
2934

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

3540
#### Show and hide delays
3641

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

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

5964
#### Disabling the tooltip from showing
6065

61-
To completely disable a tooltip, set `matTooltipDisabled`. While disabled, a tooltip will never be
66+
To completely disable a tooltip, set `matTooltipDisabled`. While disabled, a tooltip will never be
6267
shown.
6368

6469
### Accessibility

src/lib/tooltip/tooltip.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
SCROLL_THROTTLE_MS,
3737
TOOLTIP_PANEL_CLASS,
3838
MAT_TOOLTIP_DEFAULT_OPTIONS,
39+
TooltipPosition,
3940
} from './index';
4041

4142

@@ -691,6 +692,80 @@ describe('MatTooltip', () => {
691692
expect(overlayRef.detach).not.toHaveBeenCalled();
692693
}));
693694

695+
it('should set a class on the overlay panel that reflects the position', fakeAsync(() => {
696+
// Move the element so that the primary position is always used.
697+
buttonElement.style.position = 'fixed';
698+
buttonElement.style.top = buttonElement.style.left = '200px';
699+
700+
fixture.componentInstance.message = 'hi';
701+
fixture.detectChanges();
702+
setPositionAndShow('below');
703+
704+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
705+
expect(classList).toContain('mat-tooltip-panel-below');
706+
707+
setPositionAndShow('above');
708+
expect(classList).not.toContain('mat-tooltip-panel-below');
709+
expect(classList).toContain('mat-tooltip-panel-above');
710+
711+
setPositionAndShow('left');
712+
expect(classList).not.toContain('mat-tooltip-panel-above');
713+
expect(classList).toContain('mat-tooltip-panel-left');
714+
715+
setPositionAndShow('right');
716+
expect(classList).not.toContain('mat-tooltip-panel-left');
717+
expect(classList).toContain('mat-tooltip-panel-right');
718+
719+
function setPositionAndShow(position: TooltipPosition) {
720+
tooltipDirective.hide(0);
721+
fixture.detectChanges();
722+
tick(0);
723+
tooltipDirective.position = position;
724+
tooltipDirective.show(0);
725+
fixture.detectChanges();
726+
tick(0);
727+
fixture.detectChanges();
728+
tick(500);
729+
}
730+
}));
731+
732+
it('should account for RTL when setting the tooltip position class', fakeAsync(() => {
733+
// Move the element so that the primary position is always used.
734+
buttonElement.style.position = 'fixed';
735+
buttonElement.style.top = buttonElement.style.left = '200px';
736+
fixture.componentInstance.message = 'hi';
737+
fixture.detectChanges();
738+
739+
dir.value = 'ltr';
740+
tooltipDirective.position = 'after';
741+
tooltipDirective.show(0);
742+
fixture.detectChanges();
743+
tick(0);
744+
fixture.detectChanges();
745+
tick(500);
746+
747+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
748+
expect(classList).not.toContain('mat-tooltip-panel-after');
749+
expect(classList).not.toContain('mat-tooltip-panel-before');
750+
expect(classList).not.toContain('mat-tooltip-panel-left');
751+
expect(classList).toContain('mat-tooltip-panel-right');
752+
753+
tooltipDirective.hide(0);
754+
fixture.detectChanges();
755+
tick(0);
756+
dir.value = 'rtl';
757+
tooltipDirective.show(0);
758+
fixture.detectChanges();
759+
tick(0);
760+
fixture.detectChanges();
761+
tick(500);
762+
763+
expect(classList).not.toContain('mat-tooltip-panel-after');
764+
expect(classList).not.toContain('mat-tooltip-panel-before');
765+
expect(classList).not.toContain('mat-tooltip-panel-right');
766+
expect(classList).toContain('mat-tooltip-panel-left');
767+
}));
768+
694769
});
695770

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

src/lib/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} from '@angular/cdk/platform';
2526
import {ComponentPortal} from '@angular/cdk/portal';
@@ -127,6 +128,7 @@ export class MatTooltip implements OnDestroy, OnInit {
127128
private _disabled: boolean = false;
128129
private _tooltipClass: string|string[]|Set<string>|{[key: string]: any};
129130
private _scrollStrategy: () => ScrollStrategy;
131+
private _currentPosition: TooltipPosition;
130132

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

363365
strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => {
366+
this._updateCurrentPositionClass(change.connectionPair);
367+
364368
if (this._tooltipInstance) {
365369
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
366370
// After position changes occur and the overlay is clipped by
@@ -518,6 +522,39 @@ export class MatTooltip implements OnDestroy, OnInit {
518522

519523
return {x, y};
520524
}
525+
526+
/** Updates the class on the overlay panel based on the current position of the tooltip. */
527+
private _updateCurrentPositionClass(connectionPair: ConnectionPositionPair): void {
528+
const {overlayY, originX, originY} = connectionPair;
529+
let newPosition: TooltipPosition;
530+
531+
// If the overlay is in the middle along the Y axis,
532+
// it means that it's either before or after.
533+
if (overlayY === 'center') {
534+
// Note that since this information is used for styling, we want to
535+
// resolve `start` and `end` to their real values, otherwise consumers
536+
// would have to remember to do it themselves on each consumption.
537+
if (this._dir && this._dir.value === 'rtl') {
538+
newPosition = originX === 'end' ? 'left' : 'right';
539+
} else {
540+
newPosition = originX === 'start' ? 'left' : 'right';
541+
}
542+
} else {
543+
newPosition = overlayY === 'bottom' && originY === 'top' ? 'above' : 'below';
544+
}
545+
546+
if (newPosition !== this._currentPosition) {
547+
const overlayRef = this._overlayRef;
548+
549+
if (overlayRef) {
550+
const classPrefix = 'mat-tooltip-panel-';
551+
overlayRef.removePanelClass(classPrefix + this._currentPosition);
552+
overlayRef.addPanelClass(classPrefix + newPosition);
553+
}
554+
555+
this._currentPosition = newPosition;
556+
}
557+
}
521558
}
522559

523560
export type TooltipVisibility = 'initial' | 'visible' | 'hidden';

0 commit comments

Comments
 (0)