Skip to content
Snippets Groups Projects
tooltip.directive.ts 4.39 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
     * Ministerpräsidenten des Landes Schleswig-Holstein
     * Staatskanzlei
     * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
     *
     * Lizenziert unter der EUPL, Version 1.2 oder - sobald
     * diese von der Europäischen Kommission genehmigt wurden -
     * Folgeversionen der EUPL ("Lizenz");
     * Sie dürfen dieses Werk ausschließlich gemäß
     * dieser Lizenz nutzen.
     * Eine Kopie der Lizenz finden Sie hier:
     *
     * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
     *
     * Sofern nicht durch anwendbare Rechtsvorschriften
     * gefordert oder in schriftlicher Form vereinbart, wird
     * die unter der Lizenz verbreitete Software "so wie sie
     * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
     * ausdrücklich oder stillschweigend - verbreitet.
     * Die sprachspezifischen Genehmigungen und Beschränkungen
     * unter der Lizenz sind dem Lizenztext zu entnehmen.
     */
    
    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 {
    
      AfterViewInit,
    
    OZGCloud's avatar
    OZGCloud committed
      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 AfterViewInit, OnDestroy {
    
    OZGCloud's avatar
    OZGCloud committed
      @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
    
    
      ngAfterViewInit(): void {
    
    OZGCloud's avatar
    OZGCloud committed
      ngOnDestroy(): void {
        this.destroy();
      }
    
      @HostListener('mouseenter')
    
    OZGCloud's avatar
    OZGCloud committed
      @HostListener('focusin')
    
      showTooltip(): void {
    
    OZGCloud's avatar
    OZGCloud committed
        const nativeElement: HTMLElement = this.elementRef.nativeElement;
        const attachedToFocused: boolean = nativeElement.contains(document.activeElement);
        this.setTooltipProperties(attachedToFocused);
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
      @HostListener('mouseleave')
      @HostListener('window:scroll')
    
    OZGCloud's avatar
    OZGCloud committed
      @HostListener('focusout')
    
      @HostListener('window:resize')
    
      hideTooltip(): void {
        this.hide();
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
    
    OZGCloud's avatar
    OZGCloud committed
      @HostListener('keydown', ['$event'])
      onKeydown(e: KeyboardEvent): void {
        if (isEscapeKey(e)) {
    
          this.hide();
        }
      }
    
      createTooltip(): void {
        if (isNotNull(this.componentRef)) {
          return;
    
    OZGCloud's avatar
    OZGCloud committed
        }
    
    
        const nativeElement: HTMLElement = this.elementRef.nativeElement;
        this.componentRef = this.viewContainerRef.createComponent(TooltipComponent);
    
        this.focusableElement =
          this.interactivityChecker.isFocusable(nativeElement) ? nativeElement : this.getFocusableElement(nativeElement);
        this.focusableElement.appendChild(this.componentRef.location.nativeElement);
    
        this.setAriaLabeledBy();
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
      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;
    
        this.componentRef.instance.show = true;
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
    
      setAriaLabeledBy(): void {
    
    OZGCloud's avatar
    OZGCloud committed
        this.tooltipId = uniqueId('tooltip');
    
        this.renderer.setAttribute(this.focusableElement, 'aria-labeledby', this.tooltipId);
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
    
      removeAriaLabeledBy(): void {
        this.renderer.removeAttribute(this.focusableElement, 'aria-labeledby');
    
    OZGCloud's avatar
    OZGCloud committed
      }
    
      getFocusableElement(element: HTMLElement): HTMLElement {
        return element.querySelector('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])');
      }
    
    
      hide(): void {
        if (isNull(this.componentRef)) {
          return;
        }
    
        this.componentRef.instance.show = false;
      }
    
    
    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.removeAriaLabeledBy();
    
    OZGCloud's avatar
    OZGCloud committed
        this.focusableElement = null;
    
    OZGCloud's avatar
    OZGCloud committed
      }
    }