mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
[CL-724] Vault list item keyboard nav dialog (#15591)
* add focus-visible-within util * wip: add grid a11y dialog to vault * add story and i18n
This commit is contained in:
@@ -5419,5 +5419,17 @@
|
||||
"wasmNotSupported": {
|
||||
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
|
||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||
},
|
||||
"keyboardNavigationBehavior": {
|
||||
"message": "Keyboard navigation behavior"
|
||||
},
|
||||
"keyboardNavigationBehaviorUpDown": {
|
||||
"message": "Use up/down arrow keys (↑ ↓) to move between items"
|
||||
},
|
||||
"keyboardNavigationBehaviorRightLeft": {
|
||||
"message": "Use right/left arrow keys (→ ←) to move between item actions"
|
||||
},
|
||||
"keyboardNavigationBehaviorTab": {
|
||||
"message": "Use tab key (↹) to jump to the next focusable section on the page"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<bit-simple-dialog>
|
||||
<ng-container bitDialogIcon>
|
||||
<i class="bwi bwi-info-circle tw-text-3xl tw-text-info" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
<ng-container bitDialogTitle>
|
||||
{{ "keyboardNavigationBehavior" | i18n }}
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<ul class="!tw-mb-0 tw-text-start">
|
||||
<li bitTypography="body1">
|
||||
{{ "keyboardNavigationBehaviorUpDown" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1">
|
||||
{{ "keyboardNavigationBehaviorRightLeft" | i18n }}
|
||||
</li>
|
||||
<li bitTypography="body1">
|
||||
{{ "keyboardNavigationBehaviorTab" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton type="button" buttonType="primary" bitDialogClose>
|
||||
{{ "gotIt" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ButtonModule, DialogModule, TypographyModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
imports: [DialogModule, ButtonModule, TypographyModule, I18nPipe],
|
||||
templateUrl: "vault-item-group-navigation-dialog.component.html",
|
||||
})
|
||||
export class VaultItemGroupNavigationDialogComponent {}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
VAULT_ITEM_GROUP_NAVIGATION_DIALOG,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { VaultItemGroupNavigationDialogComponent } from "./vault-item-group-navigation-dialog.component";
|
||||
|
||||
const VAULT_ITEM_GROUP_NAVIGATION_DIALOG_SHOWN = new KeyDefinition<boolean>(
|
||||
VAULT_ITEM_GROUP_NAVIGATION_DIALOG,
|
||||
"dialogShown",
|
||||
{
|
||||
deserializer: (obj) => obj,
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class VaultItemGroupNavigationDialogService {
|
||||
private dialogService = inject(DialogService);
|
||||
private shownState = inject(StateProvider).getGlobal(VAULT_ITEM_GROUP_NAVIGATION_DIALOG_SHOWN);
|
||||
|
||||
/** Opens the dialog if it hasn't been opened before. */
|
||||
async openOnce() {
|
||||
if (await firstValueFrom(this.shownState.state$)) {
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialogService.open(VaultItemGroupNavigationDialogComponent);
|
||||
await this.shownState.update(() => true);
|
||||
return dialogRef;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { provideNoopAnimations } from "@angular/platform-browser/animations";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { VaultItemGroupNavigationDialogComponent } from "./vault-item-group-navigation-dialog.component";
|
||||
|
||||
export default {
|
||||
title: "Browser/Vault/Item Group Navigation Dialog",
|
||||
component: VaultItemGroupNavigationDialogComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () =>
|
||||
new I18nMockService({
|
||||
keyboardNavigationBehavior: "Keyboard navigation behavior",
|
||||
keyboardNavigationBehaviorUpDown:
|
||||
"Use up/down arrow keys (↑ ↓) to move between items",
|
||||
keyboardNavigationBehaviorRightLeft:
|
||||
"Use right/left arrow keys (→ ←) to move between item actions",
|
||||
keyboardNavigationBehaviorTab:
|
||||
"Use tab key (↹) to jump to the next focusable section on the page",
|
||||
gotIt: "Got it",
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [provideNoopAnimations()],
|
||||
}),
|
||||
],
|
||||
} as Meta<VaultItemGroupNavigationDialogComponent>;
|
||||
|
||||
export const Default: StoryObj<VaultItemGroupNavigationDialogComponent> = {};
|
||||
@@ -33,9 +33,7 @@
|
||||
|
||||
<ng-template #sectionHeader>
|
||||
<bit-section-header class="tw-p-0.5 -tw-mx-0.5">
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<h2 bitTypography="h6">foobar</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
@@ -89,7 +87,7 @@
|
||||
</ng-container>
|
||||
|
||||
<cdk-virtual-scroll-viewport [itemSize]="itemHeight$ | async" bitScrollLayout>
|
||||
<bit-item-group>
|
||||
<bit-item-group (bitFocusVisibleWithin)="handleItemGroupFocusVisibleWithin()">
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
|
||||
<button
|
||||
bit-item-content
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
TypographyModule,
|
||||
MenuModule,
|
||||
ScrollLayoutDirective,
|
||||
FocusVisibleWithinDirective,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
CopyCipherFieldDirective,
|
||||
@@ -60,6 +61,8 @@ import { VaultPopupSectionService } from "../../../services/vault-popup-section.
|
||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
||||
|
||||
import { VaultItemGroupNavigationDialogService } from "./navigation-dialog/vault-item-group-navigation-dialog.service";
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -80,6 +83,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
MenuModule,
|
||||
CopyCipherFieldDirective,
|
||||
ScrollLayoutDirective,
|
||||
FocusVisibleWithinDirective,
|
||||
],
|
||||
selector: "app-vault-list-items-container",
|
||||
templateUrl: "vault-list-items-container.component.html",
|
||||
@@ -88,6 +92,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
|
||||
private compactModeService = inject(CompactModeService);
|
||||
private vaultPopupSectionService = inject(VaultPopupSectionService);
|
||||
private vaultItemGroupNavigationDialogService = inject(VaultItemGroupNavigationDialogService);
|
||||
protected copyButtonsService = inject(VaultPopupCopyButtonsService);
|
||||
|
||||
protected CipherType = CipherType;
|
||||
@@ -308,6 +313,10 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
async handleItemGroupFocusVisibleWithin() {
|
||||
await this.vaultItemGroupNavigationDialogService.openOnce();
|
||||
}
|
||||
|
||||
async primaryActionOnSelect(cipher: CipherView) {
|
||||
const isBlocked = await firstValueFrom(this.currentURIIsBlocked$);
|
||||
|
||||
|
||||
@@ -200,3 +200,7 @@ export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
|
||||
"vaultBrowserIntroCarousel",
|
||||
"disk",
|
||||
);
|
||||
export const VAULT_ITEM_GROUP_NAVIGATION_DIALOG = new StateDefinition(
|
||||
"vaultItemGroupNavigationDialog",
|
||||
"disk",
|
||||
);
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export * from "./a11y-cell.directive";
|
||||
export * from "./a11y-grid.directive";
|
||||
export * from "./a11y-row.directive";
|
||||
export * from "./a11y-title.directive";
|
||||
|
||||
47
libs/components/src/utils/focus-visible-within.ts
Normal file
47
libs/components/src/utils/focus-visible-within.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Directive, ElementRef, inject, input, output } from "@angular/core";
|
||||
import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
|
||||
import { filter, fromEvent, map, switchMap } from "rxjs";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitFocusVisibleWithin]",
|
||||
})
|
||||
export class FocusVisibleWithinDirective {
|
||||
private elementRef = inject(ElementRef) as ElementRef<HTMLElement>;
|
||||
|
||||
/**
|
||||
* Emits when the host element has a child with `:focus-visible`.
|
||||
*
|
||||
* The target selector can be narrowed with the `selector` input.
|
||||
**/
|
||||
bitFocusVisibleWithin = output<void>();
|
||||
|
||||
/**
|
||||
* The child selector to watch.
|
||||
*
|
||||
* Defaults to `:focus-visble`, but sometimes it may be useful to be more specific, e.g. `foo-bar:focus-visible`.
|
||||
**/
|
||||
selector = input<`${string}:focus-visible`>(":focus-visible");
|
||||
|
||||
constructor() {
|
||||
toObservable(this.selector)
|
||||
.pipe(
|
||||
switchMap((selector) =>
|
||||
fromEvent(this.elementRef.nativeElement, "focusin").pipe(
|
||||
map(() => {
|
||||
const activeEl = document.activeElement;
|
||||
return (
|
||||
!!activeEl &&
|
||||
this.elementRef.nativeElement.contains(activeEl) &&
|
||||
activeEl.matches(selector)
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
filter((hasFocusVisibleWithin) => hasFocusVisibleWithin),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.bitFocusVisibleWithin.emit();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./focus-visible-within";
|
||||
export * from "./function-to-observable";
|
||||
export * from "./i18n-mock.service";
|
||||
|
||||
Reference in New Issue
Block a user