From fc04d81dece75a460d67b65eb24e8176fde4168f Mon Sep 17 00:00:00 2001 From: Pierre Ecarlat <pecarlat@grandlyon.com> Date: Tue, 6 Aug 2024 17:10:11 +0200 Subject: [PATCH 1/4] Added the focus trap in orientation, missing the anchor --- .../navigation/navigation.component.ts | 17 +++++++++-------- .../orientation-form-view.component.html | 2 +- .../orientation-form-view.component.ts | 2 ++ .../orientation-routing.module.ts | 2 +- .../orientation-form-view/orientation.module.ts | 3 ++- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts b/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts index 2485d9024..82f57d68a 100644 --- a/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts +++ b/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { NeedsType, OnlineDemarche } from '../../enums/orientation.enums'; import { MediationStepType, MediationType } from '../../types/orientation.types'; @@ -18,15 +18,14 @@ export class NavigationComponent { @Input() hideNavButtons = false; @Input() failedOrientation = false; - @Output() goNext = new EventEmitter<any>(); - @Output() goPrev = new EventEmitter<any>(); - @Output() goReset = new EventEmitter<any>(); + @Output() goNext = new EventEmitter<boolean>(); + @Output() goPrev = new EventEmitter<void>(); + @Output() goReset = new EventEmitter<void>(); public NeedsTypeEnum = NeedsType; - constructor( - private router: Router, - private route: ActivatedRoute, - ) {} + + constructor(private router: Router) {} + public nextPage(isPrint?: boolean): void { this.goNext.emit(isPrint); } @@ -34,10 +33,12 @@ export class NavigationComponent { public prevPage(): void { this.goPrev.emit(); } + public goCarto(): void { this.goReset.emit(); this.router.navigateByUrl('/acteurs'); } + public resetOrientation(): void { this.goReset.emit(); } diff --git a/src/app/form/orientation-form-view/orientation-form-view.component.html b/src/app/form/orientation-form-view/orientation-form-view.component.html index 067376c77..89958704b 100644 --- a/src/app/form/orientation-form-view/orientation-form-view.component.html +++ b/src/app/form/orientation-form-view/orientation-form-view.component.html @@ -1,4 +1,4 @@ -<div class="orientation"> +<div class="orientation" cdkTrapFocus [cdkTrapFocusAutoCapture]="true"> <h1 class="visually-hidden">Orientation</h1> <app-progress-bar [currentPage]="currentStep" [nbSteps]="nbSteps" [formType]="formType.orientation" /> <div class="container" [ngClass]="{ 'no-max-width': fullScreen }"> diff --git a/src/app/form/orientation-form-view/orientation-form-view.component.ts b/src/app/form/orientation-form-view/orientation-form-view.component.ts index b7587902b..f1d0bd040 100644 --- a/src/app/form/orientation-form-view/orientation-form-view.component.ts +++ b/src/app/form/orientation-form-view/orientation-form-view.component.ts @@ -130,6 +130,7 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked ) { this.setCategories(); } + async ngOnInit(): Promise<void> { this.orientationService.rdvUser = null; if (history.state.rdvUser) { @@ -211,6 +212,7 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked private isOnlineDemarcheOrLearnSkills(): boolean { return this.needType === NeedsType.onlineDemarch || this.needType === NeedsType.learnSkills; } + /** * In online procedures (online mediation and appointment), a request is sent to the server before the orientation * summary. isPrevHidden flag prevents from going back in the form after validation. diff --git a/src/app/form/orientation-form-view/orientation-routing.module.ts b/src/app/form/orientation-form-view/orientation-routing.module.ts index 466c491d0..de5dafc98 100644 --- a/src/app/form/orientation-form-view/orientation-routing.module.ts +++ b/src/app/form/orientation-form-view/orientation-routing.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { DeactivateGuard } from '../../guards/deactivate.guard'; import { OrientationFormViewComponent } from './orientation-form-view.component'; diff --git a/src/app/form/orientation-form-view/orientation.module.ts b/src/app/form/orientation-form-view/orientation.module.ts index 978ffc8e3..3057ae869 100644 --- a/src/app/form/orientation-form-view/orientation.module.ts +++ b/src/app/form/orientation-form-view/orientation.module.ts @@ -1,3 +1,4 @@ +import { A11yModule } from '@angular/cdk/a11y'; import { NgModule } from '@angular/core'; import { CartoModule } from '../../carto/carto.module'; import { SharedModule } from '../../shared/shared.module'; @@ -56,6 +57,6 @@ import { OrientationStructureListComponent } from './orientation-structure-list/ AppointmentEndComponent, InformationScreenComponent, ], - imports: [OrientationRoutingModule, CartoModule, SharedModule], + imports: [OrientationRoutingModule, CartoModule, SharedModule, A11yModule], }) export class OrientationModule {} -- GitLab From 799cc42e41001f97da368a3fbbd7437bc0449206 Mon Sep 17 00:00:00 2001 From: Pierre Ecarlat <pecarlat@grandlyon.com> Date: Wed, 7 Aug 2024 10:35:15 +0200 Subject: [PATCH 2/4] Added the f shortcut in orientation --- .../navigation/navigation.component.html | 6 +++-- .../navigation/navigation.component.ts | 20 +++++++++++++++- .../orientation-form-view.component.html | 3 +++ .../orientation-form-view.component.ts | 23 ++++++++++++++++--- .../components/button/button.component.html | 1 + .../components/button/button.component.ts | 8 ++++++- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/app/form/orientation-form-view/global-components/navigation/navigation.component.html b/src/app/form/orientation-form-view/global-components/navigation/navigation.component.html index ee4229d05..a8121829c 100644 --- a/src/app/form/orientation-form-view/global-components/navigation/navigation.component.html +++ b/src/app/form/orientation-form-view/global-components/navigation/navigation.component.html @@ -1,14 +1,16 @@ <div class="footerForm"> <ng-container *ngIf="!failedOrientation"> <app-button - *ngIf="currentStep !== null && !(isPrevHidden || isLastStep)" + *ngIf="showPrevButton()" + #prevButton [variant]="'secondary'" [label]="'Précédent'" [iconName]="'arrowBack'" (action)="prevPage()" /> <app-button - *ngIf="!hideNavButtons" + *ngIf="showNextButton()" + #nextButton [variant]="'primary'" [label]="isLastStep ? 'Imprimer' : 'Suivant'" [iconName]="isLastStep ? 'printer' : 'arrowForward'" diff --git a/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts b/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts index 2485d9024..65650971e 100644 --- a/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts +++ b/src/app/form/orientation-form-view/global-components/navigation/navigation.component.ts @@ -1,5 +1,6 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { ButtonComponent } from '../../../../shared/components'; import { NeedsType, OnlineDemarche } from '../../enums/orientation.enums'; import { MediationStepType, MediationType } from '../../types/orientation.types'; @@ -22,11 +23,21 @@ export class NavigationComponent { @Output() goPrev = new EventEmitter<any>(); @Output() goReset = new EventEmitter<any>(); + @ViewChild('prevButton', { read: ButtonComponent }) prevButton: ButtonComponent; + @ViewChild('nextButton', { read: ButtonComponent }) nextButton: ButtonComponent; + public NeedsTypeEnum = NeedsType; constructor( private router: Router, private route: ActivatedRoute, ) {} + + public showPrevButton(): boolean { + return this.currentStep !== null && !(this.isPrevHidden || this.isLastStep); + } + public showNextButton(): boolean { + return !this.hideNavButtons; + } public nextPage(isPrint?: boolean): void { this.goNext.emit(isPrint); } @@ -41,4 +52,11 @@ export class NavigationComponent { public resetOrientation(): void { this.goReset.emit(); } + public focusFirstButton(): void { + if (this.showPrevButton()) { + this.prevButton.focus(); + } else if (this.showNextButton()) { + this.nextButton.focus(); + } + } } diff --git a/src/app/form/orientation-form-view/orientation-form-view.component.html b/src/app/form/orientation-form-view/orientation-form-view.component.html index 067376c77..43a2110fd 100644 --- a/src/app/form/orientation-form-view/orientation-form-view.component.html +++ b/src/app/form/orientation-form-view/orientation-form-view.component.html @@ -1,5 +1,8 @@ <div class="orientation"> <h1 class="visually-hidden">Orientation</h1> + <div class="visually-hidden"> + <p>Utilisez <strong>f</strong> (footer) pour aller directement aux boutons de validation.</p> + </div> <app-progress-bar [currentPage]="currentStep" [nbSteps]="nbSteps" [formType]="formType.orientation" /> <div class="container" [ngClass]="{ 'no-max-width': fullScreen }"> <app-needs-selection diff --git a/src/app/form/orientation-form-view/orientation-form-view.component.ts b/src/app/form/orientation-form-view/orientation-form-view.component.ts index b7587902b..b6d9e418d 100644 --- a/src/app/form/orientation-form-view/orientation-form-view.component.ts +++ b/src/app/form/orientation-form-view/orientation-form-view.component.ts @@ -1,4 +1,4 @@ -import { AfterContentChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { AfterContentChecked, ChangeDetectorRef, Component, HostListener, OnInit, ViewChild } from '@angular/core'; import { AbstractControl, FormGroup, UntypedFormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { lastValueFrom } from 'rxjs'; @@ -28,6 +28,7 @@ import { OnlineDemarchesCommonSteps, StructuresListSteps, } from './enums/orientation.enums'; +import { NavigationComponent } from './global-components/navigation/navigation.component'; import { IAppointment } from './interfaces/appointment.interface'; import { FiltersForm } from './interfaces/filtersForm.interface'; import { IOnlineMediation } from './interfaces/onlineMediation.interface'; @@ -116,6 +117,21 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked public showConfirmationModal = false; private resolve: CanExitResolver; + @ViewChild(NavigationComponent) navComponent!: NavigationComponent; + + // Listener, keyboard shortcuts + @HostListener('window:keydown', ['$event']) + handleKeyboardEvent(event: KeyboardEvent): void { + switch (event.key) { + case 'f': // 'f' to go to the navigation footer + this.navComponent.focusFirstButton(); + event.preventDefault(); + break; + default: + break; + } + } + constructor( public orientationService: OrientationService, private notificationService: NotificationService, @@ -124,12 +140,13 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked private profileService: ProfileService, private searchService: SearchService, private structureService: StructureService, - private cdref: ChangeDetectorRef, + private cdRef: ChangeDetectorRef, private indicatorService: IndicatorService, private router: Router, ) { this.setCategories(); } + async ngOnInit(): Promise<void> { this.orientationService.rdvUser = null; if (history.state.rdvUser) { @@ -167,7 +184,7 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked } ngAfterContentChecked(): void { - this.cdref.detectChanges(); + this.cdRef.detectChanges(); } public validatePage(event: boolean): void { diff --git a/src/app/shared/components/button/button.component.html b/src/app/shared/components/button/button.component.html index 98ac1d017..2dd9c6192 100644 --- a/src/app/shared/components/button/button.component.html +++ b/src/app/shared/components/button/button.component.html @@ -1,4 +1,5 @@ <button + #buttonElement [attr.aria-label]="ariaLabel" [type]="type" [ngClass]="classes" diff --git a/src/app/shared/components/button/button.component.ts b/src/app/shared/components/button/button.component.ts index b9da4f71b..bd8e2220c 100644 --- a/src/app/shared/components/button/button.component.ts +++ b/src/app/shared/components/button/button.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { SpriteFolderType } from '../svg-icon/SpriteFolder.type'; /** values will be used for css selectors */ @@ -54,7 +54,13 @@ export class ButtonComponent { /** Click handler */ @Output() action = new EventEmitter<Event>(); + @ViewChild('buttonElement') buttonElement!: ElementRef; + public get classes(): string[] { return [this.variant, this.size, this.wide ? 'wide' : '']; } + + public focus(): void { + this.buttonElement.nativeElement.focus(); + } } -- GitLab From 3e795ffe6a018c2c58af138d431d8f25b83d7f80 Mon Sep 17 00:00:00 2001 From: Pierre Ecarlat <pecarlat@grandlyon.com> Date: Wed, 7 Aug 2024 10:45:43 +0200 Subject: [PATCH 3/4] Added the header shortcut to return to resin logo anytime --- src/app/app.component.html | 3 +++ src/app/app.component.ts | 18 +++++++++++++++++- src/app/header/header.component.html | 1 + src/app/header/header.component.ts | 10 +++++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 9adfa5413..fb62f78b6 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,3 +1,6 @@ +<div class="visually-hidden"> + <p>Utilisez <strong>h</strong> (header) pour revenir au logo Res'in à tout moment.</p> +</div> <app-header /> <main class="app-container"> <div class="app-body" id="app-body" (scroll)="onScroll($event)"> diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 85005ac0a..24c1c4ed0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, HostListener, OnInit, ViewChild } from '@angular/core'; import { GuardsCheckStart, NavigationCancel, @@ -8,6 +8,7 @@ import { Router, } from '@angular/router'; import { MatomoInitializerService } from 'ngx-matomo-client'; +import { HeaderComponent } from './header/header.component'; import { ProfileService } from './profile/services/profile.service'; import { AuthService } from './services/auth.service'; import { ConfigService } from './services/config.service'; @@ -24,6 +25,21 @@ import { WindowScrollService } from './shared/service/windowScroll.service'; export class AppComponent implements OnInit { public loading = true; + @ViewChild(HeaderComponent) headerComponent!: HeaderComponent; + + // Listener, keyboard shortcuts + @HostListener('window:keydown', ['$event']) + handleKeyboardEvent(event: KeyboardEvent): void { + switch (event.key) { + case 'h': // 'h' to go to the header (resin logo) + this.headerComponent.focusLogo(); + event.preventDefault(); + break; + default: + break; + } + } + constructor( public printService: PrintService, private authService: AuthService, diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 959540200..2cf2d5b3a 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -10,6 +10,7 @@ /> </div> <div + #clickableLogo class="logo clickable" aria-label="Retour accueil" role="button" diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 324ca2e28..eb66efb8a 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -1,5 +1,5 @@ import { animate, animateChild, query, style, transition, trigger } from '@angular/animations'; -import { Component } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { Structure } from '../models/structure.model'; import { ProfileService } from '../profile/services/profile.service'; @@ -39,6 +39,8 @@ export class HeaderComponent { private displayDataShare = false; private loadingDataShare = false; + @ViewChild('clickableLogo') clickableLogoDiv: ElementRef; + constructor( private authService: AuthService, private profileService: ProfileService, @@ -85,6 +87,12 @@ export class HeaderComponent { } } + public focusLogo(): void { + if (this.clickableLogoDiv) { + this.clickableLogoDiv.nativeElement.focus(); + } + } + public get isLoggedIn(): boolean { return this.authService.isLoggedIn(); } -- GitLab From b410845eea5bc8f41a7e460d58a557994fc3d45e Mon Sep 17 00:00:00 2001 From: Pierre Ecarlat <pecarlat@grandlyon.com> Date: Wed, 7 Aug 2024 11:21:12 +0200 Subject: [PATCH 4/4] Added a check that we're not within an input nor textarea --- src/app/app.component.ts | 8 ++++++++ .../orientation-form-view.component.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 24c1c4ed0..10e885723 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -30,6 +30,14 @@ export class AppComponent implements OnInit { // Listener, keyboard shortcuts @HostListener('window:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent): void { + const target = event.target as HTMLElement; + const tagName = target.tagName.toLowerCase(); + + // Check if the focus is within an input, textarea + if (['input', 'textarea'].includes(tagName) || target.isContentEditable) { + return; + } + switch (event.key) { case 'h': // 'h' to go to the header (resin logo) this.headerComponent.focusLogo(); diff --git a/src/app/form/orientation-form-view/orientation-form-view.component.ts b/src/app/form/orientation-form-view/orientation-form-view.component.ts index b6d9e418d..469e3e935 100644 --- a/src/app/form/orientation-form-view/orientation-form-view.component.ts +++ b/src/app/form/orientation-form-view/orientation-form-view.component.ts @@ -122,6 +122,14 @@ export class OrientationFormViewComponent implements OnInit, AfterContentChecked // Listener, keyboard shortcuts @HostListener('window:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent): void { + const target = event.target as HTMLElement; + const tagName = target.tagName.toLowerCase(); + + // Check if the focus is within an input, textarea + if (['input', 'textarea'].includes(tagName) || target.isContentEditable) { + return; + } + switch (event.key) { case 'f': // 'f' to go to the navigation footer this.navComponent.focusFirstButton(); -- GitLab