UNPKG

@engie-group/fluid-design-system-angular

Version:

Fluid Design System Angular

188 lines (166 loc) 5.92 kB
import { ConnectedPosition, Overlay, OverlayPositionBuilder, OverlayRef, ScrollDispatcher } from '@angular/cdk/overlay'; import { ComponentPortal } from '@angular/cdk/portal'; import { DOCUMENT } from '@angular/common'; import { ComponentRef, Directive, ElementRef, HostListener, Inject, Input, NgZone, OnDestroy, OnInit, TemplateRef } from '@angular/core'; import { fromEvent, merge, Subject, takeUntil } from 'rxjs'; import { TooltipComponent } from '../tooltip.component'; import { TooltipOptions } from '../tooltip.model'; @Directive({ selector: '[njTooltip]', exportAs: 'njTooltip', standalone: true }) export class TooltipDirective implements OnInit, OnDestroy { private unsubscribe: Subject<void> = new Subject<void>(); private _tooltipOptions: TooltipOptions; private overlayRef: OverlayRef; private tooltipRef: ComponentRef<TooltipComponent>; private intersectionObserver: IntersectionObserver; @Input() set tooltipOptions(value: TooltipOptions) { this._tooltipOptions = value; this.setTooltipValues(); const scrollableAncestors = this.scrollDispatcher.getAncestorScrollContainers(this.el); const positionStrategy = this.overlayPositionBuilder .flexibleConnectedTo(this.el) .withPositions([this.getPositionOptions()]) .withScrollableContainers(scrollableAncestors); this.overlayRef?.updatePositionStrategy(positionStrategy); } get tooltipOptions(): TooltipOptions { return this._tooltipOptions; } @Input() tooltipCustomContent: TemplateRef<any>; constructor( private el: ElementRef, private overlayPositionBuilder: OverlayPositionBuilder, private overlay: Overlay, private scrollDispatcher: ScrollDispatcher, @Inject(DOCUMENT) private doc: Document, private zone: NgZone ) {} ngOnInit(): void { const scrollableAncestors = this.scrollDispatcher.getAncestorScrollContainers(this.el); const positionStrategy = this.overlayPositionBuilder .flexibleConnectedTo(this.el) .withPositions([this.getPositionOptions()]) .withScrollableContainers(scrollableAncestors); this.overlayRef = this.overlay.create({ positionStrategy, scrollStrategy: this.overlay.scrollStrategies.reposition() }); } ngOnDestroy() { this.overlayRef?.dispose(); this.unsubscribe.next(); this.unsubscribe.complete(); } getPositionOptions(): ConnectedPosition { const defaultPosition: ConnectedPosition = { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' }; switch (this.tooltipOptions?.placement) { case 'bottom': return { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top' }; case 'top': return defaultPosition; case 'left': return { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center' }; case 'right': return { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center' }; default: return defaultPosition; } } @HostListener('mouseenter') @HostListener('focusin') show() { if (this.tooltipRef) { return; } this.tooltipRef = this.overlayRef.attach(new ComponentPortal(TooltipComponent)); merge( fromEvent(this.tooltipRef?.location?.nativeElement, 'mouseleave'), fromEvent(this.tooltipRef?.location?.nativeElement, 'focusout') ) .pipe(takeUntil(this.unsubscribe)) .subscribe((event) => { this.hide(event); }); this.zone.runOutsideAngular(() => { this.intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { const isElementVisible = entry.isIntersecting; if (!isElementVisible) { this.zone.run(() => { this.hide(); }); } }); }); this.intersectionObserver.observe(this.el?.nativeElement); }); this.setTooltipValues(); this.el?.nativeElement?.firstElementChild?.setAttribute('aria-describedby', this.tooltipOptions.tooltipId); } @HostListener('mouseleave', ['$event']) @HostListener('focusout', ['$event']) hide(event?) { const focusedElement = this.doc?.activeElement; const isFocusedElement = focusedElement && this.el.nativeElement.contains(focusedElement); const newTarget = (event as MouseEvent)?.relatedTarget as Node | null; const isNextTargetTooltip = newTarget && this.overlayRef?.overlayElement?.contains(newTarget); const isNextTargetElement = newTarget && this.el?.nativeElement?.contains(newTarget); if (!newTarget || (!isNextTargetTooltip && !isNextTargetElement && !isFocusedElement)) { this.overlayRef.detach(); this.tooltipRef = null; this.unsubscribe.next(); this.intersectionObserver.disconnect(); this.el?.nativeElement?.firstElementChild?.removeAttribute('aria-describedby'); } } setTooltipValues() { const tooltipComponent = this.tooltipRef?.instance; if (!tooltipComponent) { return; } tooltipComponent.label = this.tooltipCustomContent ? null : this.tooltipOptions?.label; tooltipComponent.isInverse = this.tooltipOptions?.isInverse; tooltipComponent.hasArrow = this.tooltipOptions?.hasArrow ?? true; tooltipComponent.tooltipId = this.tooltipOptions?.tooltipId; tooltipComponent.arrowPlacement = this.tooltipOptions?.arrowPlacement ?? 'center'; tooltipComponent.placement = this.tooltipOptions?.placement ?? 'top'; tooltipComponent.isStandalone = this.tooltipOptions?.isStandalone ?? true; tooltipComponent.isAnimated = this.tooltipOptions?.isAnimated ?? true; tooltipComponent.contentTemplateRef = this.tooltipCustomContent; } }