From 052675d7fbc83b84d2ed9d84370c9f2df13fb8b6 Mon Sep 17 00:00:00 2001 From: xidedix Date: Tue, 14 May 2024 12:09:26 +0200 Subject: [PATCH] feat(tooltip): reference input for positioning the tooltip on reference element, refactor with signals --- .../src/lib/popover/popover.directive.spec.ts | 8 +- .../src/lib/tooltip/tooltip.directive.spec.ts | 7 +- .../src/lib/tooltip/tooltip.directive.ts | 146 +++++++++--------- 3 files changed, 82 insertions(+), 79 deletions(-) diff --git a/projects/coreui-angular/src/lib/popover/popover.directive.spec.ts b/projects/coreui-angular/src/lib/popover/popover.directive.spec.ts index f018ba0d..f7124e42 100644 --- a/projects/coreui-angular/src/lib/popover/popover.directive.spec.ts +++ b/projects/coreui-angular/src/lib/popover/popover.directive.spec.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, ElementRef, Renderer2, ViewContainerRef } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { IntersectionService, ListenersService } from '../services'; import { PopoverDirective } from './popover.directive'; -import { TestBed } from '@angular/core/testing'; describe('PopoverDirective', () => { let document: Document; @@ -11,11 +11,11 @@ describe('PopoverDirective', () => { let changeDetectorRef: ChangeDetectorRef; it('should create an instance', () => { - const listenersService = new ListenersService(renderer); TestBed.configureTestingModule({ - providers: [IntersectionService] + providers: [IntersectionService, Renderer2, ListenersService], }); const intersectionService = TestBed.inject(IntersectionService); + const listenersService = TestBed.inject(ListenersService); TestBed.runInInjectionContext(() => { const directive = new PopoverDirective( document, @@ -24,7 +24,7 @@ describe('PopoverDirective', () => { viewContainerRef, listenersService, changeDetectorRef, - intersectionService + intersectionService, ); expect(directive).toBeTruthy(); }); diff --git a/projects/coreui-angular/src/lib/tooltip/tooltip.directive.spec.ts b/projects/coreui-angular/src/lib/tooltip/tooltip.directive.spec.ts index d169b0a8..9efa37b6 100644 --- a/projects/coreui-angular/src/lib/tooltip/tooltip.directive.spec.ts +++ b/projects/coreui-angular/src/lib/tooltip/tooltip.directive.spec.ts @@ -11,11 +11,11 @@ describe('TooltipDirective', () => { let changeDetectorRef: ChangeDetectorRef; it('should create an instance', () => { - const listenersService = new ListenersService(renderer); TestBed.configureTestingModule({ - providers: [IntersectionService] + providers: [IntersectionService, Renderer2, ListenersService], }); const intersectionService = TestBed.inject(IntersectionService); + const listenersService = TestBed.inject(ListenersService); TestBed.runInInjectionContext(() => { const directive = new TooltipDirective( document, @@ -24,10 +24,9 @@ describe('TooltipDirective', () => { viewContainerRef, listenersService, changeDetectorRef, - intersectionService + intersectionService, ); expect(directive).toBeTruthy(); }); - }); }); diff --git a/projects/coreui-angular/src/lib/tooltip/tooltip.directive.ts b/projects/coreui-angular/src/lib/tooltip/tooltip.directive.ts index a7417b24..6788a611 100644 --- a/projects/coreui-angular/src/lib/tooltip/tooltip.directive.ts +++ b/projects/coreui-angular/src/lib/tooltip/tooltip.directive.ts @@ -2,6 +2,7 @@ import { AfterViewInit, ChangeDetectorRef, ComponentRef, + computed, DestroyRef, Directive, effect, @@ -10,75 +11,95 @@ import { inject, Inject, input, - Input, - OnChanges, + model, OnDestroy, OnInit, Renderer2, - SimpleChanges, TemplateRef, - ViewContainerRef + ViewContainerRef, } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DOCUMENT } from '@angular/common'; -import { debounceTime, filter, finalize } from 'rxjs/operators'; import { createPopper, Instance, Options } from '@popperjs/core'; import { Triggers } from '../coreui.types'; import { TooltipComponent } from './tooltip/tooltip.component'; -import { IListenersConfig, ListenersService } from '../services/listeners.service'; -import { IntersectionService } from '../services'; +import { IListenersConfig, IntersectionService, ListenersService } from '../services'; +import { debounceTime, filter, finalize } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ElementRefDirective } from '../shared'; @Directive({ selector: '[cTooltip]', exportAs: 'cTooltip', providers: [ListenersService, IntersectionService], - standalone: true + standalone: true, }) -export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit { - +export class TooltipDirective implements OnDestroy, OnInit, AfterViewInit { /** * Content of tooltip * @type {string | TemplateRef} */ - readonly content = input>('', { alias: 'cTooltip' }); + readonly content = input | undefined>(undefined, { alias: 'cTooltip' }); + + contentEffect = effect(() => { + if (this.content()) { + this.destroyTooltipElement(); + } + }); /** * Optional popper Options object, takes precedence over cPopoverPlacement prop * @type Partial */ - @Input('cTooltipOptions') - set popperOptions(value: Partial) { - this._popperOptions = { ...this._popperOptions, placement: this.placement, ...value }; - }; + readonly popperOptions = input>({}, { alias: 'cTooltipOptions' }); - get popperOptions(): Partial { - return { placement: this.placement, ...this._popperOptions }; - } + popperOptionsEffect = effect(() => { + this._popperOptions = { + ...this._popperOptions, + placement: this.placement(), + ...this.popperOptions(), + }; + }); + + popperOptionsComputed = computed(() => { + return { placement: this.placement(), ...this._popperOptions }; + }); /** * 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. + * @type: 'top' | 'bottom' | 'left' | 'right' + * @default: 'top' + */ + readonly placement = input<'top' | 'bottom' | 'left' | 'right'>('top', { + alias: 'cTooltipPlacement', + }); + + /** + * ElementRefDirective for positioning the tooltip on reference element + * @type: ElementRefDirective + * @default: undefined */ - @Input('cTooltipPlacement') placement: 'top' | 'bottom' | 'left' | 'right' = 'top'; + readonly reference = input(undefined, { + alias: 'cTooltipRef', + }); + + readonly referenceRef = computed(() => this.reference()?.elementRef ?? this.hostElement); + /** * Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them. - * @type {'hover' | 'focus' | 'click'} + * @type: 'Triggers | Triggers[] */ - @Input('cTooltipTrigger') trigger: Triggers | Triggers[] = 'hover'; + readonly trigger = input('hover', { alias: 'cTooltipTrigger' }); /** * Toggle the visibility of tooltip component. + * @type boolean */ - @Input('cTooltipVisible') - set visible(value: boolean) { - this._visible = value; - } + readonly visible = model(false, { alias: 'cTooltipVisible' }); - get visible() { - return this._visible; - } - - private _visible = false; + visibleEffect = effect(() => { + this.visible() ? this.addTooltipElement() : this.removeTooltipElement(); + }); @HostBinding('attr.aria-describedby') get ariaDescribedBy(): string | null { return this.tooltipId ? this.tooltipId : null; @@ -94,10 +115,10 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView { name: 'offset', options: { - offset: [0, 5] - } - } - ] + offset: [0, 5], + }, + }, + ], }; readonly #destroyRef = inject(DestroyRef); @@ -109,24 +130,13 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView private viewContainerRef: ViewContainerRef, private listenersService: ListenersService, private changeDetectorRef: ChangeDetectorRef, - private intersectionService: IntersectionService + private intersectionService: IntersectionService, ) {} - contentEffect = effect(() => { - this.destroyTooltipElement(); - this.content() ? this.addTooltipElement() : this.removeTooltipElement(); - }); - ngAfterViewInit(): void { this.intersectionServiceSubscribe(); } - ngOnChanges(changes: SimpleChanges): void { - if (changes['visible']) { - changes['visible'].currentValue ? this.addTooltipElement() : this.removeTooltipElement(); - } - } - ngOnDestroy(): void { this.clearListeners(); this.destroyTooltipElement(); @@ -139,19 +149,16 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView private setListeners(): void { const config: IListenersConfig = { hostElement: this.hostElement, - trigger: this.trigger, + trigger: this.trigger(), callbackToggle: () => { - this.visible = !this.visible; - this.visible ? this.addTooltipElement() : this.removeTooltipElement(); + this.visible.set(!this.visible()); }, callbackOff: () => { - this.visible = false; - this.removeTooltipElement(); + this.visible.set(false); }, callbackOn: () => { - this.visible = true; - this.addTooltipElement(); - } + this.visible.set(true); + }, }; this.listenersService.setListeners(config); } @@ -161,19 +168,18 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView } private intersectionServiceSubscribe(): void { - this.intersectionService.createIntersectionObserver(this.hostElement); + this.intersectionService.createIntersectionObserver(this.referenceRef()); this.intersectionService.intersecting$ .pipe( - filter(next => next.hostElement === this.hostElement), + filter((next) => next.hostElement === this.referenceRef()), debounceTime(100), finalize(() => { - this.intersectionService.unobserve(this.hostElement); + this.intersectionService.unobserve(this.referenceRef()); }), - takeUntilDestroyed(this.#destroyRef) + takeUntilDestroyed(this.#destroyRef), ) - .subscribe(next => { - this.visible = next.isIntersecting ? this.visible : false; - !this.visible && this.removeTooltipElement(); + .subscribe((next) => { + this.visible.set(next.isIntersecting ? this.visible() : false); }); } @@ -205,6 +211,7 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView private addTooltipElement(): void { if (!this.content()) { + this.destroyTooltipElement(); return; } @@ -214,7 +221,7 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView this.tooltipId = this.getUID('tooltip'); this.tooltipRef.instance.id = this.tooltipId; - this.tooltipRef.instance.content = this.content(); + this.tooltipRef.instance.content = this.content() ?? ''; this.tooltip = this.tooltipRef.location.nativeElement; this.renderer.addClass(this.tooltip, 'd-none'); @@ -225,12 +232,10 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView this.viewContainerRef.insert(this.tooltipRef.hostView); this.renderer.appendChild(this.document.body, this.tooltip); - this.popperInstance = createPopper( - this.hostElement.nativeElement, - this.tooltip, - { ...this.popperOptions } - ); - if (!this.visible) { + this.popperInstance = createPopper(this.referenceRef().nativeElement, this.tooltip, { + ...this.popperOptionsComputed(), + }); + if (!this.visible()) { this.removeTooltipElement(); return; } @@ -238,11 +243,10 @@ export class TooltipDirective implements OnChanges, OnDestroy, OnInit, AfterView this.changeDetectorRef.markForCheck(); setTimeout(() => { - this.tooltipRef && (this.tooltipRef.instance.visible = this.visible); + this.tooltipRef && (this.tooltipRef.instance.visible = this.visible()); this.popperInstance?.forceUpdate(); this.changeDetectorRef?.markForCheck(); }, 100); - } private removeTooltipElement(): void {