mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[CL-807] Improve aria a11y of nav group (#17078)
This commit is contained in:
@@ -8,7 +8,8 @@
|
|||||||
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
||||||
(mainContentClicked)="handleMainContentClicked()"
|
(mainContentClicked)="handleMainContentClicked()"
|
||||||
[ariaLabel]="ariaLabel()"
|
[ariaLabel]="ariaLabel()"
|
||||||
[hideActiveStyles]="parentHideActiveStyles"
|
[hideActiveStyles]="parentHideActiveStyles()"
|
||||||
|
[ariaCurrentWhenActive]="ariaCurrent()"
|
||||||
>
|
>
|
||||||
<ng-template #button>
|
<ng-template #button>
|
||||||
<button
|
<button
|
||||||
@@ -18,7 +19,6 @@
|
|||||||
[buttonType]="'nav-contrast'"
|
[buttonType]="'nav-contrast'"
|
||||||
(click)="toggle($event)"
|
(click)="toggle($event)"
|
||||||
size="small"
|
size="small"
|
||||||
aria-haspopup="true"
|
|
||||||
[attr.aria-expanded]="open().toString()"
|
[attr.aria-expanded]="open().toString()"
|
||||||
[attr.aria-controls]="contentId"
|
[attr.aria-controls]="contentId"
|
||||||
[label]="['toggleCollapse' | i18n, text()].join(' ')"
|
[label]="['toggleCollapse' | i18n, text()].join(' ')"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-nav-item>
|
</bit-nav-item>
|
||||||
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
||||||
@if (sideNavService.open$ | async) {
|
@if (sideNavOpen()) {
|
||||||
@if (open()) {
|
@if (open()) {
|
||||||
<div
|
<div
|
||||||
[attr.id]="contentId"
|
[attr.id]="contentId"
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
input,
|
input,
|
||||||
model,
|
model,
|
||||||
contentChildren,
|
contentChildren,
|
||||||
|
computed,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
import { toSignal } from "@angular/core/rxjs-interop";
|
||||||
|
import { RouterLinkActive } from "@angular/router";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
@@ -33,10 +36,33 @@ import { SideNavService } from "./side-nav.service";
|
|||||||
export class NavGroupComponent extends NavBaseComponent {
|
export class NavGroupComponent extends NavBaseComponent {
|
||||||
readonly nestedNavComponents = contentChildren(NavBaseComponent, { descendants: true });
|
readonly nestedNavComponents = contentChildren(NavBaseComponent, { descendants: true });
|
||||||
|
|
||||||
|
readonly sideNavOpen = toSignal(this.sideNavService.open$);
|
||||||
|
|
||||||
|
readonly sideNavAndGroupOpen = computed(() => {
|
||||||
|
return this.open() && this.sideNavOpen();
|
||||||
|
});
|
||||||
|
|
||||||
/** When the side nav is open, the parent nav item should not show active styles when open. */
|
/** When the side nav is open, the parent nav item should not show active styles when open. */
|
||||||
protected get parentHideActiveStyles(): boolean {
|
readonly parentHideActiveStyles = computed(() => {
|
||||||
return this.hideActiveStyles() || (this.open() && this.sideNavService.open);
|
return this.hideActiveStyles() || this.sideNavAndGroupOpen();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow overriding of the RouterLink['ariaCurrentWhenActive'] property.
|
||||||
|
*
|
||||||
|
* By default, assuming that the nav group navigates to its first child page instead of its
|
||||||
|
* own page, the nav group will be `current` when the side nav is collapsed or the nav group
|
||||||
|
* is collapsed (since child pages don't show in either collapsed view) and not `current`
|
||||||
|
* when the side nav and nav group are open (since the child page will show as `current`).
|
||||||
|
*
|
||||||
|
* If the nav group navigates to its own page, use this property to always set it to announce
|
||||||
|
* as `current` by passing in `"page"`.
|
||||||
|
*/
|
||||||
|
readonly ariaCurrentWhenActive = input<RouterLinkActive["ariaCurrentWhenActive"]>();
|
||||||
|
|
||||||
|
readonly ariaCurrent = computed(() => {
|
||||||
|
return this.ariaCurrentWhenActive() ?? (this.sideNavAndGroupOpen() ? undefined : "page");
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UID for `[attr.aria-controls]`
|
* UID for `[attr.aria-controls]`
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="tw-relative tw-flex tw-items-center tw-h-full">
|
<div class="tw-relative tw-flex tw-items-center tw-h-full">
|
||||||
<ng-container *ngIf="route; then isAnchor; else isButton"></ng-container>
|
<ng-container *ngIf="route(); then isAnchor; else isButton"></ng-container>
|
||||||
|
|
||||||
<!-- Main content of `NavItem` -->
|
<!-- Main content of `NavItem` -->
|
||||||
<ng-template #anchorAndButtonContent>
|
<ng-template #anchorAndButtonContent>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Show if a value was passed to `this.to` -->
|
<!-- Show if a value was passed to `this.route` -->
|
||||||
<ng-template #isAnchor>
|
<ng-template #isAnchor>
|
||||||
<!-- The `data-fvw` attribute passes focus to `this.focusVisibleWithin$` -->
|
<!-- The `data-fvw` attribute passes focus to `this.focusVisibleWithin$` -->
|
||||||
<!-- The following `class` field should match the `#isButton` class field below -->
|
<!-- The following `class` field should match the `#isButton` class field below -->
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
[attr.aria-label]="ariaLabel() || text()"
|
[attr.aria-label]="ariaLabel() || text()"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
||||||
[ariaCurrentWhenActive]="'page'"
|
[ariaCurrentWhenActive]="ariaCurrentWhenActive()"
|
||||||
(isActiveChange)="setIsActive($event)"
|
(isActiveChange)="setIsActive($event)"
|
||||||
(click)="mainContentClicked.emit()"
|
(click)="mainContentClicked.emit()"
|
||||||
>
|
>
|
||||||
@@ -51,12 +51,13 @@
|
|||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<!-- Show if `this.to` is falsy -->
|
<!-- Show if `this.route` is falsy -->
|
||||||
<ng-template #isButton>
|
<ng-template #isButton>
|
||||||
<!-- Class field should match `#isAnchor` class field above -->
|
<!-- Class field should match `#isAnchor` class field above -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-size-full tw-px-4 tw-pe-3 tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
|
class="tw-size-full tw-px-4 tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
|
||||||
|
[ngClass]="open ? 'tw-pe-3' : 'tw-pe-4'"
|
||||||
data-fvw
|
data-fvw
|
||||||
(click)="mainContentClicked.emit()"
|
(click)="mainContentClicked.emit()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, HostListener, Optional, input } from "@angular/core";
|
import { Component, HostListener, Optional, input } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterLinkActive, RouterModule } from "@angular/router";
|
||||||
import { BehaviorSubject, map } from "rxjs";
|
import { BehaviorSubject, map } from "rxjs";
|
||||||
|
|
||||||
import { IconButtonModule } from "../icon-button";
|
import { IconButtonModule } from "../icon-button";
|
||||||
@@ -39,6 +39,14 @@ export class NavItemComponent extends NavBaseComponent {
|
|||||||
return this.forceActiveStyles() || (this._isActive && !this.hideActiveStyles());
|
return this.forceActiveStyles() || (this._isActive && !this.hideActiveStyles());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow overriding of the RouterLink['ariaCurrentWhenActive'] property.
|
||||||
|
*
|
||||||
|
* Useful for situations like nav-groups that navigate to their first child page and should
|
||||||
|
* not be marked `current` while the child page is marked as `current`
|
||||||
|
*/
|
||||||
|
readonly ariaCurrentWhenActive = input<RouterLinkActive["ariaCurrentWhenActive"]>("page");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The design spec calls for the an outline to wrap the entire element when the template's
|
* The design spec calls for the an outline to wrap the entire element when the template's
|
||||||
* anchor/button has :focus-visible. Usually, we would use :focus-within for this. However, that
|
* anchor/button has :focus-visible. Usually, we would use :focus-within for this. However, that
|
||||||
|
|||||||
Reference in New Issue
Block a user