diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index b6a8d1834b4..9aed702017d 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -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"
}
}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.component.html
new file mode 100644
index 00000000000..3f017427ad8
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.component.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+ {{ "keyboardNavigationBehavior" | i18n }}
+
+
+
+
+ {{ "keyboardNavigationBehaviorUpDown" | i18n }}
+
+
+ {{ "keyboardNavigationBehaviorRightLeft" | i18n }}
+
+
+ {{ "keyboardNavigationBehaviorTab" | i18n }}
+
+
+
+
+
+ {{ "gotIt" | i18n }}
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.component.ts
new file mode 100644
index 00000000000..04e058f9368
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.component.ts
@@ -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 {}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.service.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.service.ts
new file mode 100644
index 00000000000..ab2f96bd2e8
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.service.ts
@@ -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(
+ 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;
+ }
+}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.stories.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.stories.ts
new file mode 100644
index 00000000000..689cf46e25d
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/navigation-dialog/vault-item-group-navigation-dialog.stories.ts
@@ -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;
+
+export const Default: StoryObj = {};
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
index adb672f33df..ab732c003a6 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
@@ -33,9 +33,7 @@
-
- {{ title }}
-
+ foobar
-
+
;
+
+ /**
+ * Emits when the host element has a child with `:focus-visible`.
+ *
+ * The target selector can be narrowed with the `selector` input.
+ **/
+ bitFocusVisibleWithin = output();
+
+ /**
+ * 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();
+ });
+ }
+}
diff --git a/libs/components/src/utils/index.ts b/libs/components/src/utils/index.ts
index afadd6b3b41..a9e07c54f04 100644
--- a/libs/components/src/utils/index.ts
+++ b/libs/components/src/utils/index.ts
@@ -1,2 +1,3 @@
+export * from "./focus-visible-within";
export * from "./function-to-observable";
export * from "./i18n-mock.service";