diff options
Diffstat (limited to 'deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip')
6 files changed, 938 insertions, 0 deletions
diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts new file mode 100644 index 00000000..4a19dd17 --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts @@ -0,0 +1,11 @@ +import {PlxTooltipConfig} from './plx-tooltip-config'; + +describe('plx-tooltip-config', () => { + it('should have sensible default values', () => { + const config = new PlxTooltipConfig(); + + expect(config.placement).toBe('top'); + expect(config.triggers).toBe('hover'); + expect(config.container).toBeUndefined(); + }); +}); diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts new file mode 100644 index 00000000..dd1cc254 --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts @@ -0,0 +1,13 @@ +import {Injectable} from '@angular/core'; + +/** + * Configuration service for the PlxTooltip directive. + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the tooltips used in the application. + */ +@Injectable() +export class PlxTooltipConfig { + public placement: 'top' | 'bottom' | 'left' | 'right' = 'top'; + public triggers = 'hover'; + public container: string; +} diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less new file mode 100644 index 00000000..4be58f9a --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less @@ -0,0 +1,241 @@ +@import "../../assets/components/themes/common/plx-input.less"; + +@tooltip-arrow-border-width: 4px; +@tooltip-arrow-border-width-before: 5px; +@tooltip-arrow-border-height: @tooltip-arrow-border-width-before - @tooltip-arrow-border-width; +@tooltip-arrow-away: 5px; +@tooltip-arrow-background-color: #595959; +@tooltip-arrow-border-color: #595959; +@tooltip-away-host: 3px; + +.plx-tooltip { + font-family: @font-family; + font-size: @font-size; + opacity: 1; + position: absolute; + z-index: 10001; + display: block; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-break: auto; + line-height: 1.5; + text-align: left; + text-decoration: none; + text-shadow: none; + text-transform: none; + white-space: normal; + word-break: normal; + word-spacing: normal; + word-wrap: break-word; + &::before, + &::after { + content: ""; + position: absolute; + display: block; + width: 0; + height: 0; + border: solid transparent; + } + &::before { + border-width: @tooltip-arrow-border-width-before; + } + &::after { + border-width: @tooltip-arrow-border-width; + } +} + +.plx-tooltip-inner { + min-width: 60px; + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; +} + +.plx-tooltip.show { + font-size: @font-size; + opacity: 1; +} +.plx-tooltip.show .plx-tooltip-inner { + background-color: #595959; + border-radius: @radius; + padding: 0px 12px; + height: 30px; + line-height: 30px; +} + +.plx-tooltip-top-common { + margin-top: -(@tooltip-arrow-border-width + @tooltip-away-host); + &::before { + border-top-color: @tooltip-arrow-border-color; + border-bottom-width: 0; + bottom: -@tooltip-arrow-border-width-before; + } + &::after { + border-top-color: @tooltip-arrow-background-color; + border-bottom-width: 0; + bottom: -@tooltip-arrow-border-width; + } +} +.plx-tooltip-top { + .plx-tooltip-top-common; + &::before { + left: 50%; + margin-left: -@tooltip-arrow-border-width-before; + } + &::after { + left: 50%; + margin-left: -@tooltip-arrow-border-width; + } +} +.plx-tooltip.plx-tooltip-top-left { + .plx-tooltip-top-common; + &::before { + left: @tooltip-arrow-away; + } + &::after { + left: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} +.plx-tooltip.plx-tooltip-top-right { + .plx-tooltip-top-common; + &::before { + right: @tooltip-arrow-away; + } + &::after { + right: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} + +.plx-tooltip-right-common { + margin-left: @tooltip-arrow-border-width + @tooltip-away-host; + &::before { + border-right-color: @tooltip-arrow-border-color; + border-left-width: 0; + left: -@tooltip-arrow-border-width-before; + } + &::after { + border-right-color: @tooltip-arrow-background-color; + border-left-width: 0; + left: -@tooltip-arrow-border-width; + } +} +.plx-tooltip.plx-tooltip-right { + .plx-tooltip-right-common; + &::before { + top: 50%; + margin-top: -@tooltip-arrow-border-width-before; + } + &::after { + top: 50%; + margin-top: -@tooltip-arrow-border-width; + } +} +.plx-tooltip.plx-tooltip-right-top { + .plx-tooltip-right-common; + &::before { + top: @tooltip-arrow-away; + } + &::after { + top: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} +.plx-tooltip.plx-tooltip-right-bottom { + .plx-tooltip-right-common; + &::before { + bottom: @tooltip-arrow-away; + } + &::after { + bottom: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} + +.plx-tooltip-bottom-common { + margin-top: @tooltip-arrow-border-width + @tooltip-away-host; + &::before { + border-bottom-color: @tooltip-arrow-border-color; + border-top-width: 0; + top: -@tooltip-arrow-border-width-before; + } + &::after { + border-bottom-color: @tooltip-arrow-background-color; + border-top-width: 0; + top: -@tooltip-arrow-border-width; + } +} +.plx-tooltip.plx-tooltip-bottom { + .plx-tooltip-bottom-common; + &::before { + left: 50%; + margin-left: -@tooltip-arrow-border-width-before; + } + &::after { + left: 50%; + margin-left: -@tooltip-arrow-border-width; + } +} +.plx-tooltip.plx-tooltip-bottom-left { + .plx-tooltip-bottom-common; + &::before { + left: @tooltip-arrow-away; + } + &::after { + left: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} +.plx-tooltip.plx-tooltip-bottom-right { + .plx-tooltip-bottom-common; + &::before { + right: @tooltip-arrow-away; + } + &::after { + right: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} + +.plx-tooltip-left-common { + margin-left: -(@tooltip-arrow-border-width + @tooltip-away-host); + &::before { + border-left-color: @tooltip-arrow-border-color; + border-right-width: 0; + right: -@tooltip-arrow-border-width-before; + } + &::after { + border-left-color: @tooltip-arrow-background-color; + border-right-width: 0; + right: -@tooltip-arrow-border-width; + } +} + +.plx-tooltip.plx-tooltip-left { + .plx-tooltip-left-common; + &::before { + top: 50%; + margin-top: -@tooltip-arrow-border-width-before; + } + &::after { + top: 50%; + margin-top: -@tooltip-arrow-border-width; + } +} + +.plx-tooltip.plx-tooltip-left-top { + .plx-tooltip-left-common; + &::before { + top: @tooltip-arrow-away; + } + &::after { + top: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +} +.plx-tooltip.plx-tooltip-left-bottom { + .plx-tooltip-left-common; + &::before { + bottom: @tooltip-arrow-away; + } + &::after { + bottom: @tooltip-arrow-away + @tooltip-arrow-border-height; + } +}
\ No newline at end of file diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts new file mode 100644 index 00000000..062ded1c --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts @@ -0,0 +1,12 @@ +import {NgModule, ModuleWithProviders} from '@angular/core'; + +import {PlxTooltip, PlxTooltipWindow} from './plx-tooltip'; +import {PlxTooltipConfig} from './plx-tooltip-config'; + +export {PlxTooltipConfig} from './plx-tooltip-config'; +export {PlxTooltip} from './plx-tooltip'; + +@NgModule({declarations: [PlxTooltip, PlxTooltipWindow], exports: [PlxTooltip], entryComponents: [PlxTooltipWindow]}) +export class PlxTooltipModule { + public static forRoot(): ModuleWithProviders { return {ngModule: PlxTooltipModule, providers: [PlxTooltipConfig]}; } +} diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts new file mode 100644 index 00000000..cd2d8a6a --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts @@ -0,0 +1,485 @@ +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {By} from '@angular/platform-browser'; +import {Component, ViewChild, ChangeDetectionStrategy} from '@angular/core'; + +import {PlxTooltipModule} from './plx-tooltip.module'; +import {PlxTooltipWindow, PlxTooltip} from './plx-tooltip'; +import {PlxTooltipConfig} from './plx-tooltip-config'; + +const createTestComponent = + (html: string) => <ComponentFixture<TestComponent>>createGenericTestComponent(html, TestComponent); + +const createOnPushTestComponent = + (html: string) => <ComponentFixture<TestOnPushComponent>>createGenericTestComponent(html, TestOnPushComponent); + +describe('plx-tooltip-window', () => { + beforeEach(() => { TestBed.configureTestingModule({imports: [PlxTooltipModule.forRoot()]}); }); + + it('should render tooltip on top by default', () => { + const fixture = TestBed.createComponent(PlxTooltipWindow); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveCssClass('tooltip'); + expect(fixture.nativeElement).toHaveCssClass('tooltip-top'); + expect(fixture.nativeElement.getAttribute('role')).toBe('tooltip'); + }); + + it('should position tooltips as requested', () => { + const fixture = TestBed.createComponent(PlxTooltipWindow); + fixture.componentInstance.placement = 'left'; + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveCssClass('tooltip-left'); + }); +}); + +describe('plx-tooltip', () => { + + beforeEach(() => { + TestBed.configureTestingModule( + {declarations: [TestComponent, TestOnPushComponent], imports: [PlxTooltipModule.forRoot()]}); + }); + + function getWindow(element) { return element.querySelector('plx-tooltip-window'); } + + describe('basic functionality', () => { + + it('should open and close a tooltip - default settings and content as string', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + const defaultConfig = new PlxTooltipConfig(); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass(`tooltip-${defaultConfig.placement}`); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.getAttribute('id')).toBe('plx-tooltip-0'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe('plx-tooltip-0'); + + directive.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a tooltip - default settings and content from a template', () => { + const fixture = createTestComponent(`<template #t>Hello, {{name}}!</template><div [plxTooltip]="t"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('tooltip-top'); + expect(windowEl.textContent.trim()).toBe('Hello, World!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.getAttribute('id')).toBe('plx-tooltip-1'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe('plx-tooltip-1'); + + directive.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a tooltip - default settings, content from a template and context supplied', () => { + const fixture = createTestComponent(`<template #t let-name="name">Hello, {{name}}!</template><div [plxTooltip]="t"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.context.tooltip.open({name: 'John'}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('tooltip-top'); + expect(windowEl.textContent.trim()).toBe('Hello, John!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.getAttribute('id')).toBe('plx-tooltip-2'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe('plx-tooltip-2'); + + directive.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should not open a tooltip if content is falsy', () => { + const fixture = createTestComponent(`<div [plxTooltip]="notExisting"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toBeNull(); + }); + + it('should close the tooltip tooltip if content becomes falsy', () => { + const fixture = createTestComponent(`<div [plxTooltip]="name"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.name = null; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow re-opening previously closed tooltips', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + directive.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should not leave dangling tooltips in the DOM', () => { + const fixture = createTestComponent(`<template [ngIf]="show"><div plxTooltip="Great tip!"></div></template>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should properly cleanup tooltips with manual triggers', () => { + const fixture = createTestComponent(` + <template [ngIf]="show"> + <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip" (mouseenter)="t.open()"></div> + </template>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + describe('positioning', () => { + + it('should use requested position', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!" placement="left"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('tooltip-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should properly position tooltips when a component is using the OnPush strategy', () => { + const fixture = createOnPushTestComponent(`<div plxTooltip="Great tip!" placement="left"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('tooltip-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + }); + + describe('triggers', () => { + + it('should support toggle triggers', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="click"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should non-default toggle triggers', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="mouseenter:click"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should support multiple triggers', () => { + const fixture = + createTestComponent(`<div plxTooltip="Great tip!" triggers="mouseenter:mouseleave click"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not use default for manual triggers', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="manual"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow toggling for manual triggers', () => { + const fixture = createTestComponent(` + <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div> + <button (click)="t.toggle()">T</button>`); + const button = fixture.nativeElement.querySelector('button'); + + button.click(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + button.click(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow open / close for manual triggers', () => { + const fixture = createTestComponent(` + <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div> + <button (click)="t.open()">O</button> + <button (click)="t.close()">C</button>`); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + + buttons[0].click(); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + buttons[1].click(); // close + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not throw when open called for manual triggers and open tooltip', () => { + const fixture = createTestComponent(` + <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div> + <button (click)="t.open()">O</button>`); + const button = fixture.nativeElement.querySelector('button'); + + button.click(); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + button.click(); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should not throw when closed called for manual triggers and closed tooltip', () => { + const fixture = createTestComponent(` + <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div> + <button (click)="t.close()">C</button>`); + + const button = fixture.nativeElement.querySelector('button'); + + button.click(); // close + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + }); + }); + + describe('container', () => { + + it('should be appended to the element matching the selector passed to "container"', () => { + const selector = 'body'; + const fixture = createTestComponent(`<div plxTooltip="Great tip!" container="` + selector + `"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + }); + + it('should properly destroy tooltips when the "container" option is used', () => { + const selector = 'body'; + const fixture = + createTestComponent(`<div *ngIf="show" plxTooltip="Great tip!" container="` + selector + `"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + directive.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + fixture.componentRef.instance.show = false; + fixture.detectChanges(); + expect(getWindow(document.querySelector(selector))).toBeNull(); + }); + }); + + describe('visibility', () => { + it('should emit events when showing and hiding popover', () => { + const fixture = createTestComponent( + `<div plxTooltip="Great tip!" triggers="click" (shown)="shown()" (hidden)="hidden()"></div>`); + const directive = fixture.debugElement.query(By.directive(PlxTooltip)); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(shownSpy).toHaveBeenCalled(); + + directive.triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(hiddenSpy).toHaveBeenCalled(); + }); + + it('should not emit close event when already closed', () => { + const fixture = createTestComponent( + `<div plxTooltip="Great tip!" triggers="manual" (shown)="shown()" (hidden)="hidden()"></div>`); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + fixture.componentInstance.tooltip.open(); + fixture.detectChanges(); + + fixture.componentInstance.tooltip.open(); + fixture.detectChanges(); + + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(shownSpy).toHaveBeenCalled(); + expect(shownSpy.calls.count()).toEqual(1); + expect(hiddenSpy).not.toHaveBeenCalled(); + }); + + it('should not emit open event when already opened', () => { + const fixture = createTestComponent( + `<div plxTooltip="Great tip!" triggers="manual" (shown)="shown()" (hidden)="hidden()"></div>`); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + fixture.componentInstance.tooltip.close(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(shownSpy).not.toHaveBeenCalled(); + expect(hiddenSpy).toHaveBeenCalled(); + }); + + it('should report correct visibility', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="manual"></div>`); + fixture.detectChanges(); + + expect(fixture.componentInstance.tooltip.isOpen()).toBeFalsy(); + + fixture.componentInstance.tooltip.open(); + fixture.detectChanges(); + expect(fixture.componentInstance.tooltip.isOpen()).toBeTruthy(); + + fixture.componentInstance.tooltip.close(); + fixture.detectChanges(); + expect(fixture.componentInstance.tooltip.isOpen()).toBeFalsy(); + }); + }); + + describe('Custom config', () => { + let config: PlxTooltipConfig; + + beforeEach(() => { + TestBed.configureTestingModule({imports: [PlxTooltipModule.forRoot()]}); + TestBed.overrideComponent(TestComponent, {set: {template: `<div plxTooltip="Great tip!"></div>`}}); + }); + + beforeEach(inject([PlxTooltipConfig], (c: PlxTooltipConfig) => { + config = c; + config.placement = 'bottom'; + config.triggers = 'click'; + config.container = 'body'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const tooltip = fixture.componentInstance.tooltip; + + expect(tooltip.placement).toBe(config.placement); + expect(tooltip.triggers).toBe(config.triggers); + expect(tooltip.container).toBe(config.container); + }); + }); + + describe('Custom config as provider', () => { + let config = new PlxTooltipConfig(); + config.placement = 'bottom'; + config.triggers = 'click'; + config.container = 'body'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [PlxTooltipModule.forRoot()], providers: [{provide: PlxTooltipConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = createTestComponent(`<div plxTooltip="Great tip!"></div>`); + const tooltip = fixture.componentInstance.tooltip; + + expect(tooltip.placement).toBe(config.placement); + expect(tooltip.triggers).toBe(config.triggers); + expect(tooltip.container).toBe(config.container); + }); + }); +}); + +@Component({selector: 'test-cmpt', template: ``}) +export class TestComponent { + name = 'World'; + show = true; + + @ViewChild(PlxTooltip) tooltip: PlxTooltip; + + shown() {} + hidden() {} +} + +@Component({selector: 'test-onpush-cmpt', changeDetection: ChangeDetectionStrategy.OnPush, template: ``}) +export class TestOnPushComponent { +} diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts new file mode 100644 index 00000000..f52cc11d --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts @@ -0,0 +1,176 @@ +import { + Component, + Directive, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy, + OnInit, + OnDestroy, + Injector, + Renderer, + ComponentRef, + ElementRef, + TemplateRef, + ViewContainerRef, + ComponentFactoryResolver, + NgZone, ViewEncapsulation +} from '@angular/core'; +import {listenToTriggers} from '../util/triggers'; +import {positionElements, getPlacement} from '../util/positioning'; +import {PopupService} from '../util/popup'; +import {PlxTooltipConfig} from './plx-tooltip-config'; + +let nextId = 0; + +@Component({ + selector: 'plx-tooltip-window', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: {'[class]': '"plx-tooltip show plx-tooltip-" + placement', 'role': 'tooltip', '[id]': 'id'}, + template: ` + <div class="plx-tooltip-inner"><ng-content></ng-content></div> + `, + styleUrls: ['./plx-tooltip.less'] +}) +export class PlxTooltipWindow { + @Input() public placement: 'top' | 'bottom' | 'left' | 'right' = 'top'; + @Input() public id: string; +} + +/** + * A lightweight, extensible directive for fancy tooltip creation. + */ +@Directive({selector: '[plxTooltip]', exportAs: 'plxTooltip'}) +export class PlxTooltip implements OnInit, OnDestroy { + /** + * Placement of a tooltip. Accepts: "top", "bottom", "left", "right" + */ + @Input() public placement: 'top' | 'bottom' | 'left' | 'right'; + /** + * Specifies events that should trigger. Supports a space separated list of event names. + */ + @Input() public triggers: string; + /** + * A selector specifying the element the tooltip should be appended to. + * Currently only supports "body". + */ + @Input() public container: string; + /** + * Emits an event when the tooltip is shown + */ + @Output() public shown = new EventEmitter(); + /** + * Emits an event when the tooltip is hidden + */ + @Output() public hidden = new EventEmitter(); + + private _plxTooltip: string | TemplateRef<any>; + private _plxTooltipWindowId = `plx-tooltip-${nextId++}`; + private _popupService: PopupService<PlxTooltipWindow>; + private _windowRef: ComponentRef<PlxTooltipWindow>; + private _unregisterListenersFn; + private _zoneSubscription: any; + + constructor(private _elementRef: ElementRef, private _renderer: Renderer, injector: Injector, + componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: PlxTooltipConfig, + ngZone: NgZone) { + this.placement = config.placement; + this.triggers = config.triggers; + this.container = config.container; + this._popupService = new PopupService<PlxTooltipWindow>( + PlxTooltipWindow, injector, viewContainerRef, _renderer, componentFactoryResolver); + + this._zoneSubscription = ngZone.onStable.subscribe(() => { + if (this._windowRef) { + positionElements( + this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement, + this.container === 'body'); + let tmpPlace = getPlacement(this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement); + this._windowRef.instance.placement = tmpPlace; + this._windowRef.changeDetectorRef.detectChanges(); + } + }); + } + + /** + * Content to be displayed as tooltip. If falsy, the tooltip won't open. + */ + @Input() + set plxTooltip(value: string | TemplateRef<any>) { + this._plxTooltip = value; + if (!value && this._windowRef) { + this.close(); + } + } + + get plxTooltip() { + return this._plxTooltip; + } + + /** + * Opens an element’s tooltip. This is considered a “manual” triggering of the tooltip. + * The context is an optional value to be injected into the tooltip template when it is created. + */ + public open(context?: any) { + if (!this._windowRef && this._plxTooltip) { + this._windowRef = this._popupService.open(this._plxTooltip, context); + // let tmpPlace = getPlacement(this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement); + this._windowRef.instance.placement = this.placement; + this._windowRef.instance.id = this._plxTooltipWindowId; + + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', this._plxTooltipWindowId); + + if (this.container === 'body') { + window.document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement); + } + + // we need to manually invoke change detection since events registered via + // Renderer::listen() - to be determined if this is a bug in the Angular itself + this._windowRef.changeDetectorRef.markForCheck(); + this.shown.emit(); + } + } + + /** + * Closes an element’s tooltip. This is considered a “manual” triggering of the tooltip. + */ + public close(): void { + if (this._windowRef !== null) { + this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', null); + this._popupService.close(); + this._windowRef = null; + this.hidden.emit(); + } + } + + /** + * Toggles an element’s tooltip. This is considered a “manual” triggering of the tooltip. + */ + public toggle(): void { + if (this._windowRef) { + this.close(); + } else { + this.open(); + } + } + + /** + * Returns whether or not the tooltip is currently being shown + */ + public isOpen(): boolean { + return !!this._windowRef; + } + + public ngOnInit() { + this._unregisterListenersFn = listenToTriggers( + this._renderer, this._elementRef.nativeElement, this.triggers, this.open.bind(this), this.close.bind(this), + this.toggle.bind(this)); + } + + public ngOnDestroy() { + this.close(); + this._unregisterListenersFn(); + this._zoneSubscription.unsubscribe(); + } +} |