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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /** * Copyright (c) Siemens 2016 - 2025 * SPDX-License-Identifier: MIT */ import { A11yModule, CdkTrapFocus } from '@angular/cdk/a11y'; import { NgClass } from '@angular/common'; import { ChangeDetectionStrategy, Component, DOCUMENT, ElementRef, HostListener, inject, OnDestroy, signal, viewChild } from '@angular/core'; import { calculateOverlayArrowPosition, isRTL, OverlayArrowPosition } from '@siemens/element-ng/common'; import { addIcons, elementCancel, SiIconComponent } from '@siemens/element-ng/icon'; import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; import { Subscription } from 'rxjs'; import { PositionChange, SI_TOUR_TOKEN, TourAction, TourStepInternal } from './si-tour-token.model'; @Component({ selector: 'si-tour', imports: [A11yModule, NgClass, SiIconComponent, SiTranslatePipe], templateUrl: './si-tour.component.html', styleUrl: './si-tour.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[attr.data-step-id]': 'step()?.step?.id' } }) export class SiTourComponent implements OnDestroy { protected readonly positionClass = signal(''); protected readonly arrowPos = signal<OverlayArrowPosition | undefined>(undefined); protected readonly step = signal<TourStepInternal | undefined>(undefined); protected readonly show = signal(false); protected readonly icons = addIcons({ elementCancel }); protected tourToken = inject(SI_TOUR_TOKEN); protected backText = t(() => $localize`:@@SI_TOUR.BACK:Back`); protected nextText = t(() => $localize`:@@SI_TOUR.NEXT:Next`); protected skipText = t(() => $localize`:@@SI_TOUR.SKIP:Skip tour`); protected doneText = t(() => $localize`:@@SI_TOUR.DONE:Done`); protected ariaLabelClose = t(() => $localize`:@@SI_TOUR.CLOSE:Close`); protected progressText = t(() => $localize`:@@SI_TOUR.PROGRESS: {{step}} of {{total}}`); private elementRef: ElementRef<HTMLElement> = inject(ElementRef); private subscription?: Subscription; private prevFocus: Element | null = null; private readonly focusTrap = viewChild<CdkTrapFocus>('focusTrap'); private document = inject(DOCUMENT); constructor() { this.subscription = this.tourToken.currentStep.subscribe(step => { this.step.set(step); this.show.set(false); // prevents flickering during overlay reposition and arrow direction change setTimeout(() => { this.show.set(true); this.ensureFocused(); }, 50); }); this.subscription.add( this.tourToken.positionChange.subscribe(update => this.updatePosition(update)) ); } ngOnDestroy(): void { this.subscription?.unsubscribe(); } protected action(action: TourAction): void { this.prevFocus = this.document.activeElement; this.tourToken.control.next(action); } protected ensureFocused(): void { // ensure focus is inside si-tour as other element might steal it, e.g. opening a menu const element = this.elementRef.nativeElement; Iif (!this.document.activeElement || !element.contains(this.document.activeElement)) { if (element.contains(this.prevFocus)) { (this.prevFocus as HTMLElement)?.focus?.(); } else { this.focusTrap()?.focusTrap.focusInitialElement(); } } } @HostListener('window:keydown', ['$event']) protected keyListener(event: KeyboardEvent): void { switch (event.key) { case 'Escape': this.action('cancel'); break; case 'ArrowLeft': this.action(isRTL() ? 'next' : 'back'); break; case 'ArrowRight': this.action(isRTL() ? 'back' : 'next'); break; } } private updatePosition(update: PositionChange | undefined): void { Iif (!update) { this.positionClass.set(''); this.arrowPos.set(undefined); return; } const connPair = update.change.connectionPair; this.positionClass.set(`popover-${connPair.overlayX}-${connPair.overlayY}`); setTimeout(() => this.arrowPos.set( calculateOverlayArrowPosition(update.change, this.elementRef, update.anchor) ) ); } } |