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:
@@ -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": {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/browser/src/popup/tabs-v2.component.html
Normal file
3
apps/browser/src/popup/tabs-v2.component.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<popup-tab-navigation [navButtons]="navButtons$ | async">
|
||||
<router-outlet></router-outlet>
|
||||
</popup-tab-navigation>
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./has-items-nudge.service";
|
||||
export * from "./has-nudge.service";
|
||||
|
||||
Reference in New Issue
Block a user