-
Notifications
You must be signed in to change notification settings - Fork 6.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(tooltip): initial tooltip implementation #799
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you copy the comment from my dialog PR? |
||
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'; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we make this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving for now (currently, most of Material uses hyphens) - need to discuss a standard convention. |
||
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pulled this from the |
||
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]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once #843 goes in, the imports for jasmine functions (
it
,beforeEach
, etc.) go away. AlsobeforeEachProviders
andprovide
go away.