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

[CL-420][PM-18798] - Berry component and tab navigation (#14135)

* berry component and nav slot

* remove debug

* don't worry about routes

* add announce and tests

* fix story

* use existing notification color. fix border radius

* fix berry component class

* finalize berry component

* fix tests

* fix story

* move logic to tabs-v2 component.

* move navButtons to tabs-v2.component

* fix layout

* move story.

* cleanup
This commit is contained in:
Jordan Aasen
2025-04-22 10:18:10 -07:00
committed by GitHub
parent 490a46e9b6
commit 18d47a29df
9 changed files with 189 additions and 39 deletions

View File

@@ -1059,6 +1059,19 @@
"notificationAddSave": {
"message": "Save"
},
"newNotification": {
"message": "New notification"
},
"labelWithNotification": {
"message": "$LABEL$: New notification",
"description": "Label for the notification with a new login suggestion.",
"placeholders": {
"label": {
"content": "$1",
"example": "Login"
}
}
},
"loginSaveSuccessDetails": {
"message": "$USERNAME$ saved to Bitwarden.",
"placeholders": {

View File

@@ -340,6 +340,7 @@ export default {
generator: "Generator",
send: "Send",
settings: "Settings",
labelWithNotification: (label: string) => `${label}: New Notification`,
});
},
},
@@ -398,17 +399,64 @@ export default {
type Story = StoryObj<PopupPageComponent>;
export const PopupTabNavigation: Story = {
type PopupTabNavigationStory = StoryObj<PopupTabNavigationComponent>;
const navButtons = (showBerry = false) => [
{
label: "vault",
page: "/tabs/vault",
iconKey: "lock",
iconKeyActive: "lock-f",
},
{
label: "generator",
page: "/tabs/generator",
iconKey: "generate",
iconKeyActive: "generate-f",
},
{
label: "send",
page: "/tabs/send",
iconKey: "send",
iconKeyActive: "send-f",
},
{
label: "settings",
page: "/tabs/settings",
iconKey: "cog",
iconKeyActive: "cog-f",
showBerry: showBerry,
},
];
export const DefaultPopupTabNavigation: PopupTabNavigationStory = {
render: (args) => ({
props: args,
template: /* HTML */ `
template: /*html*/ `
<extension-container>
<popup-tab-navigation>
<popup-tab-navigation [navButtons]="navButtons">
<router-outlet></router-outlet>
</popup-tab-navigation>
</extension-container>
`,
</extension-container>`,
}),
args: {
navButtons: navButtons(),
},
};
export const PopupTabNavigationWithBerry: PopupTabNavigationStory = {
render: (args) => ({
props: args,
template: /*html*/ `
<extension-container>
<popup-tab-navigation [navButtons]="navButtons">
<router-outlet></router-outlet>
</popup-tab-navigation>
</extension-container>`,
}),
args: {
navButtons: navButtons(true),
},
};
export const PopupPage: Story = {

View File

@@ -5,12 +5,13 @@
<div class="tw-max-w-screen-sm tw-mx-auto">
<nav>
<ul class="tw-flex tw-flex-1 tw-mb-0 tw-p-0">
<li *ngFor="let button of navButtons" class="tw-flex-1 tw-list-none">
<li *ngFor="let button of navButtons" class="tw-flex-1 tw-list-none tw-relative">
<button
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 bit-compact:tw-pb-1 tw-pt-3 bit-compact:tw-pt-2 tw-text-sm tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
title="{{ button.label | i18n }}"
[routerLink]="button.page"
[appA11yTitle]="buttonTitle(button)"
routerLinkActive
#rla="routerLinkActive"
ariaCurrentWhenActive="page"
@@ -31,6 +32,9 @@
{{ button.label | i18n }}
</span>
</button>
<div *ngIf="button.showBerry" class="tw-absolute tw-top-1.5 tw-left-[calc(50%+5px)]">
<div class="tw-bg-notification-600 tw-size-2.5 tw-rounded-full"></div>
</div>
</li>
</ul>
</nav>

View File

@@ -1,10 +1,19 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, Input } from "@angular/core";
import { RouterModule } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LinkModule } from "@bitwarden/components";
export type NavButton = {
label: string;
page: string;
iconKey: string;
iconKeyActive: string;
showBerry?: boolean;
};
@Component({
selector: "popup-tab-navigation",
templateUrl: "popup-tab-navigation.component.html",
@@ -15,30 +24,12 @@ import { LinkModule } from "@bitwarden/components";
},
})
export class PopupTabNavigationComponent {
navButtons = [
{
label: "vault",
page: "/tabs/vault",
iconKey: "lock",
iconKeyActive: "lock-f",
},
{
label: "generator",
page: "/tabs/generator",
iconKey: "generate",
iconKeyActive: "generate-f",
},
{
label: "send",
page: "/tabs/send",
iconKey: "send",
iconKeyActive: "send-f",
},
{
label: "settings",
page: "/tabs/settings",
iconKey: "cog",
iconKeyActive: "cog-f",
},
];
@Input() navButtons: NavButton[] = [];
constructor(private i18nService: I18nService) {}
buttonTitle(navButton: NavButton) {
const labelText = this.i18nService.t(navButton.label);
return navButton.showBerry ? this.i18nService.t("labelWithNotification", labelText) : labelText;
}
}

View File

@@ -0,0 +1,3 @@
<popup-tab-navigation [navButtons]="navButtons$ | async">
<router-outlet></router-outlet>
</popup-tab-navigation>

View File

@@ -1,11 +1,53 @@
import { Component } from "@angular/core";
import { combineLatest, map } from "rxjs";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { HasNudgeService } from "@bitwarden/vault";
@Component({
selector: "app-tabs-v2",
template: `
<popup-tab-navigation>
<router-outlet></router-outlet>
</popup-tab-navigation>
`,
templateUrl: "./tabs-v2.component.html",
providers: [HasNudgeService],
})
export class TabsV2Component {}
export class TabsV2Component {
constructor(
private readonly hasNudgeService: HasNudgeService,
private readonly configService: ConfigService,
) {}
protected navButtons$ = combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
this.hasNudgeService.shouldShowNudge$(),
]).pipe(
map(([onboardingFeatureEnabled, showNudge]) => {
return [
{
label: "vault",
page: "/tabs/vault",
iconKey: "lock",
iconKeyActive: "lock-f",
},
{
label: "generator",
page: "/tabs/generator",
iconKey: "generate",
iconKeyActive: "generate-f",
},
{
label: "send",
page: "/tabs/send",
iconKey: "send",
iconKeyActive: "send-f",
},
{
label: "settings",
page: "/tabs/settings",
iconKey: "cog",
iconKeyActive: "cog-f",
showBerry: onboardingFeatureEnabled && showNudge,
},
];
}),
);
}

View File

@@ -24,6 +24,7 @@ export * from "./components/carousel";
export * as VaultIcons from "./icons";
export * from "./services/vault-nudges.service";
export * from "./services/custom-nudges-services";
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
export { SshImportPromptService } from "./services/ssh-import-prompt.service";

View File

@@ -0,0 +1,47 @@
import { inject, Injectable } from "@angular/core";
import { combineLatest, distinctUntilChanged, map, Observable, of, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { VaultNudgeType } from "../vault-nudges.service";
/**
* Custom Nudge Service used for showing if the user has any existing nudge in the Vault.
*/
@Injectable({
providedIn: "root",
})
export class HasNudgeService extends DefaultSingleNudgeService {
private accountService = inject(AccountService);
private nudgeTypes: VaultNudgeType[] = [
VaultNudgeType.HasVaultItems,
VaultNudgeType.IntroCarouselDismissal,
// add additional nudge types here as needed
];
/**
* Returns an observable that emits true if any of the provided nudge types are present
*/
shouldShowNudge$(): Observable<boolean> {
return this.accountService.activeAccount$.pipe(
switchMap((activeAccount) => {
const userId: UserId | undefined = activeAccount?.id;
if (!userId) {
return of(false);
}
const nudgeObservables: Observable<boolean>[] = this.nudgeTypes.map((nudge) =>
super.shouldShowNudge$(nudge, userId),
);
return combineLatest(nudgeObservables).pipe(
map((nudgeStates) => nudgeStates.some((state) => state)),
distinctUntilChanged(),
);
}),
);
}
}

View File

@@ -1 +1,2 @@
export * from "./has-items-nudge.service";
export * from "./has-nudge.service";