/* * 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. */ import { isEscapeKey, isNotNull } from '@alfa-client/tech-shared'; import { InteractivityChecker } from '@angular/cdk/a11y'; import { AfterViewInit, ComponentRef, Directive, ElementRef, HostListener, inject, Input, OnDestroy, Renderer2, ViewContainerRef, } from '@angular/core'; import { isNull, uniqueId } from 'lodash-es'; import { TooltipComponent } from './tooltip.component'; const OUTLINE_INDENT = 4; // Outline offset (2) + outline width (2) @Directive({ selector: '[tooltip]', standalone: true, }) export class TooltipDirective implements AfterViewInit, OnDestroy { @Input() tooltip: string = ''; 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); ngAfterViewInit(): void { this.createTooltip(); } ngOnDestroy(): void { this.destroy(); } @HostListener('mouseenter') @HostListener('focusin') showTooltip(): void { const nativeElement: HTMLElement = this.elementRef.nativeElement; const attachedToFocused: boolean = nativeElement.contains(document.activeElement); this.setTooltipProperties(attachedToFocused); } @HostListener('mouseleave') @HostListener('window:scroll') @HostListener('focusout') @HostListener('window:resize') hideTooltip(): void { this.hide(); } @HostListener('keydown', ['$event']) onKeydown(e: KeyboardEvent): void { if (isEscapeKey(e)) { this.hide(); } } createTooltip(): void { if (isNotNull(this.componentRef)) { return; } 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(); } setTooltipProperties(attachedToFocused = false): void { if (isNull(this.componentRef)) { return; } 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; } setAriaLabeledBy(): void { this.tooltipId = uniqueId('tooltip'); this.renderer.setAttribute(this.focusableElement, 'aria-labeledby', this.tooltipId); } removeAriaLabeledBy(): void { this.renderer.removeAttribute(this.focusableElement, 'aria-labeledby'); } 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; } destroy(): void { if (isNull(this.componentRef)) { return; } this.componentRef.destroy(); this.componentRef = null; this.removeAriaLabeledBy(); this.focusableElement = null; } }