diff --git a/src/material-experimental/mdc-tooltip/tooltip.spec.ts b/src/material-experimental/mdc-tooltip/tooltip.spec.ts index 2bde17aa835c..b8d833e7588e 100644 --- a/src/material-experimental/mdc-tooltip/tooltip.spec.ts +++ b/src/material-experimental/mdc-tooltip/tooltip.spec.ts @@ -40,6 +40,7 @@ import { TOOLTIP_PANEL_CLASS, MAT_TOOLTIP_DEFAULT_OPTIONS, TooltipTouchGestures, + TooltipPosition, } from './index'; @@ -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', () => { diff --git a/src/material/tooltip/tooltip.md b/src/material/tooltip/tooltip.md index c158b711084a..b1c548a4bd6a 100644 --- a/src/material/tooltip/tooltip.md +++ b/src/material/tooltip/tooltip.md @@ -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`. diff --git a/src/material/tooltip/tooltip.spec.ts b/src/material/tooltip/tooltip.spec.ts index b5ef3c043d84..3d10f82891cc 100644 --- a/src/material/tooltip/tooltip.spec.ts +++ b/src/material/tooltip/tooltip.spec.ts @@ -40,6 +40,7 @@ import { TOOLTIP_PANEL_CLASS, MAT_TOOLTIP_DEFAULT_OPTIONS, TooltipTouchGestures, + TooltipPosition, } from './index'; @@ -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', () => { diff --git a/src/material/tooltip/tooltip.ts b/src/material/tooltip/tooltip.ts index 64c1ac6dbe2c..69a6886cfab3 100644 --- a/src/material/tooltip/tooltip.ts +++ b/src/material/tooltip/tooltip.ts @@ -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'; @@ -141,6 +142,7 @@ export abstract class _MatTooltipBase implement protected abstract readonly _tooltipComponent: ComponentType; 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') @@ -396,6 +398,8 @@ export abstract class _MatTooltipBase 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 @@ -559,6 +563,39 @@ export abstract class _MatTooltipBase 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.