Skip to content

Commit

Permalink
client(fix): the skip-links target should not be generally accessible…
Browse files Browse the repository at this point in the history
… via the TAB key (#688)
  • Loading branch information
avine authored Aug 29, 2024
1 parent 276cb6b commit 2ae951a
Show file tree
Hide file tree
Showing 15 changed files with 97 additions and 74 deletions.
4 changes: 2 additions & 2 deletions client/src/app/layout/layout.component.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<app-skip-to-main-content />
<app-skip-links [target]="skipLinksTarget" />

<header class="app-layout__header">
<div class="app-layout__container">
<ng-content select="[appLayoutHeader]" />
</div>
</header>

<main class="app-layout__main app-layout__container" appSkipToMainContent>
<main class="app-layout__main app-layout__container" appSkipLinksTarget #skipLinksTarget="appSkipLinksTarget">
<ng-content select="[appLayoutMain]" />

<app-loading />
Expand Down
4 changes: 2 additions & 2 deletions client/src/app/layout/layout.component.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/layout/skip-links/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './skip-links-target.directive';
export * from './skip-links.component';
37 changes: 37 additions & 0 deletions client/src/app/layout/skip-links/skip-links-target.directive.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement>>(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);
}
}
4 changes: 4 additions & 0 deletions client/src/app/layout/skip-links/skip-links.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<a (click)="focusTarget()" (keydown.enter)="focusTarget()" tabindex="0" class="app-skip-links__link">
<ng-container i18n="@@Component.SkipLinks.Link">Accéder au contenu principal</ng-container>
<mat-icon class="app-skip-links__icon">step_over</mat-icon>
</a>
29 changes: 29 additions & 0 deletions client/src/app/layout/skip-links/skip-links.component.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
19 changes: 19 additions & 0 deletions client/src/app/layout/skip-links/skip-links.component.ts
Original file line number Diff line number Diff line change
@@ -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<SkipLinksTargetDirective>();

protected focusTarget() {
this.target()?.focus();
}
}
3 changes: 0 additions & 3 deletions client/src/app/layout/skip-to-main-content/index.ts

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

2 changes: 1 addition & 1 deletion client/src/locales/messages.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion client/src/locales/messages.fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ",
Expand Down

0 comments on commit 2ae951a

Please sign in to comment.