Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/layout-test/src/tour/special-tour.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { NgxTourStepComponent } from '@ngx/tour';
standalone: true,
styleUrl: './special-tour.component.scss',
template: `
{{ title }}
<p #stepTitle>
{{ title }}
</p>
{{ content }}
@if(currentStep < amountOfSteps -1) {
<button (click)="handleInteraction.emit('next')">Next</button>
Expand Down
4 changes: 3 additions & 1 deletion apps/layout-test/src/tour/tour.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { NgxTourStepComponent } from '@ngx/tour';
standalone: true,
styleUrl: './tour.component.scss',
template: `
{{ title }}
<p #stepTitle>
{{ title }}
</p>
{{ content }}
@if(currentStep < amountOfSteps -1) {
<button (click)="handleInteraction.emit('next')">Next</button>
Expand Down
2 changes: 2 additions & 0 deletions libs/tour/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ The `NgxTourStepComponent` presents us with 7 Inputs and one Output we need to h

By default, the two most important Inputs are `title` and `content`, which correspond with the two data properties we passed in the step. Additionally, the amount of steps in the tour and the current index of the step can be visualized using `amountOfSteps` and `currentIndex`. To maximize customisability, we can also pass a `data` property to the component. This data can be anything, and can be used to enrich a step.

For accessibility reasons it is important that there is an element in the tour-step that has the tag `#stepTitle`. By doing so, the package will automatically set the correct `aria-labbeledby` properties.

The `position` and `stepClass` inputs are used to automatically set classes to the tour step, but can still be used freely. By default, the tour step component always gets the `ngx-tour-step` class, depending on its position it will also have a corresponding `ngx-tour-step-position-left|right|below|above` class. The step class will be set automatically as well.

In order to navigate through the tour and close it when needed, the component has an Output called `handleInteraction` that takes three possible states, being `next`, `back` and `close`. Each of these interactions will continue the tour, go back in the tour or close the tour respectively.
Expand Down
8 changes: 6 additions & 2 deletions libs/tour/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"help tour",
"joyride",
"guided tour",
"walkthrough"
"walkthrough",
"wcag",
"wai aria",
"accessibility"
],
"homepage": "https://github.com/studiohyperdrive/ngx-tools/tree/master/libs/tour",
"license": "MIT",
Expand All @@ -28,7 +31,8 @@
"peerDependencies": {
"@angular/common": "^18.0.2",
"@angular/core": "^18.0.2",
"@angular/cdk": "^18.0.2"
"@angular/cdk": "^18.0.2",
"angular2-uuid": "^1.1.1"
},
"sideEffects": false
}
75 changes: 72 additions & 3 deletions libs/tour/src/lib/abstracts/tour-step/tour-step.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,55 @@
import { Directive, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core';
import {
AfterViewInit,
Directive,
ElementRef,
EventEmitter,
HostBinding,
HostListener,
Input,
OnInit,
Output,
signal,
ViewChild,
WritableSignal,
} from '@angular/core';
import { UUID } from 'angular2-uuid';

import { NgxTourInteraction, NgxTourStepPosition } from '../../types';
import { NgxTourService } from '../../services';

/**
* An abstract class that defines the minimum properties needed for the step component to be rendered
*/
@Directive()
export abstract class NgxTourStepComponent<DataType = any> implements OnInit {
@Directive({
host: {
role: 'dialog',
'[attr.aria-modal]': 'true',
'[attr.aria-labelledby]': 'titleId()',
},
})
export abstract class NgxTourStepComponent<DataType = any> implements OnInit, AfterViewInit {
/**
* Close the tour on escape pressed
*/
@HostListener('document:keydown.escape') private onEscape() {
this.tourService.closeTour().subscribe();
}

/**
* The ngx-tour-step class of the component
*/
@HostBinding('class') protected rootClass: string;

/**
* The id of the element that the tour-step describes
*/
@HostBinding('attr.aria-details') @Input({ required: true }) public elementId: string;

/**
* The element of the tour-step that is seen as the title
*/
@ViewChild('stepTitle') public titleElement: ElementRef<HTMLElement>;

/**
* The position of the step
*/
Expand Down Expand Up @@ -55,9 +94,39 @@ export abstract class NgxTourStepComponent<DataType = any> implements OnInit {
@Output() public handleInteraction: EventEmitter<NgxTourInteraction> =
new EventEmitter<NgxTourInteraction>();

/**
* The aria-labelledby id of the title element
*/
private titleId: WritableSignal<string> = signal('');

public ngOnInit(): void {
// Iben: We set the correct host class. As this step is rendered and not changed afterwards, we do not have to adjust this in the onChanges
const positionClass = this.position ? `ngx-tour-step-position-${this.position}` : '';
this.rootClass = `ngx-tour-step ${positionClass} ${this.stepClass || ''}`;
}

public ngAfterViewInit(): void {
// Iben: If no title element was found, we throw an error
if (!this.titleElement) {
console.error(
'NgxTourService: The tour step component does not have an element marked with `stepTitle`. Because of that, the necessary accessibility attributes could not be set. Please add the `stepTitle` tag to the element that represents the title of the step.'
);

return;
}

// Iben: Connect the aria-labbledby tag to the title element
let id = this.titleElement.nativeElement.getAttribute('id');

// Iben: If the title element does not have an id, we generate one
if (!id) {
id = UUID.UUID();
this.titleElement.nativeElement.setAttribute('id', id);
}

// Iben: To prevent issues with changeDetection, we use a signal here to update the id
this.titleId.set(id);
}

constructor(private readonly tourService: NgxTourService) {}
}
14 changes: 14 additions & 0 deletions libs/tour/src/lib/directives/tour-item/tour-item.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Input,
OnDestroy,
} from '@angular/core';
import { UUID } from 'angular2-uuid';

import { NgxTourService } from '../../services';

/**
Expand Down Expand Up @@ -46,9 +48,21 @@ export class NgxTourItemDirective implements AfterViewInit, OnDestroy {
this.cdRef.detectChanges();
}

/**
* Returns the id of the element. Uses for the `aria-details` on the tour-item component
*/
public get elementId(): string {
return this.elementRef.nativeElement.getAttribute('id');
}

public ngAfterViewInit(): void {
// Iben: Register the element when rendered
this.tourService.registerElement(this);

// Iben: Check if the element has an id, if not, give it a new id for accessibility
if (!this.elementRef.nativeElement.getAttribute('id')) {
this.elementRef.nativeElement.setAttribute('id', UUID.UUID());
}
}

public ngOnDestroy(): void {
Expand Down
1 change: 1 addition & 0 deletions libs/tour/src/lib/services/tour-service/tour.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export class NgxTourService implements OnDestroy {
component.amountOfSteps = this.amountOfSteps;
component.position = item ? currentStep.position || 'below' : undefined;
component.stepClass = currentStep.stepClass;
component.elementId = item?.elementId;

// Iben: Highlight the current html item as active if one is provided
if (item) {
Expand Down