mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(tooltip): initial tooltip implementation (#799)
This commit is contained in:
@@ -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. |
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,7 +20,8 @@ const components = [
|
||||
'slide-toggle',
|
||||
'button-toggle',
|
||||
'tabs',
|
||||
'toolbar'
|
||||
'toolbar',
|
||||
'tooltip',
|
||||
];
|
||||
|
||||
/** Map relative paths to URLs. */
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
.demo-tooltip {
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
md-radio-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -20,7 +20,8 @@ const components = [
|
||||
'sidenav',
|
||||
'slide-toggle',
|
||||
'tabs',
|
||||
'toolbar'
|
||||
'toolbar',
|
||||
'tooltip',
|
||||
];
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user