Skip to content
Snippets Groups Projects
tooltip.directive.ts 3.15 KiB
Newer Older
  • Learn to ignore specific revisions
  • OZGCloud's avatar
    OZGCloud committed
    import { isEscapeKey, isNotNull } from '@alfa-client/tech-shared';
    
    OZGCloud's avatar
    OZGCloud committed
    import { InteractivityChecker } from '@angular/cdk/a11y';
    
    OZGCloud's avatar
    OZGCloud committed
    import {
      ComponentRef,
      Directive,
      ElementRef,
      HostListener,
      inject,
      Input,
      OnDestroy,
    
    OZGCloud's avatar
    OZGCloud committed
      Renderer2,
    
    OZGCloud's avatar
    OZGCloud committed
      ViewContainerRef,
    } from '@angular/core';
    
    OZGCloud's avatar
    OZGCloud committed
    import { isNull, uniqueId } from 'lodash-es';
    
    OZGCloud's avatar
    OZGCloud committed
    import { TooltipComponent } from './tooltip.component';
    
    
    OZGCloud's avatar
    OZGCloud committed
    const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2)
    
    
    OZGCloud's avatar
    OZGCloud committed
    @Directive({
      selector: '[tooltip]',
    
    OZGCloud's avatar
    OZGCloud committed
      standalone: true,
    
    OZGCloud's avatar
    OZGCloud committed
    })
    export class TooltipDirective implements OnDestroy {
      @Input() tooltip: string = '';
    
    
    OZGCloud's avatar
    OZGCloud committed
      componentRef: ComponentRef<TooltipComponent> = null;
      focusableElement: HTMLElement = null;
      tooltipId: string;
    
      public viewContainerRef: ViewContainerRef = inject(ViewContainerRef);
      public elementRef: ElementRef<HTMLElement> = inject(ElementRef);
      public renderer: Renderer2 = inject(Renderer2);
      public interactivityChecker: InteractivityChecker = inject(InteractivityChecker);
    
    OZGCloud's avatar
    OZGCloud committed
    
      ngOnDestroy(): void {
        this.destroy();
      }
    
      @HostListener('mouseenter')
    
    OZGCloud's avatar
    OZGCloud committed
      @HostListener('focusin')
      createTooltip(): void {
    
    OZGCloud's avatar
    OZGCloud committed
        if (isNotNull(this.componentRef)) {
          return;
        }
    
    
    OZGCloud's avatar
    OZGCloud committed
        const nativeElement: HTMLElement = this.elementRef.nativeElement;
        const attachedToFocused: boolean = nativeElement.contains(document.activeElement);
        this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
        nativeElement.appendChild(this.componentRef.location.nativeElement);
        this.setAriaDescribedBy();
        this.setTooltipProperties(attachedToFocused);
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
      @HostListener('mouseleave')
      @HostListener('window:scroll')
    
    OZGCloud's avatar
    OZGCloud committed
      @HostListener('focusout')
    
    OZGCloud's avatar
    OZGCloud committed
      destroyTooltip(): void {
        this.destroy();
      }
    
    
    OZGCloud's avatar
    OZGCloud committed
      @HostListener('keydown', ['$event'])
      onKeydown(e: KeyboardEvent): void {
        if (isEscapeKey(e)) {
    
    OZGCloud's avatar
    OZGCloud committed
          this.destroy();
        }
      }
    
      setTooltipProperties(attachedToFocused = false): void {
    
    OZGCloud's avatar
    OZGCloud committed
        if (isNull(this.componentRef)) {
          return;
        }
    
    
    OZGCloud's avatar
    OZGCloud committed
        const { left, right, bottom } = this.elementRef.nativeElement.getBoundingClientRect();
        this.componentRef.instance.left = (right + left) / 2;
        this.componentRef.instance.top = attachedToFocused ? bottom + OUTLINE_INDENT : bottom;
        this.componentRef.instance.text = this.tooltip;
        this.componentRef.instance.id = this.tooltipId;
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
    
    OZGCloud's avatar
    OZGCloud committed
      setAriaDescribedBy(): void {
    
    OZGCloud's avatar
    OZGCloud committed
        const nativeElement: HTMLElement = this.elementRef.nativeElement;
        this.tooltipId = uniqueId('tooltip');
        this.focusableElement =
          this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement);
        this.renderer.setAttribute(this.focusableElement, 'aria-describedby', this.tooltipId);
      }
    
    
    OZGCloud's avatar
    OZGCloud committed
      removeAriaDescribedBy(): void {
    
    OZGCloud's avatar
    OZGCloud committed
        this.renderer.removeAttribute(this.focusableElement, 'aria-describedby');
      }
    
      getFocusableElement(element: HTMLElement): HTMLElement {
        return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
      }
    
    
    OZGCloud's avatar
    OZGCloud committed
      destroy(): void {
    
    OZGCloud's avatar
    OZGCloud committed
        if (isNull(this.componentRef)) {
          return;
        }
    
    
    OZGCloud's avatar
    OZGCloud committed
        this.componentRef.destroy();
        this.componentRef = null;
        this.removeAriaDescribedBy();
        this.focusableElement = null;
    
    OZGCloud's avatar
    OZGCloud committed
      }
    }