1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[CL-806] Focus main content after SPA navigation occurs (#17112)

This commit is contained in:
Vicki League
2025-11-26 12:30:10 -05:00
committed by GitHub
parent f824530b11
commit 2128894152
5 changed files with 81 additions and 2 deletions

View File

@@ -1,3 +1,4 @@
export * from "./a11y-title.directive";
export * from "./aria-disabled-click-capture.service";
export * from "./aria-disable.directive";
export * from "./router-focus-manager.service";

View File

@@ -0,0 +1,65 @@
import { inject, Injectable } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import { skip, filter, map, combineLatestWith, tap } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@Injectable({ providedIn: "root" })
export class RouterFocusManagerService {
private router = inject(Router);
private configService = inject(ConfigService);
/**
* Handles SPA route focus management. SPA apps don't automatically notify screenreader
* users that navigation has occured or move the user's focus to the content they are
* navigating to, so we need to do it.
*
* By default, we focus the `main` after an internal route navigation.
*
* Consumers can opt out of the passing the following to the `info` input:
* `<a [routerLink]="route()" [info]="{ focusMainAfterNav: false }"></a>`
*
* Or, consumers can use the autofocus directive on an applicable interactive element.
* The autofocus directive will take precedence over this route focus pipeline.
*
* Example of where you might want to manually opt out:
* - Tab component causes a route navigation, but the tab content should be focused,
* not the whole `main`
*
* Note that router events that cause a fully new page to load (like switching between
* products) will not follow this pipeline. Instead, those will automatically bring
* focus to the top of the html document as if it were a full page load. So those links
* do not need to manually opt out of this pipeline.
*/
start$ = this.router.events.pipe(
takeUntilDestroyed(),
filter((navEvent) => navEvent instanceof NavigationEnd),
/**
* On first page load, we do not want to skip the user over the navigation content,
* so we opt out of the default focus management behavior.
*/
skip(1),
combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.RouterFocusManagement)),
filter(([_navEvent, flagEnabled]) => flagEnabled),
map(() => {
const currentNavData = this.router.getCurrentNavigation()?.extras;
const info = currentNavData?.info as { focusMainAfterNav?: boolean } | undefined;
return info;
}),
filter((currentNavInfo) => {
return currentNavInfo === undefined ? true : currentNavInfo?.focusMainAfterNav !== false;
}),
tap(() => {
const mainEl = document.querySelector<HTMLElement>("main");
if (mainEl) {
mainEl.focus();
}
}),
);
}

View File

@@ -5,6 +5,7 @@
[routerLinkActiveOptions]="routerLinkMatchOptions"
#rla="routerLinkActive"
[active]="rla.isActive"
[info]="{ focusMainAfterNav: false }"
[disabled]="disabled"
[attr.aria-disabled]="disabled"
ariaCurrentWhenActive="page"