mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
Merge branch 'master' into feature/org-admin-refresh
This commit is contained in:
@@ -21,8 +21,6 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
|
||||
formPromise: Promise<string>;
|
||||
disabledByPolicy = false;
|
||||
showFilePassword: boolean;
|
||||
showConfirmFilePassword: boolean;
|
||||
|
||||
exportForm = this.formBuilder.group({
|
||||
format: ["json"],
|
||||
@@ -199,16 +197,6 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
return this.exportForm.get("fileEncryptionType").value;
|
||||
}
|
||||
|
||||
toggleFilePassword() {
|
||||
this.showFilePassword = !this.showFilePassword;
|
||||
document.getElementById("filePassword").focus();
|
||||
}
|
||||
|
||||
toggleConfirmFilePassword() {
|
||||
this.showConfirmFilePassword = !this.showConfirmFilePassword;
|
||||
document.getElementById("confirmFilePassword").focus();
|
||||
}
|
||||
|
||||
adjustValidators() {
|
||||
this.exportForm.get("confirmFilePassword").reset();
|
||||
this.exportForm.get("filePassword").reset();
|
||||
|
||||
@@ -21,8 +21,6 @@ import { SelectCopyDirective } from "./directives/select-copy.directive";
|
||||
import { StopClickDirective } from "./directives/stop-click.directive";
|
||||
import { StopPropDirective } from "./directives/stop-prop.directive";
|
||||
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
||||
import { ColorPasswordCountPipe } from "./pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "./pipes/color-password.pipe";
|
||||
import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe";
|
||||
import { EllipsisPipe } from "./pipes/ellipsis.pipe";
|
||||
import { I18nPipe } from "./pipes/i18n.pipe";
|
||||
@@ -50,8 +48,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
||||
AutofocusDirective,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
ColorPasswordCountPipe,
|
||||
ColorPasswordPipe,
|
||||
CreditCardNumberPipe,
|
||||
EllipsisPipe,
|
||||
ExportScopeCalloutComponent,
|
||||
@@ -81,8 +77,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
||||
BitwardenToastModule,
|
||||
BoxRowDirective,
|
||||
CalloutComponent,
|
||||
ColorPasswordCountPipe,
|
||||
ColorPasswordPipe,
|
||||
CreditCardNumberPipe,
|
||||
EllipsisPipe,
|
||||
ExportScopeCalloutComponent,
|
||||
|
||||
34
libs/common/spec/importers/passky-json-importer.spec.ts
Normal file
34
libs/common/spec/importers/passky-json-importer.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer";
|
||||
|
||||
import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json";
|
||||
import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json";
|
||||
|
||||
describe("Passky Json Importer", () => {
|
||||
let importer: Importer;
|
||||
beforeEach(() => {
|
||||
importer = new Importer();
|
||||
});
|
||||
|
||||
it("should not import encrypted backups", async () => {
|
||||
const testDataJson = JSON.stringify(EncryptedData);
|
||||
const result = await importer.parse(testDataJson);
|
||||
expect(result != null).toBe(true);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorMessage).toBe("Unable to import an encrypted passky backup.");
|
||||
});
|
||||
|
||||
it("should parse login data", async () => {
|
||||
const testDataJson = JSON.stringify(UnencryptedData);
|
||||
const result = await importer.parse(testDataJson);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.name).toEqual("https://bitwarden.com/");
|
||||
expect(cipher.login.username).toEqual("testUser");
|
||||
expect(cipher.login.password).toEqual("testPassword");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://bitwarden.com/");
|
||||
expect(cipher.notes).toEqual("my notes");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
|
||||
|
||||
export const testData: PasskyJsonExport = {
|
||||
encrypted: true,
|
||||
passwords: [
|
||||
{
|
||||
website:
|
||||
"w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp",
|
||||
username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==",
|
||||
password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+",
|
||||
message:
|
||||
"w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
|
||||
|
||||
export const testData: PasskyJsonExport = {
|
||||
encrypted: false,
|
||||
passwords: [
|
||||
{
|
||||
website: "https://bitwarden.com/",
|
||||
username: "testUser",
|
||||
password: "testPassword",
|
||||
message: "my notes",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -67,6 +67,7 @@ export const regularImportOptions = [
|
||||
{ id: "encryptrcsv", name: "Encryptr (csv)" },
|
||||
{ id: "yoticsv", name: "Yoti (csv)" },
|
||||
{ id: "nordpasscsv", name: "Nordpass (csv)" },
|
||||
{ id: "passkyjson", name: "Passky (json)" },
|
||||
] as const;
|
||||
|
||||
export type ImportType =
|
||||
|
||||
43
libs/common/src/importers/passky/passky-json-importer.ts
Normal file
43
libs/common/src/importers/passky/passky-json-importer.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ImportResult } from "../../models/domain/import-result";
|
||||
import { BaseImporter } from "../base-importer";
|
||||
import { Importer } from "../importer";
|
||||
|
||||
import { PasskyJsonExport } from "./passky-json-types";
|
||||
|
||||
export class PasskyJsonImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
const result = new ImportResult();
|
||||
const passkyExport: PasskyJsonExport = JSON.parse(data);
|
||||
if (
|
||||
passkyExport == null ||
|
||||
passkyExport.passwords == null ||
|
||||
passkyExport.passwords.length === 0
|
||||
) {
|
||||
result.success = false;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
if (passkyExport.encrypted == true) {
|
||||
result.success = false;
|
||||
result.errorMessage = "Unable to import an encrypted passky backup.";
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
passkyExport.passwords.forEach((record) => {
|
||||
const cipher = this.initLoginCipher();
|
||||
cipher.name = record.website;
|
||||
cipher.login.username = record.username;
|
||||
cipher.login.password = record.password;
|
||||
|
||||
cipher.login.uris = this.makeUriArray(record.website);
|
||||
cipher.notes = record.message;
|
||||
|
||||
this.convertToNoteIfNeeded(cipher);
|
||||
this.cleanupCipher(cipher);
|
||||
result.ciphers.push(cipher);
|
||||
});
|
||||
|
||||
result.success = true;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
11
libs/common/src/importers/passky/passky-json-types.ts
Normal file
11
libs/common/src/importers/passky/passky-json-types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface PasskyJsonExport {
|
||||
encrypted: boolean;
|
||||
passwords: LoginEntry[];
|
||||
}
|
||||
|
||||
export interface LoginEntry {
|
||||
website: string;
|
||||
username: string;
|
||||
password: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import { OnePasswordMacCsvImporter } from "../importers/onepassword/onepassword-
|
||||
import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer";
|
||||
import { PadlockCsvImporter } from "../importers/padlock-csv-importer";
|
||||
import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer";
|
||||
import { PasskyJsonImporter } from "../importers/passky/passky-json-importer";
|
||||
import { PassmanJsonImporter } from "../importers/passman-json-importer";
|
||||
import { PasspackCsvImporter } from "../importers/passpack-csv-importer";
|
||||
import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer";
|
||||
@@ -279,6 +280,8 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
return new YotiCsvImporter();
|
||||
case "nordpasscsv":
|
||||
return new NordPassCsvImporter();
|
||||
case "passkyjson":
|
||||
return new PasskyJsonImporter();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const template = `
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
<button type="button" bitSuffix bitIconButton="bwi-refresh" bitFormButton [bitAction]="refresh"></button>
|
||||
</bit-form-field>
|
||||
|
||||
<button class="tw-mr-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button>
|
||||
@@ -47,6 +48,12 @@ class PromiseExampleComponent {
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
|
||||
refresh = async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
submit = async () => {
|
||||
this.formObj.markAllAsTouched();
|
||||
|
||||
@@ -78,6 +85,10 @@ class ObservableExampleComponent {
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
|
||||
refresh = () => {
|
||||
return of("fake observable").pipe(delay(2000));
|
||||
};
|
||||
|
||||
submit = () => {
|
||||
this.formObj.markAllAsTouched();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<span class="tw-relative">
|
||||
<span [ngClass]="{ 'tw-invisible': loading }">
|
||||
<i class="bwi bwi-lg" [ngClass]="iconClass" aria-hidden="true" *ngIf="icon"></i>
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
<span
|
||||
|
||||
@@ -41,6 +41,19 @@ describe("Button", () => {
|
||||
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
|
||||
expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true);
|
||||
|
||||
testAppComponent.buttonType = "unstyled";
|
||||
fixture.detectChanges();
|
||||
expect(
|
||||
Array.from(buttonDebugElement.nativeElement.classList).some((klass: string) =>
|
||||
klass.startsWith("tw-bg")
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
Array.from(linkDebugElement.nativeElement.classList).some((klass: string) =>
|
||||
klass.startsWith("tw-bg")
|
||||
)
|
||||
).toBe(false);
|
||||
|
||||
testAppComponent.buttonType = null;
|
||||
fixture.detectChanges();
|
||||
expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Input, HostBinding, Component } from "@angular/core";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
||||
|
||||
export type ButtonTypes = "primary" | "secondary" | "danger";
|
||||
const focusRing = [
|
||||
"focus-visible:tw-ring",
|
||||
"focus-visible:tw-ring-offset-2",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
"focus-visible:tw-z-10",
|
||||
];
|
||||
|
||||
const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
const buttonStyles: Record<ButtonType, string[]> = {
|
||||
primary: [
|
||||
"tw-border-primary-500",
|
||||
"tw-bg-primary-500",
|
||||
@@ -15,6 +20,7 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
"disabled:tw-border-primary-500/60",
|
||||
"disabled:!tw-text-contrast/60",
|
||||
"disabled:tw-bg-clip-padding",
|
||||
...focusRing,
|
||||
],
|
||||
secondary: [
|
||||
"tw-bg-transparent",
|
||||
@@ -26,6 +32,7 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
"disabled:tw-bg-transparent",
|
||||
"disabled:tw-border-text-muted/60",
|
||||
"disabled:!tw-text-muted/60",
|
||||
...focusRing,
|
||||
],
|
||||
danger: [
|
||||
"tw-bg-transparent",
|
||||
@@ -37,7 +44,9 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
|
||||
"disabled:tw-bg-transparent",
|
||||
"disabled:tw-border-danger-500/60",
|
||||
"disabled:!tw-text-danger/60",
|
||||
...focusRing,
|
||||
],
|
||||
unstyled: [],
|
||||
};
|
||||
|
||||
@Component({
|
||||
@@ -58,10 +67,6 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
"tw-text-center",
|
||||
"hover:tw-no-underline",
|
||||
"focus:tw-outline-none",
|
||||
"focus-visible:tw-ring",
|
||||
"focus-visible:tw-ring-offset-2",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
"focus-visible:tw-z-10",
|
||||
]
|
||||
.concat(
|
||||
this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"]
|
||||
@@ -75,17 +80,14 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
return disabled || this.loading ? true : null;
|
||||
}
|
||||
|
||||
@Input() buttonType: ButtonTypes = null;
|
||||
|
||||
@Input() buttonType: ButtonType;
|
||||
@Input() block?: boolean;
|
||||
|
||||
@Input() loading = false;
|
||||
|
||||
@Input() disabled = false;
|
||||
|
||||
@Input("bitIconButton") icon: string;
|
||||
|
||||
get iconClass() {
|
||||
return [this.icon, "!tw-m-0"];
|
||||
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||
this.buttonType = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,17 +101,3 @@ export const Block = BlockTemplate.bind({});
|
||||
Block.args = {
|
||||
block: true,
|
||||
};
|
||||
|
||||
const IconTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitButton [bitIconButton]="icon" buttonType="primary" class="tw-mr-2"></button>
|
||||
<button bitButton [bitIconButton]="icon"buttonType="secondary" class="tw-mr-2"></button>
|
||||
<button bitButton [bitIconButton]="icon" buttonType="danger" class="tw-mr-2"></button>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Icon = IconTemplate.bind({});
|
||||
Icon.args = {
|
||||
icon: "bwi-eye",
|
||||
};
|
||||
|
||||
@@ -48,12 +48,12 @@ export class ColorPasswordComponent {
|
||||
|
||||
if (this.showCount) {
|
||||
return charClass.concat([
|
||||
"tw-inline-flex",
|
||||
"tw-flex-col",
|
||||
"tw-items-center",
|
||||
"tw-w-7",
|
||||
"tw-py-1",
|
||||
"odd:tw-bg-secondary-100",
|
||||
"even:tw-bg-background",
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { AsyncActionsModule } from "../async-actions";
|
||||
import { ButtonModule } from "../button";
|
||||
import { CheckboxModule } from "../checkbox";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { InputModule } from "../input/input.module";
|
||||
import { RadioButtonModule } from "../radio-button";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
@@ -31,6 +33,8 @@ export default {
|
||||
FormFieldModule,
|
||||
InputModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
AsyncActionsModule,
|
||||
CheckboxModule,
|
||||
RadioButtonModule,
|
||||
],
|
||||
@@ -177,10 +181,13 @@ const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldCom
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" type="password" />
|
||||
<button bitSuffix bitButton bitIconButton="bwi-eye"></button>
|
||||
<button bitSuffix bitButton bitIconButton="bwi-clone"></button>
|
||||
<button bitPrefix bitIconButton="bwi-star"></button>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
<button bitSuffix bitIconButton="bwi-eye"></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone"></button>
|
||||
<button bitSuffix bitButton>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
@@ -195,9 +202,13 @@ const DisabledButtonInputGroupTemplate: Story<BitFormFieldComponent> = (
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<button bitPrefix bitIconButton="bwi-star" disabled></button>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
<button bitSuffix bitButton bitIconButton="bwi-eye" disabled></button>
|
||||
<button bitSuffix bitButton bitIconButton="bwi-clone"></button>
|
||||
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
|
||||
<button bitSuffix bitButton disabled>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -3,13 +3,16 @@ import {
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Host,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
|
||||
import { ButtonComponent } from "../button";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
|
||||
@@ -17,9 +20,18 @@ import { BitFormFieldComponent } from "./form-field.component";
|
||||
selector: "[bitPasswordInputToggle]",
|
||||
})
|
||||
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
|
||||
@Input() toggled = false;
|
||||
/**
|
||||
* Whether the input is toggled to show the password.
|
||||
*/
|
||||
@HostBinding("attr.aria-pressed") @Input() toggled = false;
|
||||
@Output() toggledChange = new EventEmitter<boolean>();
|
||||
|
||||
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
|
||||
@HostBinding("attr.aria-label") label = this.i18nService.t("toggleVisibility");
|
||||
|
||||
/**
|
||||
* Click handler to toggle the state of the input type.
|
||||
*/
|
||||
@HostListener("click") onClick() {
|
||||
this.toggled = !this.toggled;
|
||||
this.toggledChange.emit(this.toggled);
|
||||
@@ -29,7 +41,11 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
||||
this.formField.input?.focus();
|
||||
}
|
||||
|
||||
constructor(@Host() private button: ButtonComponent, private formField: BitFormFieldComponent) {}
|
||||
constructor(
|
||||
@Host() private button: BitIconButtonComponent,
|
||||
private formField: BitFormFieldComponent,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
get icon() {
|
||||
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
|
||||
|
||||
@@ -2,8 +2,12 @@ import { Component, DebugElement } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
|
||||
import { ButtonComponent, ButtonModule } from "../button";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||
import { InputModule } from "../input/input.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { BitFormFieldControl } from "./form-field-control";
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
@@ -17,7 +21,7 @@ import { BitPasswordInputToggleDirective } from "./password-input-toggle.directi
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
@@ -26,21 +30,22 @@ class TestFormFieldComponent {}
|
||||
|
||||
describe("PasswordInputToggle", () => {
|
||||
let fixture: ComponentFixture<TestFormFieldComponent>;
|
||||
let button: ButtonComponent;
|
||||
let button: BitIconButtonComponent;
|
||||
let input: BitFormFieldControl;
|
||||
let toggle: DebugElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FormFieldModule, ButtonModule, InputModule],
|
||||
imports: [FormFieldModule, IconButtonModule, InputModule],
|
||||
declarations: [TestFormFieldComponent],
|
||||
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestFormFieldComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective));
|
||||
const buttonEl = fixture.debugElement.query(By.directive(ButtonComponent));
|
||||
const buttonEl = fixture.debugElement.query(By.directive(BitIconButtonComponent));
|
||||
button = buttonEl.componentInstance;
|
||||
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
|
||||
const formField: BitFormFieldComponent = formFieldEl.componentInstance;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { InputModule } from "../input/input.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { FormFieldModule } from "./form-field.module";
|
||||
import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive";
|
||||
@@ -12,7 +15,13 @@ export default {
|
||||
component: BitPasswordInputToggleDirective,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
|
||||
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, IconButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
@@ -40,7 +49,7 @@ const Template: Story<BitPasswordInputToggleDirective> = (
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
@@ -60,7 +69,7 @@ const TemplateBinding: Story<BitPasswordInputToggleDirective> = (
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
|
||||
</bit-form-field>
|
||||
|
||||
<label class="tw-text-main">
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
import { Directive, HostBinding, Input } from "@angular/core";
|
||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
|
||||
export const PrefixClasses = [
|
||||
"tw-block",
|
||||
"tw-px-3",
|
||||
"tw-py-1.5",
|
||||
"tw-bg-background-alt",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-border-secondary-500",
|
||||
"tw-text-muted",
|
||||
"tw-rounded-none",
|
||||
"disabled:!tw-text-muted",
|
||||
"disabled:tw-border-secondary-500",
|
||||
];
|
||||
|
||||
export const PrefixButtonClasses = [
|
||||
"hover:tw-bg-text-muted",
|
||||
"hover:tw-text-contrast",
|
||||
"disabled:tw-opacity-100",
|
||||
"disabled:tw-bg-secondary-100",
|
||||
"disabled:hover:tw-bg-secondary-100",
|
||||
"disabled:hover:tw-text-muted",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
|
||||
"focus-visible:tw-border-primary-700",
|
||||
"focus-visible:tw-ring-1",
|
||||
"focus-visible:tw-ring-inset",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
"focus-visible:tw-z-10",
|
||||
];
|
||||
|
||||
export const PrefixStaticContentClasses = ["tw-block", "tw-px-3", "tw-py-1.5"];
|
||||
|
||||
@Directive({
|
||||
selector: "[bitPrefix]",
|
||||
})
|
||||
export class BitPrefixDirective {
|
||||
export class BitPrefixDirective implements OnInit {
|
||||
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
|
||||
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return PrefixClasses.concat(["tw-border-r-0", "first:tw-rounded-l"]);
|
||||
return PrefixClasses.concat([
|
||||
"tw-border-r-0",
|
||||
"first:tw-rounded-l",
|
||||
|
||||
"focus-visible:tw-border-r",
|
||||
"focus-visible:tw-mr-[-1px]",
|
||||
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.buttonComponent?.setButtonType("unstyled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { Directive, HostBinding, Input } from "@angular/core";
|
||||
import { Directive, HostBinding, Input, Optional } from "@angular/core";
|
||||
|
||||
import { PrefixClasses } from "./prefix.directive";
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
|
||||
import { PrefixButtonClasses, PrefixClasses, PrefixStaticContentClasses } from "./prefix.directive";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitSuffix]",
|
||||
})
|
||||
export class BitSuffixDirective {
|
||||
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
|
||||
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return PrefixClasses.concat(["tw-border-l-0", "last:tw-rounded-r"]);
|
||||
return PrefixClasses.concat([
|
||||
"tw-border-l-0",
|
||||
"last:tw-rounded-r",
|
||||
|
||||
"focus-visible:tw-border-l",
|
||||
"focus-visible:tw-ml-[-1px]",
|
||||
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.buttonComponent?.setButtonType("unstyled");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
||||
|
||||
export type IconButtonType = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
|
||||
export type IconButtonType = ButtonType | "contrast" | "main" | "muted";
|
||||
|
||||
const focusRing = [
|
||||
// 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",
|
||||
"before:tw-ring-transparent",
|
||||
"focus-visible:tw-z-10",
|
||||
];
|
||||
|
||||
const styles: Record<IconButtonType, string[]> = {
|
||||
contrast: [
|
||||
@@ -12,8 +30,10 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-text-contrast",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
...focusRing,
|
||||
],
|
||||
main: [
|
||||
"tw-bg-transparent",
|
||||
@@ -22,8 +42,10 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-text-main",
|
||||
"focus-visible:before:tw-ring-text-main",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
...focusRing,
|
||||
],
|
||||
muted: [
|
||||
"tw-bg-transparent",
|
||||
@@ -32,8 +54,10 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
...focusRing,
|
||||
],
|
||||
primary: [
|
||||
"tw-bg-primary-500",
|
||||
@@ -42,8 +66,10 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
"hover:tw-bg-primary-700",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-primary-500",
|
||||
"disabled:hover:tw-bg-primary-500",
|
||||
...focusRing,
|
||||
],
|
||||
secondary: [
|
||||
"tw-bg-transparent",
|
||||
@@ -52,10 +78,12 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
"hover:!tw-text-contrast",
|
||||
"hover:tw-bg-text-muted",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-text-muted",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
"disabled:hover:!tw-text-muted",
|
||||
"disabled:hover:tw-border-text-muted",
|
||||
...focusRing,
|
||||
],
|
||||
danger: [
|
||||
"tw-bg-transparent",
|
||||
@@ -64,11 +92,14 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
"hover:!tw-text-contrast",
|
||||
"hover:tw-bg-danger-500",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-danger-500",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
"disabled:hover:!tw-text-danger",
|
||||
"disabled:hover:tw-border-danger-500",
|
||||
...focusRing,
|
||||
],
|
||||
unstyled: [],
|
||||
};
|
||||
|
||||
export type IconButtonSize = "default" | "small";
|
||||
@@ -86,7 +117,7 @@ const sizes: Record<IconButtonSize, string[]> = {
|
||||
export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
||||
@Input("bitIconButton") icon: string;
|
||||
|
||||
@Input() buttonType: IconButtonType = "main";
|
||||
@Input() buttonType: IconButtonType;
|
||||
|
||||
@Input() size: IconButtonSize = "default";
|
||||
|
||||
@@ -98,27 +129,9 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
||||
"tw-rounded",
|
||||
"tw-transition",
|
||||
"hover:tw-no-underline",
|
||||
"disabled:tw-opacity-60",
|
||||
"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",
|
||||
"before:tw-ring-transparent",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"focus-visible:tw-z-10",
|
||||
]
|
||||
.concat(styles[this.buttonType])
|
||||
.concat(styles[this.buttonType ?? "main"])
|
||||
.concat(sizes[this.size]);
|
||||
}
|
||||
|
||||
@@ -134,4 +147,8 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
||||
|
||||
@Input() loading = false;
|
||||
@Input() disabled = false;
|
||||
|
||||
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||
this.buttonType = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
"focus:tw-outline-none",
|
||||
"focus:tw-border-primary-700",
|
||||
"focus:tw-ring-1",
|
||||
"focus:tw-ring-inset",
|
||||
"focus:tw-ring-primary-700",
|
||||
"focus:tw-z-10",
|
||||
"disabled:tw-bg-secondary-100",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
|
||||
|
||||
export abstract class ButtonLikeAbstraction {
|
||||
loading: boolean;
|
||||
disabled: boolean;
|
||||
setButtonType: (value: ButtonType) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user