mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
Merge branch 'master' into feature/org-admin-refresh
This commit is contained in:
@@ -97,7 +97,7 @@ export class ChangePasswordComponent implements OnInit {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassRequired")
|
||||
this.i18nService.t("masterPasswordRequired")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class ChangePasswordComponent implements OnInit {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassLength")
|
||||
this.i18nService.t("masterPasswordMinlength")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassRequired")
|
||||
this.i18nService.t("masterPasswordRequired")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassRequired")
|
||||
this.i18nService.t("masterPasswordRequired")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PasswordLogInCredentials } from "@bitwarden/common/models/domain/logInC
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keysRequest";
|
||||
import { ReferenceEventRequest } from "@bitwarden/common/models/request/referenceEventRequest";
|
||||
import { RegisterRequest } from "@bitwarden/common/models/request/registerRequest";
|
||||
import { RegisterResponse } from "@bitwarden/common/models/response/authentication/registerResponse";
|
||||
|
||||
import { PasswordColorText } from "../shared/components/password-strength/password-strength.component";
|
||||
|
||||
@@ -32,7 +33,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
@Output() createdAccount = new EventEmitter<string>();
|
||||
|
||||
showPassword = false;
|
||||
formPromise: Promise<any>;
|
||||
formPromise: Promise<RegisterResponse>;
|
||||
referenceData: ReferenceEventRequest;
|
||||
showTerms = true;
|
||||
showErrorSummary = false;
|
||||
@@ -70,6 +71,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
|
||||
protected accountCreated = false;
|
||||
|
||||
protected captchaBypassToken: string = null;
|
||||
|
||||
constructor(
|
||||
protected formValidationErrorService: FormValidationErrorsService,
|
||||
protected formBuilder: UntypedFormBuilder,
|
||||
@@ -107,6 +110,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
if (!registerResponse.successful) {
|
||||
return;
|
||||
}
|
||||
this.captchaBypassToken = registerResponse.captchaBypassToken;
|
||||
this.accountCreated = true;
|
||||
}
|
||||
if (this.isInTrialFlow) {
|
||||
@@ -117,7 +121,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
this.i18nService.t("trialAccountCreated")
|
||||
);
|
||||
}
|
||||
const loginResponse = await this.logIn(email, masterPassword, this.captchaToken);
|
||||
const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken);
|
||||
if (loginResponse.captchaRequired) {
|
||||
return;
|
||||
}
|
||||
@@ -258,14 +262,14 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
private async registerAccount(
|
||||
request: RegisterRequest,
|
||||
showToast: boolean
|
||||
): Promise<{ successful: boolean }> {
|
||||
): Promise<{ successful: boolean; captchaBypassToken?: string }> {
|
||||
if (!(await this.validateRegistration(showToast)).isValid) {
|
||||
return { successful: false };
|
||||
}
|
||||
this.formPromise = this.apiService.postRegister(request);
|
||||
try {
|
||||
await this.formPromise;
|
||||
return { successful: true };
|
||||
const response = await this.formPromise;
|
||||
return { successful: true, captchaBypassToken: response.captchaBypassToken };
|
||||
} catch (e) {
|
||||
if (this.handleCaptchaRequired(e)) {
|
||||
return { successful: false };
|
||||
|
||||
@@ -69,7 +69,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("masterPassRequired")
|
||||
this.i18nService.t("masterPasswordRequired")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,4 +36,8 @@ export class ViewCustomFieldsComponent {
|
||||
|
||||
field.showCount = !field.showCount;
|
||||
}
|
||||
|
||||
setTextDataOnDrag(event: DragEvent, data: string) {
|
||||
event.dataTransfer.setData("text", data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
showCardNumber: boolean;
|
||||
showCardCode: boolean;
|
||||
canAccessPremium: boolean;
|
||||
showPremiumRequiredTotp: boolean;
|
||||
totpCode: string;
|
||||
totpCodeFormatted: string;
|
||||
totpDash: number;
|
||||
@@ -108,6 +109,8 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
this.cipher = await cipher.decrypt();
|
||||
this.canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
this.showPremiumRequiredTotp =
|
||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
||||
|
||||
if (
|
||||
this.cipher.type === CipherType.Login &&
|
||||
|
||||
@@ -84,6 +84,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
||||
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||
import { RegisterResponse } from "../models/response/authentication/registerResponse";
|
||||
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
|
||||
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
|
||||
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
|
||||
@@ -189,7 +190,7 @@ export abstract class ApiService {
|
||||
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
|
||||
getAccountRevisionDate: () => Promise<number>;
|
||||
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
|
||||
postRegister: (request: RegisterRequest) => Promise<any>;
|
||||
postRegister: (request: RegisterRequest) => Promise<RegisterResponse>;
|
||||
postPremium: (data: FormData) => Promise<PaymentResponse>;
|
||||
postIapCheck: (request: IapCheckRequest) => Promise<any>;
|
||||
postReinstatePremium: () => Promise<any>;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ICaptchaProtectedResponse {
|
||||
captchaBypassToken: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseResponse } from "../baseResponse";
|
||||
|
||||
import { ICaptchaProtectedResponse } from "./ICaptchaProtectedResponse";
|
||||
|
||||
export class RegisterResponse extends BaseResponse implements ICaptchaProtectedResponse {
|
||||
captchaBypassToken: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.captchaBypassToken = this.getResponseProperty("CaptchaBypassToken");
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
||||
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||
import { RegisterResponse } from "../models/response/authentication/registerResponse";
|
||||
import { BillingHistoryResponse } from "../models/response/billingHistoryResponse";
|
||||
import { BillingPaymentResponse } from "../models/response/billingPaymentResponse";
|
||||
import { BreachAccountResponse } from "../models/response/breachAccountResponse";
|
||||
@@ -337,17 +338,18 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return this.send("POST", "/accounts/password-hint", request, false, false);
|
||||
}
|
||||
|
||||
postRegister(request: RegisterRequest): Promise<any> {
|
||||
return this.send(
|
||||
async postRegister(request: RegisterRequest): Promise<RegisterResponse> {
|
||||
const r = await this.send(
|
||||
"POST",
|
||||
"/accounts/register",
|
||||
request,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
this.platformUtilsService.isDev()
|
||||
? this.environmentService.getIdentityUrl()
|
||||
: this.environmentService.getApiUrl()
|
||||
);
|
||||
return new RegisterResponse(r);
|
||||
}
|
||||
|
||||
async postPremium(data: FormData): Promise<PaymentResponse> {
|
||||
|
||||
@@ -81,7 +81,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||
if (verification.type === VerificationType.OTP) {
|
||||
throw new Error(this.i18nService.t("verificationCodeRequired"));
|
||||
} else {
|
||||
throw new Error(this.i18nService.t("masterPassRequired"));
|
||||
throw new Error(this.i18nService.t("masterPasswordRequired"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-gap-2 tw-py-2.5 tw-px-4 tw-text-contrast"
|
||||
class="tw-flex tw-items-center tw-gap-2 tw-py-2.5 tw-px-4 tw-pr-2.5 tw-text-contrast"
|
||||
[ngClass]="bannerClass"
|
||||
[attr.role]="useAlertRole ? 'status' : null"
|
||||
[attr.aria-live]="useAlertRole ? 'polite' : null"
|
||||
@@ -8,7 +8,12 @@
|
||||
<span class="tw-grow tw-text-base">
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
<button class="tw-border-0 tw-bg-transparent tw-p-0 tw-text-contrast" (click)="onClose.emit()">
|
||||
<i class="bwi bwi-close tw-text-sm" *ngIf="icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="contrast"
|
||||
size="default"
|
||||
(click)="onClose.emit()"
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { BannerComponent } from "./banner.component";
|
||||
|
||||
describe("BannerComponent", () => {
|
||||
@@ -8,7 +13,17 @@ describe("BannerComponent", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [BannerComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () =>
|
||||
new I18nMockService({
|
||||
close: "Close",
|
||||
}),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BannerComponent);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
|
||||
import { BannerComponent } from "./banner.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
imports: [CommonModule, SharedModule, IconButtonModule],
|
||||
exports: [BannerComponent],
|
||||
declarations: [BannerComponent],
|
||||
})
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { BannerComponent } from "./banner.component";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Banner",
|
||||
component: BannerComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [SharedModule, IconButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
bannerType: "warning",
|
||||
},
|
||||
argTypes: {
|
||||
onClose: { action: "onClose" },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BannerComponent> = (args: BannerComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-banner [bannerType]="bannerType">Content Really Long Text Lorem Ipsum Ipsum Ipsum <button>Button</button></bit-banner>
|
||||
<bit-banner [bannerType]="bannerType" (onClose)="onClose($event)">Content Really Long Text Lorem Ipsum Ipsum Ipsum <button>Button</button></bit-banner>
|
||||
`,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
"!tw-text-contrast",
|
||||
"hover:tw-bg-primary-700",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus:tw-bg-primary-700",
|
||||
"focus:tw-border-primary-700",
|
||||
"disabled:tw-bg-primary-500/60",
|
||||
"disabled:tw-border-primary-500/60",
|
||||
"disabled:!tw-text-contrast/60",
|
||||
@@ -23,9 +21,6 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
"hover:tw-bg-secondary-500",
|
||||
"hover:tw-border-secondary-500",
|
||||
"hover:!tw-text-contrast",
|
||||
"focus:tw-bg-secondary-500",
|
||||
"focus:tw-border-secondary-500",
|
||||
"focus:!tw-text-contrast",
|
||||
"disabled:tw-bg-transparent",
|
||||
"disabled:tw-border-text-muted/60",
|
||||
"disabled:!tw-text-muted/60",
|
||||
@@ -37,9 +32,6 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
"hover:tw-bg-danger-500",
|
||||
"hover:tw-border-danger-500",
|
||||
"hover:!tw-text-contrast",
|
||||
"focus:tw-bg-danger-500",
|
||||
"focus:tw-border-danger-500",
|
||||
"focus:!tw-text-contrast",
|
||||
"disabled:tw-bg-transparent",
|
||||
"disabled:tw-border-danger-500/60",
|
||||
"disabled:!tw-text-danger/60",
|
||||
@@ -62,18 +54,17 @@ export class ButtonDirective {
|
||||
"tw-text-center",
|
||||
"hover:tw-no-underline",
|
||||
"focus:tw-outline-none",
|
||||
"focus:tw-ring",
|
||||
"focus:tw-ring-offset-2",
|
||||
"focus:tw-ring-primary-700",
|
||||
"focus:tw-z-10",
|
||||
"focus-visible:tw-ring",
|
||||
"focus-visible:tw-ring-offset-2",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
"focus-visible:tw-z-10",
|
||||
]
|
||||
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
|
||||
.concat(
|
||||
this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"]
|
||||
)
|
||||
.concat(buttonStyles[this.buttonType ?? "secondary"]);
|
||||
}
|
||||
|
||||
@Input()
|
||||
buttonType: ButtonTypes = null;
|
||||
|
||||
@Input()
|
||||
block = false;
|
||||
@Input() buttonType: ButtonTypes = null;
|
||||
@Input() block?: boolean;
|
||||
}
|
||||
|
||||
@@ -52,3 +52,21 @@ export const Disabled = DisabledTemplate.bind({});
|
||||
Disabled.args = {
|
||||
size: "small",
|
||||
};
|
||||
|
||||
const BlockTemplate: Story<ButtonDirective> = (args: ButtonDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-flex">
|
||||
<button bitButton [buttonType]="buttonType" [block]="block">[block]="true" Button</button>
|
||||
<a bitButton [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">[block]="true" Link</a>
|
||||
|
||||
<button bitButton [buttonType]="buttonType" block class="tw-ml-2">block Button</button>
|
||||
<a bitButton [buttonType]="buttonType" block href="#" class="tw-ml-2">block Link</a>
|
||||
</span>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Block = BlockTemplate.bind({});
|
||||
Block.args = {
|
||||
block: true,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
import { DialogService } from "./dialog.service";
|
||||
@@ -10,11 +11,11 @@ import { DialogTitleContainerDirective } from "./directives/dialog-title-contain
|
||||
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CdkDialogModule],
|
||||
imports: [SharedModule, IconButtonModule, CdkDialogModule],
|
||||
declarations: [
|
||||
DialogCloseDirective,
|
||||
DialogComponent,
|
||||
DialogTitleContainerDirective,
|
||||
DialogComponent,
|
||||
SimpleDialogComponent,
|
||||
],
|
||||
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
class="tw-my-4 tw-flex tw-max-h-screen tw-flex-col tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main"
|
||||
>
|
||||
<div
|
||||
class="tw-flex tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
|
||||
class="tw-flex tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
|
||||
>
|
||||
<h1 bitDialogTitleContainer class="tw-mb-0 tw-grow tw-text-lg tw-uppercase">
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="main"
|
||||
size="default"
|
||||
bitDialogClose
|
||||
class="tw-border-0 tw-bg-transparent tw-p-0"
|
||||
title="{{ 'close' | i18n }}"
|
||||
attr.aria-label="{{ 'close' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-close tw-text-xs tw-font-bold tw-text-main" aria-hidden="true"></i>
|
||||
</button>
|
||||
[attr.title]="'close' | i18n"
|
||||
[attr.aria-label]="'close' | i18n"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="tw-overflow-y-auto tw-p-4 tw-pb-8">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../../button";
|
||||
import { IconButtonModule } from "../../icon-button";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { I18nMockService } from "../../utils/i18n-mock.service";
|
||||
import { DialogCloseDirective } from "../directives/dialog-close.directive";
|
||||
@@ -15,7 +16,7 @@ export default {
|
||||
component: DialogComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [SharedModule, ButtonModule],
|
||||
imports: [ButtonModule, SharedModule, IconButtonModule],
|
||||
declarations: [DialogTitleContainerDirective, DialogCloseDirective],
|
||||
providers: [
|
||||
{
|
||||
@@ -46,9 +47,16 @@ const Template: Story<DialogComponent> = (args: DialogComponent) => ({
|
||||
<bit-dialog [dialogSize]="dialogSize">
|
||||
<span bitDialogTitle>{{title}}</span>
|
||||
<span bitDialogContent>Dialog body text goes here.</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<div bitDialogFooter class="tw-flex tw-items-center tw-flex-row tw-gap-2">
|
||||
<button bitButton buttonType="primary">Save</button>
|
||||
<button bitButton buttonType="secondary">Cancel</button>
|
||||
<button
|
||||
class="tw-ml-auto"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
size="default"
|
||||
title="Delete"
|
||||
aria-label="Delete"></button>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
`,
|
||||
|
||||
@@ -2,7 +2,12 @@ import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { DialogService } from "./dialog.service";
|
||||
import { DialogCloseDirective } from "./directives/dialog-close.directive";
|
||||
@@ -60,13 +65,23 @@ export default {
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [
|
||||
DialogCloseDirective,
|
||||
SimpleDialogComponent,
|
||||
DialogTitleContainerDirective,
|
||||
StoryDialogContentComponent,
|
||||
DialogCloseDirective,
|
||||
DialogTitleContainerDirective,
|
||||
SimpleDialogComponent,
|
||||
],
|
||||
imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule],
|
||||
providers: [
|
||||
DialogService,
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [ButtonModule, DialogModule],
|
||||
providers: [DialogService],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
|
||||
115
libs/components/src/icon-button/icon-button.component.ts
Normal file
115
libs/components/src/icon-button/icon-button.component.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
export type IconButtonStyle = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
|
||||
|
||||
const styles: Record<IconButtonStyle, string[]> = {
|
||||
contrast: [
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-contrast",
|
||||
"tw-border-transparent",
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-text-contrast",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
],
|
||||
main: [
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-main",
|
||||
"tw-border-transparent",
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-text-main",
|
||||
"focus-visible:before:tw-ring-text-main",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
],
|
||||
muted: [
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-muted",
|
||||
"tw-border-transparent",
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
],
|
||||
primary: [
|
||||
"tw-bg-primary-500",
|
||||
"!tw-text-contrast",
|
||||
"tw-border-primary-500",
|
||||
"hover:tw-bg-primary-700",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-bg-primary-500",
|
||||
],
|
||||
secondary: [
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-muted",
|
||||
"tw-border-text-muted",
|
||||
"hover:!tw-text-contrast",
|
||||
"hover:tw-bg-text-muted",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
"disabled:hover:!tw-text-muted",
|
||||
"disabled:hover:tw-border-text-muted",
|
||||
],
|
||||
danger: [
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-danger",
|
||||
"tw-border-danger-500",
|
||||
"hover:!tw-text-contrast",
|
||||
"hover:tw-bg-danger-500",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
"disabled:hover:!tw-text-danger",
|
||||
"disabled:hover:tw-border-danger-500",
|
||||
],
|
||||
};
|
||||
|
||||
export type IconButtonSize = "default" | "small";
|
||||
|
||||
const sizes: Record<IconButtonSize, string[]> = {
|
||||
default: ["tw-px-2.5", "tw-py-1.5"],
|
||||
small: ["tw-leading-none", "tw-text-base", "tw-p-1"],
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "button[bitIconButton]",
|
||||
template: `<i class="bwi" [ngClass]="icon" aria-hidden="true"></i>`,
|
||||
})
|
||||
export class BitIconButtonComponent {
|
||||
@Input("bitIconButton") icon: string;
|
||||
|
||||
@Input() buttonType: IconButtonStyle = "main";
|
||||
|
||||
@Input() size: IconButtonSize = "default";
|
||||
|
||||
@HostBinding("class") get classList() {
|
||||
return [
|
||||
"tw-font-semibold",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-rounded",
|
||||
"tw-transition",
|
||||
"hover:tw-no-underline",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"focus:tw-outline-none",
|
||||
|
||||
// Workaround for box-shadow with transparent offset issue:
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/3595
|
||||
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
|
||||
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
|
||||
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
|
||||
"tw-relative",
|
||||
"before:tw-content-['']",
|
||||
"before:tw-block",
|
||||
"before:tw-absolute",
|
||||
"before:-tw-inset-[3px]",
|
||||
"before:tw-rounded-md",
|
||||
"before:tw-transition",
|
||||
"before:tw-ring",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"focus-visible:tw-z-10",
|
||||
]
|
||||
.concat(styles[this.buttonType])
|
||||
.concat(sizes[this.size]);
|
||||
}
|
||||
}
|
||||
11
libs/components/src/icon-button/icon-button.module.ts
Normal file
11
libs/components/src/icon-button/icon-button.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BitIconButtonComponent } from "./icon-button.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [BitIconButtonComponent],
|
||||
exports: [BitIconButtonComponent],
|
||||
})
|
||||
export class IconButtonModule {}
|
||||
59
libs/components/src/icon-button/icon-button.stories.ts
Normal file
59
libs/components/src/icon-button/icon-button.stories.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
|
||||
import { BitIconButtonComponent } from "./icon-button.component";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Icon Button",
|
||||
component: BitIconButtonComponent,
|
||||
args: {
|
||||
bitIconButton: "bwi-plus",
|
||||
buttonType: "primary",
|
||||
size: "default",
|
||||
disabled: false,
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BitIconButtonComponent> = (args: BitIconButtonComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-5" [class.tw-bg-primary-500]="buttonType === 'contrast'">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
[disabled]="disabled"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Contrast = Template.bind({});
|
||||
Contrast.args = {
|
||||
buttonType: "contrast",
|
||||
};
|
||||
|
||||
export const Main = Template.bind({});
|
||||
Main.args = {
|
||||
buttonType: "main",
|
||||
};
|
||||
|
||||
export const Muted = Template.bind({});
|
||||
Muted.args = {
|
||||
buttonType: "muted",
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
buttonType: "primary",
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
buttonType: "secondary",
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
buttonType: "danger",
|
||||
};
|
||||
1
libs/components/src/icon-button/index.ts
Normal file
1
libs/components/src/icon-button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./icon-button.module";
|
||||
@@ -4,6 +4,7 @@ export * from "./button";
|
||||
export * from "./callout";
|
||||
export * from "./form-field";
|
||||
export * from "./icon";
|
||||
export * from "./icon-button";
|
||||
export * from "./menu";
|
||||
export * from "./dialog";
|
||||
export * from "./submit-button";
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<button bitButton type="submit" [buttonType]="buttonType" [disabled]="loading || disabled">
|
||||
<button
|
||||
bitButton
|
||||
type="submit"
|
||||
[block]="block"
|
||||
[buttonType]="buttonType"
|
||||
[disabled]="loading || disabled"
|
||||
>
|
||||
<span class="tw-relative">
|
||||
<span [ngClass]="{ 'tw-invisible': loading }">
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { ButtonTypes } from "../button";
|
||||
|
||||
@@ -10,4 +10,10 @@ export class SubmitButtonComponent {
|
||||
@Input() buttonType: ButtonTypes = "primary";
|
||||
@Input() disabled = false;
|
||||
@Input() loading: boolean;
|
||||
|
||||
@Input() block?: boolean;
|
||||
|
||||
@HostBinding("class") get classList() {
|
||||
return this.block == null || this.block === false ? [] : ["tw-w-full", "tw-block"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
args: {
|
||||
buttonType: "primary",
|
||||
loading: false,
|
||||
block: false,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
@@ -25,7 +26,7 @@ export default {
|
||||
|
||||
const Template: Story<SubmitButtonComponent> = (args: SubmitButtonComponent) => ({
|
||||
props: args,
|
||||
template: `<bit-submit-button [buttonType]="buttonType" [loading]="loading" [disabled]="disabled">
|
||||
template: `<bit-submit-button [buttonType]="buttonType" [loading]="loading" [disabled]="disabled" [block]="block">
|
||||
Submit
|
||||
</bit-submit-button>`,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
:root {
|
||||
--color-transparent-hover: rgb(0 0 0 / 0.03);
|
||||
|
||||
--color-background: 255 255 255;
|
||||
--color-background-alt: 251 251 251;
|
||||
--color-background-alt2: 23 92 219;
|
||||
@@ -37,6 +39,8 @@
|
||||
}
|
||||
|
||||
.theme_dark {
|
||||
--color-transparent-hover: rgb(255 255 255 / 0.12);
|
||||
|
||||
--color-background: 31 36 46;
|
||||
--color-background-alt: 22 28 38;
|
||||
--color-background-alt2: 47 52 61;
|
||||
|
||||
@@ -12,7 +12,10 @@ module.exports = {
|
||||
corePlugins: { preflight: false },
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: colors.transparent,
|
||||
transparent: {
|
||||
DEFAULT: colors.transparent,
|
||||
hover: "var(--color-transparent-hover)",
|
||||
},
|
||||
current: colors.current,
|
||||
black: colors.black,
|
||||
primary: {
|
||||
|
||||
Reference in New Issue
Block a user