mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +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": {
|
"notificationAddSave": {
|
||||||
"message": "Save"
|
"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": {
|
"loginSaveSuccessDetails": {
|
||||||
"message": "$USERNAME$ saved to Bitwarden.",
|
"message": "$USERNAME$ saved to Bitwarden.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ export default {
|
|||||||
generator: "Generator",
|
generator: "Generator",
|
||||||
send: "Send",
|
send: "Send",
|
||||||
settings: "Settings",
|
settings: "Settings",
|
||||||
|
labelWithNotification: (label: string) => `${label}: New Notification`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -398,17 +399,64 @@ export default {
|
|||||||
|
|
||||||
type Story = StoryObj<PopupPageComponent>;
|
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) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
template: /* HTML */ `
|
template: /*html*/ `
|
||||||
<extension-container>
|
<extension-container>
|
||||||
<popup-tab-navigation>
|
<popup-tab-navigation [navButtons]="navButtons">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</popup-tab-navigation>
|
</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 = {
|
export const PopupPage: Story = {
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
<div class="tw-max-w-screen-sm tw-mx-auto">
|
<div class="tw-max-w-screen-sm tw-mx-auto">
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="tw-flex tw-flex-1 tw-mb-0 tw-p-0">
|
<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
|
<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"
|
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'"
|
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
|
||||||
title="{{ button.label | i18n }}"
|
title="{{ button.label | i18n }}"
|
||||||
[routerLink]="button.page"
|
[routerLink]="button.page"
|
||||||
|
[appA11yTitle]="buttonTitle(button)"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
#rla="routerLinkActive"
|
#rla="routerLinkActive"
|
||||||
ariaCurrentWhenActive="page"
|
ariaCurrentWhenActive="page"
|
||||||
@@ -31,6 +32,9 @@
|
|||||||
{{ button.label | i18n }}
|
{{ button.label | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LinkModule } from "@bitwarden/components";
|
import { LinkModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export type NavButton = {
|
||||||
|
label: string;
|
||||||
|
page: string;
|
||||||
|
iconKey: string;
|
||||||
|
iconKeyActive: string;
|
||||||
|
showBerry?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "popup-tab-navigation",
|
selector: "popup-tab-navigation",
|
||||||
templateUrl: "popup-tab-navigation.component.html",
|
templateUrl: "popup-tab-navigation.component.html",
|
||||||
@@ -15,30 +24,12 @@ import { LinkModule } from "@bitwarden/components";
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class PopupTabNavigationComponent {
|
export class PopupTabNavigationComponent {
|
||||||
navButtons = [
|
@Input() navButtons: NavButton[] = [];
|
||||||
{
|
|
||||||
label: "vault",
|
constructor(private i18nService: I18nService) {}
|
||||||
page: "/tabs/vault",
|
|
||||||
iconKey: "lock",
|
buttonTitle(navButton: NavButton) {
|
||||||
iconKeyActive: "lock-f",
|
const labelText = this.i18nService.t(navButton.label);
|
||||||
},
|
return navButton.showBerry ? this.i18nService.t("labelWithNotification", labelText) : labelText;
|
||||||
{
|
}
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
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 { 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({
|
@Component({
|
||||||
selector: "app-tabs-v2",
|
selector: "app-tabs-v2",
|
||||||
template: `
|
templateUrl: "./tabs-v2.component.html",
|
||||||
<popup-tab-navigation>
|
providers: [HasNudgeService],
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</popup-tab-navigation>
|
|
||||||
`,
|
|
||||||
})
|
})
|
||||||
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 * as VaultIcons from "./icons";
|
||||||
export * from "./services/vault-nudges.service";
|
export * from "./services/vault-nudges.service";
|
||||||
|
export * from "./services/custom-nudges-services";
|
||||||
|
|
||||||
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
|
||||||
export { SshImportPromptService } from "./services/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-items-nudge.service";
|
||||||
|
export * from "./has-nudge.service";
|
||||||
|
|||||||
Reference in New Issue
Block a user