All files / src/app/shared/components/common/skip-content-link skip-content-link.component.ts

70.58% Statements 12/17
64.28% Branches 9/14
83.33% Functions 5/6
68.75% Lines 11/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 9812x 12x                     12x                                                                     12x       5x                     5x       4x   3x           4x             4x                         4x      
import { DOCUMENT } from '@angular/common';
import {
  AfterContentInit,
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  Inject,
  Input,
  Renderer2,
  ViewChild,
} from '@angular/core';
import { v4 as uuid } from 'uuid';
 
/**
 * @description
 * This component can be wrapped around listings to provide better accessibility support for
 * longer listings by adding a skip link.
 *
 * The skip link is only visible when it receives keyboard focus (similar to the "Skip to main content" link)
 * and skips to the element with the ID provided by the `skipToElementId` input parameter.
 *
 * If no `skipToElementId` is provided or the ID is invalid, the component generates a
 * target element after the listing where the focus is set on.
 *
 * If a valid `skipToElementId` is provided, the component does not have to be wrapped around a listing element,
 * since it does not have to generate a target element after the listing.
 *
 * IMPORTANT: If the target element is not natively focusable (like a button or link),
 * the element needs a `tabindex="-1"` to be programmatically focusable (like `breadcrumb.component.html`).
 *
 * The skip link is not visible on mobile and tablet.
 *
 * @example
 * <ish-skip-content-link>
 *   <ul>...</ul>
 * </ish-skip-content-link>
 *
 * @example
 * <ish-skip-content-link skipToElementId="validElementId" />
 * <ul>...</ul>
 */
@Component({
  selector: 'ish-skip-content-link',
  templateUrl: './skip-content-link.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkipContentLinkComponent implements AfterContentInit, AfterViewInit {
  /**
   * Translation key for the skip link text.
   */
  @Input() linkText = 'common.skip_content.link.text.default';
  /**
   * A valid element-ID to skip to when the link is clicked.
   * If not natively focusable (like a button or link), add `tabindex="-1"` to the target element.
   */
  @Input() skipToElementId: string;
 
  @ViewChild('skipContentLink') skipContentLinkElementRef: ElementRef;
 
  generatedElementAfterListingId: string;
 
  constructor(@Inject(DOCUMENT) private document: Document, private renderer: Renderer2) {}
 
  // cannot set the ID in `ngAfterViewInit()` because the ID in the HTML would be undefined
  ngAfterContentInit() {
    if (this.isTargetElementIdMissingOrInvalid()) {
      // setting this variable will generate an empty target element after the listing
      this.generatedElementAfterListingId = `element-after-listing-${uuid()}`;
    }
  }
 
  // click-listener has to be set here because the `skipContentLinkElementRef` is not yet available in `ngAfterContentInit()`
  ngAfterViewInit() {
    this.setCustomClickListenerForSkipLink();
  }
 
  /**
   * Sets up a custom click listener for the skip link to handle focus and scrolling.
   */
  private setCustomClickListenerForSkipLink() {
    this.renderer.listen(this.skipContentLinkElementRef.nativeElement, 'click', (event: Event) => {
      event.preventDefault();
 
      const targetElement = this.document.getElementById(this.skipToElementId || this.generatedElementAfterListingId);
 
      Iif (targetElement) {
        targetElement.focus({ preventScroll: true });
        targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    });
  }
 
  private isTargetElementIdMissingOrInvalid(): boolean {
    return !this.skipToElementId || !this.document.getElementById(this.skipToElementId);
  }
}