Skip to content

Commit

Permalink
feat(tooltip): reference input for positioning the tooltip on referen…
Browse files Browse the repository at this point in the history
…ce element, refactor with signals
  • Loading branch information
xidedix committed May 14, 2024
1 parent bf44bd0 commit 052675d
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand All @@ -24,7 +24,7 @@ describe('PopoverDirective', () => {
viewContainerRef,
listenersService,
changeDetectorRef,
intersectionService
intersectionService,
);
expect(directive).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,10 +24,9 @@ describe('TooltipDirective', () => {
viewContainerRef,
listenersService,
changeDetectorRef,
intersectionService
intersectionService,
);
expect(directive).toBeTruthy();
});

});
});
146 changes: 75 additions & 71 deletions projects/coreui-angular/src/lib/tooltip/tooltip.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
AfterViewInit,
ChangeDetectorRef,
ComponentRef,
computed,
DestroyRef,
Directive,
effect,
Expand All @@ -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<string | TemplateRef<any>>('', { alias: 'cTooltip' });
readonly content = input<string | TemplateRef<any> | undefined>(undefined, { alias: 'cTooltip' });

contentEffect = effect(() => {
if (this.content()) {
this.destroyTooltipElement();
}
});

/**
* Optional popper Options object, takes precedence over cPopoverPlacement prop
* @type Partial<Options>
*/
@Input('cTooltipOptions')
set popperOptions(value: Partial<Options>) {
this._popperOptions = { ...this._popperOptions, placement: this.placement, ...value };
};
readonly popperOptions = input<Partial<Options>>({}, { alias: 'cTooltipOptions' });

get popperOptions(): Partial<Options> {
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<ElementRefDirective | undefined>(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<Triggers | Triggers[]>('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;
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
}
Expand All @@ -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);
});
}

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

private addTooltipElement(): void {
if (!this.content()) {
this.destroyTooltipElement();
return;
}

Expand All @@ -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');
Expand All @@ -225,24 +232,21 @@ 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;
}
this.renderer.removeClass(this.tooltip, 'd-none');
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 {
Expand Down

0 comments on commit 052675d

Please sign in to comment.