From 969688340cb1bf6291a040a36e5a2542c6a68487 Mon Sep 17 00:00:00 2001 From: Enmanuel <8032887+Enlcxx@users.noreply.github.com> Date: Tue, 27 Nov 2018 03:25:11 -0500 Subject: [PATCH] feat(tooltip): initial commit for Tooltip (#66) --- .package.conf.yml | 1 + src/app/components/routes-app.service.ts | 1 + .../basic-tooltip.component.html | 8 + .../basic-tooltip.component.spec.ts | 25 +++ .../basic-tooltip/basic-tooltip.component.ts | 7 + .../basic-tooltip.module.spec.ts | 13 ++ .../basic-tooltip/basic-tooltip.module.ts | 19 +++ .../tooltip-demo/tooltip-demo.component.html | 4 + .../tooltip-demo.component.spec.ts | 25 +++ .../tooltip-demo/tooltip-demo.component.ts | 14 ++ src/app/docs/docs.module.ts | 13 +- src/app/docs/docs.routing.ts | 4 +- src/lib/src/dom/overlay.ts | 15 +- src/lib/src/theme/theme-config.ts | 2 + src/lib/src/theme/variables/tooltip.ts | 5 + src/lib/themes/minima/dark.ts | 6 + src/lib/themes/minima/light.ts | 6 + src/lib/tooltip/index.ts | 1 + src/lib/tooltip/public_api.ts | 2 + src/lib/tooltip/tooltip.module.ts | 8 + src/lib/tooltip/tooltip.ts | 156 ++++++++++++++++++ tsconfig.json | 1 + tslint.json | 1 - 23 files changed, 328 insertions(+), 9 deletions(-) create mode 100644 src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.html create mode 100644 src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.spec.ts create mode 100644 src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.ts create mode 100644 src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.spec.ts create mode 100644 src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.ts create mode 100644 src/app/docs/components/tooltip-demo/tooltip-demo.component.html create mode 100644 src/app/docs/components/tooltip-demo/tooltip-demo.component.spec.ts create mode 100644 src/app/docs/components/tooltip-demo/tooltip-demo.component.ts create mode 100644 src/lib/src/theme/variables/tooltip.ts create mode 100644 src/lib/tooltip/index.ts create mode 100644 src/lib/tooltip/public_api.ts create mode 100644 src/lib/tooltip/tooltip.module.ts create mode 100644 src/lib/tooltip/tooltip.ts diff --git a/.package.conf.yml b/.package.conf.yml index ad5e293a4..cc59779fe 100644 --- a/.package.conf.yml +++ b/.package.conf.yml @@ -21,3 +21,4 @@ components: '@alyle/ui/badge': badge '@alyle/ui/field': field '@alyle/ui/snack-bar': snack-bar + '@alyle/ui/tooltip': tooltip diff --git a/src/app/components/routes-app.service.ts b/src/app/components/routes-app.service.ts index 4204920a9..140f8dfda 100644 --- a/src/app/components/routes-app.service.ts +++ b/src/app/components/routes-app.service.ts @@ -50,6 +50,7 @@ export class RoutesAppService { { route: 'resizing-cropping-images', name: 'Resizing & cropping' }, { route: 'snack-bar', name: 'SnackBar' }, { route: 'toolbar', name: 'Toolbar' }, + { route: 'tooltip', name: 'Tooltip' }, { route: 'typography', name: 'Typography' } ] } diff --git a/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.html b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.html new file mode 100644 index 000000000..889add63d --- /dev/null +++ b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.html @@ -0,0 +1,8 @@ + +
+ diff --git a/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.spec.ts b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.spec.ts new file mode 100644 index 000000000..4063bb110 --- /dev/null +++ b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BasicTooltipComponent } from './basic-tooltip.component'; + +describe('BasicTooltipComponent', () => { + let component: BasicTooltipComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BasicTooltipComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BasicTooltipComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.ts b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.ts new file mode 100644 index 000000000..78352481d --- /dev/null +++ b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'aui-basic-tooltip', + templateUrl: './basic-tooltip.component.html' +}) +export class BasicTooltipComponent { } diff --git a/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.spec.ts b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.spec.ts new file mode 100644 index 000000000..978fdaa80 --- /dev/null +++ b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.spec.ts @@ -0,0 +1,13 @@ +import { BasicTooltipModule } from './basic-tooltip.module'; + +describe('BasicTooltipModule', () => { + let basicTooltipModule: BasicTooltipModule; + + beforeEach(() => { + basicTooltipModule = new BasicTooltipModule(); + }); + + it('should create an instance', () => { + expect(basicTooltipModule).toBeTruthy(); + }); +}); diff --git a/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.ts b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.ts new file mode 100644 index 000000000..fcb6ee0d5 --- /dev/null +++ b/src/app/docs/components/tooltip-demo/basic-tooltip/basic-tooltip.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LyTooltipModule } from '@alyle/ui/tooltip'; +import { LyButtonModule } from '@alyle/ui/button'; +import { LyIconModule } from '@alyle/ui/icon'; + +import { BasicTooltipComponent } from './basic-tooltip.component'; + +@NgModule({ + imports: [ + CommonModule, + LyTooltipModule, + LyButtonModule, + LyIconModule + ], + exports: [BasicTooltipComponent], + declarations: [BasicTooltipComponent] +}) +export class BasicTooltipModule { } diff --git a/src/app/docs/components/tooltip-demo/tooltip-demo.component.html b/src/app/docs/components/tooltip-demo/tooltip-demo.component.html new file mode 100644 index 000000000..84845a3c1 --- /dev/null +++ b/src/app/docs/components/tooltip-demo/tooltip-demo.component.html @@ -0,0 +1,4 @@ +

Basic Tooltip

+ + + diff --git a/src/app/docs/components/tooltip-demo/tooltip-demo.component.spec.ts b/src/app/docs/components/tooltip-demo/tooltip-demo.component.spec.ts new file mode 100644 index 000000000..12eeb36ce --- /dev/null +++ b/src/app/docs/components/tooltip-demo/tooltip-demo.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TooltipDemoComponent } from './tooltip-demo.component'; + +describe('TooltipDemoComponent', () => { + let component: TooltipDemoComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TooltipDemoComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TooltipDemoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/docs/components/tooltip-demo/tooltip-demo.component.ts b/src/app/docs/components/tooltip-demo/tooltip-demo.component.ts new file mode 100644 index 000000000..10ee601a3 --- /dev/null +++ b/src/app/docs/components/tooltip-demo/tooltip-demo.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'aui-tooltip-demo', + templateUrl: './tooltip-demo.component.html' +}) +export class TooltipDemoComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/docs/docs.module.ts b/src/app/docs/docs.module.ts index 5d6dee7cf..4939cb6d5 100644 --- a/src/app/docs/docs.module.ts +++ b/src/app/docs/docs.module.ts @@ -72,6 +72,10 @@ import { PaperWithColorModule } from './customization/paper-demo/paper-with-colo /** Icon demo */ import { IconDemoComponent } from './components/icon-demo/icon-demo.component'; import { IconsModule } from './components/icon-demo/icons/icons.module'; +import { TooltipDemoComponent } from './components/tooltip-demo/tooltip-demo.component'; + +/** Tooltip */ +import { BasicTooltipModule } from './components/tooltip-demo/basic-tooltip/basic-tooltip.module'; @NgModule({ imports: [ @@ -118,7 +122,9 @@ import { IconsModule } from './components/icon-demo/icons/icons.module'; BasicPaperModule, PaperWithColorModule, /** Icon */ - IconsModule + IconsModule, + /** Tooltip */ + BasicTooltipModule ], declarations: [ ThemingComponent, @@ -147,8 +153,9 @@ import { IconsModule } from './components/icon-demo/icons/icons.module'; /** Paper */ PaperDemoComponent, /** Icon */ - IconDemoComponent - + IconDemoComponent, + /** Tooltip */ + TooltipDemoComponent ] }) export class DocsModule { } diff --git a/src/app/docs/docs.routing.ts b/src/app/docs/docs.routing.ts index 8139d4a39..8f0335bb3 100644 --- a/src/app/docs/docs.routing.ts +++ b/src/app/docs/docs.routing.ts @@ -14,6 +14,7 @@ import { ResponsiveDemoComponent } from './layout/responsive/responsive-demo.com import { SnackBarDemoComponent } from './components/snack-bar-demo/snack-bar-demo.component'; import { PaperDemoComponent } from './customization/paper-demo/paper-demo.component'; import { IconDemoComponent } from './components/icon-demo/icon-demo.component'; +import { TooltipDemoComponent } from './components/tooltip-demo/tooltip-demo.component'; const routes: Routes = [ /** layout */ @@ -48,7 +49,8 @@ const routes: Routes = [ { path: 'field', component: FieldDemoComponent }, { path: 'checkbox', component: CheckboxDemoComponent }, { path: 'snack-bar', component: SnackBarDemoComponent }, - { path: 'icon', component: IconDemoComponent } + { path: 'icon', component: IconDemoComponent }, + { path: 'tooltip', component: TooltipDemoComponent }, ] } ]; diff --git a/src/lib/src/dom/overlay.ts b/src/lib/src/dom/overlay.ts index 8e5f1b656..cd3a2a632 100644 --- a/src/lib/src/dom/overlay.ts +++ b/src/lib/src/dom/overlay.ts @@ -35,7 +35,7 @@ class CreateFromTemplateRef implements OverlayFromTemplateRef { constructor( private _componentFactoryResolver: ComponentFactoryResolver, private _appRef: ApplicationRef, - _templateRef: TemplateRef, + _templateRef: TemplateRef | string, private _overlayContainer: LyOverlayContainer, _context: any, private _injector: Injector, @@ -109,7 +109,7 @@ class CreateFromTemplateRef implements OverlayFromTemplateRef { } } - private _appendComponentToBody(type: TemplateRef | Type, context, injector: Injector) { + private _appendComponentToBody(type: TemplateRef | Type | string, context, injector: Injector) { if (type instanceof TemplateRef) { // Create a component reference from the component const viewRef = this._viewRef = type.createEmbeddedView(context || {}); @@ -120,8 +120,11 @@ class CreateFromTemplateRef implements OverlayFromTemplateRef { // Append DOM element to the body this._overlayContainer._add(this._el); + } else if (typeof type === 'string') { + this._el.innerText = type; + this._overlayContainer._add(this._el); } else { - this._compRef = this.generateComponent(type, injector); + this._compRef = this.generateComponent(type as Type, injector); this._el = this._compRef.location.nativeElement; this._overlayContainer._add(this._el); } @@ -147,6 +150,10 @@ class CreateFromTemplateRef implements OverlayFromTemplateRef { this._compRef.destroy(); this._overlayContainer._remove(this._el); this._el = null; + } else if (this._el) { + // remove if content is string + this._overlayContainer._remove(this._el); + this._el = null; } if (this._compRefOverlayBackdrop) { this._appRef.detachView(this._compRefOverlayBackdrop.hostView); @@ -176,7 +183,7 @@ export class LyOverlay { private _windowScroll: WindowScrollService ) { } - create(template: TemplateRef, context?: any, config?: OverlayConfig): OverlayFromTemplateRef { + create(template: TemplateRef | string, context?: any, config?: OverlayConfig): OverlayFromTemplateRef { return new CreateFromTemplateRef(this._componentFactoryResolver, this._appRef, template, this._overlayContainer, context, this._injector, this._windowScroll, config); } } diff --git a/src/lib/src/theme/theme-config.ts b/src/lib/src/theme/theme-config.ts index 21713deaa..5cea7abe8 100644 --- a/src/lib/src/theme/theme-config.ts +++ b/src/lib/src/theme/theme-config.ts @@ -6,6 +6,7 @@ import { TypographyVariables } from './variables/typography'; import { CheckboxVariables } from './variables/checkbox'; import { SnackBarVariables } from './variables/snack-bar'; import { ButtonVariables } from './variables/button'; +import { TooltipVariables } from './variables/tooltip'; export const LY_THEME_GLOBAL_VARIABLES = new InjectionToken('ly.theme.global.variables'); export const LY_THEME = new InjectionToken('ly_theme_config'); @@ -108,6 +109,7 @@ export interface ThemeConfig { checkbox: CheckboxVariables; snackBar: SnackBarVariables; button: ButtonVariables; + tooltip: TooltipVariables; } export type ThemeVariables = LyStyleUtils & ThemeConfig; diff --git a/src/lib/src/theme/variables/tooltip.ts b/src/lib/src/theme/variables/tooltip.ts new file mode 100644 index 000000000..3fe48b796 --- /dev/null +++ b/src/lib/src/theme/variables/tooltip.ts @@ -0,0 +1,5 @@ +import { StyleContainer } from '../theme2.service'; + +export interface TooltipVariables { + root: StyleContainer; +} diff --git a/src/lib/themes/minima/dark.ts b/src/lib/themes/minima/dark.ts index 8f9dfacc1..54286d66b 100644 --- a/src/lib/themes/minima/dark.ts +++ b/src/lib/themes/minima/dark.ts @@ -71,5 +71,11 @@ export class MinimaDark extends MinimaBase implements ThemeConfig { color: 'rgba(0,0,0,.87)' } }; + tooltip = { + root: { + background: 'rgba(250, 250, 250, 0.85)', + color: 'rgba(0,0,0,.87)' + } + }; // direction = Dir.rtl; // beta } diff --git a/src/lib/themes/minima/light.ts b/src/lib/themes/minima/light.ts index 1fc3d3072..46b082a72 100644 --- a/src/lib/themes/minima/light.ts +++ b/src/lib/themes/minima/light.ts @@ -71,4 +71,10 @@ export class MinimaLight extends MinimaBase implements ThemeConfig { color: 'rgba(255,255,255,.7)' } }; + tooltip = { + root: { + background: 'rgba(50, 50, 50, 0.85)', + color: 'rgba(255,255,255,.7)' + } + }; } diff --git a/src/lib/tooltip/index.ts b/src/lib/tooltip/index.ts new file mode 100644 index 000000000..4aaf8f92e --- /dev/null +++ b/src/lib/tooltip/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/src/lib/tooltip/public_api.ts b/src/lib/tooltip/public_api.ts new file mode 100644 index 000000000..9867b7866 --- /dev/null +++ b/src/lib/tooltip/public_api.ts @@ -0,0 +1,2 @@ +export * from './tooltip'; +export * from './tooltip.module'; diff --git a/src/lib/tooltip/tooltip.module.ts b/src/lib/tooltip/tooltip.module.ts new file mode 100644 index 000000000..d32b5a5ee --- /dev/null +++ b/src/lib/tooltip/tooltip.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; +import { LyTooltip } from './tooltip'; + +@NgModule({ + declarations: [LyTooltip], + exports: [LyTooltip] +}) +export class LyTooltipModule { } diff --git a/src/lib/tooltip/tooltip.ts b/src/lib/tooltip/tooltip.ts new file mode 100644 index 000000000..6623e0eee --- /dev/null +++ b/src/lib/tooltip/tooltip.ts @@ -0,0 +1,156 @@ +import { Directive, Input, TemplateRef, OnDestroy, ElementRef, NgZone, ChangeDetectorRef } from '@angular/core'; +import { LyTheme2, LY_COMMON_STYLES, LyOverlay, OverlayFromTemplateRef, Platform, LyFocusState, ThemeVariables, WindowScrollService } from '@alyle/ui'; +import { Subscription } from 'rxjs'; + +const STYLE_PRIORITY = -2; +const styles = ({ + root: { + ...LY_COMMON_STYLES.fill + } +}); + +@Directive({ + selector: '[lyTooltip]', + exportAs: 'lyTooltip' +}) +export class LyTooltip implements OnDestroy { + readonly classes = this._theme.addStyleSheet(styles, STYLE_PRIORITY); + private _tooltip: string | TemplateRef | null; + private _tooltipOverlay: OverlayFromTemplateRef; + private _listeners = new Map(); + private _scrollSub: Subscription; + // private _scrollVal = 0; + private _showTimeoutId: number | null; + private _hideTimeoutId: number | null; + @Input('lyTooltip') + set tooltip(val: string | TemplateRef) { + this._tooltip = val; + } + get tooltip() { + return this._tooltip; + } + @Input() lyTooltipShowDelay: number = 0; + @Input() lyTooltipHideDelay: number = 1000; + constructor( + private _theme: LyTheme2, + private _overlay: LyOverlay, + private _el: ElementRef, + private _cd: ChangeDetectorRef, + focusState: LyFocusState, + ngZone: NgZone, + scroll: WindowScrollService + ) { + if (Platform.isBrowser) { + const element: HTMLElement = _el.nativeElement; + if (!Platform.IOS && !Platform.ANDROID) { + this._listeners + .set('mouseenter', () => this.show()) + .set('mouseleave', () => this.hide()); + } else { + this._listeners.set('touchstart', () => this.show()); + } + + this._listeners.forEach((listener, event) => element.addEventListener(event, listener)); + + this._scrollSub = scroll.scroll$.subscribe(() => { + if (this._tooltipOverlay) { + // this._scrollVal++; + // if (this._scrollVal > 10) { + ngZone.run(() => this.hide(0)); + // this._scrollVal = 0; + // } + } + }); + + focusState.listen(element).subscribe(ev => { + if (ev.by === 'keyboard' && ev.event.type === 'focus') { + ngZone.run(() => this.show()); + } else if (ev.event.type === 'blur') { + ngZone.run(() => this.hide()); + } + }); + } + } + + ngOnDestroy() { + this.hide(0); + + // Clean up the event listeners set in the constructor + this._listeners.forEach((listener, event) => { + this._el.nativeElement.removeEventListener(event, listener); + }); + + if (this._scrollSub) { + this._scrollSub.unsubscribe(); + } + } + + show(delay?: number) { + delay = typeof delay === 'number' ? delay : this.lyTooltipShowDelay; + if (this._hideTimeoutId) { + clearTimeout(this._hideTimeoutId); + this._hideTimeoutId = null; + } + if (!this._tooltipOverlay && this.tooltip && !this._showTimeoutId) { + + this._showTimeoutId = setTimeout(() => { + const rect = this._el.nativeElement.getBoundingClientRect(); + const tooltip = this._tooltipOverlay = this._overlay.create(this.tooltip, undefined, { + styles: { + top: rect.y, + left: rect.x, + pointerEvents: null + }, + classes: [ + this._theme.addStyle('LyTooltip', (theme: ThemeVariables) => ({ + borderRadius: '4px', + ...theme.tooltip.root, + fontSize: '10px', + padding: '6px 8px', + [theme.getBreakpoint('XSmall')]: { + padding: '8px 16px', + fontSize: '14px', + } + })) + ] + }); + const tooltipRect = tooltip.containerElement.getBoundingClientRect(); + tooltip.containerElement.style.transform = `translate3d(${Math.round(rect.width / 2 - tooltipRect.width / 2)}px,${Math.round(rect.height * .2 + rect.height)}px,0px)`; + + this._showTimeoutId = null; + this._markForCheck(); + }, delay); + } + } + + hide(delay?: number) { + const tooltipOverlay = this._tooltipOverlay; + delay = typeof delay === 'number' ? delay : this.lyTooltipHideDelay; + if (this._showTimeoutId) { + clearTimeout(this._showTimeoutId); + this._showTimeoutId = null; + } + if (tooltipOverlay && !this._hideTimeoutId) { + + this._hideTimeoutId = setTimeout(() => { + tooltipOverlay.destroy(); + this._tooltipOverlay = null; + + this._hideTimeoutId = null; + this._markForCheck(); + }, delay); + } + } + + toggle() { + if (this._tooltipOverlay) { + this.hide(); + } else { + this.show(); + } + } + + private _markForCheck() { + this._cd.markForCheck(); + } +} diff --git a/tsconfig.json b/tsconfig.json index e67c04615..2b7919bd0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,7 @@ "@alyle/ui/field": ["lib/field"], "@alyle/ui/checkbox": ["lib/checkbox"], "@alyle/ui/snack-bar": ["lib/snack-bar"], + "@alyle/ui/tooltip": ["lib/tooltip"], "@env/*": ["environments/*"], "@docs/*": ["app/docs/*"] } diff --git a/tslint.json b/tslint.json index a660091d4..543047d42 100644 --- a/tslint.json +++ b/tslint.json @@ -118,7 +118,6 @@ ], "no-output-on-prefix": true, "use-output-property-decorator": true, - "use-host-property-decorator": true, "no-input-rename": true, "no-output-rename": true, "use-life-cycle-interface": true,