Skip to content

Commit 1a10027

Browse files
authored
feat(material/tooltip): add class to tooltip element based on the current position (#15217)
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 d0c53ac commit 1a10027

File tree

4 files changed

+194
-2
lines changed

4 files changed

+194
-2
lines changed

src/material-experimental/mdc-tooltip/tooltip.spec.ts

+75
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
TOOLTIP_PANEL_CLASS,
4141
MAT_TOOLTIP_DEFAULT_OPTIONS,
4242
TooltipTouchGestures,
43+
TooltipPosition,
4344
} from './index';
4445

4546

@@ -750,6 +751,80 @@ describe('MDC-based MatTooltip', () => {
750751
expect(overlayRef.detach).not.toHaveBeenCalled();
751752
}));
752753

754+
it('should set a class on the overlay panel that reflects the position', fakeAsync(() => {
755+
// Move the element so that the primary position is always used.
756+
buttonElement.style.position = 'fixed';
757+
buttonElement.style.top = buttonElement.style.left = '200px';
758+
759+
fixture.componentInstance.message = 'hi';
760+
fixture.detectChanges();
761+
setPositionAndShow('below');
762+
763+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
764+
expect(classList).toContain('mat-tooltip-panel-below');
765+
766+
setPositionAndShow('above');
767+
expect(classList).not.toContain('mat-tooltip-panel-below');
768+
expect(classList).toContain('mat-tooltip-panel-above');
769+
770+
setPositionAndShow('left');
771+
expect(classList).not.toContain('mat-tooltip-panel-above');
772+
expect(classList).toContain('mat-tooltip-panel-left');
773+
774+
setPositionAndShow('right');
775+
expect(classList).not.toContain('mat-tooltip-panel-left');
776+
expect(classList).toContain('mat-tooltip-panel-right');
777+
778+
function setPositionAndShow(position: TooltipPosition) {
779+
tooltipDirective.hide(0);
780+
fixture.detectChanges();
781+
tick(0);
782+
tooltipDirective.position = position;
783+
tooltipDirective.show(0);
784+
fixture.detectChanges();
785+
tick(0);
786+
fixture.detectChanges();
787+
tick(500);
788+
}
789+
}));
790+
791+
it('should account for RTL when setting the tooltip position class', fakeAsync(() => {
792+
// Move the element so that the primary position is always used.
793+
buttonElement.style.position = 'fixed';
794+
buttonElement.style.top = buttonElement.style.left = '200px';
795+
fixture.componentInstance.message = 'hi';
796+
fixture.detectChanges();
797+
798+
dir.value = 'ltr';
799+
tooltipDirective.position = 'after';
800+
tooltipDirective.show(0);
801+
fixture.detectChanges();
802+
tick(0);
803+
fixture.detectChanges();
804+
tick(500);
805+
806+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
807+
expect(classList).not.toContain('mat-tooltip-panel-after');
808+
expect(classList).not.toContain('mat-tooltip-panel-before');
809+
expect(classList).not.toContain('mat-tooltip-panel-left');
810+
expect(classList).toContain('mat-tooltip-panel-right');
811+
812+
tooltipDirective.hide(0);
813+
fixture.detectChanges();
814+
tick(0);
815+
dir.value = 'rtl';
816+
tooltipDirective.show(0);
817+
fixture.detectChanges();
818+
tick(0);
819+
fixture.detectChanges();
820+
tick(500);
821+
822+
expect(classList).not.toContain('mat-tooltip-panel-after');
823+
expect(classList).not.toContain('mat-tooltip-panel-before');
824+
expect(classList).not.toContain('mat-tooltip-panel-right');
825+
expect(classList).toContain('mat-tooltip-panel-left');
826+
}));
827+
753828
});
754829

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

src/material/tooltip/tooltip.md

+7-2
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

+75
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
TOOLTIP_PANEL_CLASS,
4141
MAT_TOOLTIP_DEFAULT_OPTIONS,
4242
TooltipTouchGestures,
43+
TooltipPosition,
4344
} from './index';
4445

4546

@@ -749,6 +750,80 @@ describe('MatTooltip', () => {
749750
expect(overlayRef.detach).not.toHaveBeenCalled();
750751
}));
751752

753+
it('should set a class on the overlay panel that reflects the position', fakeAsync(() => {
754+
// Move the element so that the primary position is always used.
755+
buttonElement.style.position = 'fixed';
756+
buttonElement.style.top = buttonElement.style.left = '200px';
757+
758+
fixture.componentInstance.message = 'hi';
759+
fixture.detectChanges();
760+
setPositionAndShow('below');
761+
762+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
763+
expect(classList).toContain('mat-tooltip-panel-below');
764+
765+
setPositionAndShow('above');
766+
expect(classList).not.toContain('mat-tooltip-panel-below');
767+
expect(classList).toContain('mat-tooltip-panel-above');
768+
769+
setPositionAndShow('left');
770+
expect(classList).not.toContain('mat-tooltip-panel-above');
771+
expect(classList).toContain('mat-tooltip-panel-left');
772+
773+
setPositionAndShow('right');
774+
expect(classList).not.toContain('mat-tooltip-panel-left');
775+
expect(classList).toContain('mat-tooltip-panel-right');
776+
777+
function setPositionAndShow(position: TooltipPosition) {
778+
tooltipDirective.hide(0);
779+
fixture.detectChanges();
780+
tick(0);
781+
tooltipDirective.position = position;
782+
tooltipDirective.show(0);
783+
fixture.detectChanges();
784+
tick(0);
785+
fixture.detectChanges();
786+
tick(500);
787+
}
788+
}));
789+
790+
it('should account for RTL when setting the tooltip position class', fakeAsync(() => {
791+
// Move the element so that the primary position is always used.
792+
buttonElement.style.position = 'fixed';
793+
buttonElement.style.top = buttonElement.style.left = '200px';
794+
fixture.componentInstance.message = 'hi';
795+
fixture.detectChanges();
796+
797+
dir.value = 'ltr';
798+
tooltipDirective.position = 'after';
799+
tooltipDirective.show(0);
800+
fixture.detectChanges();
801+
tick(0);
802+
fixture.detectChanges();
803+
tick(500);
804+
805+
const classList = tooltipDirective._overlayRef!.overlayElement.classList;
806+
expect(classList).not.toContain('mat-tooltip-panel-after');
807+
expect(classList).not.toContain('mat-tooltip-panel-before');
808+
expect(classList).not.toContain('mat-tooltip-panel-left');
809+
expect(classList).toContain('mat-tooltip-panel-right');
810+
811+
tooltipDirective.hide(0);
812+
fixture.detectChanges();
813+
tick(0);
814+
dir.value = 'rtl';
815+
tooltipDirective.show(0);
816+
fixture.detectChanges();
817+
tick(0);
818+
fixture.detectChanges();
819+
tick(500);
820+
821+
expect(classList).not.toContain('mat-tooltip-panel-after');
822+
expect(classList).not.toContain('mat-tooltip-panel-before');
823+
expect(classList).not.toContain('mat-tooltip-panel-right');
824+
expect(classList).toContain('mat-tooltip-panel-left');
825+
}));
826+
752827
});
753828

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

src/material/tooltip/tooltip.ts

+37
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
OverlayRef,
2222
ScrollStrategy,
2323
VerticalConnectionPos,
24+
ConnectionPositionPair,
2425
} from '@angular/cdk/overlay';
2526
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
2627
import {ComponentPortal, ComponentType} from '@angular/cdk/portal';
@@ -141,6 +142,7 @@ export abstract class _MatTooltipBase<T extends _TooltipComponentBase> implement
141142
protected abstract readonly _tooltipComponent: ComponentType<T>;
142143
protected abstract readonly _transformOriginSelector: string;
143144
protected _viewportMargin = 8;
145+
private _currentPosition: TooltipPosition;
144146

145147
/** Allows the user to define the position of the tooltip relative to the parent element */
146148
@Input('matTooltipPosition')
@@ -396,6 +398,8 @@ export abstract class _MatTooltipBase<T extends _TooltipComponentBase> implement
396398
.withScrollableContainers(scrollableAncestors);
397399

398400
strategy.positionChanges.pipe(takeUntil(this._destroyed)).subscribe(change => {
401+
this._updateCurrentPositionClass(change.connectionPair);
402+
399403
if (this._tooltipInstance) {
400404
if (change.scrollableViewProperties.isOverlayClipped && this._tooltipInstance.isVisible()) {
401405
// After position changes occur and the overlay is clipped by
@@ -559,6 +563,39 @@ export abstract class _MatTooltipBase<T extends _TooltipComponentBase> implement
559563
return {x, y};
560564
}
561565

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

0 commit comments

Comments
 (0)