From 2ae951a447dd92535a942e4f8ca251db8418dc1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Francel?= Date: Thu, 29 Aug 2024 22:19:04 +0000 Subject: [PATCH] client(fix): the skip-links target should not be generally accessible via the TAB key (#688) --- client/src/app/layout/layout.component.html | 4 +- client/src/app/layout/layout.component.ts | 4 +- client/src/app/layout/skip-links/index.ts | 2 + .../skip-links/skip-links-target.directive.ts | 37 +++++++++++++++++++ .../skip-links/skip-links.component.html | 4 ++ .../skip-links/skip-links.component.scss | 29 +++++++++++++++ .../layout/skip-links/skip-links.component.ts | 19 ++++++++++ .../app/layout/skip-to-main-content/index.ts | 3 -- .../skip-to-main-content.component.html | 4 -- .../skip-to-main-content.component.scss | 27 -------------- .../skip-to-main-content.component.ts | 20 ---------- .../skip-to-main-content.constants.ts | 1 - .../skip-to-main-content.directive.ts | 13 ------- client/src/locales/messages.en.json | 2 +- client/src/locales/messages.fr.json | 2 +- 15 files changed, 97 insertions(+), 74 deletions(-) create mode 100644 client/src/app/layout/skip-links/index.ts create mode 100644 client/src/app/layout/skip-links/skip-links-target.directive.ts create mode 100644 client/src/app/layout/skip-links/skip-links.component.html create mode 100644 client/src/app/layout/skip-links/skip-links.component.scss create mode 100644 client/src/app/layout/skip-links/skip-links.component.ts delete mode 100644 client/src/app/layout/skip-to-main-content/index.ts delete mode 100644 client/src/app/layout/skip-to-main-content/skip-to-main-content.component.html delete mode 100644 client/src/app/layout/skip-to-main-content/skip-to-main-content.component.scss delete mode 100644 client/src/app/layout/skip-to-main-content/skip-to-main-content.component.ts delete mode 100644 client/src/app/layout/skip-to-main-content/skip-to-main-content.constants.ts delete mode 100644 client/src/app/layout/skip-to-main-content/skip-to-main-content.directive.ts diff --git a/client/src/app/layout/layout.component.html b/client/src/app/layout/layout.component.html index 5e51506b..d76f266f 100644 --- a/client/src/app/layout/layout.component.html +++ b/client/src/app/layout/layout.component.html @@ -1,4 +1,4 @@ - +
@@ -6,7 +6,7 @@
-
+
diff --git a/client/src/app/layout/layout.component.ts b/client/src/app/layout/layout.component.ts index d8c2b6d4..6e67aa62 100644 --- a/client/src/app/layout/layout.component.ts +++ b/client/src/app/layout/layout.component.ts @@ -1,12 +1,12 @@ import { Component, ViewEncapsulation } from '@angular/core'; import { LoadingComponent } from '../shared/loading'; -import { SkipToMainContentComponent, SkipToMainContentDirective } from './skip-to-main-content'; +import { SkipLinksComponent, SkipLinksTargetDirective } from './skip-links'; @Component({ selector: 'app-layout', host: { class: 'app-layout' }, standalone: true, - imports: [LoadingComponent, SkipToMainContentComponent, SkipToMainContentDirective], + imports: [LoadingComponent, SkipLinksComponent, SkipLinksTargetDirective], templateUrl: './layout.component.html', styleUrl: './layout.component.scss', encapsulation: ViewEncapsulation.None, diff --git a/client/src/app/layout/skip-links/index.ts b/client/src/app/layout/skip-links/index.ts new file mode 100644 index 00000000..5346521d --- /dev/null +++ b/client/src/app/layout/skip-links/index.ts @@ -0,0 +1,2 @@ +export * from './skip-links-target.directive'; +export * from './skip-links.component'; diff --git a/client/src/app/layout/skip-links/skip-links-target.directive.ts b/client/src/app/layout/skip-links/skip-links-target.directive.ts new file mode 100644 index 00000000..9cb074fc --- /dev/null +++ b/client/src/app/layout/skip-links/skip-links-target.directive.ts @@ -0,0 +1,37 @@ +import { Directive, ElementRef, inject, OnDestroy } from '@angular/core'; + +@Directive({ + selector: '[appSkipLinksTarget]', + exportAs: 'appSkipLinksTarget', + host: { + '[style.outline]': '"none"', + }, + standalone: true, +}) +export class SkipLinksTargetDirective implements OnDestroy { + private elementRef = inject>(ElementRef); + + private tabIndexObserver = new MutationObserver(() => { + if (this.elementRef.nativeElement.tabIndex === 0) { + // Let's call the `focus` method once the `elementRef` is focusable + this.elementRef.nativeElement.focus({ preventScroll: true }); + } + }); + + constructor() { + this.tabIndexObserver.observe(this.elementRef.nativeElement, { attributeFilter: ['tabindex'] }); + } + + ngOnDestroy(): void { + this.tabIndexObserver.disconnect(); + } + + focus() { + // The `focus` method of the directive, simply makes the `elementRef` temporarily focusable, by setting its `tabindex` attribute. + // While the `focus` method of the `elementRef.nativeElement` is actually performed in the `MutationObserver` callback. + this.elementRef.nativeElement.tabIndex = 0; + + // The `elementRef` should not be generally accessible via the TAB key. + setTimeout(() => this.elementRef.nativeElement.removeAttribute('tabindex'), 500); + } +} diff --git a/client/src/app/layout/skip-links/skip-links.component.html b/client/src/app/layout/skip-links/skip-links.component.html new file mode 100644 index 00000000..9a8e27d1 --- /dev/null +++ b/client/src/app/layout/skip-links/skip-links.component.html @@ -0,0 +1,4 @@ + + Accéder au contenu principal + step_over + diff --git a/client/src/app/layout/skip-links/skip-links.component.scss b/client/src/app/layout/skip-links/skip-links.component.scss new file mode 100644 index 00000000..0e93f0dc --- /dev/null +++ b/client/src/app/layout/skip-links/skip-links.component.scss @@ -0,0 +1,29 @@ +.app-skip-links { + &__link { + position: fixed; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + padding: 1rem; + border-radius: 0.5rem; + background-color: var(--sys-secondary); + color: var(--sys-on-secondary); + text-decoration: none; + font-size: 1.125rem; + cursor: pointer; + + top: -9999px; + opacity: 0; + transition: ease 300ms opacity; + + &:focus-visible { + top: 4rem; + opacity: 1; + } + } + + &__icon { + margin-left: 0.5rem; + vertical-align: bottom; + } +} diff --git a/client/src/app/layout/skip-links/skip-links.component.ts b/client/src/app/layout/skip-links/skip-links.component.ts new file mode 100644 index 00000000..13778838 --- /dev/null +++ b/client/src/app/layout/skip-links/skip-links.component.ts @@ -0,0 +1,19 @@ +import { Component, input, ViewEncapsulation } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { SkipLinksTargetDirective } from './skip-links-target.directive'; + +@Component({ + selector: 'app-skip-links', + standalone: true, + imports: [MatIconModule], + templateUrl: './skip-links.component.html', + styleUrls: ['./skip-links.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class SkipLinksComponent { + target = input(); + + protected focusTarget() { + this.target()?.focus(); + } +} diff --git a/client/src/app/layout/skip-to-main-content/index.ts b/client/src/app/layout/skip-to-main-content/index.ts deleted file mode 100644 index ea53880c..00000000 --- a/client/src/app/layout/skip-to-main-content/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './skip-to-main-content.component'; -export * from './skip-to-main-content.constants'; -export * from './skip-to-main-content.directive'; diff --git a/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.html b/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.html deleted file mode 100644 index 317b7a28..00000000 --- a/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.html +++ /dev/null @@ -1,4 +0,0 @@ - - Accéder au contenu principal - step_over - diff --git a/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.scss b/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.scss deleted file mode 100644 index a59a886b..00000000 --- a/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.scss +++ /dev/null @@ -1,27 +0,0 @@ -.app-skip-to-main-content { - position: fixed; - left: 50%; - transform: translateX(-50%); - z-index: 9999; - padding: 1rem; - border-radius: 0.5rem; - background-color: var(--sys-secondary); - color: var(--sys-on-secondary); - text-decoration: none; - font-size: 1.125rem; - cursor: pointer; - - top: -9999px; - opacity: 0; - transition: ease 300ms opacity; - - &:focus-visible { - top: 4rem; - opacity: 1; - } - - &__icon { - margin-left: 0.5rem; - vertical-align: bottom; - } -} diff --git a/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.ts b/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.ts deleted file mode 100644 index b8b51dd1..00000000 --- a/client/src/app/layout/skip-to-main-content/skip-to-main-content.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DOCUMENT } from '@angular/common'; -import { Component, inject, ViewEncapsulation } from '@angular/core'; -import { MatIconModule } from '@angular/material/icon'; -import { SKIP_TO_MAIN_CONTENT_ID } from './skip-to-main-content.constants'; - -@Component({ - selector: 'app-skip-to-main-content', - standalone: true, - imports: [MatIconModule], - templateUrl: './skip-to-main-content.component.html', - styleUrls: ['./skip-to-main-content.component.scss'], - encapsulation: ViewEncapsulation.None, -}) -export class SkipToMainContentComponent { - #document = inject(DOCUMENT); - - focusMainContent() { - this.#document.getElementById(SKIP_TO_MAIN_CONTENT_ID)!.focus({ preventScroll: true }); - } -} diff --git a/client/src/app/layout/skip-to-main-content/skip-to-main-content.constants.ts b/client/src/app/layout/skip-to-main-content/skip-to-main-content.constants.ts deleted file mode 100644 index 99e43060..00000000 --- a/client/src/app/layout/skip-to-main-content/skip-to-main-content.constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const SKIP_TO_MAIN_CONTENT_ID = 'skip-to-main-content-target'; diff --git a/client/src/app/layout/skip-to-main-content/skip-to-main-content.directive.ts b/client/src/app/layout/skip-to-main-content/skip-to-main-content.directive.ts deleted file mode 100644 index f84fb7f3..00000000 --- a/client/src/app/layout/skip-to-main-content/skip-to-main-content.directive.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Directive } from '@angular/core'; -import { SKIP_TO_MAIN_CONTENT_ID } from './skip-to-main-content.constants'; - -@Directive({ - selector: '[appSkipToMainContent]', - host: { - id: SKIP_TO_MAIN_CONTENT_ID, - tabindex: '0', - '[style.outline]': '"none"', - }, - standalone: true, -}) -export class SkipToMainContentDirective {} diff --git a/client/src/locales/messages.en.json b/client/src/locales/messages.en.json index 9969d52c..23d9fe9f 100644 --- a/client/src/locales/messages.en.json +++ b/client/src/locales/messages.en.json @@ -95,7 +95,7 @@ "Component.RequestFeedbackSuccess.RequestAnother": "Request another feedZback", "Component.RequestFeedbackSuccess.Title": "FeedZback requested from:", "Component.Settings.UpdateSuccess": "Your settings have been updated.", - "Component.SkipToMainContent.Title": "Skip to main content", + "Component.SkipLinks.Link": "Skip to main content", "Demo.LoremIpsum": "{$INTERPOLATION} dolor sit amet", "Feedback.Comment": "Comment", "Feedback.Give": "Give", diff --git a/client/src/locales/messages.fr.json b/client/src/locales/messages.fr.json index 87069d47..e642f0a8 100644 --- a/client/src/locales/messages.fr.json +++ b/client/src/locales/messages.fr.json @@ -95,7 +95,7 @@ "Component.RequestFeedbackSuccess.RequestAnother": "Demander un autre feedZback", "Component.RequestFeedbackSuccess.Title": "FeedZback demandé à :", "Component.Settings.UpdateSuccess": "Vos paramètres ont bien été mis à jour.", - "Component.SkipToMainContent.Title": "Accéder au contenu principal", + "Component.SkipLinks.Link": "Accéder au contenu principal", "Demo.LoremIpsum": "{$INTERPOLATION} dolor sit amet", "Feedback.Comment": "Commentaire", "Feedback.Give": " Donner ",