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