From 54bf6ceb1af9d071b5f5cb10873107578bd89c70 Mon Sep 17 00:00:00 2001 From: Andrew Seguin Date: Mon, 5 Dec 2016 13:51:19 -0800 Subject: [PATCH] fix(tooltip) add RTL support (#1830) * feat(tooltip): support rtl * remove fdescribe * more changes for before, after * fix test * rebase * minor change --- src/demo-app/tooltip/tooltip-demo.html | 2 + src/lib/tooltip/README.md | 12 ++-- src/lib/tooltip/tooltip.spec.ts | 76 +++++++++++++++++++++++++- src/lib/tooltip/tooltip.ts | 69 ++++++++++++++++------- 4 files changed, 135 insertions(+), 24 deletions(-) diff --git a/src/demo-app/tooltip/tooltip-demo.html b/src/demo-app/tooltip/tooltip-demo.html index c96a266f0be..6da3a8b8efc 100644 --- a/src/demo-app/tooltip/tooltip-demo.html +++ b/src/demo-app/tooltip/tooltip-demo.html @@ -15,6 +15,8 @@ Below Above + Left + Right Before After diff --git a/src/lib/tooltip/README.md b/src/lib/tooltip/README.md index a13bd047a58..033404c7528 100644 --- a/src/lib/tooltip/README.md +++ b/src/lib/tooltip/README.md @@ -1,24 +1,28 @@ # MdTooltip -Tooltip allows the user to specify text to be displayed when the mouse hover over an element. +Tooltip allows the user to specify text to be displayed when the mouse hovers over an element. + +The positions `before` and `after` should be used instead of `left` and `right` respectively when the tooltip positioning should change depending on the HTML dir global attribute for left-to-right or right-to-left layout. ### Examples A button with a tooltip ```html - + ``` + + ## `[md-tooltip]` ### Properties | Name | Type | Description | | --- | --- | --- | | `md-tooltip` | `string` | The message to be displayed. | -| `tooltip-position` | `"above"|"below"|"before"|"after"` | The position of the tooltip. | +| `tooltip-position` | `"before"|"after"|"above"|"below"|"left"|"right"` | The position of the tooltip. | ### Methods | Name | Description | -| --- | --- | --- | +| --- | --- | | `show` | Displays the tooltip. | | `hide` | Removes the tooltip. | | `toggle` | Displays or hides the tooltip. | diff --git a/src/lib/tooltip/tooltip.spec.ts b/src/lib/tooltip/tooltip.spec.ts index 05e894fc504..6421cde3f52 100644 --- a/src/lib/tooltip/tooltip.spec.ts +++ b/src/lib/tooltip/tooltip.spec.ts @@ -10,12 +10,13 @@ import {Component, DebugElement, AnimationTransitionEvent} from '@angular/core'; import {By} from '@angular/platform-browser'; import {TooltipPosition, MdTooltip, MdTooltipModule} from './tooltip'; import {OverlayContainer} from '../core'; +import {Dir, LayoutDirection} from '../core/rtl/dir'; const initialTooltipMessage = 'initial tooltip message'; describe('MdTooltip', () => { let overlayContainerElement: HTMLElement; - + let dir: {value: LayoutDirection}; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -25,6 +26,9 @@ describe('MdTooltip', () => { {provide: OverlayContainer, useFactory: () => { overlayContainerElement = document.createElement('div'); return {getContainerElement: () => overlayContainerElement}; + }}, + {provide: Dir, useFactory: () => { + return dir = { value: 'ltr' }; }} ] }); @@ -155,6 +159,76 @@ describe('MdTooltip', () => { phaseName: '', })); })); + + it('should consistently position before and after overlay origin in ltr and rtl dir', () => { + tooltipDirective.position = 'left'; + const leftOrigin = tooltipDirective._getOrigin(); + tooltipDirective.position = 'right'; + const rightOrigin = tooltipDirective._getOrigin(); + + // Test expectations in LTR + tooltipDirective.position = 'before'; + expect(tooltipDirective._getOrigin()).toEqual(leftOrigin); + tooltipDirective.position = 'after'; + expect(tooltipDirective._getOrigin()).toEqual(rightOrigin); + + // Test expectations in LTR + dir.value = 'rtl'; + tooltipDirective.position = 'before'; + expect(tooltipDirective._getOrigin()).toEqual(rightOrigin); + tooltipDirective.position = 'after'; + expect(tooltipDirective._getOrigin()).toEqual(leftOrigin); + }); + + it('should consistently position before and after overlay position in ltr and rtl dir', () => { + tooltipDirective.position = 'left'; + const leftOverlayPosition = tooltipDirective._getOverlayPosition(); + tooltipDirective.position = 'right'; + const rightOverlayPosition = tooltipDirective._getOverlayPosition(); + + // Test expectations in LTR + tooltipDirective.position = 'before'; + expect(tooltipDirective._getOverlayPosition()).toEqual(leftOverlayPosition); + tooltipDirective.position = 'after'; + expect(tooltipDirective._getOverlayPosition()).toEqual(rightOverlayPosition); + + // Test expectations in LTR + dir.value = 'rtl'; + tooltipDirective.position = 'before'; + expect(tooltipDirective._getOverlayPosition()).toEqual(rightOverlayPosition); + tooltipDirective.position = 'after'; + expect(tooltipDirective._getOverlayPosition()).toEqual(leftOverlayPosition); + }); + + it('should have consistent left transform origin in any dir', () => { + tooltipDirective.position = 'right'; + tooltipDirective.show(); + expect(tooltipDirective._tooltipInstance._transformOrigin).toBe('left'); + + tooltipDirective.position = 'after'; + tooltipDirective.show(); + expect(tooltipDirective._tooltipInstance._transformOrigin).toBe('left'); + + dir.value = 'rtl'; + tooltipDirective.position = 'before'; + tooltipDirective.show(); + expect(tooltipDirective._tooltipInstance._transformOrigin).toBe('left'); + }); + + it('should have consistent right transform origin in any dir', () => { + tooltipDirective.position = 'left'; + tooltipDirective.show(); + expect(tooltipDirective._tooltipInstance._transformOrigin).toBe('right'); + + tooltipDirective.position = 'before'; + tooltipDirective.show(); + expect(tooltipDirective._tooltipInstance._transformOrigin).toBe('right'); + + dir.value = 'rtl'; + tooltipDirective.position = 'after'; + tooltipDirective.show(); + expect(tooltipDirective._tooltipInstance._transformOrigin).toBe('right'); + }); }); }); diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts index a9621d78840..cd46c7891f3 100644 --- a/src/lib/tooltip/tooltip.ts +++ b/src/lib/tooltip/tooltip.ts @@ -13,6 +13,7 @@ import { animate, AnimationTransitionEvent, NgZone, + Optional, } from '@angular/core'; import { Overlay, @@ -27,9 +28,9 @@ import { } from '../core'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; +import {Dir} from '../core/rtl/dir'; - -export type TooltipPosition = 'before' | 'after' | 'above' | 'below'; +export type TooltipPosition = 'left' | 'right' | 'above' | 'below' | 'before' | 'after'; /** Time in ms to delay before changing the tooltip visibility to hidden */ export const TOUCHEND_HIDE_DELAY = 1500; @@ -85,7 +86,8 @@ export class MdTooltip { } constructor(private _overlay: Overlay, private _elementRef: ElementRef, - private _viewContainerRef: ViewContainerRef, private _ngZone: NgZone) {} + private _viewContainerRef: ViewContainerRef, private _ngZone: NgZone, + @Optional() private _dir: Dir) {} /** Dispose the tooltip when destroyed */ ngOnDestroy() { @@ -155,22 +157,46 @@ export class MdTooltip { } /** Returns the origin position based on the user's position preference */ - private _getOrigin(): OriginConnectionPosition { - switch (this.position) { - case 'before': return { originX: 'start', originY: 'center' }; - case 'after': return { originX: 'end', originY: 'center' }; - case 'above': return { originX: 'center', originY: 'top' }; - case 'below': return { originX: 'center', originY: 'bottom' }; + _getOrigin(): OriginConnectionPosition { + if (this.position == 'above' || this.position == 'below') { + return {originX: 'center', originY: this.position == 'above' ? 'top' : 'bottom'}; + } + + const isDirectionLtr = !this._dir || this._dir.value == 'ltr'; + if (this.position == 'left' || + this.position == 'before' && isDirectionLtr || + this.position == 'after' && !isDirectionLtr) { + return {originX: 'start', originY: 'center'}; + } + + if (this.position == 'right' || + this.position == 'after' && isDirectionLtr || + this.position == 'before' && !isDirectionLtr) { + return {originX: 'end', originY: 'center'}; } } /** Returns the overlay position based on the user's preference */ - private _getOverlayPosition(): OverlayConnectionPosition { - switch (this.position) { - case 'before': return { overlayX: 'end', overlayY: 'center' }; - case 'after': return { overlayX: 'start', overlayY: 'center' }; - case 'above': return { overlayX: 'center', overlayY: 'bottom' }; - case 'below': return { overlayX: 'center', overlayY: 'top' }; + _getOverlayPosition(): OverlayConnectionPosition { + if (this.position == 'above') { + return {overlayX: 'center', overlayY: 'bottom'}; + } + + if (this.position == 'below') { + return {overlayX: 'center', overlayY: 'top'}; + } + + const isLtr = !this._dir || this._dir.value == 'ltr'; + if (this.position == 'left' || + this.position == 'before' && isLtr || + this.position == 'after' && !isLtr) { + return {overlayX: 'end', overlayY: 'center'}; + } + + if (this.position == 'right' || + this.position == 'after' && isLtr || + this.position == 'before' && !isLtr) { + return {overlayX: 'start', overlayY: 'center'}; } } @@ -226,6 +252,8 @@ export class TooltipComponent { /** Subject for notifying that the tooltip has been hidden from the view */ private _onHide: Subject = new Subject(); + constructor(@Optional() private _dir: Dir) {} + /** Shows the tooltip with an animation originating from the provided origin */ show(position: TooltipPosition): void { this._closeOnInteraction = false; @@ -262,11 +290,14 @@ export class TooltipComponent { /** Sets the tooltip transform origin according to the tooltip position */ _setTransformOrigin(value: TooltipPosition) { + const isLtr = !this._dir || this._dir.value == 'ltr'; switch (value) { - case 'before': this._transformOrigin = 'right'; break; - case 'after': this._transformOrigin = 'left'; break; - case 'above': this._transformOrigin = 'bottom'; break; - case 'below': this._transformOrigin = 'top'; break; + case 'before': this._transformOrigin = isLtr ? 'right' : 'left'; break; + case 'after': this._transformOrigin = isLtr ? 'left' : 'right'; break; + case 'left': this._transformOrigin = 'right'; break; + case 'right': this._transformOrigin = 'left'; break; + case 'above': this._transformOrigin = 'bottom'; break; + case 'below': this._transformOrigin = 'top'; break; } }