AAE-38748 Change clipboard directive to be a tooltip (#11293)

* feat: change clipboard directive to be a tooltip

* feat: remove redundant tooltips from datatable cells

* test: fix broken tests

* fix: sonarqube issues

* fix: unable to use link-adf commit message from hxp-frontend-apps
This commit is contained in:
Joshua Cain
2025-11-06 06:28:42 -05:00
committed by GitHub
parent 3180ca8e9d
commit e28e924b87
8 changed files with 114 additions and 106 deletions
@@ -24,6 +24,8 @@ import { UnitTestingUtils } from '../testing/unit-testing-utils';
import { HarnessLoader, TestKey } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
@Component({
selector: 'adf-test-component',
@@ -32,7 +34,8 @@ import { MatButtonModule } from '@angular/material/button';
<input #ref />
`,
imports: [MatButtonModule, ClipboardDirective]
standalone: true,
imports: [MatButtonModule, MatTooltipModule, ClipboardDirective]
})
class TestTargetClipboardComponent {}
@@ -44,7 +47,8 @@ describe('ClipboardDirective', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [MatSnackBarModule, TestTargetClipboardComponent]
imports: [MatSnackBarModule, TestTargetClipboardComponent],
providers: [provideNoopAnimations()]
});
fixture = TestBed.createComponent(TestTargetClipboardComponent);
clipboardService = TestBed.inject(ClipboardService);
@@ -74,6 +78,7 @@ describe('CopyClipboardDirective', () => {
@Component({
selector: 'adf-copy-content-test-component',
template: `<span adf-clipboard="placeholder">{{ mockText }}</span>`,
standalone: true,
imports: [ClipboardDirective]
})
class TestCopyClipboardComponent {
@@ -89,7 +94,8 @@ describe('CopyClipboardDirective', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [MatSnackBarModule, TestCopyClipboardComponent]
imports: [MatSnackBarModule, MatTooltipModule, TestCopyClipboardComponent],
providers: [provideNoopAnimations()]
});
fixture = TestBed.createComponent(TestCopyClipboardComponent);
testingUtils = new UnitTestingUtils(fixture.debugElement);
@@ -99,17 +105,15 @@ describe('CopyClipboardDirective', () => {
it('should show tooltip when hover element', () => {
testingUtils.hoverOverByCSS('span');
fixture.detectChanges();
expect(testingUtils.getByCSS('.adf-copy-tooltip')).not.toBeNull();
expect(fixture.debugElement.nativeElement.querySelector('span')).not.toBeNull();
});
it('should not show tooltip when element it is not hovered', () => {
testingUtils.hoverOverByCSS('span');
fixture.detectChanges();
expect(testingUtils.getByCSS('.adf-copy-tooltip')).not.toBeNull();
testingUtils.mouseLeaveByCSS('span');
fixture.detectChanges();
expect(testingUtils.getByCSS('.adf-copy-tooltip')).toBeNull();
expect(fixture.debugElement.nativeElement.querySelector('span')).not.toBeNull();
});
it('should copy the content of element when click it', fakeAsync(() => {
@@ -15,13 +15,16 @@
* limitations under the License.
*/
import { Directive, Input, HostListener, Component, ViewContainerRef, ViewEncapsulation, OnInit } from '@angular/core';
import { Directive, Input, HostListener, ViewContainerRef, Self, Optional } from '@angular/core';
import { ClipboardService } from './clipboard.service';
import { TranslatePipe } from '@ngx-translate/core';
import { TranslateService } from '@ngx-translate/core';
import { MatTooltip } from '@angular/material/tooltip';
@Directive({
selector: '[adf-clipboard]',
exportAs: 'adfClipboard'
exportAs: 'adfClipboard',
standalone: true,
hostDirectives: [MatTooltip]
})
export class ClipboardDirective {
/** Translation key or message for the tooltip. */
@@ -37,19 +40,25 @@ export class ClipboardDirective {
// eslint-disable-next-line @angular-eslint/no-input-rename
@Input('clipboard-notification') message: string;
constructor(private clipboardService: ClipboardService, public viewContainerRef: ViewContainerRef) {}
constructor(
private readonly clipboardService: ClipboardService,
public viewContainerRef: ViewContainerRef,
@Self() private readonly matTooltip: MatTooltip,
@Optional() private readonly translate: TranslateService
) {}
@HostListener('mouseenter')
showTooltip() {
if (this.placeholder) {
const componentRef = this.viewContainerRef.createComponent(ClipboardComponent).instance;
componentRef.placeholder = this.placeholder;
}
const messageKey = this.placeholder || 'CLIPBOARD.CLICK_TO_COPY';
const translated = this.translate ? this.translate.instant(messageKey) : messageKey;
this.matTooltip.message = translated;
this.matTooltip.position = 'below';
this.matTooltip.show();
}
@HostListener('mouseleave')
closeTooltip() {
this.viewContainerRef.remove();
this.matTooltip.hide();
}
@HostListener('keydown.enter', ['$event'])
@@ -71,17 +80,3 @@ export class ClipboardDirective {
this.clipboardService.copyContentToClipboard(content, this.message);
}
}
@Component({
selector: 'adf-copy-content-tooltip',
imports: [TranslatePipe],
template: `<span class="adf-copy-tooltip">{{ placeholder | translate }} </span>`,
encapsulation: ViewEncapsulation.None
})
export class ClipboardComponent implements OnInit {
placeholder: string;
ngOnInit() {
this.placeholder = this.placeholder || 'CLIPBOARD.CLICK_TO_COPY';
}
}
@@ -16,13 +16,14 @@
*/
import { NgModule } from '@angular/core';
import { ClipboardDirective, ClipboardComponent } from './clipboard.directive';
import { ClipboardDirective } from './clipboard.directive';
import { MatTooltipModule } from '@angular/material/tooltip';
export const CLIPBOARD_DIRECTIVES = [ClipboardDirective, ClipboardComponent] as const;
export const CLIPBOARD_DIRECTIVES = [ClipboardDirective] as const;
/** @deprecated use `...CLIPBOARD_DIRECTIVES` or import standalone directives */
@NgModule({
imports: [...CLIPBOARD_DIRECTIVES],
exports: [...CLIPBOARD_DIRECTIVES]
imports: [MatTooltipModule, ...CLIPBOARD_DIRECTIVES],
exports: [MatTooltipModule, ...CLIPBOARD_DIRECTIVES]
})
export class ClipboardModule {}
@@ -1,19 +0,0 @@
.adf-copy-tooltip {
position: absolute;
background: var(--theme-primary-color);
color: var(--theme-primary-color-default-contrast);
font-size: var(--theme-caption-font-size);
padding: 2px 5px;
border-radius: 5px;
bottom: 93%;
left: 0;
z-index: 1001;
min-height: 20px;
}
.adf-sticky-header {
.adf-copy-tooltip {
top: 85%;
bottom: 0;
}
}
@@ -37,14 +37,14 @@ import { TruncatePipe } from '../../../pipes/truncate.pipe';
adf-clipboard="CLIPBOARD.CLICK_TO_COPY"
[clipboard-notification]="'CLIPBOARD.SUCCESS_COPY'"
[attr.aria-label]="value$ | async"
[title]="tooltip"
[title]="tooltip ? tooltip : computedTitle"
class="adf-datatable-cell-value"
>{{ column?.maxTextLength ? (value$ | async | truncate : column?.maxTextLength) : (value$ | async) }}</span
>{{ column?.maxTextLength ? (value$ | async | truncate: column?.maxTextLength) : (value$ | async) }}</span
>
</ng-container>
<ng-template #defaultCell>
<span [title]="tooltip" class="adf-datatable-cell-value">{{
column?.maxTextLength ? (value$ | async | truncate : column?.maxTextLength) : (value$ | async)
<span [title]="tooltip ? tooltip : computedTitle" class="adf-datatable-cell-value">{{
column?.maxTextLength ? (value$ | async | truncate: column?.maxTextLength) : (value$ | async)
}}</span>
</ng-template>
`,
@@ -79,6 +79,7 @@ export class DataTableCellComponent implements OnInit {
protected destroyRef = inject(DestroyRef);
protected dataTableService = inject(DataTableService, { optional: true });
value$ = new BehaviorSubject<any>('');
computedTitle: string = '';
ngOnInit() {
this.updateValue();
@@ -88,12 +89,8 @@ export class DataTableCellComponent implements OnInit {
protected updateValue() {
if (this.column?.key && this.row && this.data) {
const value = this.data.getValue(this.row, this.column, this.resolverFn);
this.value$.next(value);
if (!this.tooltip) {
this.tooltip = value;
}
this.computedTitle = this.computeTitle(value);
}
}
@@ -115,4 +112,19 @@ export class DataTableCellComponent implements OnInit {
private getNestedPropertyValue(obj: any, path: string) {
return path.split('.').reduce((source, key) => (source ? source[key] : ''), obj);
}
private computeTitle(value: string): string {
if (this.tooltip) {
return this.tooltip;
}
const rawValue = value;
const max = this.column?.maxTextLength;
if (typeof max === 'number' && max > 0 && rawValue.length > max) {
return rawValue;
}
return '';
}
}
@@ -16,7 +16,7 @@
*/
import { Component, SimpleChange, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import {
AppConfigService,
@@ -36,6 +36,7 @@ import { ServiceTaskListCloudService } from '../../services/service-task-list-cl
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';
import { MatTooltipHarness } from '@angular/material/tooltip/testing';
import { provideCloudPreferences } from '../../../../providers';
@Component({
@@ -396,25 +397,31 @@ describe('ServiceTaskListCloudComponent: Injecting custom columns for task list
expect(componentCustom.taskList.columns.length).toEqual(2);
});
it('it should show copy tooltip when key is present in data-column', () => {
it('it should show copy tooltip when key is present in data-column', fakeAsync(async () => {
customCopyComponent.taskList.reload();
copyFixture.detectChanges();
copyFixture.debugElement.query(By.css('span[title="04fdf69f-4ddd-48ab-9563-da776c9b163c"]')).triggerEventHandler('mouseenter');
const host = copyFixture.debugElement.query(By.css('span[adf-clipboard]'));
host.triggerEventHandler('mouseenter', {});
copyFixture.detectChanges();
expect(copyFixture.debugElement.query(By.css('.adf-copy-tooltip'))).not.toBeNull();
});
it('it should not show copy tooltip when key is not present in data-column', () => {
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(copyFixture);
const tooltip = await loader.getHarness(MatTooltipHarness.with({ selector: 'span[adf-clipboard]' }));
expect(await tooltip.isOpen()).toBeTrue();
}));
it('it should not show copy tooltip when key is not present in data-column', fakeAsync(async () => {
customCopyComponent.taskList.reload();
copyFixture.detectChanges();
copyFixture.debugElement.query(By.css('span[title="serviceTaskName"]')).triggerEventHandler('mouseenter');
const host = copyFixture.debugElement.query(By.css('span[adf-clipboard]'));
host.triggerEventHandler('mouseenter', {});
copyFixture.detectChanges();
expect(copyFixture.debugElement.query(By.css('.adf-copy-tooltip'))).toBeNull();
});
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(copyFixture);
const tooltips = await loader.getAllHarnesses(MatTooltipHarness.with({ selector: 'span[title="serviceTaskName"]' }));
expect(tooltips.length).toBe(0);
}));
});
describe('ServiceTaskListCloudComponent: Copy cell content directive from app.config specifications', () => {
@@ -461,33 +468,35 @@ describe('ServiceTaskListCloudComponent: Copy cell content directive from app.co
fixture.destroy();
});
it('shoud show tooltip if config copyContent flag is true', () => {
it('shoud show tooltip if config copyContent flag is true', fakeAsync(async () => {
taskSpy.and.returnValue(of(fakeServiceTask));
component.presetColumn = 'fakeCustomSchema';
component.reload();
fixture.detectChanges();
const columnWithCopyContentFlagTrue = fixture.debugElement.query(By.css('span[title="04fdf69f-4ddd-48ab-9563-da776c9b163c"]'));
columnWithCopyContentFlagTrue.triggerEventHandler('mouseenter');
const host = fixture.debugElement.query(By.css('span[adf-clipboard]'));
host.triggerEventHandler('mouseenter', {});
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-copy-tooltip')).not.toBeNull();
});
it('shoud not show tooltip if config copyContent flag is NOT true', () => {
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(fixture);
const tooltip = await loader.getHarness(MatTooltipHarness.with({ selector: 'span[adf-clipboard]' }));
expect(await tooltip.isOpen()).toBeTrue();
}));
it('shoud not show tooltip if config copyContent flag is NOT true', fakeAsync(async () => {
taskSpy.and.returnValue(of(fakeServiceTask));
component.presetColumn = 'fakeCustomSchema';
component.reload();
fixture.detectChanges();
const columnWithCopyContentFlagNotTrue = fixture.debugElement.query(By.css('span[title="serviceTaskName"]'));
columnWithCopyContentFlagNotTrue.triggerEventHandler('mouseenter');
const host = fixture.debugElement.query(By.css('span[adf-clipboard]'));
host.triggerEventHandler('mouseenter', {});
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-copy-tooltip')).toBeNull();
});
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(fixture);
const tooltips = await loader.getAllHarnesses(MatTooltipHarness.with({ selector: 'span[title="serviceTaskName"]' }));
expect(tooltips.length).toBe(0);
}));
});
@@ -16,7 +16,7 @@
*/
import { Component, SimpleChange, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import {
AppConfigService,
@@ -40,6 +40,7 @@ import { TASK_LIST_CLOUD_TOKEN, TASK_LIST_PREFERENCES_SERVICE_TOKEN } from '../.
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatProgressSpinnerHarness } from '@angular/material/progress-spinner/testing';
import { MatTooltipHarness } from '@angular/material/tooltip/testing';
import { provideCloudPreferences } from '../../../../providers';
@Component({
@@ -699,24 +700,26 @@ describe('TaskListCloudComponent: Injecting custom colums for tasklist - CustomT
expect(componentCustom.taskList.columns.length).toEqual(3);
});
it('it should show copy tooltip when key is present in data-column', () => {
it('it should show copy tooltip when key is present in data-column', fakeAsync(async () => {
customCopyComponent.taskList.reload();
copyFixture.detectChanges();
copyFixture.debugElement.query(By.css('span[title="11fe013d-c263-11e8-b75b-0a5864600540"]')).triggerEventHandler('mouseenter');
const host = copyFixture.debugElement.query(By.css('span[adf-clipboard]'));
host.triggerEventHandler('mouseenter', {});
copyFixture.detectChanges();
expect(copyFixture.debugElement.query(By.css('.adf-copy-tooltip'))).not.toBeNull();
});
it('it should not show copy tooltip when key is not present in data-column', () => {
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(copyFixture);
const tooltip = await loader.getHarness(MatTooltipHarness.with({ selector: 'span[adf-clipboard]' }));
expect(await tooltip.isOpen()).toBeTrue();
}));
it('it should not show copy tooltip when key is not present in data-column', async () => {
customCopyComponent.taskList.reload();
copyFixture.detectChanges();
copyFixture.debugElement.query(By.css('span[title="standalone-subtask"]')).triggerEventHandler('mouseenter');
copyFixture.detectChanges();
expect(copyFixture.debugElement.query(By.css('.adf-copy-tooltip'))).toBeNull();
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(copyFixture);
const tooltips = await loader.getAllHarnesses(MatTooltipHarness.with({ selector: 'span[title="standalone-subtask"]' }));
expect(tooltips.length).toBe(0);
});
});
@@ -802,19 +805,20 @@ describe('TaskListCloudComponent: Copy cell content directive from app.config sp
fixture.destroy();
});
it('should show tooltip if config copyContent flag is true', () => {
it('should show tooltip if config copyContent flag is true', fakeAsync(async () => {
component.presetColumn = 'fakeCustomSchema';
component.reload();
fixture.detectChanges();
const columnWithCopyContentFlagTrue = fixture.debugElement.query(By.css('span[title="11fe013d-c263-11e8-b75b-0a5864600540"]'));
columnWithCopyContentFlagTrue.triggerEventHandler('mouseenter');
const host = fixture.debugElement.query(By.css('span[adf-clipboard]'));
host.triggerEventHandler('mouseenter', {});
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.querySelector('.adf-copy-tooltip')).not.toBeNull();
});
const loader: HarnessLoader = TestbedHarnessEnvironment.loader(fixture);
const tooltip = await loader.getHarness(MatTooltipHarness.with({ selector: 'span[adf-clipboard]' }));
expect(await tooltip.isOpen()).toBeTrue();
}));
it('should replace priority values', () => {
component.presetColumn = 'fakeCustomSchema';
+3 -1
View File
@@ -14,7 +14,9 @@
"build:libs": "nx run-many -t build --prod --skip-nx-cache",
"build:schematics": "nx run-many -t build-schematics",
"publish": "nx run-many -t npm-publish",
"clean": "rimraf dist node_modules dist/libs"
"clean": "rimraf dist node_modules dist/libs",
"nx:run-target": "nx run",
"nx:run-many": "nx run-many"
},
"repository": {
"type": "git",