Skip to content

Commit 052675d

Browse files
committed
feat(tooltip): reference input for positioning the tooltip on reference element, refactor with signals
1 parent bf44bd0 commit 052675d

File tree

3 files changed

+82
-79
lines changed

3 files changed

+82
-79
lines changed

projects/coreui-angular/src/lib/popover/popover.directive.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChangeDetectorRef, ElementRef, Renderer2, ViewContainerRef } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
23
import { IntersectionService, ListenersService } from '../services';
34
import { PopoverDirective } from './popover.directive';
4-
import { TestBed } from '@angular/core/testing';
55

66
describe('PopoverDirective', () => {
77
let document: Document;
@@ -11,11 +11,11 @@ describe('PopoverDirective', () => {
1111
let changeDetectorRef: ChangeDetectorRef;
1212

1313
it('should create an instance', () => {
14-
const listenersService = new ListenersService(renderer);
1514
TestBed.configureTestingModule({
16-
providers: [IntersectionService]
15+
providers: [IntersectionService, Renderer2, ListenersService],
1716
});
1817
const intersectionService = TestBed.inject(IntersectionService);
18+
const listenersService = TestBed.inject(ListenersService);
1919
TestBed.runInInjectionContext(() => {
2020
const directive = new PopoverDirective(
2121
document,
@@ -24,7 +24,7 @@ describe('PopoverDirective', () => {
2424
viewContainerRef,
2525
listenersService,
2626
changeDetectorRef,
27-
intersectionService
27+
intersectionService,
2828
);
2929
expect(directive).toBeTruthy();
3030
});

projects/coreui-angular/src/lib/tooltip/tooltip.directive.spec.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ describe('TooltipDirective', () => {
1111
let changeDetectorRef: ChangeDetectorRef;
1212

1313
it('should create an instance', () => {
14-
const listenersService = new ListenersService(renderer);
1514
TestBed.configureTestingModule({
16-
providers: [IntersectionService]
15+
providers: [IntersectionService, Renderer2, ListenersService],
1716
});
1817
const intersectionService = TestBed.inject(IntersectionService);
18+
const listenersService = TestBed.inject(ListenersService);
1919
TestBed.runInInjectionContext(() => {
2020
const directive = new TooltipDirective(
2121
document,
@@ -24,10 +24,9 @@ describe('TooltipDirective', () => {
2424
viewContainerRef,
2525
listenersService,
2626
changeDetectorRef,
27-
intersectionService
27+
intersectionService,
2828
);
2929
expect(directive).toBeTruthy();
3030
});
31-
3231
});
3332
});

projects/coreui-angular/src/lib/tooltip/tooltip.directive.ts

+75-71
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
AfterViewInit,
33
ChangeDetectorRef,
44
ComponentRef,
5+
computed,
56
DestroyRef,
67
Directive,
78
effect,
@@ -10,75 +11,95 @@ import {
1011
inject,
1112
Inject,
1213
input,
13-
Input,
14-
OnChanges,
14+
model,
1515
OnDestroy,
1616
OnInit,
1717
Renderer2,
18-
SimpleChanges,
1918
TemplateRef,
20-
ViewContainerRef
19+
ViewContainerRef,
2120
} from '@angular/core';
22-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2321
import { DOCUMENT } from '@angular/common';
24-
import { debounceTime, filter, finalize } from 'rxjs/operators';
2522
import { createPopper, Instance, Options } from '@popperjs/core';
2623

2724
import { Triggers } from '../coreui.types';
2825
import { TooltipComponent } from './tooltip/tooltip.component';
29-
import { IListenersConfig, ListenersService } from '../services/listeners.service';
30-
import { IntersectionService } from '../services';
26+
import { IListenersConfig, IntersectionService, ListenersService } from '../services';
27+
import { debounceTime, filter, finalize } from 'rxjs/operators';
28+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
29+
import { ElementRefDirective } from '../shared';
3130

3231
@Directive({
3332
selector: '[cTooltip]',
3433
exportAs: 'cTooltip',
3534
providers: [ListenersService, IntersectionService],
36-
standalone: true
35+
standalone: true,
3736
})
38-
export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit {
39-
37+
export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit {
4038
/**
4139
* Content of tooltip
4240
* @type {string | TemplateRef}
4341
*/
44-
readonly content = input<string | TemplateRef<any>>('', { alias: 'cTooltip' });
42+
readonly content = input<string | TemplateRef<any> | undefined>(undefined, { alias: 'cTooltip' });
43+
44+
contentEffect = effect(() => {
45+
if (this.content()) {
46+
this.destroyTooltipElement();
47+
}
48+
});
4549

4650
/**
4751
* Optional popper Options object, takes precedence over cPopoverPlacement prop
4852
* @type Partial<Options>
4953
*/
50-
@Input('cTooltipOptions')
51-
set popperOptions(value: Partial<Options>) {
52-
this._popperOptions = { ...this._popperOptions, placement: this.placement, ...value };
53-
};
54+
readonly popperOptions = input<Partial<Options>>({}, { alias: 'cTooltipOptions' });
5455

55-
get popperOptions(): Partial<Options> {
56-
return { placement: this.placement, ...this._popperOptions };
57-
}
56+
popperOptionsEffect = effect(() => {
57+
this._popperOptions = {
58+
...this._popperOptions,
59+
placement: this.placement(),
60+
...this.popperOptions(),
61+
};
62+
});
63+
64+
popperOptionsComputed = computed(() => {
65+
return { placement: this.placement(), ...this._popperOptions };
66+
});
5867

5968
/**
6069
* Describes the placement of your component after Popper.js has applied all the modifiers that may have flipped or altered the originally provided placement property.
70+
* @type: 'top' | 'bottom' | 'left' | 'right'
71+
* @default: 'top'
72+
*/
73+
readonly placement = input<'top' | 'bottom' | 'left' | 'right'>('top', {
74+
alias: 'cTooltipPlacement',
75+
});
76+
77+
/**
78+
* ElementRefDirective for positioning the tooltip on reference element
79+
* @type: ElementRefDirective
80+
* @default: undefined
6181
*/
62-
@Input('cTooltipPlacement') placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
82+
readonly reference = input<ElementRefDirective | undefined>(undefined, {
83+
alias: 'cTooltipRef',
84+
});
85+
86+
readonly referenceRef = computed(() => this.reference()?.elementRef ?? this.hostElement);
87+
6388
/**
6489
* Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them.
65-
* @type {'hover' | 'focus' | 'click'}
90+
* @type: 'Triggers | Triggers[]
6691
*/
67-
@Input('cTooltipTrigger') trigger: Triggers | Triggers[] = 'hover';
92+
readonly trigger = input<Triggers | Triggers[]>('hover', { alias: 'cTooltipTrigger' });
6893

6994
/**
7095
* Toggle the visibility of tooltip component.
96+
* @type boolean
7197
*/
72-
@Input('cTooltipVisible')
73-
set visible(value: boolean) {
74-
this._visible = value;
75-
}
98+
readonly visible = model(false, { alias: 'cTooltipVisible' });
7699

77-
get visible() {
78-
return this._visible;
79-
}
80-
81-
private _visible = false;
100+
visibleEffect = effect(() => {
101+
this.visible() ? this.addTooltipElement() : this.removeTooltipElement();
102+
});
82103

83104
@HostBinding('attr.aria-describedby') get ariaDescribedBy(): string | null {
84105
return this.tooltipId ? this.tooltipId : null;
@@ -94,10 +115,10 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
94115
{
95116
name: 'offset',
96117
options: {
97-
offset: [0, 5]
98-
}
99-
}
100-
]
118+
offset: [0, 5],
119+
},
120+
},
121+
],
101122
};
102123

103124
readonly #destroyRef = inject(DestroyRef);
@@ -109,24 +130,13 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
109130
private viewContainerRef: ViewContainerRef,
110131
private listenersService: ListenersService,
111132
private changeDetectorRef: ChangeDetectorRef,
112-
private intersectionService: IntersectionService
133+
private intersectionService: IntersectionService,
113134
) {}
114135

115-
contentEffect = effect(() => {
116-
this.destroyTooltipElement();
117-
this.content() ? this.addTooltipElement() : this.removeTooltipElement();
118-
});
119-
120136
ngAfterViewInit(): void {
121137
this.intersectionServiceSubscribe();
122138
}
123139

124-
ngOnChanges(changes: SimpleChanges): void {
125-
if (changes['visible']) {
126-
changes['visible'].currentValue ? this.addTooltipElement() : this.removeTooltipElement();
127-
}
128-
}
129-
130140
ngOnDestroy(): void {
131141
this.clearListeners();
132142
this.destroyTooltipElement();
@@ -139,19 +149,16 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
139149
private setListeners(): void {
140150
const config: IListenersConfig = {
141151
hostElement: this.hostElement,
142-
trigger: this.trigger,
152+
trigger: this.trigger(),
143153
callbackToggle: () => {
144-
this.visible = !this.visible;
145-
this.visible ? this.addTooltipElement() : this.removeTooltipElement();
154+
this.visible.set(!this.visible());
146155
},
147156
callbackOff: () => {
148-
this.visible = false;
149-
this.removeTooltipElement();
157+
this.visible.set(false);
150158
},
151159
callbackOn: () => {
152-
this.visible = true;
153-
this.addTooltipElement();
154-
}
160+
this.visible.set(true);
161+
},
155162
};
156163
this.listenersService.setListeners(config);
157164
}
@@ -161,19 +168,18 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
161168
}
162169

163170
private intersectionServiceSubscribe(): void {
164-
this.intersectionService.createIntersectionObserver(this.hostElement);
171+
this.intersectionService.createIntersectionObserver(this.referenceRef());
165172
this.intersectionService.intersecting$
166173
.pipe(
167-
filter(next => next.hostElement === this.hostElement),
174+
filter((next) => next.hostElement === this.referenceRef()),
168175
debounceTime(100),
169176
finalize(() => {
170-
this.intersectionService.unobserve(this.hostElement);
177+
this.intersectionService.unobserve(this.referenceRef());
171178
}),
172-
takeUntilDestroyed(this.#destroyRef)
179+
takeUntilDestroyed(this.#destroyRef),
173180
)
174-
.subscribe(next => {
175-
this.visible = next.isIntersecting ? this.visible : false;
176-
!this.visible && this.removeTooltipElement();
181+
.subscribe((next) => {
182+
this.visible.set(next.isIntersecting ? this.visible() : false);
177183
});
178184
}
179185

@@ -205,6 +211,7 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
205211

206212
private addTooltipElement(): void {
207213
if (!this.content()) {
214+
this.destroyTooltipElement();
208215
return;
209216
}
210217

@@ -214,7 +221,7 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
214221

215222
this.tooltipId = this.getUID('tooltip');
216223
this.tooltipRef.instance.id = this.tooltipId;
217-
this.tooltipRef.instance.content = this.content();
224+
this.tooltipRef.instance.content = this.content() ?? '';
218225

219226
this.tooltip = this.tooltipRef.location.nativeElement;
220227
this.renderer.addClass(this.tooltip, 'd-none');
@@ -225,24 +232,21 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView
225232
this.viewContainerRef.insert(this.tooltipRef.hostView);
226233
this.renderer.appendChild(this.document.body, this.tooltip);
227234

228-
this.popperInstance = createPopper(
229-
this.hostElement.nativeElement,
230-
this.tooltip,
231-
{ ...this.popperOptions }
232-
);
233-
if (!this.visible) {
235+
this.popperInstance = createPopper(this.referenceRef().nativeElement, this.tooltip, {
236+
...this.popperOptionsComputed(),
237+
});
238+
if (!this.visible()) {
234239
this.removeTooltipElement();
235240
return;
236241
}
237242
this.renderer.removeClass(this.tooltip, 'd-none');
238243
this.changeDetectorRef.markForCheck();
239244

240245
setTimeout(() => {
241-
this.tooltipRef && (this.tooltipRef.instance.visible = this.visible);
246+
this.tooltipRef && (this.tooltipRef.instance.visible = this.visible());
242247
this.popperInstance?.forceUpdate();
243248
this.changeDetectorRef?.markForCheck();
244249
}, 100);
245-
246250
}
247251

248252
private removeTooltipElement(): void {

0 commit comments

Comments
 (0)