Skip to content

Commit 4923baa

Browse files
committed
feat(tooltip): add custom close button template
1 parent 39bb9e2 commit 4923baa

File tree

6 files changed

+209
-127
lines changed

6 files changed

+209
-127
lines changed
Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1-
import { Component, Output, EventEmitter } from '@angular/core';
1+
import { Component, Output, EventEmitter, HostBinding, HostListener, Input, TemplateRef } from '@angular/core';
22
import { IgxIconComponent } from '../../icon/icon.component';
3+
import { CommonModule } from '@angular/common';
34

45
@Component({
56
selector: 'igx-tooltip-close-button',
67
template: `
7-
<div class="close-button">
8-
<igx-icon
9-
name="close"
10-
ariaLabel="Close tooltip"
11-
(click)="handleClick()">
12-
</igx-icon>
13-
</div>
8+
<ng-container *ngIf="customTemplate; else defaultTemplate">
9+
<ng-container *ngTemplateOutlet="customTemplate"></ng-container>
10+
</ng-container>
11+
<ng-template #defaultTemplate>
12+
<igx-icon aria-hidden="true" family="default" name="close"></igx-icon>
13+
</ng-template>
1414
`,
15-
imports: [IgxIconComponent],
15+
imports: [IgxIconComponent, CommonModule],
1616
})
17-
export class TooltipCloseButtonComponent {
18-
@Output() public clicked = new EventEmitter<void>();
17+
export class IgxTooltipCloseButtonComponent {
18+
@Input()
19+
public customTemplate: TemplateRef<any>;
1920

20-
public handleClick(): void {
21-
this.clicked.emit();
22-
}
21+
@Output()
22+
public clicked = new EventEmitter<void>();
23+
24+
@HostBinding('class')
25+
private _defaultClass = 'igx-tooltip-close-button';
26+
27+
@HostListener('click')
28+
public handleClick() {
29+
this.clicked.emit();
30+
}
2331
}

projects/igniteui-angular/src/lib/directives/tooltip/tooltip-target.directive.ts

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useAnimation } from '@angular/animations';
2-
import { Directive, OnInit, OnDestroy, Output, ElementRef, Optional, ViewContainerRef, HostListener, Input, EventEmitter, booleanAttribute, TemplateRef } from '@angular/core';
2+
import { Directive, OnInit, OnDestroy, Output, ElementRef, Optional, ViewContainerRef, HostListener, Input, EventEmitter, booleanAttribute, TemplateRef, ComponentRef, Renderer2 } from '@angular/core';
33
import { Subject } from 'rxjs';
44
import { takeUntil } from 'rxjs/operators';
55
import { IgxNavigationService } from '../../core/navigation';
@@ -8,6 +8,7 @@ import { AutoPositionStrategy, HorizontalAlignment, PositionSettings } from '../
88
import { IgxToggleActionDirective } from '../toggle/toggle.directive';
99
import { IgxTooltipComponent } from './tooltip.component';
1010
import { IgxTooltipDirective } from './tooltip.directive';
11+
import { IgxTooltipCloseButtonComponent } from './tooltip-close-button.component';
1112
import { fadeOut, scaleInCenter } from 'igniteui-angular/animations';
1213

1314
export interface ITooltipShowEventArgs extends IBaseEventArgs {
@@ -41,6 +42,11 @@ export interface ITooltipHideEventArgs extends IBaseEventArgs {
4142
standalone: true
4243
})
4344
export class IgxTooltipTargetDirective extends IgxToggleActionDirective implements OnInit, OnDestroy {
45+
46+
private _closeButtonRef?: ComponentRef<IgxTooltipCloseButtonComponent>;
47+
private _closeTemplate: TemplateRef<any>;
48+
private _sticky = false;
49+
4450
/**
4551
* Gets/sets the amount of milliseconds that should pass before showing the tooltip.
4652
*
@@ -96,11 +102,11 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
96102
*/
97103
@Input()
98104
public set disableArrow(value: boolean) {
99-
this.target._disableArrow = value;
105+
this.target.disableArrow = value;
100106
}
101107

102108
public get disableArrow(): boolean {
103-
return this.target._disableArrow;
109+
return this.target.disableArrow;
104110
}
105111

106112
/**
@@ -123,13 +129,20 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
123129
*/
124130
@Input()
125131
public set sticky (value: boolean) {
126-
this.target._sticky = value;
132+
const changed = this._sticky !== value;
133+
this._sticky = value;
134+
135+
if (changed) {
136+
this._evaluateCloseButtonState();
137+
this.target.role = value ? "status" : "tooltip"
138+
}
127139
};
128140

129141
public get sticky (): boolean {
130-
return this.target._sticky;
142+
return this._sticky;
131143
}
132144

145+
133146
/**
134147
* Allows full control over the appearance of the close button inside the tooltip.
135148
*
@@ -152,17 +165,12 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
152165
* ```
153166
*/
154167
@Input('closeButtonTemplate')
155-
public set customCloseTemplate(value: TemplateRef<any>) {
156-
this.target._customCloseTemplate = value;
157-
if (value) {
158-
this.target.renderCustomCloseTemplate();
159-
} else {
160-
this.target.appendDefaultCloseIcon();
161-
}
168+
public set closeTemplate(value: TemplateRef<any>) {
169+
this._closeTemplate = value;
170+
this._evaluateCloseButtonState();
162171
}
163-
164-
public get customCloseTemplate(): TemplateRef<any> | undefined {
165-
return this.target._customCloseTemplate;
172+
public get closeTemplate(): TemplateRef<any> | undefined {
173+
return this._closeTemplate;
166174
}
167175

168176
/**
@@ -280,7 +288,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
280288
private _isForceClosed = false;
281289

282290
constructor(private _element: ElementRef,
283-
@Optional() private _navigationService: IgxNavigationService, private _viewContainerRef: ViewContainerRef) {
291+
@Optional() private _navigationService: IgxNavigationService, private _viewContainerRef: ViewContainerRef, private _renderer: Renderer2) {
284292
super(_element, _navigationService);
285293
}
286294

@@ -358,7 +366,7 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
358366
const positionSettings: PositionSettings = {
359367
horizontalDirection: HorizontalAlignment.Center,
360368
horizontalStartPoint: HorizontalAlignment.Center,
361-
openAnimation: useAnimation(scaleInCenter, { params: { duration: '150ms' } }),
369+
openAnimation: useAnimation(scaleInCenter, { params: { duration: '1150ms' } }),
362370
closeAnimation: useAnimation(fadeOut, { params: { duration: '75ms' } })
363371
};
364372

@@ -499,4 +507,45 @@ export class IgxTooltipTargetDirective extends IgxToggleActionDirective implemen
499507
this._isForceClosed = true;
500508
}
501509
}
510+
511+
512+
private _evaluateCloseButtonState(): void {
513+
if (!this.target || !(this.target instanceof IgxTooltipDirective)) {
514+
return;
515+
}
516+
517+
if (this._sticky) {
518+
this._removeCloseButton();
519+
this._createCloseTemplate(this._closeTemplate);
520+
} else {
521+
this._removeCloseButton();
522+
}
523+
}
524+
525+
526+
private _createCloseTemplate(template?: TemplateRef<any> | undefined) {
527+
if(this.target instanceof IgxTooltipDirective) {
528+
529+
this._closeTemplate = template;
530+
531+
const componentRef = this._viewContainerRef.createComponent(IgxTooltipCloseButtonComponent);
532+
this._closeButtonRef = componentRef;
533+
534+
componentRef.instance.customTemplate = template;
535+
componentRef.instance.clicked.pipe(takeUntil(this._destroy$)).subscribe(() => {
536+
this.hideTooltip();
537+
});
538+
539+
if (this.target.element instanceof HTMLElement) {
540+
this._renderer.appendChild(this.target.element, componentRef.location.nativeElement);
541+
}
542+
}
543+
}
544+
545+
private _removeCloseButton(){
546+
if (this._closeButtonRef) {
547+
this._closeButtonRef.destroy();
548+
this._closeButtonRef = undefined;
549+
}
550+
}
502551
}

projects/igniteui-angular/src/lib/directives/tooltip/tooltip.directive.spec.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fakeAsync, TestBed, tick, flush, waitForAsync } from '@angular/core/testing';
22
import { By } from '@angular/platform-browser';
33
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
4-
import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent } from '../../test-utils/tooltip-components.spec';
4+
import { IgxTooltipSingleTargetComponent, IgxTooltipMultipleTargetsComponent, IgxTooltipPlainStringComponent, IgxTooltipWithToggleActionComponent, IgxTooltipWithCloseButtonComponent } from '../../test-utils/tooltip-components.spec';
55
import { UIInteractions } from '../../test-utils/ui-interactions.spec';
66
import { configureTestSuite } from '../../test-utils/configure-suite';
77
import { HorizontalAlignment, VerticalAlignment, AutoPositionStrategy } from '../../services/public_api';
@@ -26,7 +26,8 @@ describe('IgxTooltip', () => {
2626
IgxTooltipSingleTargetComponent,
2727
IgxTooltipMultipleTargetsComponent,
2828
IgxTooltipPlainStringComponent,
29-
IgxTooltipWithToggleActionComponent
29+
IgxTooltipWithToggleActionComponent,
30+
IgxTooltipWithCloseButtonComponent
3031
]
3132
}).compileComponents();
3233
UIInteractions.clearOverlay();
@@ -554,6 +555,73 @@ describe('IgxTooltip', () => {
554555
expect(fix.componentInstance.toggleDir.collapsed).toBe(false);
555556
}));
556557
});
558+
559+
describe('Tooltip Sticky with Close Button', () => {
560+
beforeEach(() => {
561+
fix = TestBed.createComponent(IgxTooltipWithCloseButtonComponent);
562+
fix.detectChanges();
563+
tooltipNativeElement = fix.debugElement.query(By.directive(IgxTooltipDirective)).nativeElement;
564+
tooltipTarget = fix.componentInstance.tooltipTarget as IgxTooltipTargetDirective;
565+
button = fix.debugElement.query(By.directive(IgxTooltipTargetDirective));
566+
});
567+
568+
it('should render custom close button when sticky is true', fakeAsync(() => {
569+
const closeBtn = document.querySelector('.my-close-btn');
570+
expect(closeBtn).toBeTruthy();
571+
}));
572+
573+
it('should destroy close button when sticky is set to false', fakeAsync(() => {
574+
tooltipTarget.sticky = false;
575+
fix.detectChanges();
576+
tick();
577+
578+
hoverElement(button);
579+
flush();
580+
verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true);
581+
582+
const closeBtn = document.querySelector('.my-close-btn');
583+
expect(closeBtn).toBeFalsy();
584+
585+
}));
586+
587+
it('should call hideTooltip when custom close button is clicked', fakeAsync(() => {
588+
const spy = spyOn(tooltipTarget, 'hideTooltip');
589+
590+
hoverElement(button);
591+
flush();
592+
verifyTooltipVisibility(tooltipNativeElement, tooltipTarget, true);
593+
594+
const closeBtn = document.querySelector('.my-close-btn') as HTMLElement;
595+
closeBtn.click();
596+
fix.detectChanges();
597+
tick();
598+
599+
expect(spy).toHaveBeenCalledOnceWith();
600+
}));
601+
602+
it('should use default close icon when no custom template is passed', fakeAsync(() => {
603+
// Clear custom template
604+
tooltipTarget.closeTemplate = null;
605+
fix.detectChanges();
606+
tick();
607+
608+
const icon = document.querySelector('igx-icon');
609+
expect(icon).toBeTruthy();
610+
expect(icon?.textContent?.trim().toLowerCase()).toBe('close');
611+
}));
612+
613+
it('should update the DOM role attribute correctly when sticky changes', fakeAsync(() => {
614+
// Initially sticky
615+
fix.detectChanges();
616+
tick();
617+
expect(tooltipNativeElement.getAttribute('role')).toBe('status');
618+
619+
tooltipTarget.sticky = false;
620+
fix.detectChanges();
621+
tick();
622+
expect(tooltipNativeElement.getAttribute('role')).toBe('tooltip');
623+
}));
624+
});
557625
});
558626

559627
const hoverElement = (element) => element.nativeElement.dispatchEvent(new MouseEvent('mouseenter'));

0 commit comments

Comments
 (0)