aboutsummaryrefslogtreecommitdiffstats
path: root/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip
diff options
context:
space:
mode:
Diffstat (limited to 'sdc-workflow-designer-ui/src/app/paletx/plx-tooltip')
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts11
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts13
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less241
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts12
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts485
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts176
6 files changed, 938 insertions, 0 deletions
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts
new file mode 100644
index 00000000..4a19dd17
--- /dev/null
+++ b/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/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts
new file mode 100644
index 00000000..dd1cc254
--- /dev/null
+++ b/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/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less
new file mode 100644
index 00000000..d7ce015f
--- /dev/null
+++ b/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/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts
new file mode 100644
index 00000000..062ded1c
--- /dev/null
+++ b/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/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts
new file mode 100644
index 00000000..cd2d8a6a
--- /dev/null
+++ b/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/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts
new file mode 100644
index 00000000..f52cc11d
--- /dev/null
+++ b/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();
+ }
+}