feat(tooltip): initial tooltip implementation (#799)

This commit is contained in:
Robert Messerle
2016-07-21 15:46:07 -07:00
committed by GitHub
parent 9a32489d1a
commit f5e2a81eea
13 changed files with 388 additions and 7 deletions
+24
View File
@@ -0,0 +1,24 @@
# MdTooltip
Tooltip allows the user to specify text to be displayed when the mouse hover over an element.
### Examples
A button with a tooltip
```html
<button md-tooltip="some message" tooltip-position="below">Button</button>
```
## `[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. |
### Methods
| Name | Description |
| --- | --- | --- |
| `show` | Displays the tooltip. |
| `hide` | Removes the tooltip. |
| `toggle` | Displays or hides the tooltip. |
View File
+22
View File
@@ -0,0 +1,22 @@
@import 'variables';
@import 'theme-functions';
@import 'palette';
$md-tooltip-height: 22px;
$md-tooltip-margin: 14px;
$md-tooltip-padding: 8px;
:host {
pointer-events: none;
}
.md-tooltip {
background: md-color($md-grey, 700, 0.9);
color: white;
padding: 0 $md-tooltip-padding;
border-radius: 2px;
font-family: $md-font-family;
font-size: 10px;
margin: $md-tooltip-margin;
height: $md-tooltip-height;
line-height: $md-tooltip-height;
}
+101
View File
@@ -0,0 +1,101 @@
import {
it,
describe,
expect,
beforeEach,
inject,
async,
beforeEachProviders,
} from '@angular/core/testing';
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
import {Component, provide, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MD_TOOLTIP_DIRECTIVES, TooltipPosition, MdTooltip} from
'@angular2-material/tooltip/tooltip';
import {OVERLAY_PROVIDERS, OVERLAY_CONTAINER_TOKEN} from '@angular2-material/core/overlay/overlay';
describe('MdTooltip', () => {
let builder: TestComponentBuilder;
let overlayContainerElement: HTMLElement;
beforeEachProviders(() => [
OVERLAY_PROVIDERS,
provide(OVERLAY_CONTAINER_TOKEN, {
useFactory: () => {
overlayContainerElement = document.createElement('div');
return overlayContainerElement;
}
})
]);
beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
builder = tcb;
}));
describe('basic usage', () => {
let fixture: ComponentFixture<BasicTooltipDemo>;
let buttonDebugElement: DebugElement;
let buttonElement: HTMLButtonElement;
let tooltipDirective: MdTooltip;
beforeEach(async(() => {
builder.createAsync(BasicTooltipDemo).then(f => {
fixture = f;
fixture.detectChanges();
buttonDebugElement = fixture.debugElement.query(By.css('button'));
buttonElement = <HTMLButtonElement> buttonDebugElement.nativeElement;
tooltipDirective = buttonDebugElement.injector.get(MdTooltip);
});
}));
it('should show/hide on mouse enter/leave', async(() => {
expect(tooltipDirective.visible).toBeFalsy();
tooltipDirective._handleMouseEnter(null);
expect(tooltipDirective.visible).toBeTruthy();
fixture.detectChanges();
whenStable([
() => {
expect(overlayContainerElement.textContent).toBe('some message');
tooltipDirective._handleMouseLeave(null);
},
() => {
expect(overlayContainerElement.textContent).toBe('');
}
]);
}));
/**
* Utility function to make it easier to use multiple `whenStable` checks.
* Accepts an array of callbacks, each to wait for stability before running.
* TODO: Remove the `setTimeout()` when a viable alternative is available
* @param callbacks
*/
function whenStable(callbacks: Array<Function>) {
if (callbacks.length) {
fixture.detectChanges();
fixture.whenStable().then(() => {
// TODO(jelbourn): figure out why the test zone is "stable" when there are still pending
// tasks, such that we have to use `setTimeout` to run the second round of change
// detection. Two rounds of change detection are necessary: one to *create* the tooltip,
// and another to cause the lifecycle events of the tooltip to run and load the tooltip
// content.
setTimeout(() => {
callbacks[0]();
whenStable(callbacks.slice(1));
}, 50);
});
}
}
});
});
@Component({
selector: 'app',
directives: [MD_TOOLTIP_DIRECTIVES],
template: `<button md-tooltip="some message" [tooltip-position]="position">Button</button>`
})
class BasicTooltipDemo {
position: TooltipPosition = 'below';
}
+183
View File
@@ -0,0 +1,183 @@
import {Component, ComponentRef, Directive, Input, ElementRef, ViewContainerRef,
ChangeDetectorRef} from '@angular/core';
import {Overlay} from '@angular2-material/core/overlay/overlay';
import {OverlayState} from '@angular2-material/core/overlay/overlay-state';
import {OverlayRef} from '@angular2-material/core/overlay/overlay-ref';
import {ComponentPortal} from '@angular2-material/core/portal/portal';
import {OverlayConnectionPosition, OriginConnectionPosition} from
'@angular2-material/core/overlay/position/connected-position';
export type TooltipPosition = 'before' | 'after' | 'above' | 'below';
@Directive({
selector: '[md-tooltip]',
host: {
'(mouseenter)': '_handleMouseEnter($event)',
'(mouseleave)': '_handleMouseLeave($event)',
}
})
export class MdTooltip {
visible: boolean = false;
/** Allows the user to define the position of the tooltip relative to the parent element */
private _position: TooltipPosition = 'below';
@Input('tooltip-position') get position(): TooltipPosition {
return this._position;
}
set position(value: TooltipPosition) {
if (value !== this._position) {
this._position = value;
this._createOverlay();
this._updatePosition();
}
}
/** The message to be displayed in the tooltip */
private _message: string;
@Input('md-tooltip') get message() {
return this._message;
}
set message(value: string) {
this._message = value;
this._updatePosition();
}
private _overlayRef: OverlayRef;
constructor(private _overlay: Overlay, private _elementRef: ElementRef,
private _viewContainerRef: ViewContainerRef,
private _changeDetectionRef: ChangeDetectorRef) {}
/**
* Create overlay on init
* TODO: @internal
*/
ngOnInit() {
this._createOverlay();
}
/**
* Create the overlay config and position strategy
*/
private _createOverlay() {
if (this._overlayRef) {
if (this.visible) {
// if visible, hide before destroying
this.hide().then(() => this._createOverlay());
} else {
// if not visible, dispose and recreate
this._overlayRef.dispose();
this._overlayRef = null;
this._createOverlay();
}
} else {
let origin = this._getOrigin();
let position = this._getOverlayPosition();
let strategy = this._overlay.position().connectedTo(this._elementRef, origin, position);
let config = new OverlayState();
config.positionStrategy = strategy;
this._overlay.create(config).then(ref => {
this._overlayRef = ref;
});
}
}
/**
* 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' };
}
}
/**
* 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' };
}
}
/**
* Shows the tooltip on mouse enter
* @param event
*/
_handleMouseEnter(event: MouseEvent) {
this.show();
}
/**
* Hides the tooltip on mouse leave
* @param event
*/
_handleMouseLeave(event: MouseEvent) {
this.hide();
}
/**
* Shows the tooltip and returns a promise that will resolve when the tooltip is visible
*/
show(): Promise<any> {
if (!this.visible && this._overlayRef && !this._overlayRef.hasAttached()) {
this.visible = true;
let promise = this._overlayRef.attach(new ComponentPortal(TooltipComponent,
this._viewContainerRef));
promise.then((ref: ComponentRef<TooltipComponent>) => {
ref.instance.message = this.message;
this._updatePosition();
});
return promise;
}
}
/**
* Hides the tooltip and returns a promise that will resolve when the tooltip is hidden
*/
hide(): Promise<any> {
if (this.visible && this._overlayRef && this._overlayRef.hasAttached()) {
this.visible = false;
return this._overlayRef.detach();
}
}
/**
* Shows/hides the tooltip and returns a promise that will resolve when it is done
*/
toggle(): Promise<any> {
if (this.visible) {
return this.hide();
} else {
return this.show();
}
}
/**
* Updates the tooltip's position
*/
private _updatePosition() {
if (this._overlayRef) {
this._changeDetectionRef.detectChanges();
this._overlayRef.updatePosition();
}
}
}
@Component({
moduleId: module.id,
selector: 'md-tooltip-component',
template: `<div class="md-tooltip">{{message}}</div>`,
styleUrls: ['tooltip.css'],
})
class TooltipComponent {
message: string;
}
export const MD_TOOLTIP_DIRECTIVES = [MdTooltip];
+2 -2
View File
@@ -17,7 +17,7 @@ export class OverlayRef implements PortalHost {
// Don't chain the .then() call in the return because we want the result of portalHost.attach
// to be returned from this method.
attachPromise.then(() => {
this._updatePosition();
this.updatePosition();
});
return attachPromise;
@@ -41,7 +41,7 @@ export class OverlayRef implements PortalHost {
}
/** Updates the position of the overlay based on the position strategy. */
private _updatePosition() {
updatePosition() {
if (this._state.positionStrategy) {
this._state.positionStrategy.apply(this._pane);
}
+3 -2
View File
@@ -15,14 +15,15 @@
<a md-list-item [routerLink]="['live-announcer']">Live Announcer</a>
<a md-list-item [routerLink]="['overlay']">Overlay</a>
<a md-list-item [routerLink]="['portal']">Portal</a>
<a md-list-item [routerLink]="['progress-circle']">Progress Circle</a>
<a md-list-item [routerLink]="['progress-bar']">Progress Bar</a>
<a md-list-item [routerLink]="['progress-circle']">Progress Circle</a>
<a md-list-item [routerLink]="['radio']">Radio</a>
<a md-list-item [routerLink]="['sidenav']">Sidenav</a>
<a md-list-item [routerLink]="['slider']">Slider</a>
<a md-list-item [routerLink]="['slide-toggle']">Slide Toggle</a>
<a md-list-item [routerLink]="['toolbar']">Toolbar</a>
<a md-list-item [routerLink]="['tabs']">Tabs</a>
<a md-list-item [routerLink]="['toolbar']">Toolbar</a>
<a md-list-item [routerLink]="['tooltip']">Tooltip</a>
<hr>
<a md-list-item [routerLink]="['baseline']">Baseline</a>
</md-nav-list>
+2 -1
View File
@@ -23,7 +23,7 @@ import {RadioDemo} from '../radio/radio-demo';
import {CardDemo} from '../card/card-demo';
import {MenuDemo} from '../menu/menu-demo';
import {DialogDemo} from '../dialog/dialog-demo';
import {TooltipDemo} from '../tooltip/tooltip-demo';
export const routes: RouterConfig = [
@@ -51,6 +51,7 @@ export const routes: RouterConfig = [
{path: 'button-toggle', component: ButtonToggleDemo},
{path: 'baseline', component: BaselineDemo},
{path: 'dialog', component: DialogDemo},
{path: 'tooltip', component: TooltipDemo},
];
export const DEMO_APP_ROUTE_PROVIDER = provideRouter(routes);
+2 -1
View File
@@ -20,7 +20,8 @@ const components = [
'slide-toggle',
'button-toggle',
'tabs',
'toolbar'
'toolbar',
'tooltip',
];
/** Map relative paths to URLs. */
+20
View File
@@ -0,0 +1,20 @@
<div class="demo-tooltip">
<h1>Tooltip Demo</h1>
<p class="centered">
<button
md-raised-button
color="primary"
md-tooltip="to see a tooltip"
[tooltip-position]="position">
Mouse over this link
</button>
</p>
<p>
<md-radio-group [(ngModel)]="position">
<md-radio-button value="below">Below</md-radio-button>
<md-radio-button value="above">Above</md-radio-button>
<md-radio-button value="before">Before</md-radio-button>
<md-radio-button value="after">After</md-radio-button>
</md-radio-group>
</p>
</div>
+8
View File
@@ -0,0 +1,8 @@
.demo-tooltip {
.centered {
text-align: center;
}
md-radio-button {
display: block;
}
}
+19
View File
@@ -0,0 +1,19 @@
import {Component} from '@angular/core';
import {MD_TOOLTIP_DIRECTIVES, TooltipPosition} from '@angular2-material/tooltip/tooltip';
import {OVERLAY_PROVIDERS} from '@angular2-material/core/overlay/overlay';
import {MD_RADIO_DIRECTIVES} from '@angular2-material/radio/radio';
import {MdUniqueSelectionDispatcher} from
'@angular2-material/core/coordination/unique-selection-dispatcher';
import {MD_BUTTON_DIRECTIVES} from '@angular2-material/button/button';
@Component({
moduleId: module.id,
selector: 'tooltip-demo',
templateUrl: 'tooltip-demo.html',
styleUrls: ['tooltip-demo.css'],
directives: [MD_TOOLTIP_DIRECTIVES, MD_RADIO_DIRECTIVES, MD_BUTTON_DIRECTIVES],
providers: [OVERLAY_PROVIDERS, MdUniqueSelectionDispatcher],
})
export class TooltipDemo {
position: TooltipPosition = 'below';
}
+2 -1
View File
@@ -20,7 +20,8 @@ const components = [
'sidenav',
'slide-toggle',
'tabs',
'toolbar'
'toolbar',
'tooltip',
];