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)
)
);
}
}
|