mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
Merged with master and fixed conflicts
This commit is contained in:
@@ -2,8 +2,9 @@ const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../shared/tsconfig.libs");
|
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.base");
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
displayName: "libs/angular tests",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<label class="environment-selector-btn">
|
||||
{{ "region" | i18n }}:
|
||||
<a
|
||||
(click)="toggle(null)"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="cdk-overlay-container"
|
||||
[ngSwitch]="selectedEnvironment"
|
||||
>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.US" class="text-primary">{{ "us" | i18n }}</label>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.EU" class="text-primary">{{ "eu" | i18n }}</label>
|
||||
<label *ngSwitchCase="ServerEnvironmentType.SelfHosted" class="text-primary">{{
|
||||
"selfHosted" | i18n
|
||||
}}</label>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</a>
|
||||
</label>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPostition"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="environment-selector-dialog" [@transformPanel]="'open'" role="dialog">
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.US)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="
|
||||
selectedEnvironment === ServerEnvironmentType.US ? 'visible' : 'hidden'
|
||||
"
|
||||
></i>
|
||||
<img class="img-us" alt="" />
|
||||
<span>{{ "us" | i18n }}</span>
|
||||
</button>
|
||||
<br />
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.EU)"
|
||||
*ngIf="euServerFlagEnabled"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="
|
||||
selectedEnvironment === ServerEnvironmentType.EU ? 'visible' : 'hidden'
|
||||
"
|
||||
></i>
|
||||
<img class="img-eu" alt="" />
|
||||
<span>{{ "eu" | i18n }}</span>
|
||||
</button>
|
||||
<br *ngIf="euServerFlagEnabled" />
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="
|
||||
selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'visible' : 'hidden'
|
||||
"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-pencil-square"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "selfHosted" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,109 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
templateUrl: "environment-selector.component.html",
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
})
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
})
|
||||
)
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
||||
euServerFlagEnabled: boolean;
|
||||
isOpen = false;
|
||||
showingModal = false;
|
||||
selectedEnvironment: ServerEnvironment;
|
||||
ServerEnvironmentType = ServerEnvironment;
|
||||
overlayPostition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
overlayX: "start",
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
protected componentDestroyed$: Subject<void> = new Subject();
|
||||
|
||||
constructor(
|
||||
protected environmentService: EnvironmentService,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
protected router: Router
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.configService.serverConfig$.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
|
||||
this.updateEnvironmentInfo();
|
||||
});
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.componentDestroyed$.next();
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
async toggle(option: ServerEnvironment) {
|
||||
this.isOpen = !this.isOpen;
|
||||
if (option === null) {
|
||||
return;
|
||||
}
|
||||
if (option === ServerEnvironment.EU) {
|
||||
await this.environmentService.setUrls({ base: "https://vault.bitwarden.eu" });
|
||||
} else if (option === ServerEnvironment.US) {
|
||||
await this.environmentService.setUrls({ base: "https://vault.bitwarden.com" });
|
||||
} else if (option === ServerEnvironment.SelfHosted) {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
}
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
|
||||
async updateEnvironmentInfo() {
|
||||
this.euServerFlagEnabled = await this.configService.getFeatureFlagBool(
|
||||
FeatureFlag.DisplayEuEnvironmentFlag
|
||||
);
|
||||
const webvaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (this.environmentService.isSelfHosted()) {
|
||||
this.selectedEnvironment = ServerEnvironment.SelfHosted;
|
||||
} else if (webvaultUrl != null && webvaultUrl.includes("bitwarden.eu")) {
|
||||
this.selectedEnvironment = ServerEnvironment.EU;
|
||||
} else {
|
||||
this.selectedEnvironment = ServerEnvironment.US;
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.updateEnvironmentInfo();
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerEnvironment {
|
||||
US = "US",
|
||||
EU = "EU",
|
||||
SelfHosted = "Self-hosted",
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
@@ -35,7 +35,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
selfHosted = false;
|
||||
showLoginWithDevice: boolean;
|
||||
validatedEmail = false;
|
||||
paramEmailSet = false;
|
||||
@@ -55,7 +54,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected devicesApiService: DevicesApiServiceAbstraction,
|
||||
protected appIdService: AppIdService,
|
||||
protected authService: AuthService,
|
||||
protected router: Router,
|
||||
@@ -73,7 +72,6 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
protected loginService: LoginService
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
get selfHostedDomain() {
|
||||
@@ -295,9 +293,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
||||
async getLoginWithDevice(email: string) {
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const res = await this.apiService.getKnownDevice(email, deviceIdentifier);
|
||||
//ensure the application is not self-hosted
|
||||
this.showLoginWithDevice = res && !this.selfHosted;
|
||||
this.showLoginWithDevice = await this.devicesApiService.getKnownDevice(
|
||||
email,
|
||||
deviceIdentifier
|
||||
);
|
||||
} catch (e) {
|
||||
this.showLoginWithDevice = false;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.loginService.clearValues();
|
||||
await this.onSuccessfulLogin();
|
||||
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||
// before nagivating to the success route.
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
if (response.resetMasterPassword) {
|
||||
this.successRoute = "set-password";
|
||||
|
||||
@@ -4,6 +4,8 @@ import { EnvironmentService } from "@bitwarden/common/abstractions/environment.s
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
|
||||
import { ModalService } from "../services/modal.service";
|
||||
|
||||
@Directive()
|
||||
export class EnvironmentComponent {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
@@ -19,7 +21,8 @@ export class EnvironmentComponent {
|
||||
constructor(
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected i18nService: I18nService
|
||||
protected i18nService: I18nService,
|
||||
private modalService: ModalService
|
||||
) {
|
||||
const urls = this.environmentService.getUrls();
|
||||
|
||||
@@ -59,5 +62,6 @@ export class EnvironmentComponent {
|
||||
|
||||
protected saved() {
|
||||
this.onSaved.emit();
|
||||
this.modalService.closeAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface SelectOptions {
|
||||
name: string;
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { ConfigApiServiceAbstraction } from "@bitwarden/common/abstractions/conf
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/abstractions/config/config.service.abstraction";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { DeviceCryptoServiceAbstraction } from "@bitwarden/common/abstractions/device-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
|
||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@bitwarden/common/abstractions/environment.service";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@@ -90,6 +92,8 @@ import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service
|
||||
import { CryptoService } from "@bitwarden/common/services/crypto.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DeviceCryptoService } from "@bitwarden/common/services/device-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { EnvironmentService } from "@bitwarden/common/services/environment.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
@@ -351,6 +355,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
PlatformUtilsServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
DevicesApiServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -615,7 +621,12 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
{
|
||||
provide: ConfigServiceAbstraction,
|
||||
useClass: ConfigService,
|
||||
deps: [StateServiceAbstraction, ConfigApiServiceAbstraction, AuthServiceAbstraction],
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
ConfigApiServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: ConfigApiServiceAbstraction,
|
||||
@@ -651,6 +662,23 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
useClass: OrgDomainApiService,
|
||||
deps: [OrgDomainServiceAbstraction, ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: DevicesApiServiceAbstraction,
|
||||
useClass: DevicesApiServiceImplementation,
|
||||
deps: [ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: DeviceCryptoServiceAbstraction,
|
||||
useClass: DeviceCryptoService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
StateServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
DevicesApiServiceAbstraction,
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
|
||||
@@ -63,7 +63,7 @@ export class ThemingService implements AbstractThemingService {
|
||||
|
||||
protected monitorSystemThemeChanges(): void {
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
this.window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
"change"
|
||||
).subscribe((event) => {
|
||||
this.updateSystemTheme(event.matches ? ThemeType.Dark : ThemeType.Light);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Directive, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
import { merge, takeUntil, Subject, startWith } from "rxjs";
|
||||
import { merge, startWith, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
@@ -21,7 +21,11 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
formPromise: Promise<string>;
|
||||
disabledByPolicy = false;
|
||||
private _disabledByPolicy = false;
|
||||
|
||||
protected get disabledByPolicy(): boolean {
|
||||
return this._disabledByPolicy;
|
||||
}
|
||||
|
||||
exportForm = this.formBuilder.group({
|
||||
format: ["json"],
|
||||
@@ -59,11 +63,12 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((policyAppliesToActiveUser) => {
|
||||
this.disabledByPolicy = policyAppliesToActiveUser;
|
||||
this._disabledByPolicy = policyAppliesToActiveUser;
|
||||
if (this.disabledByPolicy) {
|
||||
this.exportForm.disable();
|
||||
}
|
||||
});
|
||||
|
||||
await this.checkExportDisabled();
|
||||
|
||||
merge(
|
||||
this.exportForm.get("format").valueChanges,
|
||||
this.exportForm.get("fileEncryptionType").valueChanges
|
||||
@@ -77,12 +82,6 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.next();
|
||||
}
|
||||
|
||||
async checkExportDisabled() {
|
||||
if (this.disabledByPolicy) {
|
||||
this.exportForm.disable();
|
||||
}
|
||||
}
|
||||
|
||||
get encryptedFormat() {
|
||||
return this.format === "encrypted_json";
|
||||
}
|
||||
|
||||
@@ -198,4 +198,18 @@ export class FormSelectionList<
|
||||
this.selectItem(selectedItem.id, selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to iterate over each "selected" form control and its corresponding item
|
||||
* @param fn - The function to call for each form control and its corresponding item
|
||||
*/
|
||||
forEachControlItem(
|
||||
fn: (control: AbstractControl<Partial<TControlValue>, TControlValue>, value: TItem) => void
|
||||
) {
|
||||
for (let i = 0; i < this.formArray.length; i++) {
|
||||
// The selectedItems array and formArray are explicitly kept in sync,
|
||||
// so we can safely assume the index of the form control and item are the same
|
||||
fn(this.formArray.at(i), this.selectedItems[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../shared/tsconfig.libs");
|
||||
|
||||
const sharedConfig = require("../shared/jest.config.base");
|
||||
const sharedConfig = require("../shared/jest.config.ts");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
displayName: "libs/common tests",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
}
|
||||
|
||||
@@ -361,7 +361,6 @@ export abstract class ApiService {
|
||||
putDeviceVerificationSettings: (
|
||||
request: DeviceVerificationRequest
|
||||
) => Promise<DeviceVerificationResponse>;
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>;
|
||||
getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>;
|
||||
|
||||
@@ -2,8 +2,20 @@ export interface MessageBase {
|
||||
command: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
export abstract class BroadcasterService {
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
send: (message: MessageBase, id?: string) => void;
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
subscribe: (id: string, messageCallback: (message: MessageBase) => void) => void;
|
||||
/**
|
||||
* @deprecated Use the observable from the appropriate service instead.
|
||||
*/
|
||||
unsubscribe: (id: string) => void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { DeviceKey } from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { DeviceResponse } from "./devices/responses/device.response";
|
||||
|
||||
export abstract class DeviceCryptoServiceAbstraction {
|
||||
trustDevice: () => Promise<DeviceResponse>;
|
||||
getDeviceKey: () => Promise<DeviceKey>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DeviceResponse } from "./responses/device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
}
|
||||
@@ -7,6 +7,9 @@ export class DeviceResponse extends BaseResponse {
|
||||
identifier: string;
|
||||
type: DeviceType;
|
||||
creationDate: string;
|
||||
encryptedUserKey: string;
|
||||
encryptedPublicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -15,5 +18,8 @@ export class DeviceResponse extends BaseResponse {
|
||||
this.identifier = this.getResponseProperty("Identifier");
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
|
||||
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
|
||||
this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey");
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountSettingsSettings } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { WindowState } from "../models/domain/window-state";
|
||||
import { GeneratedPasswordHistory } from "../tools/generator/password";
|
||||
import { SendData } from "../tools/send/models/data/send.data";
|
||||
@@ -163,6 +163,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
|
||||
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
describe("ServerConfigData", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should create a ServerConfigData from a JSON object", () => {
|
||||
const serverConfigData = ServerConfigData.fromJSON({
|
||||
const json = {
|
||||
version: "1.0.0",
|
||||
gitHash: "1234567890",
|
||||
server: {
|
||||
@@ -22,18 +22,11 @@ describe("ServerConfigData", () => {
|
||||
sso: "https://sso.com",
|
||||
},
|
||||
utcDate: "2020-01-01T00:00:00.000Z",
|
||||
});
|
||||
featureStates: { feature: "state" },
|
||||
};
|
||||
const serverConfigData = ServerConfigData.fromJSON(json);
|
||||
|
||||
expect(serverConfigData.version).toEqual("1.0.0");
|
||||
expect(serverConfigData.gitHash).toEqual("1234567890");
|
||||
expect(serverConfigData.server.name).toEqual("test");
|
||||
expect(serverConfigData.server.url).toEqual("https://test.com");
|
||||
expect(serverConfigData.environment.vault).toEqual("https://vault.com");
|
||||
expect(serverConfigData.environment.api).toEqual("https://api.com");
|
||||
expect(serverConfigData.environment.identity).toEqual("https://identity.com");
|
||||
expect(serverConfigData.environment.notifications).toEqual("https://notifications.com");
|
||||
expect(serverConfigData.environment.sso).toEqual("https://sso.com");
|
||||
expect(serverConfigData.utcDate).toEqual("2020-01-01T00:00:00.000Z");
|
||||
expect(serverConfigData).toEqual(json);
|
||||
});
|
||||
|
||||
it("should be an instance of ServerConfigData", () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { EventData } from "../data/event.data";
|
||||
import { ServerConfigData } from "../data/server-config.data";
|
||||
|
||||
import { EncString } from "./enc-string";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
@@ -107,6 +107,7 @@ export class AccountKeys {
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
deviceKey?: DeviceKey;
|
||||
organizationKeys?: EncryptionPair<
|
||||
{ [orgId: string]: EncryptedOrganizationKeyData },
|
||||
Record<string, SymmetricCryptoKey>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
@@ -75,3 +75,6 @@ export class SymmetricCryptoKey {
|
||||
return SymmetricCryptoKey.fromString(obj?.keyB64);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup all separate key types as opaque types
|
||||
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
||||
|
||||
@@ -1110,14 +1110,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new DeviceVerificationResponse(r);
|
||||
}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.send("GET", "/devices/knowndevice", null, false, true, null, (headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
});
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
// Emergency Access APIs
|
||||
|
||||
async getEmergencyAccessTrusted(): Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>> {
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
import { BehaviorSubject, concatMap, timer } from "rxjs";
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { BehaviorSubject, Subject, concatMap, from, takeUntil, timer } from "rxjs";
|
||||
|
||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction";
|
||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
|
||||
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
|
||||
export class ConfigService implements ConfigServiceAbstraction {
|
||||
@Injectable()
|
||||
export class ConfigService implements ConfigServiceAbstraction, OnDestroy {
|
||||
protected _serverConfig = new BehaviorSubject<ServerConfig | null>(null);
|
||||
serverConfig$ = this._serverConfig.asObservable();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private configApiService: ConfigApiServiceAbstraction,
|
||||
private authService: AuthService
|
||||
private authService: AuthService,
|
||||
private environmentService: EnvironmentService
|
||||
) {
|
||||
// Re-fetch the server config every hour
|
||||
timer(0, 1000 * 3600)
|
||||
.pipe(
|
||||
concatMap(async () => {
|
||||
return await this.fetchServerConfig();
|
||||
})
|
||||
)
|
||||
.pipe(concatMap(() => from(this.fetchServerConfig())))
|
||||
.subscribe((serverConfig) => {
|
||||
this._serverConfig.next(serverConfig);
|
||||
});
|
||||
|
||||
this.environmentService.urls.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
this.fetchServerConfig();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async fetchServerConfig(): Promise<ServerConfig> {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { DeviceCryptoServiceAbstraction } from "../abstractions/device-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../types/csprng";
|
||||
|
||||
export class DeviceCryptoService implements DeviceCryptoServiceAbstraction {
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected encryptService: EncryptService,
|
||||
protected stateService: StateService,
|
||||
protected appIdService: AppIdService,
|
||||
protected devicesApiService: DevicesApiServiceAbstraction
|
||||
) {}
|
||||
|
||||
async trustDevice(): Promise<DeviceResponse> {
|
||||
// Attempt to get user symmetric key
|
||||
const userSymKey: SymmetricCryptoKey = await this.cryptoService.getEncKey();
|
||||
|
||||
// If user symmetric key is not found, throw error
|
||||
if (!userSymKey) {
|
||||
throw new Error("User symmetric key not found");
|
||||
}
|
||||
|
||||
// Generate deviceKey
|
||||
const deviceKey = await this.makeDeviceKey();
|
||||
|
||||
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
|
||||
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
|
||||
2048
|
||||
);
|
||||
|
||||
const [
|
||||
devicePublicKeyEncryptedUserSymKey,
|
||||
userSymKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user symmetric key with the DevicePublicKey
|
||||
this.cryptoService.rsaEncrypt(userSymKey.encKey, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user symmetric key
|
||||
this.encryptService.encrypt(devicePublicKey, userSymKey),
|
||||
|
||||
// Encrypt devicePrivateKey with deviceKey
|
||||
this.encryptService.encrypt(devicePrivateKey, deviceKey),
|
||||
]);
|
||||
|
||||
// Send encrypted keys to server
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
return this.devicesApiService.updateTrustedDeviceKeys(
|
||||
deviceIdentifier,
|
||||
devicePublicKeyEncryptedUserSymKey.encryptedString,
|
||||
userSymKeyEncryptedDevicePublicKey.encryptedString,
|
||||
deviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
}
|
||||
|
||||
async getDeviceKey(): Promise<DeviceKey> {
|
||||
// Check if device key is already stored
|
||||
const existingDeviceKey = await this.stateService.getDeviceKey();
|
||||
|
||||
if (existingDeviceKey != null) {
|
||||
return existingDeviceKey;
|
||||
} else {
|
||||
return this.makeDeviceKey();
|
||||
}
|
||||
}
|
||||
|
||||
private async makeDeviceKey(): Promise<DeviceKey> {
|
||||
// Create 512-bit device key
|
||||
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
|
||||
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
|
||||
|
||||
// Save device key in secure storage
|
||||
await this.stateService.setDeviceKey(deviceKey);
|
||||
|
||||
return deviceKey;
|
||||
}
|
||||
}
|
||||
317
libs/common/src/services/device-crypto.service.spec.ts
Normal file
317
libs/common/src/services/device-crypto.service.spec.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { EncryptionType } from "../enums/encryption-type.enum";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../services/crypto.service";
|
||||
import { CsprngArray } from "../types/csprng";
|
||||
|
||||
import { DeviceCryptoService } from "./device-crypto.service.implementation";
|
||||
|
||||
describe("deviceCryptoService", () => {
|
||||
let deviceCryptoService: DeviceCryptoService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const stateService = mock<StateService>();
|
||||
const appIdService = mock<AppIdService>();
|
||||
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(cryptoFunctionService);
|
||||
mockReset(encryptService);
|
||||
mockReset(stateService);
|
||||
mockReset(appIdService);
|
||||
mockReset(devicesApiService);
|
||||
|
||||
deviceCryptoService = new DeviceCryptoService(
|
||||
cryptoFunctionService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
stateService,
|
||||
appIdService,
|
||||
devicesApiService
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(deviceCryptoService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("Trusted Device Encryption", () => {
|
||||
const deviceKeyBytesLength = 64;
|
||||
const userSymKeyBytesLength = 64;
|
||||
|
||||
describe("getDeviceKey", () => {
|
||||
let mockRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: SymmetricCryptoKey;
|
||||
let existingDeviceKey: DeviceKey;
|
||||
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
|
||||
existingDeviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
|
||||
makeDeviceKeySpy = jest.spyOn(deviceCryptoService as any, "makeDeviceKey");
|
||||
});
|
||||
|
||||
it("gets a device key when there is not an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
|
||||
makeDeviceKeySpy.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
const deviceKey = await deviceCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(mockDeviceKey);
|
||||
});
|
||||
|
||||
it("returns the existing device key without creating a new one when there is an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
|
||||
|
||||
const deviceKey = await deviceCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(makeDeviceKeySpy).not.toHaveBeenCalled();
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(existingDeviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeDeviceKey", () => {
|
||||
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
|
||||
const cryptoFuncSvcRandomBytesSpy = jest
|
||||
.spyOn(cryptoFunctionService, "randomBytes")
|
||||
.mockResolvedValue(mockRandomBytes);
|
||||
|
||||
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
// This is a hacky workaround, but it allows for cleaner tests
|
||||
const deviceKey = await (deviceCryptoService as any).makeDeviceKey();
|
||||
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trustDevice", () => {
|
||||
let mockDeviceKeyRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: DeviceKey;
|
||||
|
||||
let mockUserSymKeyRandomBytes: CsprngArray;
|
||||
let mockUserSymKey: SymmetricCryptoKey;
|
||||
|
||||
const deviceRsaKeyLength = 2048;
|
||||
let mockDeviceRsaKeyPair: [ArrayBuffer, ArrayBuffer];
|
||||
let mockDevicePrivateKey: ArrayBuffer;
|
||||
let mockDevicePublicKey: ArrayBuffer;
|
||||
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
|
||||
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
|
||||
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
|
||||
|
||||
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
|
||||
Id: "mockId",
|
||||
Name: "mockName",
|
||||
Identifier: "mockIdentifier",
|
||||
Type: "mockType",
|
||||
CreationDate: "mockCreationDate",
|
||||
});
|
||||
|
||||
const mockDeviceId = "mockDeviceId";
|
||||
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
let rsaGenerateKeyPairSpy: jest.SpyInstance;
|
||||
let cryptoSvcGetEncKeySpy: jest.SpyInstance;
|
||||
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
|
||||
let encryptServiceEncryptSpy: jest.SpyInstance;
|
||||
let appIdServiceGetAppIdSpy: jest.SpyInstance;
|
||||
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup all spies and default return values for the happy path
|
||||
|
||||
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray;
|
||||
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
|
||||
|
||||
mockDeviceRsaKeyPair = [
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
new ArrayBuffer(deviceRsaKeyLength),
|
||||
];
|
||||
|
||||
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
|
||||
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
|
||||
|
||||
mockDevicePublicKeyEncryptedUserSymKey = new EncString(
|
||||
EncryptionType.Rsa2048_OaepSha1_B64,
|
||||
"mockDevicePublicKeyEncryptedUserSymKey"
|
||||
);
|
||||
|
||||
mockUserSymKeyEncryptedDevicePublicKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockUserSymKeyEncryptedDevicePublicKey"
|
||||
);
|
||||
|
||||
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockDeviceKeyEncryptedDevicePrivateKey"
|
||||
);
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
makeDeviceKeySpy = jest
|
||||
.spyOn(deviceCryptoService as any, "makeDeviceKey")
|
||||
.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
rsaGenerateKeyPairSpy = jest
|
||||
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
|
||||
.mockResolvedValue(mockDeviceRsaKeyPair);
|
||||
|
||||
cryptoSvcGetEncKeySpy = jest
|
||||
.spyOn(cryptoService, "getEncKey")
|
||||
.mockResolvedValue(mockUserSymKey);
|
||||
|
||||
cryptoSvcRsaEncryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaEncrypt")
|
||||
.mockResolvedValue(mockDevicePublicKeyEncryptedUserSymKey);
|
||||
|
||||
encryptServiceEncryptSpy = jest
|
||||
.spyOn(encryptService, "encrypt")
|
||||
.mockImplementation((plainValue, key) => {
|
||||
if (plainValue === mockDevicePublicKey && key === mockUserSymKey) {
|
||||
return Promise.resolve(mockUserSymKeyEncryptedDevicePublicKey);
|
||||
}
|
||||
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
|
||||
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
|
||||
}
|
||||
});
|
||||
|
||||
appIdServiceGetAppIdSpy = jest
|
||||
.spyOn(appIdService, "getAppId")
|
||||
.mockResolvedValue(mockDeviceId);
|
||||
|
||||
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
|
||||
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
|
||||
.mockResolvedValue(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
|
||||
const response = await deviceCryptoService.trustDevice();
|
||||
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoSvcGetEncKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
|
||||
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
|
||||
mockDeviceId,
|
||||
mockDevicePublicKeyEncryptedUserSymKey.encryptedString,
|
||||
mockUserSymKeyEncryptedDevicePublicKey.encryptedString,
|
||||
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
|
||||
expect(response).toBeInstanceOf(DeviceResponse);
|
||||
expect(response).toEqual(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("throws specific error if user symmetric key is not found", async () => {
|
||||
// setup the spy to return null
|
||||
cryptoSvcGetEncKeySpy.mockResolvedValue(null);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
|
||||
// reset the spy
|
||||
cryptoSvcGetEncKeySpy.mockReset();
|
||||
|
||||
// setup the spy to return undefined
|
||||
cryptoSvcGetEncKeySpy.mockResolvedValue(undefined);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
});
|
||||
|
||||
const methodsToTestForErrorsOrInvalidReturns = [
|
||||
{
|
||||
method: "makeDeviceKey",
|
||||
spy: () => makeDeviceKeySpy,
|
||||
errorText: "makeDeviceKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaGenerateKeyPair",
|
||||
spy: () => rsaGenerateKeyPairSpy,
|
||||
errorText: "rsaGenerateKeyPair error",
|
||||
},
|
||||
{
|
||||
method: "getEncKey",
|
||||
spy: () => cryptoSvcGetEncKeySpy,
|
||||
errorText: "getEncKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaEncrypt",
|
||||
spy: () => cryptoSvcRsaEncryptSpy,
|
||||
errorText: "rsaEncrypt error",
|
||||
},
|
||||
{
|
||||
method: "encryptService.encrypt",
|
||||
spy: () => encryptServiceEncryptSpy,
|
||||
errorText: "encryptService.encrypt error",
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(methodsToTestForErrorsOrInvalidReturns)(
|
||||
"trustDevice error handling and invalid return testing",
|
||||
({ method, spy, errorText }) => {
|
||||
// ensures that error propagation works correctly
|
||||
it(`throws an error if ${method} fails`, async () => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockRejectedValue(new Error(errorText));
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow(errorText);
|
||||
});
|
||||
|
||||
test.each([null, undefined])(
|
||||
`throws an error if ${method} returns %s`,
|
||||
async (invalidValue) => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockResolvedValue(invalidValue);
|
||||
await expect(deviceCryptoService.trustDevice()).rejects.toThrow();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { ApiService } from "../api.service";
|
||||
|
||||
import { TrustedDeviceKeysRequest } from "./requests/trusted-device-keys.request";
|
||||
|
||||
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/devices/knowndevice",
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
(headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
}
|
||||
);
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device by identifier
|
||||
* @param deviceIdentifier - client generated id (not device id in DB)
|
||||
*/
|
||||
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/devices/identifier/${deviceIdentifier}`,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new DeviceResponse(r);
|
||||
}
|
||||
|
||||
async updateTrustedDeviceKeys(
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
): Promise<DeviceResponse> {
|
||||
const request = new TrustedDeviceKeysRequest(
|
||||
devicePublicKeyEncryptedUserSymKey,
|
||||
userSymKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey
|
||||
);
|
||||
|
||||
const result = await this.apiService.send(
|
||||
"PUT",
|
||||
`/devices/${deviceIdentifier}/keys`,
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return new DeviceResponse(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class TrustedDeviceKeysRequest {
|
||||
constructor(
|
||||
public encryptedUserKey: string,
|
||||
public encryptedPublicKey: string,
|
||||
public encryptedPrivateKey: string
|
||||
) {}
|
||||
}
|
||||
@@ -218,6 +218,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
||||
return ![
|
||||
"http://vault.bitwarden.com",
|
||||
"https://vault.bitwarden.com",
|
||||
"http://vault.bitwarden.eu",
|
||||
"https://vault.bitwarden.eu",
|
||||
"http://vault.qa.bitwarden.pw",
|
||||
"https://vault.qa.bitwarden.pw",
|
||||
].includes(this.getWebVaultUrl());
|
||||
|
||||
@@ -35,7 +35,7 @@ import { EncString } from "../models/domain/enc-string";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
import { State } from "../models/domain/state";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import { WindowState } from "../models/domain/window-state";
|
||||
import { GeneratedPasswordHistory } from "../tools/generator/password";
|
||||
import { SendData } from "../tools/send/models/data/send.data";
|
||||
@@ -1054,6 +1054,32 @@ export class StateService<
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
||||
}
|
||||
|
||||
async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
return account?.keys?.deviceKey as DeviceKey;
|
||||
}
|
||||
|
||||
async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
account.keys.deviceKey = value;
|
||||
|
||||
await this.saveAccount(account, options);
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
@@ -2751,7 +2777,10 @@ export class StateService<
|
||||
|
||||
// settings persist even on reset, and are not effected by this method
|
||||
protected resetAccount(account: TAccount) {
|
||||
const persistentAccountInformation = { settings: account.settings };
|
||||
const persistentAccountInformation = {
|
||||
settings: account.settings,
|
||||
keys: { deviceKey: account.keys.deviceKey },
|
||||
};
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
@@ -2830,7 +2859,7 @@ export class StateService<
|
||||
return this.reconcileOptions(options, defaultOptions);
|
||||
}
|
||||
|
||||
private async saveSecureStorageKey<T extends JsonValue>(
|
||||
protected async saveSecureStorageKey<T extends JsonValue>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?: StorageOptions
|
||||
|
||||
4
libs/common/src/types/csprng.d.ts
vendored
4
libs/common/src/types/csprng.d.ts
vendored
@@ -1,5 +1,9 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
// You would typically use these types when you want to create a type that
|
||||
// represents an array or string value generated from a
|
||||
// cryptographic secure pseudorandom number generator (CSPRNG)
|
||||
|
||||
type CsprngArray = Opaque<ArrayBuffer, "CSPRNG">;
|
||||
|
||||
type CsprngString = Opaque<string, "CSPRNG">;
|
||||
|
||||
@@ -2,8 +2,9 @@ const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.base");
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
displayName: "libs/components tests",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule, Validators, FormBuilder } from "@angular/forms";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { delay, of } from "rxjs";
|
||||
|
||||
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||
@@ -145,16 +145,18 @@ export default {
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
const PromiseTemplate: Story<PromiseExampleComponent> = (args: PromiseExampleComponent) => ({
|
||||
props: args,
|
||||
template: `<app-promise-example></app-promise-example>`,
|
||||
});
|
||||
type Story = StoryObj<PromiseExampleComponent>;
|
||||
|
||||
export const UsingPromise = PromiseTemplate.bind({});
|
||||
export const UsingPromise: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-promise-example></app-promise-example>`,
|
||||
}),
|
||||
};
|
||||
|
||||
const ObservableTemplate: Story<PromiseExampleComponent> = (args: PromiseExampleComponent) => ({
|
||||
props: args,
|
||||
template: `<app-observable-example></app-observable-example>`,
|
||||
});
|
||||
|
||||
export const UsingObservable = ObservableTemplate.bind({});
|
||||
export const UsingObservable: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-observable-example></app-observable-example>`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import { delay, of } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
@@ -80,25 +80,24 @@ export default {
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
const PromiseTemplate: Story<PromiseExampleComponent> = (args: PromiseExampleComponent) => ({
|
||||
props: args,
|
||||
template: `<app-promise-example></app-promise-example>`,
|
||||
});
|
||||
type PromiseStory = StoryObj<PromiseExampleComponent>;
|
||||
type ObservableStory = StoryObj<ObservableExampleComponent>;
|
||||
|
||||
export const UsingPromise = PromiseTemplate.bind({});
|
||||
export const UsingPromise: PromiseStory = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-promise-example></app-promise-example>`,
|
||||
}),
|
||||
};
|
||||
|
||||
const ObservableTemplate: Story<ObservableExampleComponent> = (
|
||||
args: ObservableExampleComponent
|
||||
) => ({
|
||||
template: `<app-observable-example></app-observable-example>`,
|
||||
});
|
||||
export const UsingObservable: ObservableStory = {
|
||||
render: (args) => ({
|
||||
template: `<app-observable-example></app-observable-example>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const UsingObservable = ObservableTemplate.bind({});
|
||||
|
||||
const RejectedPromiseTemplate: Story<ObservableExampleComponent> = (
|
||||
args: ObservableExampleComponent
|
||||
) => ({
|
||||
template: `<app-rejected-promise-example></app-rejected-promise-example>`,
|
||||
});
|
||||
|
||||
export const RejectedPromise = RejectedPromiseTemplate.bind({});
|
||||
export const RejectedPromise: ObservableStory = {
|
||||
render: (args) => ({
|
||||
template: `<app-rejected-promise-example></app-rejected-promise-example>`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { AvatarComponent } from "./avatar.component";
|
||||
|
||||
@@ -18,41 +18,46 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<AvatarComponent> = (args: AvatarComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
type Story = StoryObj<AvatarComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
color: "#175ddc",
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
color: "#175ddc",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
size: "large",
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: "large",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: "small",
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: "small",
|
||||
},
|
||||
};
|
||||
|
||||
export const LightBackground = Template.bind({});
|
||||
LightBackground.args = {
|
||||
color: "#d2ffcf",
|
||||
export const LightBackground: Story = {
|
||||
args: {
|
||||
color: "#d2ffcf",
|
||||
},
|
||||
};
|
||||
|
||||
export const Border = Template.bind({});
|
||||
Border.args = {
|
||||
border: true,
|
||||
export const Border: Story = {
|
||||
args: {
|
||||
border: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const ColorByID = Template.bind({});
|
||||
ColorByID.args = {
|
||||
id: 236478,
|
||||
export const ColorByID: Story = {
|
||||
args: {
|
||||
id: "236478",
|
||||
},
|
||||
};
|
||||
|
||||
export const ColorByText = Template.bind({});
|
||||
ColorByText.args = {
|
||||
text: "Jason Doe",
|
||||
export const ColorByText: Story = {
|
||||
args: {
|
||||
text: "Jason Doe",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -38,16 +38,19 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const ListTemplate: Story<BadgeListComponent> = (args: BadgeListComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-badge-list [badgeType]="badgeType" [maxItems]="maxItems" [items]="items"></bit-badge-list>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<BadgeListComponent>;
|
||||
|
||||
export const Default = ListTemplate.bind({});
|
||||
Default.args = {
|
||||
badgeType: "info",
|
||||
maxItems: 3,
|
||||
items: ["Badge 1", "Badge 2", "Badge 3", "Badge 4", "Badge 5"],
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-badge-list [badgeType]="badgeType" [maxItems]="maxItems" [items]="items"></bit-badge-list>
|
||||
`,
|
||||
}),
|
||||
|
||||
args: {
|
||||
badgeType: "info",
|
||||
maxItems: 3,
|
||||
items: ["Badge 1", "Badge 2", "Badge 3", "Badge 4", "Badge 5"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { BadgeDirective } from "./badge.directive";
|
||||
|
||||
@@ -21,43 +21,54 @@ export default {
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
} as Meta<BadgeDirective>;
|
||||
|
||||
const Template: Story<BadgeDirective> = (args: BadgeDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-text-main">Span </span><span bitBadge [badgeType]="badgeType">Badge</span>
|
||||
<br><br>
|
||||
<span class="tw-text-main">Link </span><a href="#" bitBadge [badgeType]="badgeType">Badge</a>
|
||||
<br><br>
|
||||
<span class="tw-text-main">Button </span><button bitBadge [badgeType]="badgeType">Badge</button>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<BadgeDirective>;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
badgeType: "secondary",
|
||||
export const Primary: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-text-main">Span </span><span bitBadge [badgeType]="badgeType">Badge</span>
|
||||
<br><br>
|
||||
<span class="tw-text-main">Link </span><a href="#" bitBadge [badgeType]="badgeType">Badge</a>
|
||||
<br><br>
|
||||
<span class="tw-text-main">Button </span><button bitBadge [badgeType]="badgeType">Badge</button>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Success = Template.bind({});
|
||||
Success.args = {
|
||||
badgeType: "success",
|
||||
export const Secondary: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
badgeType: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
badgeType: "danger",
|
||||
export const Success: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
badgeType: "success",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning = Template.bind({});
|
||||
Warning.args = {
|
||||
badgeType: "warning",
|
||||
export const Danger: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
badgeType: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
export const Info = Template.bind({});
|
||||
Info.args = {
|
||||
badgeType: "info",
|
||||
export const Warning: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
badgeType: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const Info: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
badgeType: "info",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Meta, Story } from "@storybook/addon-docs";
|
||||
import { Meta, Story, Controls, Canvas, Primary } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Banner" />
|
||||
import * as stories from "./banner.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
# Banner
|
||||
|
||||
@@ -15,6 +17,10 @@ persist across all pages a user navigates to.
|
||||
- Avoid stacking multiple banners.
|
||||
- Banners support a button link (text button).
|
||||
|
||||
<Primary />
|
||||
|
||||
<Controls />
|
||||
|
||||
## Types
|
||||
|
||||
Icons should remain consistent across these types. Do not change the icon without consulting
|
||||
@@ -24,25 +30,25 @@ Use the following guidelines to help choose the correct type of banner.
|
||||
|
||||
### Premium
|
||||
|
||||
<Story id="component-library-banner--premium" />
|
||||
<Story of={stories.Premium} />
|
||||
|
||||
Used primarily to encourage user to upgrade to premium.
|
||||
|
||||
### Info
|
||||
|
||||
<Story id="component-library-banner--info" />
|
||||
<Story of={stories.Info} />
|
||||
|
||||
Used to communicate release notes, server maintenance or other informative event.
|
||||
|
||||
### Warning
|
||||
|
||||
<Story id="component-library-banner--warning" />
|
||||
<Story of={stories.Warning} />
|
||||
|
||||
Used to alert the user of outdated info or versions.
|
||||
|
||||
### Danger
|
||||
|
||||
<Story id="component-library-banner--danger" />
|
||||
<Story of={stories.Danger} />
|
||||
|
||||
Rarely used, but may be used to alert users over critical messages or very outdated versions.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -39,34 +39,46 @@ export default {
|
||||
argTypes: {
|
||||
onClose: { action: "onClose" },
|
||||
},
|
||||
} as Meta;
|
||||
} as Meta<BannerComponent>;
|
||||
|
||||
const Template: Story<BannerComponent> = (args: BannerComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-banner [bannerType]="bannerType" (onClose)="onClose($event)">
|
||||
type Story = StoryObj<BannerComponent>;
|
||||
|
||||
export const Premium: Story = {
|
||||
args: {
|
||||
bannerType: "premium",
|
||||
},
|
||||
render: (args: BannerComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-banner [bannerType]="bannerType" (onClose)="onClose($event)">
|
||||
Content Really Long Text Lorem Ipsum Ipsum Ipsum
|
||||
<button bitLink linkType="contrast">Button</button>
|
||||
</bit-banner>
|
||||
`,
|
||||
});
|
||||
</bit-banner>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Premium = Template.bind({});
|
||||
Premium.args = {
|
||||
bannerType: "premium",
|
||||
};
|
||||
|
||||
export const Info = Template.bind({});
|
||||
Info.args = {
|
||||
bannerType: "info",
|
||||
export const Info: Story = {
|
||||
...Premium,
|
||||
args: {
|
||||
bannerType: "info",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning = Template.bind({});
|
||||
Warning.args = {
|
||||
bannerType: "warning",
|
||||
export const Warning: Story = {
|
||||
...Premium,
|
||||
args: {
|
||||
bannerType: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
bannerType: "danger",
|
||||
export const Danger: Story = {
|
||||
...Premium,
|
||||
args: {
|
||||
bannerType: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, Story, moduleMetadata } from "@storybook/angular";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { LinkModule } from "../link";
|
||||
@@ -26,16 +26,19 @@ export default {
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [BreadcrumbComponent],
|
||||
imports: [
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
IconButtonModule,
|
||||
RouterModule.forRoot([{ path: "**", component: EmptyComponent }], { useHash: true }),
|
||||
imports: [LinkModule, MenuModule, IconButtonModule, RouterModule],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
importProvidersFrom(
|
||||
RouterModule.forRoot([{ path: "**", component: EmptyComponent }], { useHash: true })
|
||||
),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
items: [],
|
||||
show: 3,
|
||||
},
|
||||
argTypes: {
|
||||
breadcrumbs: {
|
||||
@@ -45,47 +48,54 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BreadcrumbsComponent> = (args: BreadcrumbsComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<h3 class="tw-text-main">Router links</h3>
|
||||
<p>
|
||||
<bit-breadcrumbs [show]="show">
|
||||
<bit-breadcrumb *ngFor="let item of items" [icon]="item.icon" [route]="[item.route]">{{item.name}}</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
</p>
|
||||
type Story = StoryObj<BreadcrumbsComponent & { items: Breadcrumb[] }>;
|
||||
|
||||
<h3 class="tw-text-main">Click emit</h3>
|
||||
<p>
|
||||
<bit-breadcrumbs [show]="show">
|
||||
<bit-breadcrumb *ngFor="let item of items" [icon]="item.icon" (click)="click($event)">{{item.name}}</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
</p>
|
||||
`,
|
||||
});
|
||||
export const TopLevel: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<h3 class="tw-text-main">Router links</h3>
|
||||
<p>
|
||||
<bit-breadcrumbs [show]="show">
|
||||
<bit-breadcrumb *ngFor="let item of items" [icon]="item.icon" [route]="[item.route]">{{item.name}}</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
</p>
|
||||
|
||||
<h3 class="tw-text-main">Click emit</h3>
|
||||
<p>
|
||||
<bit-breadcrumbs [show]="show">
|
||||
<bit-breadcrumb *ngFor="let item of items" [icon]="item.icon" (click)="click($event)">{{item.name}}</bit-breadcrumb>
|
||||
</bit-breadcrumbs>
|
||||
</p>
|
||||
`,
|
||||
}),
|
||||
|
||||
export const TopLevel = Template.bind({});
|
||||
TopLevel.args = {
|
||||
items: [{ icon: "bwi-star", name: "Top Level" }] as Breadcrumb[],
|
||||
args: {
|
||||
items: [{ icon: "bwi-star", name: "Top Level" }] as Breadcrumb[],
|
||||
},
|
||||
};
|
||||
|
||||
export const SecondLevel = Template.bind({});
|
||||
SecondLevel.args = {
|
||||
items: [
|
||||
{ name: "Acme Vault", route: "/" },
|
||||
{ icon: "bwi-collection", name: "Collection", route: "collection" },
|
||||
] as Breadcrumb[],
|
||||
export const SecondLevel: Story = {
|
||||
...TopLevel,
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Acme Vault", route: "/" },
|
||||
{ icon: "bwi-collection", name: "Collection", route: "collection" },
|
||||
] as Breadcrumb[],
|
||||
},
|
||||
};
|
||||
|
||||
export const Overflow = Template.bind({});
|
||||
Overflow.args = {
|
||||
items: [
|
||||
{ name: "Acme Vault", route: "" },
|
||||
{ icon: "bwi-collection", name: "Collection", route: "collection" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 1", route: "middle-collection-1" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 2", route: "middle-collection-2" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 3", route: "middle-collection-3" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 4", route: "middle-collection-4" },
|
||||
{ icon: "bwi-collection", name: "End Collection", route: "end-collection" },
|
||||
] as Breadcrumb[],
|
||||
export const Overflow: Story = {
|
||||
...TopLevel,
|
||||
args: {
|
||||
items: [
|
||||
{ name: "Acme Vault", route: "" },
|
||||
{ icon: "bwi-collection", name: "Collection", route: "collection" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 1", route: "middle-collection-1" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 2", route: "middle-collection-2" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 3", route: "middle-collection-3" },
|
||||
{ icon: "bwi-collection", name: "Middle-Collection 4", route: "middle-collection-4" },
|
||||
{ icon: "bwi-collection", name: "End Collection", route: "end-collection" },
|
||||
] as Breadcrumb[],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { Meta, Story } from "@storybook/addon-docs";
|
||||
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Button" />
|
||||
import * as stories from "./button.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
# Button
|
||||
|
||||
Buttons are interactive elements that can be triggered using a mouse, keyboard, or touch. They are
|
||||
used to indicate actions that can be performed by a user such as submitting a form.
|
||||
|
||||
<Primary />
|
||||
|
||||
<Controls />
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Choosing the `<a>` or `<button>`
|
||||
@@ -50,7 +56,7 @@ Both submit and async action buttons use a loading button state while an action
|
||||
button is preforming a long running task in the background like a server API call, be sure to review
|
||||
the [Async Actions Directive](?path=/story/component-library-async-actions-overview--page).
|
||||
|
||||
<Story id="component-library-button--loading" />
|
||||
<Story of={stories.Loading} />
|
||||
|
||||
## Styles
|
||||
|
||||
@@ -58,14 +64,14 @@ There are 3 main styles for the button: Primary, Secondary, and Danger.
|
||||
|
||||
### Primary
|
||||
|
||||
<Story id="component-library-button--primary" />
|
||||
<Story of={stories.Primary} />
|
||||
|
||||
Use the primary button styling for all Primary call to actions. An action is "primary" if it relates
|
||||
to the main purpose of a page. There should never be 2 primary styled buttons next to each other.
|
||||
|
||||
### Secondary
|
||||
|
||||
<Story id="component-library-button--secondary" />
|
||||
<Story of={stories.Secondary} />
|
||||
|
||||
The secondary styling should be used for secondary calls to action. An action is "secondary" if it
|
||||
relates indirectly to the purpose of a page. There may be multiple secondary buttons next to each
|
||||
@@ -73,7 +79,7 @@ other; however, generally there should only be 1 or 2 calls to action per page.
|
||||
|
||||
### Danger
|
||||
|
||||
<Story id="component-library-button--danger" />
|
||||
<Story of={stories.Danger} />
|
||||
|
||||
Use the danger styling only in settings when the user may preform a permanent action.
|
||||
|
||||
@@ -82,11 +88,11 @@ Use the danger styling only in settings when the user may preform a permanent ac
|
||||
Both the disabled and loading states use the default state’s color with a 60% opacity or
|
||||
`tw-opacity-60`.
|
||||
|
||||
<Story id="component-library-button--disabled" />
|
||||
<Story of={stories.Disabled} />
|
||||
|
||||
## Block
|
||||
|
||||
Typically button widths expand with their text. In some causes though buttons may need to be block
|
||||
where the width is fixed and the text wraps to 2 lines if exceeding the button’s width.
|
||||
|
||||
<Story id="component-library-button--block" />
|
||||
<Story of={stories.Block} />
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { ButtonComponent } from "./button.component";
|
||||
|
||||
@@ -16,88 +16,96 @@ export default {
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5115%3A26950",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
} as Meta<ButtonComponent>;
|
||||
|
||||
const Template: Story<ButtonComponent> = (args: ButtonComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Button</button>
|
||||
<a bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">Link</a>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<ButtonComponent>;
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
buttonType: "primary",
|
||||
export const Primary: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block">Button</button>
|
||||
<a bitButton [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [block]="block" href="#" class="tw-ml-2">Link</a>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
buttonType: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
buttonType: "secondary",
|
||||
export const Secondary: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
buttonType: "secondary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
buttonType: "danger",
|
||||
export const Danger: Story = {
|
||||
...Primary,
|
||||
args: {
|
||||
buttonType: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
const AllStylesTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Loading = AllStylesTemplate.bind({});
|
||||
Loading.args = {
|
||||
disabled: false,
|
||||
loading: true,
|
||||
export const Loading: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton [disabled]="disabled" [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
disabled: false,
|
||||
loading: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = AllStylesTemplate.bind({});
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
loading: false,
|
||||
export const Disabled: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
disabled: true,
|
||||
loading: false,
|
||||
},
|
||||
};
|
||||
|
||||
const DisabledWithAttributeTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ng-container *ngIf="disabled">
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!disabled">
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
</ng-container>
|
||||
`,
|
||||
});
|
||||
|
||||
export const DisabledWithAttribute = DisabledWithAttributeTemplate.bind({});
|
||||
DisabledWithAttribute.args = {
|
||||
disabled: true,
|
||||
loading: false,
|
||||
export const DisabledWithAttribute: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ng-container *ngIf="disabled">
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton disabled [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!disabled">
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<button bitButton [loading]="loading" [block]="block" buttonType="danger" class="tw-mr-2">Danger</button>
|
||||
</ng-container>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
disabled: true,
|
||||
loading: false,
|
||||
},
|
||||
};
|
||||
|
||||
const BlockTemplate: Story<ButtonComponent> = (args: ButtonComponent) => ({
|
||||
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,
|
||||
export const Block: Story = {
|
||||
render: (args: ButtonComponent) => ({
|
||||
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>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
block: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -35,31 +35,39 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<CalloutComponent> = (args: CalloutComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-callout [type]="type" [title]="title">Content</bit-callout>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<CalloutComponent>;
|
||||
|
||||
export const Success = Template.bind({});
|
||||
Success.args = {
|
||||
type: "success",
|
||||
title: "Success",
|
||||
export const Success: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-callout [type]="type" [title]="title">Content</bit-callout>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
type: "success",
|
||||
title: "Success",
|
||||
},
|
||||
};
|
||||
|
||||
export const Info = Template.bind({});
|
||||
Info.args = {
|
||||
type: "info",
|
||||
title: "Info",
|
||||
export const Info: Story = {
|
||||
...Success,
|
||||
args: {
|
||||
type: "info",
|
||||
title: "Info",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning = Template.bind({});
|
||||
Warning.args = {
|
||||
type: "warning",
|
||||
export const Warning: Story = {
|
||||
...Success,
|
||||
args: {
|
||||
type: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
type: "danger",
|
||||
export const Danger: Story = {
|
||||
...Success,
|
||||
args: {
|
||||
type: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/src/abstractions/i18n.service";
|
||||
|
||||
@@ -69,43 +69,44 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const DefaultTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
|
||||
props: args,
|
||||
template: `<app-example [checked]="checked" [disabled]="disabled"></app-example>`,
|
||||
});
|
||||
type Story = StoryObj<ExampleComponent>;
|
||||
|
||||
export const Default = DefaultTemplate.bind({});
|
||||
Default.parameters = {
|
||||
docs: {
|
||||
source: {
|
||||
code: template,
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-example [checked]="checked" [disabled]="disabled"></app-example>`,
|
||||
}),
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
code: template,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Default.args = {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
args: {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
const CustomTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-flex tw-flex-col tw-w-32">
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
A-Z
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
a-z
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
0-9
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
CustomTemplate.args = {};
|
||||
|
||||
export const Custom = CustomTemplate.bind({});
|
||||
export const Custom: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-flex tw-flex-col tw-w-32">
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
A-Z
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
a-z
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
0-9
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ enum CharacterType {
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class ColorPasswordComponent {
|
||||
@Input() private password: string = null;
|
||||
@Input() password: string = null;
|
||||
@Input() showCount = false;
|
||||
|
||||
characterStyles: Record<CharacterType, string[]> = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { ColorPasswordComponent } from "./color-password.component";
|
||||
|
||||
@@ -19,34 +19,40 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ColorPasswordComponent> = (args: ColorPasswordComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-color-password class="tw-text-base" [password]="password" [showCount]="showCount"></bit-color-password>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<ColorPasswordComponent>;
|
||||
|
||||
const WrappedTemplate: Story<ColorPasswordComponent> = (args: ColorPasswordComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-max-w-32">
|
||||
<bit-color-password class="tw-text-base" [password]="password" [showCount]="showCount"></bit-color-password>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ColorPassword = Template.bind({});
|
||||
|
||||
export const WrappedColorPassword = WrappedTemplate.bind({});
|
||||
|
||||
export const ColorPasswordCount = Template.bind({});
|
||||
ColorPasswordCount.args = {
|
||||
password: examplePassword,
|
||||
showCount: true,
|
||||
export const ColorPassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-color-password class="tw-text-base" [password]="password" [showCount]="showCount"></bit-color-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WrappedColorPasswordCount = WrappedTemplate.bind({});
|
||||
WrappedColorPasswordCount.args = {
|
||||
password: examplePassword,
|
||||
showCount: true,
|
||||
export const WrappedColorPassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-max-w-32">
|
||||
<bit-color-password class="tw-text-base" [password]="password" [showCount]="showCount"></bit-color-password>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ColorPasswordCount: Story = {
|
||||
...ColorPassword,
|
||||
args: {
|
||||
password: examplePassword,
|
||||
showCount: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WrappedColorPasswordCount: Story = {
|
||||
...WrappedColorPassword,
|
||||
args: {
|
||||
password: examplePassword,
|
||||
showCount: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DIALOG_DATA, DialogModule, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -90,8 +90,6 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<StoryDialogComponent> = (args: StoryDialogComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
type Story = StoryObj<StoryDialogComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -50,110 +50,118 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<DialogComponent> = (args: DialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize" [loading]="loading" [disablePadding]="disablePadding">
|
||||
<span bitDialogTitle>{{title}}</span>
|
||||
<ng-container bitDialogContent>Dialog body text goes here.</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
<button
|
||||
[disabled]="loading"
|
||||
class="tw-ml-auto"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
size="default"
|
||||
title="Delete"
|
||||
aria-label="Delete"></button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<DialogComponent & { title: string }>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
dialogSize: "default",
|
||||
title: "Default",
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
dialogSize: "small",
|
||||
title: "Small",
|
||||
};
|
||||
|
||||
export const LongTitle = Template.bind({});
|
||||
LongTitle.args = {
|
||||
dialogSize: "small",
|
||||
title: "Long_Title_That_Should_Be_Truncated",
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
dialogSize: "large",
|
||||
title: "Large",
|
||||
};
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
dialogSize: "large",
|
||||
loading: true,
|
||||
title: "Loading",
|
||||
};
|
||||
|
||||
const TemplateScrolling: Story<DialogComponent> = (args: DialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize" [loading]="loading" [disablePadding]="disablePadding">
|
||||
<span bitDialogTitle>Scrolling Example</span>
|
||||
<span bitDialogContent>
|
||||
Dialog body text goes here.<br>
|
||||
<ng-container *ngFor="let _ of [].constructor(100)">
|
||||
repeating lines of characters <br>
|
||||
export const Default: Story = {
|
||||
render: (args: DialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize" [loading]="loading" [disablePadding]="disablePadding">
|
||||
<span bitDialogTitle>{{title}}</span>
|
||||
<ng-container bitDialogContent>Dialog body text goes here.</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
<button
|
||||
[disabled]="loading"
|
||||
class="tw-ml-auto"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
size="default"
|
||||
title="Delete"
|
||||
aria-label="Delete"></button>
|
||||
</ng-container>
|
||||
end of sequence!
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ScrollingContent = TemplateScrolling.bind({});
|
||||
ScrollingContent.args = {
|
||||
dialogSize: "small",
|
||||
</bit-dialog>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
dialogSize: "default",
|
||||
title: "Default",
|
||||
},
|
||||
};
|
||||
|
||||
const TemplateTabbed: Story<DialogComponent> = (args: DialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
|
||||
<span bitDialogTitle>Tab Content Example</span>
|
||||
<span bitDialogContent>
|
||||
<bit-tab-group>
|
||||
<bit-tab label="First Tab">First Tab Content</bit-tab>
|
||||
<bit-tab label="Second Tab">Second Tab Content</bit-tab>
|
||||
<bit-tab label="Third Tab">Third Tab Content</bit-tab>
|
||||
</bit-tab-group>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
`,
|
||||
});
|
||||
|
||||
export const TabContent = TemplateTabbed.bind({});
|
||||
TabContent.args = {
|
||||
dialogSize: "large",
|
||||
disablePadding: true,
|
||||
export const Small: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
dialogSize: "small",
|
||||
title: "Small",
|
||||
},
|
||||
};
|
||||
TabContent.story = {
|
||||
|
||||
export const LongTitle: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
dialogSize: "small",
|
||||
title: "Long_Title_That_Should_Be_Truncated",
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
dialogSize: "large",
|
||||
title: "Large",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
dialogSize: "large",
|
||||
loading: true,
|
||||
title: "Loading",
|
||||
},
|
||||
};
|
||||
|
||||
export const ScrollingContent: Story = {
|
||||
render: (args: DialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize" [loading]="loading" [disablePadding]="disablePadding">
|
||||
<span bitDialogTitle>Scrolling Example</span>
|
||||
<span bitDialogContent>
|
||||
Dialog body text goes here.<br>
|
||||
<ng-container *ngFor="let _ of [].constructor(100)">
|
||||
repeating lines of characters <br>
|
||||
</ng-container>
|
||||
end of sequence!
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
dialogSize: "small",
|
||||
},
|
||||
};
|
||||
|
||||
export const TabContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
|
||||
<span bitDialogTitle>Tab Content Example</span>
|
||||
<span bitDialogContent>
|
||||
<bit-tab-group>
|
||||
<bit-tab label="First Tab">First Tab Content</bit-tab>
|
||||
<bit-tab label="Second Tab">Second Tab Content</bit-tab>
|
||||
<bit-tab label="Third Tab">Third Tab Content</bit-tab>
|
||||
</bit-tab-group>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="loading">Save</button>
|
||||
<button bitButton buttonType="secondary" [disabled]="loading">Cancel</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
dialogSize: "large",
|
||||
disablePadding: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
storyDescription: `An example of using the \`bitTabGroup\` component within the Dialog. The content padding should be
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { DialogModule } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { SimpleDialogType, SimpleDialogOptions } from "@bitwarden/angular/services/dialog";
|
||||
import {
|
||||
SimpleDialogType,
|
||||
SimpleDialogOptions,
|
||||
DialogServiceAbstraction,
|
||||
} from "@bitwarden/angular/services/dialog";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
import { ButtonModule } from "../../button";
|
||||
import { CalloutModule } from "../../callout";
|
||||
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";
|
||||
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
||||
import { SimpleDialogComponent } from "../simple-dialog/simple-dialog.component";
|
||||
import { DialogModule } from "../dialog.module";
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@@ -186,7 +184,7 @@ class StoryDialogComponent {
|
||||
calloutType = "info";
|
||||
dialogCloseResult: boolean;
|
||||
|
||||
constructor(public dialogService: DialogService, private i18nService: I18nService) {}
|
||||
constructor(public dialogService: DialogServiceAbstraction, private i18nService: I18nService) {}
|
||||
|
||||
async openSimpleConfigurableDialog(opts: SimpleDialogOptions) {
|
||||
this.dialogCloseResult = await this.dialogService.openSimpleDialog(opts);
|
||||
@@ -205,10 +203,10 @@ export default {
|
||||
component: StoryDialogComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [DialogCloseDirective, DialogTitleContainerDirective, SimpleDialogComponent],
|
||||
imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule, CalloutModule],
|
||||
imports: [ButtonModule, DialogModule, CalloutModule],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
DialogService,
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
@@ -239,8 +237,6 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<StoryDialogComponent> = (args: StoryDialogComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
type Story = StoryObj<StoryDialogComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -90,8 +90,6 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<StoryDialogComponent> = (args: StoryDialogComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
type Story = StoryObj<StoryDialogComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../../button";
|
||||
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
||||
@@ -22,61 +22,63 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<SimpleDialogComponent> = (args: SimpleDialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>Alert Dialog</span>
|
||||
<span bitDialogContent>Message Content</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<SimpleDialogComponent & { useDefaultIcon: boolean }>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
const TemplateWithIcon: Story<SimpleDialogComponent> = (args: SimpleDialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<i bitDialogIcon class="bwi bwi-star tw-text-3xl tw-text-success" aria-hidden="true"></i>
|
||||
<span bitDialogTitle>Premium Subscription Available</span>
|
||||
<span bitDialogContent> Message Content</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
});
|
||||
|
||||
export const CustomIcon = TemplateWithIcon.bind({});
|
||||
|
||||
const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>Alert Dialog</span>
|
||||
<span bitDialogContent>
|
||||
Message Content
|
||||
Message text goes here.<br>
|
||||
<ng-container *ngFor="let _ of [].constructor(100)">
|
||||
repeating lines of characters <br>
|
||||
</ng-container>
|
||||
end of sequence!
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ScrollingContent = TemplateScroll.bind({});
|
||||
ScrollingContent.args = {
|
||||
useDefaultIcon: true,
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>Alert Dialog</span>
|
||||
<span bitDialogContent>Message Content</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const CustomIcon: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<i bitDialogIcon class="bwi bwi-star tw-text-3xl tw-text-success" aria-hidden="true"></i>
|
||||
<span bitDialogTitle>Premium Subscription Available</span>
|
||||
<span bitDialogContent> Message Content</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ScrollingContent: Story = {
|
||||
render: (args: SimpleDialogComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>Alert Dialog</span>
|
||||
<span bitDialogContent>
|
||||
Message Content
|
||||
Message text goes here.<br>
|
||||
<ng-container *ngFor="let _ of [].constructor(100)">
|
||||
repeating lines of characters <br>
|
||||
</ng-container>
|
||||
end of sequence!
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
useDefaultIcon: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { StoryObj, Meta, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -8,6 +8,7 @@ import { InputModule } from "../input/input.module";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { forbiddenCharacters } from "./bit-validators/forbidden-characters.validator";
|
||||
import { trimValidator } from "./bit-validators/trim.validator";
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
import { FormFieldModule } from "./form-field.module";
|
||||
|
||||
@@ -24,6 +25,7 @@ export default {
|
||||
return new I18nMockService({
|
||||
inputForbiddenCharacters: (chars) =>
|
||||
`The following characters are not allowed: ${chars}`,
|
||||
inputTrimValidator: "Input must not contain only whitespace.",
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -46,11 +48,30 @@ const template = `
|
||||
</bit-form-field>
|
||||
</form>`;
|
||||
|
||||
export const ForbiddenCharacters: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: new FormBuilder().group({
|
||||
name: ["", forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"])],
|
||||
}),
|
||||
},
|
||||
template,
|
||||
});
|
||||
export const ForbiddenCharacters: StoryObj<BitFormFieldComponent> = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: new FormBuilder().group({
|
||||
name: ["", forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"])],
|
||||
}),
|
||||
},
|
||||
template,
|
||||
}),
|
||||
};
|
||||
|
||||
export const TrimValidator: StoryObj<BitFormFieldComponent> = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: new FormBuilder().group({
|
||||
name: [
|
||||
"",
|
||||
{
|
||||
updateOn: "submit",
|
||||
validators: [trimValidator],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
template,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { forbiddenCharacters } from "./forbidden-characters.validator";
|
||||
export { trimValidator } from "./trim.validator";
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { FormControl } from "@angular/forms";
|
||||
|
||||
import { trimValidator as validate } from "./trim.validator";
|
||||
|
||||
describe("trimValidator", () => {
|
||||
it("should not error when input is null", () => {
|
||||
const input = createControl(null);
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
});
|
||||
|
||||
it("should not error when input is an empty string", () => {
|
||||
const input = createControl("");
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
});
|
||||
|
||||
it("should not error when input has no whitespace", () => {
|
||||
const input = createControl("test value");
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
});
|
||||
|
||||
it("should remove beginning whitespace", () => {
|
||||
const input = createControl(" test value");
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
expect(input.value).toBe("test value");
|
||||
});
|
||||
|
||||
it("should remove trailing whitespace", () => {
|
||||
const input = createControl("test value ");
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
expect(input.value).toBe("test value");
|
||||
});
|
||||
|
||||
it("should remove beginning and trailing whitespace", () => {
|
||||
const input = createControl(" test value ");
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toBe(null);
|
||||
expect(input.value).toBe("test value");
|
||||
});
|
||||
|
||||
it("should error when input is just whitespace", () => {
|
||||
const input = createControl(" ");
|
||||
const errors = validate(input);
|
||||
|
||||
expect(errors).toEqual({ trim: { message: "input is only whitespace" } });
|
||||
});
|
||||
});
|
||||
|
||||
function createControl(input: string) {
|
||||
return new FormControl(input);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { AbstractControl, FormControl, ValidatorFn } from "@angular/forms";
|
||||
|
||||
/**
|
||||
* Automatically trims FormControl value. Errors if value only contains whitespace.
|
||||
*
|
||||
* Should be used with `updateOn: "submit"`
|
||||
*/
|
||||
export const trimValidator: ValidatorFn = (control: AbstractControl<string>) => {
|
||||
if (!(control instanceof FormControl)) {
|
||||
throw new Error("trimValidator only supports validating FormControls");
|
||||
}
|
||||
const value = control.value;
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return null;
|
||||
}
|
||||
if (!value.trim().length) {
|
||||
return {
|
||||
trim: {
|
||||
message: "input is only whitespace",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (value !== value.trim()) {
|
||||
control.setValue(value.trim());
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UntypedFormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -50,29 +50,28 @@ function submit() {
|
||||
formObj.markAllAsTouched();
|
||||
}
|
||||
|
||||
const Template: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit()">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
</bit-form-field>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
<bit-error-summary [formGroup]="formObj"></bit-error-summary>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.props = {};
|
||||
export const Default: StoryObj<BitFormFieldComponent> = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit()">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
</bit-form-field>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
<bit-error-summary [formGroup]="formObj"></bit-error-summary>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -38,6 +38,8 @@ export class BitErrorComponent {
|
||||
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
|
||||
case "multipleEmails":
|
||||
return this.i18nService.t("multipleInputEmails");
|
||||
case "trim":
|
||||
return this.i18nService.t("inputTrimValidator");
|
||||
default:
|
||||
// Attempt to show a custom error message.
|
||||
if (this.error[1]?.message) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -87,173 +87,168 @@ function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
||||
function submit() {
|
||||
defaultFormObj.markAllAsTouched();
|
||||
}
|
||||
type Story = StoryObj<BitFormFieldComponent>;
|
||||
|
||||
const Template: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: defaultFormObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: defaultFormObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
<bit-hint>Optional Hint</bit-hint>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
<bit-hint>Optional Hint</bit-hint>
|
||||
<input bitInput required placeholder="Placeholder" />
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.props = {};
|
||||
export const Hint: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
<bit-hint>Long hint text</bit-hint>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
const RequiredTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput required placeholder="Placeholder" />
|
||||
</bit-form-field>
|
||||
export const Disabled: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
export const InputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
<span bitPrefix>$</span>
|
||||
<span bitSuffix>USD</span>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Required = RequiredTemplate.bind({});
|
||||
Required.props = {};
|
||||
export const ButtonInputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<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>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
const HintTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
<bit-hint>Long hint text</bit-hint>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
export const DisabledButtonInputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<button bitPrefix bitIconButton="bwi-star" disabled></button>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
|
||||
<button bitSuffix bitButton disabled>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Hint = HintTemplate.bind({});
|
||||
Required.props = {};
|
||||
export const Select: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<select bitInput>
|
||||
<option>Select</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
const DisabledTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
export const AdvancedSelect: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<bit-select>
|
||||
<bit-option label="Select"></bit-option>
|
||||
<bit-option label="Other"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Disabled = DisabledTemplate.bind({});
|
||||
Disabled.args = {};
|
||||
|
||||
const GroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
<span bitPrefix>$</span>
|
||||
<span bitSuffix>USD</span>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const InputGroup = GroupTemplate.bind({});
|
||||
InputGroup.args = {};
|
||||
|
||||
const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<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>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ButtonInputGroup = ButtonGroupTemplate.bind({});
|
||||
ButtonInputGroup.args = {};
|
||||
|
||||
const DisabledButtonInputGroupTemplate: Story<BitFormFieldComponent> = (
|
||||
args: BitFormFieldComponent
|
||||
) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<button bitPrefix bitIconButton="bwi-star" disabled></button>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
|
||||
<button bitSuffix bitButton disabled>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const DisabledButtonInputGroup = DisabledButtonInputGroupTemplate.bind({});
|
||||
DisabledButtonInputGroup.args = {};
|
||||
|
||||
const SelectTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<select bitInput>
|
||||
<option>Select</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Select = SelectTemplate.bind({});
|
||||
Select.args = {};
|
||||
|
||||
const AdvancedSelectTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<bit-select>
|
||||
<bit-option label="Select"></bit-option>
|
||||
<bit-option label="Other"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const AdvancedSelect = AdvancedSelectTemplate.bind({});
|
||||
AdvancedSelectTemplate.args = {};
|
||||
|
||||
const TextareaTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Textarea</bit-label>
|
||||
<textarea bitInput rows="4"></textarea>
|
||||
</bit-form-field>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Textarea = TextareaTemplate.bind({});
|
||||
Textarea.args = {};
|
||||
export const Textarea: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-form-field>
|
||||
<bit-label>Textarea</bit-label>
|
||||
<textarea bitInput rows="4"></textarea>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@angular/forms";
|
||||
import { NgSelectModule } from "@ng-select/ng-select";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -75,209 +75,228 @@ function submit(formObj: FormGroup) {
|
||||
formObj.markAllAsTouched();
|
||||
}
|
||||
|
||||
const MultiSelectTemplate: Story<MultiSelectComponent> = (args: MultiSelectComponent) => ({
|
||||
props: {
|
||||
formObj: formObjFactory(),
|
||||
submit: submit,
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
type Story = StoryObj<MultiSelectComponent & { name: string; hint: string }>;
|
||||
|
||||
export const Loading: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: formObjFactory(),
|
||||
submit: submit,
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ name }}</bit-label>
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
formControlName="select"
|
||||
[baseItems]="baseItems"
|
||||
[removeSelectedItems]="removeSelectedItems"
|
||||
[loading]="loading"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||
</bit-multi-select>
|
||||
<bit-hint>{{ hint }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
baseItems: [] as any,
|
||||
name: "Loading",
|
||||
hint: "This is what a loading multi-select looks like",
|
||||
loading: true,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ name }}</bit-label>
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
formControlName="select"
|
||||
[baseItems]="baseItems"
|
||||
[removeSelectedItems]="removeSelectedItems"
|
||||
[loading]="loading"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||
</bit-multi-select>
|
||||
<bit-hint>{{ hint }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Loading = MultiSelectTemplate.bind({});
|
||||
Loading.args = {
|
||||
baseItems: [],
|
||||
name: "Loading",
|
||||
hint: "This is what a loading multi-select looks like",
|
||||
loading: "true",
|
||||
};
|
||||
|
||||
export const Disabled = MultiSelectTemplate.bind({});
|
||||
Disabled.args = {
|
||||
name: "Disabled",
|
||||
disabled: "true",
|
||||
hint: "This is what a disabled multi-select looks like",
|
||||
};
|
||||
|
||||
export const Groups = MultiSelectTemplate.bind({});
|
||||
Groups.args = {
|
||||
name: "Select groups",
|
||||
hint: "Groups will be assigned to the associated member",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
};
|
||||
|
||||
export const Members = MultiSelectTemplate.bind({});
|
||||
Members.args = {
|
||||
name: "Select members",
|
||||
hint: "Members will be assigned to the associated group/collection",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Joe Smith (jsmith@mail.me)", labelName: "Joe Smith", icon: "bwi-user" },
|
||||
{
|
||||
id: "2",
|
||||
listName: "Tania Stone (tstone@mail.me)",
|
||||
labelName: "Tania Stone",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
listName: "Matt Matters (mmatters@mail.me)",
|
||||
labelName: "Matt Matters",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
listName: "Bob Robertson (brobertson@mail.me)",
|
||||
labelName: "Bob Robertson",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
listName: "Ashley Fletcher (aflectcher@mail.me)",
|
||||
labelName: "Ashley Fletcher",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{ id: "6", listName: "Rita Olson (rolson@mail.me)", labelName: "Rita Olson", icon: "bwi-user" },
|
||||
{
|
||||
id: "7",
|
||||
listName: "Final listName (fname@mail.me)",
|
||||
labelName: "(fname@mail.me)",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const Collections = MultiSelectTemplate.bind({});
|
||||
Collections.args = {
|
||||
name: "Select collections",
|
||||
hint: "Collections will be assigned to the associated member",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Collection 1", labelName: "Collection 1", icon: "bwi-collection" },
|
||||
{ id: "2", listName: "Collection 2", labelName: "Collection 2", icon: "bwi-collection" },
|
||||
{ id: "3", listName: "Collection 3", labelName: "Collection 3", icon: "bwi-collection" },
|
||||
{
|
||||
id: "3.5",
|
||||
listName: "Child Collection 1 for Parent 1",
|
||||
labelName: "Child Collection 1 for Parent 1",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 1",
|
||||
},
|
||||
{
|
||||
id: "3.55",
|
||||
listName: "Child Collection 2 for Parent 1",
|
||||
labelName: "Child Collection 2 for Parent 1",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 1",
|
||||
},
|
||||
{
|
||||
id: "3.59",
|
||||
listName: "Child Collection 3 for Parent 1",
|
||||
labelName: "Child Collection 3 for Parent 1",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 1",
|
||||
},
|
||||
{
|
||||
id: "3.75",
|
||||
listName: "Child Collection 1 for Parent 2",
|
||||
labelName: "Child Collection 1 for Parent 2",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 2",
|
||||
},
|
||||
{ id: "4", listName: "Collection 4", labelName: "Collection 4", icon: "bwi-collection" },
|
||||
{ id: "5", listName: "Collection 5", labelName: "Collection 5", icon: "bwi-collection" },
|
||||
{ id: "6", listName: "Collection 6", labelName: "Collection 6", icon: "bwi-collection" },
|
||||
{ id: "7", listName: "Collection 7", labelName: "Collection 7", icon: "bwi-collection" },
|
||||
],
|
||||
};
|
||||
|
||||
export const MembersAndGroups = MultiSelectTemplate.bind({});
|
||||
MembersAndGroups.args = {
|
||||
name: "Select groups and members",
|
||||
hint: "Members/Groups will be assigned to the associated collection",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Joe Smith (jsmith@mail.me)", labelName: "Joe Smith", icon: "bwi-user" },
|
||||
{
|
||||
id: "7",
|
||||
listName: "Tania Stone (tstone@mail.me)",
|
||||
labelName: "(tstone@mail.me)",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const RemoveSelected = MultiSelectTemplate.bind({});
|
||||
RemoveSelected.args = {
|
||||
name: "Select groups",
|
||||
hint: "Groups will be removed from the list once the dropdown is closed",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
removeSelectedItems: "true",
|
||||
};
|
||||
|
||||
const StandaloneTemplate: Story<MultiSelectComponent> = (args: MultiSelectComponent) => ({
|
||||
props: {
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
export const Disabled: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
name: "Disabled",
|
||||
disabled: true,
|
||||
hint: "This is what a disabled multi-select looks like",
|
||||
},
|
||||
};
|
||||
|
||||
export const Groups: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
name: "Select groups",
|
||||
hint: "Groups will be assigned to the associated member",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Members: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
name: "Select members",
|
||||
hint: "Members will be assigned to the associated group/collection",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Joe Smith (jsmith@mail.me)", labelName: "Joe Smith", icon: "bwi-user" },
|
||||
{
|
||||
id: "2",
|
||||
listName: "Tania Stone (tstone@mail.me)",
|
||||
labelName: "Tania Stone",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
listName: "Matt Matters (mmatters@mail.me)",
|
||||
labelName: "Matt Matters",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
listName: "Bob Robertson (brobertson@mail.me)",
|
||||
labelName: "Bob Robertson",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
listName: "Ashley Fletcher (aflectcher@mail.me)",
|
||||
labelName: "Ashley Fletcher",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
listName: "Rita Olson (rolson@mail.me)",
|
||||
labelName: "Rita Olson",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
listName: "Final listName (fname@mail.me)",
|
||||
labelName: "(fname@mail.me)",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Collections: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
name: "Select collections",
|
||||
hint: "Collections will be assigned to the associated member",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Collection 1", labelName: "Collection 1", icon: "bwi-collection" },
|
||||
{ id: "2", listName: "Collection 2", labelName: "Collection 2", icon: "bwi-collection" },
|
||||
{ id: "3", listName: "Collection 3", labelName: "Collection 3", icon: "bwi-collection" },
|
||||
{
|
||||
id: "3.5",
|
||||
listName: "Child Collection 1 for Parent 1",
|
||||
labelName: "Child Collection 1 for Parent 1",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 1",
|
||||
},
|
||||
{
|
||||
id: "3.55",
|
||||
listName: "Child Collection 2 for Parent 1",
|
||||
labelName: "Child Collection 2 for Parent 1",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 1",
|
||||
},
|
||||
{
|
||||
id: "3.59",
|
||||
listName: "Child Collection 3 for Parent 1",
|
||||
labelName: "Child Collection 3 for Parent 1",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 1",
|
||||
},
|
||||
{
|
||||
id: "3.75",
|
||||
listName: "Child Collection 1 for Parent 2",
|
||||
labelName: "Child Collection 1 for Parent 2",
|
||||
icon: "bwi-collection",
|
||||
parentGrouping: "Parent 2",
|
||||
},
|
||||
{ id: "4", listName: "Collection 4", labelName: "Collection 4", icon: "bwi-collection" },
|
||||
{ id: "5", listName: "Collection 5", labelName: "Collection 5", icon: "bwi-collection" },
|
||||
{ id: "6", listName: "Collection 6", labelName: "Collection 6", icon: "bwi-collection" },
|
||||
{ id: "7", listName: "Collection 7", labelName: "Collection 7", icon: "bwi-collection" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const MembersAndGroups: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
name: "Select groups and members",
|
||||
hint: "Members/Groups will be assigned to the associated collection",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Joe Smith (jsmith@mail.me)", labelName: "Joe Smith", icon: "bwi-user" },
|
||||
{
|
||||
id: "7",
|
||||
listName: "Tania Stone (tstone@mail.me)",
|
||||
labelName: "(tstone@mail.me)",
|
||||
icon: "bwi-user",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const RemoveSelected: Story = {
|
||||
...Loading,
|
||||
args: {
|
||||
name: "Select groups",
|
||||
hint: "Groups will be removed from the list once the dropdown is closed",
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
removeSelectedItems: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Standalone: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
},
|
||||
template: `
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
[baseItems]="baseItems"
|
||||
[removeSelectedItems]="removeSelectedItems"
|
||||
[loading]="loading"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||
</bit-multi-select>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
removeSelectedItems: true,
|
||||
},
|
||||
template: `
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
[baseItems]="baseItems"
|
||||
[removeSelectedItems]="removeSelectedItems"
|
||||
[loading]="loading"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||
</bit-multi-select>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Standalone = StandaloneTemplate.bind({});
|
||||
Standalone.args = {
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
removeSelectedItems: "true",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -38,49 +38,42 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BitPasswordInputToggleDirective> = (
|
||||
args: BitPasswordInputToggleDirective
|
||||
) => ({
|
||||
props: {
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form>
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<BitPasswordInputToggleDirective>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.props = {};
|
||||
|
||||
const TemplateBinding: Story<BitPasswordInputToggleDirective> = (
|
||||
args: BitPasswordInputToggleDirective
|
||||
) => ({
|
||||
props: {
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form>
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
|
||||
</bit-form-field>
|
||||
|
||||
<label class="tw-text-main">
|
||||
Checked:
|
||||
<input type="checkbox" [(ngModel)]="toggled" [ngModelOptions]="{standalone: true}" />
|
||||
</label>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Binding = TemplateBinding.bind({});
|
||||
Binding.props = {
|
||||
toggled: false,
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<form>
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Binding: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<form>
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
<input bitInput type="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
|
||||
</bit-form-field>
|
||||
|
||||
<label class="tw-text-main">
|
||||
Checked:
|
||||
<input type="checkbox" [(ngModel)]="toggled" [ngModelOptions]="{standalone: true}" />
|
||||
</label>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
toggled: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -81,67 +81,70 @@ function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
|
||||
};
|
||||
}
|
||||
|
||||
const FullExampleTemplate: Story = (args) => ({
|
||||
props: {
|
||||
formObj: exampleFormObj,
|
||||
submit: () => exampleFormObj.markAllAsTouched(),
|
||||
...args,
|
||||
type Story = StoryObj;
|
||||
|
||||
export const FullExample: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: exampleFormObj,
|
||||
submit: () => exampleFormObj.markAllAsTouched(),
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit()">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Country</bit-label>
|
||||
<bit-select formControlName="country">
|
||||
<bit-option *ngFor="let country of countries" [value]="country.value" [label]="country.name"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Age</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
formControlName="age"
|
||||
min="0"
|
||||
max="150"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<bit-label>Agree to terms</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="terms">
|
||||
<bit-hint>Required for the service to work properly</bit-hint>
|
||||
</bit-form-control>
|
||||
|
||||
<bit-radio-group formControlName="updates">
|
||||
<bit-label>Subscribe to updates?</bit-label>
|
||||
<bit-radio-button value="yes">
|
||||
<bit-label>Yes</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button value="no">
|
||||
<bit-label>No</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button value="later">
|
||||
<bit-label>Decide later</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
|
||||
args: {
|
||||
countries,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit()">
|
||||
<bit-form-field>
|
||||
<bit-label>Name</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Country</bit-label>
|
||||
<bit-select formControlName="country">
|
||||
<bit-option *ngFor="let country of countries" [value]="country.value" [label]="country.name"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>Age</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
formControlName="age"
|
||||
min="0"
|
||||
max="150"
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<bit-label>Agree to terms</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="terms">
|
||||
<bit-hint>Required for the service to work properly</bit-hint>
|
||||
</bit-form-control>
|
||||
|
||||
<bit-radio-group formControlName="updates">
|
||||
<bit-label>Subscribe to updates?</bit-label>
|
||||
<bit-radio-button value="yes">
|
||||
<bit-label>Yes</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button value="no">
|
||||
<bit-label>No</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button value="later">
|
||||
<bit-label>Decide later</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const FullExample = FullExampleTemplate.bind({});
|
||||
FullExample.args = {
|
||||
countries,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { BitIconButtonComponent, IconButtonType } from "./icon-button.component";
|
||||
|
||||
@@ -31,68 +31,72 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BitIconButtonComponent> = (args: BitIconButtonComponent) => ({
|
||||
props: { ...args, buttonTypes },
|
||||
template: `
|
||||
<table class="tw-border-spacing-2 tw-text-center tw-text-main">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-capitalize tw-font-bold tw-p-4"
|
||||
[class.tw-text-contrast]="['contrast', 'light'].includes(buttonType)"
|
||||
[class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">{{buttonType}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
type Story = StoryObj<BitIconButtonComponent>;
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Default</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Disabled</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
disabled
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Loading</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
loading="true"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
size: "default",
|
||||
export const Default: Story = {
|
||||
render: (args: BitIconButtonComponent) => ({
|
||||
props: { ...args, buttonTypes },
|
||||
template: `
|
||||
<table class="tw-border-spacing-2 tw-text-center tw-text-main">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-capitalize tw-font-bold tw-p-4"
|
||||
[class.tw-text-contrast]="['contrast', 'light'].includes(buttonType)"
|
||||
[class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">{{buttonType}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Default</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Disabled</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
disabled
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Loading</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="['contrast', 'light'].includes(buttonType)">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
loading="true"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
size: "default",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: "small",
|
||||
export const Small: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
size: "small",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { BitIconComponent } from "./icon.component";
|
||||
|
||||
@@ -10,18 +10,22 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BitIconComponent> = (args: BitIconComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-bg-primary-500 tw-p-5">
|
||||
<bit-icon [icon]="icon" class="tw-text-primary-300"></bit-icon>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<BitIconComponent>;
|
||||
|
||||
export const ReportExposedPasswords = Template.bind({});
|
||||
|
||||
export const UnknownIcon = Template.bind({});
|
||||
UnknownIcon.args = {
|
||||
icon: "unknown",
|
||||
export const ReportExposedPasswords: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-bg-primary-500 tw-p-5">
|
||||
<bit-icon [icon]="icon" class="tw-text-primary-300"></bit-icon>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const UnknownIcon: Story = {
|
||||
...ReportExposedPasswords,
|
||||
args: {
|
||||
icon: "unknown" as any,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export * from "./dialog";
|
||||
export * from "./form-field";
|
||||
export * from "./icon-button";
|
||||
export * from "./icon";
|
||||
export * from "./input";
|
||||
export * from "./link";
|
||||
export * from "./menu";
|
||||
export * from "./multi-select";
|
||||
|
||||
1
libs/components/src/input/index.ts
Normal file
1
libs/components/src/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./input.module";
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive";
|
||||
import { LinkModule } from "./link.module";
|
||||
@@ -24,97 +24,99 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const ButtonTemplate: Story<ButtonLinkDirective> = (args: ButtonLinkDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }">
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">Button</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
Add Icon Button
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-right" aria-hidden="true"></i>
|
||||
Chevron Icon Button
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType" class="tw-text-sm">Small Button</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<ButtonLinkDirective>;
|
||||
|
||||
const AnchorTemplate: Story<AnchorLinkDirective> = (args: AnchorLinkDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }">
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" href="#">Anchor</a>
|
||||
export const Buttons: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }">
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">Button</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
Add Icon Button
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType">
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-right" aria-hidden="true"></i>
|
||||
Chevron Icon Button
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<button bitLink [linkType]="linkType" class="tw-text-sm">Small Button</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" href="#">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
Add Icon Anchor
|
||||
</a>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" href="#">
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-right" aria-hidden="true"></i>
|
||||
Chevron Icon Anchor
|
||||
</a>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" class="tw-text-sm" href="#">Small Anchor</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Buttons = ButtonTemplate.bind({});
|
||||
Buttons.args = {
|
||||
linkType: "primary",
|
||||
};
|
||||
|
||||
export const Anchors = AnchorTemplate.bind({});
|
||||
Anchors.args = {
|
||||
linkType: "primary",
|
||||
};
|
||||
|
||||
const InlineTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-text-main">
|
||||
On the internet paragraphs often contain <a bitLink href="#">inline links</a>, but few know that <button bitLink>buttons</button> can be used for similar purposes.
|
||||
</span>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Inline = InlineTemplate.bind({});
|
||||
Inline.args = {
|
||||
linkType: "primary",
|
||||
};
|
||||
|
||||
const DisabledTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitLink disabled linkType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitLink disabled linkType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<div class="tw-bg-primary-500 tw-p-2 tw-inline-block">
|
||||
<button bitLink disabled linkType="contrast" class="tw-mr-2">Contrast</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Disabled = DisabledTemplate.bind({});
|
||||
Disabled.parameters = {
|
||||
controls: {
|
||||
exclude: ["linkType"],
|
||||
hideNoControlsWarning: true,
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
linkType: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Anchors: StoryObj<AnchorLinkDirective> = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }">
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" href="#">Anchor</a>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" href="#">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
Add Icon Anchor
|
||||
</a>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" href="#">
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-right" aria-hidden="true"></i>
|
||||
Chevron Icon Anchor
|
||||
</a>
|
||||
</div>
|
||||
<div class="tw-block tw-p-2">
|
||||
<a bitLink [linkType]="linkType" class="tw-text-sm" href="#">Small Anchor</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
linkType: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Inline: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<span class="tw-text-main">
|
||||
On the internet paragraphs often contain <a bitLink href="#">inline links</a>, but few know that <button bitLink>buttons</button> can be used for similar purposes.
|
||||
</span>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
linkType: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<button bitLink disabled linkType="primary" class="tw-mr-2">Primary</button>
|
||||
<button bitLink disabled linkType="secondary" class="tw-mr-2">Secondary</button>
|
||||
<div class="tw-bg-primary-500 tw-p-2 tw-inline-block">
|
||||
<button bitLink disabled linkType="contrast" class="tw-mr-2">Contrast</button>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
parameters: {
|
||||
controls: {
|
||||
exclude: ["linkType"],
|
||||
hideNoControlsWarning: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OverlayModule } from "@angular/cdk/overlay";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../button/button.module";
|
||||
|
||||
@@ -30,40 +30,42 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-menu #myMenu="menuComponent">
|
||||
<a href="#" bitMenuItem>Anchor link</a>
|
||||
<a href="#" bitMenuItem>Another link</a>
|
||||
<button type="button" bitMenuItem>Button</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button type="button" bitMenuItem>Button after divider</button>
|
||||
</bit-menu>
|
||||
type Story = StoryObj<MenuTriggerForDirective>;
|
||||
|
||||
<div class="tw-h-40">
|
||||
<div class="cdk-overlay-pane bit-menu-panel">
|
||||
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
|
||||
export const OpenMenu: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-menu #myMenu="menuComponent">
|
||||
<a href="#" bitMenuItem>Anchor link</a>
|
||||
<a href="#" bitMenuItem>Another link</a>
|
||||
<button type="button" bitMenuItem>Button</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button type="button" bitMenuItem>Button after divider</button>
|
||||
</bit-menu>
|
||||
|
||||
<div class="tw-h-40">
|
||||
<div class="cdk-overlay-pane bit-menu-panel">
|
||||
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
const TemplateWithButton: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-h-40">
|
||||
<button bitButton buttonType="secondary" [bitMenuTriggerFor]="myMenu">Open menu</button>
|
||||
</div>
|
||||
|
||||
<bit-menu #myMenu>
|
||||
<a href="#" bitMenuItem>Anchor link</a>
|
||||
<a href="#" bitMenuItem>Another link</a>
|
||||
<button type="button" bitMenuItem>Button</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button type="button" bitMenuItem>Button after divider</button>
|
||||
</bit-menu>`,
|
||||
});
|
||||
|
||||
export const OpenMenu = Template.bind({});
|
||||
export const ClosedMenu = TemplateWithButton.bind({});
|
||||
`,
|
||||
}),
|
||||
};
|
||||
export const ClosedMenu: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-h-40">
|
||||
<button bitButton buttonType="secondary" [bitMenuTriggerFor]="myMenu">Open menu</button>
|
||||
</div>
|
||||
|
||||
<bit-menu #myMenu>
|
||||
<a href="#" bitMenuItem>Anchor link</a>
|
||||
<a href="#" bitMenuItem>Another link</a>
|
||||
<button type="button" bitMenuItem>Button</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button type="button" bitMenuItem>Button after divider</button>
|
||||
</bit-menu>`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { StoryObj, Meta, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -36,9 +36,10 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
export const Default: Story<NavGroupComponent> = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
export const Default: StoryObj<NavGroupComponent> = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-group text="Hello World (Anchor)" [route]="['']" icon="bwi-filter" [open]="true">
|
||||
<bit-nav-item text="Child A" route="#" icon="bwi-filter"></bit-nav-item>
|
||||
<bit-nav-item text="Child B" route="#"></bit-nav-item>
|
||||
@@ -50,25 +51,28 @@ export const Default: Story<NavGroupComponent> = (args) => ({
|
||||
<bit-nav-item text="Child C" icon="bwi-filter"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
`,
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
export const Tree: Story<NavGroupComponent> = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-group text="Tree example" icon="bwi-collection" [open]="true">
|
||||
<bit-nav-group text="Level 1 - with children (empty)" route="#" icon="bwi-collection" variant="tree"></bit-nav-group>
|
||||
<bit-nav-item text="Level 1 - no children" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
|
||||
<bit-nav-group text="Level 1 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
|
||||
<bit-nav-group text="Level 2 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
|
||||
<bit-nav-item text="Level 3 - no children, no icon" route="#" variant="tree"></bit-nav-item>
|
||||
<bit-nav-group text="Level 3 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
|
||||
<bit-nav-item text="Level 4 - no children, no icon" route="#" variant="tree"></bit-nav-item>
|
||||
export const Tree: StoryObj<NavGroupComponent> = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-group text="Tree example" icon="bwi-collection" [open]="true">
|
||||
<bit-nav-group text="Level 1 - with children (empty)" route="#" icon="bwi-collection" variant="tree"></bit-nav-group>
|
||||
<bit-nav-item text="Level 1 - no children" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
|
||||
<bit-nav-group text="Level 1 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
|
||||
<bit-nav-group text="Level 2 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
|
||||
<bit-nav-item text="Level 3 - no children, no icon" route="#" variant="tree"></bit-nav-item>
|
||||
<bit-nav-group text="Level 3 - with children" route="#" icon="bwi-collection" variant="tree" [open]="true">
|
||||
<bit-nav-item text="Level 4 - no children, no icon" route="#" variant="tree"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
</bit-nav-group>
|
||||
<bit-nav-group text="Level 2 - with children (empty)" route="#" icon="bwi-collection" variant="tree" [open]="true"></bit-nav-group>
|
||||
<bit-nav-item text="Level 2 - no children" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-group text="Level 2 - with children (empty)" route="#" icon="bwi-collection" variant="tree" [open]="true"></bit-nav-group>
|
||||
<bit-nav-item text="Level 2 - no children" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
|
||||
<bit-nav-item text="Level 1 - no children" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item text="Level 1 - no children" route="#" icon="bwi-collection" variant="tree"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
`,
|
||||
});
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { StoryObj, Meta, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
|
||||
@@ -23,35 +23,42 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<NavItemComponent> = (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="${args.text}" [route]="['']" icon="${args.icon}"></bit-nav-item>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<NavItemComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
text: "Hello World",
|
||||
icon: "bwi-filter",
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="${args.text}" [route]="['']" icon="${args.icon}"></bit-nav-item>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
text: "Hello World",
|
||||
icon: "bwi-filter",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutIcon = Template.bind({});
|
||||
WithoutIcon.args = {
|
||||
text: "Hello World",
|
||||
icon: "",
|
||||
export const WithoutIcon: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
text: "Hello World",
|
||||
icon: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutRoute: Story<NavItemComponent> = (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
`,
|
||||
});
|
||||
export const WithoutRoute: Story = {
|
||||
render: (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithChildButtons: Story<NavItemComponent> = (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
export const WithChildButtons: Story = {
|
||||
render: (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="Hello World" [route]="['']" icon="bwi-collection">
|
||||
<button
|
||||
slot-start
|
||||
@@ -79,15 +86,18 @@ export const WithChildButtons: Story<NavItemComponent> = (args: NavItemComponent
|
||||
></button>
|
||||
</bit-nav-item>
|
||||
`,
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
export const MultipleItemsWithDivider: Story<NavItemComponent> = (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
<bit-nav-divider></bit-nav-divider>
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
`,
|
||||
});
|
||||
export const MultipleItemsWithDivider: Story = {
|
||||
render: (args: NavItemComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
<bit-nav-divider></bit-nav-divider>
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
<bit-nav-item text="Hello World" icon="bwi-collection"></bit-nav-item>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
|
||||
import { NoItemsComponent } from "./no-items.component";
|
||||
import { NoItemsModule } from "./no-items.module";
|
||||
|
||||
export default {
|
||||
title: "Component Library/No Items",
|
||||
component: NoItemsComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ButtonModule, NoItemsModule],
|
||||
@@ -13,23 +15,25 @@ export default {
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-no-items class="tw-text-main">
|
||||
<ng-container slot="title">No items found</ng-container>
|
||||
<ng-container slot="description">Your description here.</ng-container>
|
||||
<button
|
||||
slot="button"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
New item
|
||||
</button>
|
||||
</bit-no-items>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<NoItemsComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-no-items class="tw-text-main">
|
||||
<ng-container slot="title">No items found</ng-container>
|
||||
<ng-container slot="description">Your description here.</ng-container>
|
||||
<button
|
||||
slot="button"
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
New item
|
||||
</button>
|
||||
</bit-no-items>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { ProgressComponent } from "./progress.component";
|
||||
|
||||
@@ -18,22 +18,23 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ProgressComponent> = (args: ProgressComponent) => ({
|
||||
props: args,
|
||||
});
|
||||
type Story = StoryObj<ProgressComponent>;
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
barWidth: 0,
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
barWidth: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const Full = Template.bind({});
|
||||
Full.args = {
|
||||
barWidth: 100,
|
||||
export const Full: Story = {
|
||||
args: {
|
||||
barWidth: 100,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomText = Template.bind({});
|
||||
CustomText.args = {
|
||||
barWidth: 25,
|
||||
text: "Loading...",
|
||||
export const CustomText: Story = {
|
||||
args: {
|
||||
barWidth: 25,
|
||||
text: "Loading...",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormsModule, ReactiveFormsModule, FormControl, FormGroup } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -34,65 +34,67 @@ export default {
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
} as Meta<RadioGroupComponent>;
|
||||
|
||||
const InlineTemplate: Story<RadioGroupComponent> = (args: RadioGroupComponent) => ({
|
||||
props: {
|
||||
formObj: new FormGroup({
|
||||
radio: new FormControl(0),
|
||||
}),
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||
<bit-label>Group of radio buttons</bit-label>
|
||||
type Story = StoryObj<RadioGroupComponent>;
|
||||
|
||||
<bit-radio-button id="radio-first" [value]="0">
|
||||
<bit-label>First</bit-label>
|
||||
</bit-radio-button>
|
||||
export const Inline: Story = {
|
||||
render: () => ({
|
||||
props: {
|
||||
formObj: new FormGroup({
|
||||
radio: new FormControl(0),
|
||||
}),
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||
<bit-label>Group of radio buttons</bit-label>
|
||||
|
||||
<bit-radio-button id="radio-first" [value]="0">
|
||||
<bit-label>First</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-second" [value]="1">
|
||||
<bit-label>Second</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-third" [value]="2">
|
||||
<bit-label>Third</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
<bit-radio-button id="radio-second" [value]="1">
|
||||
<bit-label>Second</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-third" [value]="2">
|
||||
<bit-label>Third</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Inline = InlineTemplate.bind({});
|
||||
|
||||
const BlockTemplate: Story<RadioGroupComponent> = (args: RadioGroupComponent) => ({
|
||||
props: {
|
||||
formObj: new FormGroup({
|
||||
radio: new FormControl(0),
|
||||
}),
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||
<bit-label>Group of radio buttons</bit-label>
|
||||
|
||||
<bit-radio-button id="radio-first" class="tw-block" [value]="0">
|
||||
<bit-label>First</bit-label>
|
||||
<bit-hint>This is a hint for the first option</bit-hint>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-second" class="tw-block" [value]="1">
|
||||
<bit-label>Second</bit-label>
|
||||
<bit-hint>This is a hint for the second option</bit-hint>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-third" class="tw-block" [value]="2">
|
||||
<bit-label>Third</bit-label>
|
||||
<bit-hint>This is a hint for the third option</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Block = BlockTemplate.bind({});
|
||||
export const Block: Story = {
|
||||
render: () => ({
|
||||
props: {
|
||||
formObj: new FormGroup({
|
||||
radio: new FormControl(0),
|
||||
}),
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||
<bit-label>Group of radio buttons</bit-label>
|
||||
|
||||
<bit-radio-button id="radio-first" class="tw-block" [value]="0">
|
||||
<bit-label>First</bit-label>
|
||||
<bit-hint>This is a hint for the first option</bit-hint>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-second" class="tw-block" [value]="1">
|
||||
<bit-label>Second</bit-label>
|
||||
<bit-hint>This is a hint for the second option</bit-hint>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-third" class="tw-block" [value]="2">
|
||||
<bit-label>Third</bit-label>
|
||||
<bit-hint>This is a hint for the third option</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
3
libs/components/src/search/close-button-white.svg
Normal file
3
libs/components/src/search/close-button-white.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512">
|
||||
<path fill="#fff" d="m381.1 343.16-87.47-87.76 87.67-86.62a9.24 9.24 0 0 0 0-13.06l-24.93-25.06a9.18 9.18 0 0 0-6.54-2.66c-2.46 0-4.8 1-6.53 2.66l-87.27 86.36-87.4-86.29a9.18 9.18 0 0 0-6.53-2.66c-2.47 0-4.8 1-6.53 2.66L130.7 155.8a9.24 9.24 0 0 0 0 13.06l87.67 86.62-87.4 87.69a9.22 9.22 0 0 0-2.74 6.53c0 2.46.94 4.8 2.74 6.53l24.93 25.05a9.19 9.19 0 0 0 13.07 0l87.06-87.42 87.14 87.36a9.19 9.19 0 0 0 13.06 0l24.94-25.06a9.23 9.23 0 0 0 2.73-6.53 9.55 9.55 0 0 0-2.8-6.46Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 573 B |
19
libs/components/src/search/search.component.css
Normal file
19
libs/components/src/search/search.component.css
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Tailwind doesn't have a good way to style search-cancel-button.
|
||||
*/
|
||||
bit-search input[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 21px;
|
||||
width: 21px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url("./close-button-white.svg");
|
||||
-webkit-mask-image: url("./close-button-white.svg");
|
||||
background-color: rgba(var(--color-text-muted));
|
||||
}
|
||||
|
||||
bit-search input[type="search"]::-webkit-search-cancel-button:hover {
|
||||
background-color: rgba(var(--color-text-main));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
@@ -29,12 +29,14 @@ export default {
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<SearchComponent> = (args: SearchComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-search [(ngModel)]="searchText" [placeholder]="placeholder" [disabled]="disabled"></bit-search>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj<SearchComponent>;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
export const Default: Story = {
|
||||
render: (args: SearchComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-search [(ngModel)]="searchText" [placeholder]="placeholder" [disabled]="disabled"></bit-search>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
@@ -37,22 +37,26 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const DefaultTemplate: Story<MultiSelectComponent> = (args: MultiSelectComponent) => ({
|
||||
props: {
|
||||
...args,
|
||||
},
|
||||
template: `<bit-select [disabled]="disabled">
|
||||
<bit-option value="value1" label="Value 1" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value2" label="Value 2" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value3" label="Value 3" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value4" label="Value 4" icon="bwi-collection" disabled></bit-option>
|
||||
</bit-select>`,
|
||||
});
|
||||
type Story = StoryObj<MultiSelectComponent>;
|
||||
|
||||
export const Default = DefaultTemplate.bind({});
|
||||
Default.args = {};
|
||||
|
||||
export const Disabled = DefaultTemplate.bind({});
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
...args,
|
||||
},
|
||||
template: `<bit-select [disabled]="disabled">
|
||||
<bit-option value="value1" label="Value 1" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value2" label="Value 2" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value3" label="Value 3" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value4" label="Value 4" icon="bwi-collection" disabled></bit-option>
|
||||
</bit-select>`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
...Default,
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -67,7 +67,8 @@ export const Table = (args) => (
|
||||
</table>
|
||||
);
|
||||
|
||||
<style>{`
|
||||
<style>
|
||||
{`
|
||||
table {
|
||||
border-spacing: 0.5rem;
|
||||
border-collapse: separate !important;
|
||||
@@ -85,7 +86,8 @@ td, th {
|
||||
th {
|
||||
border: none !important;
|
||||
}
|
||||
`}</style>
|
||||
`}
|
||||
</style>
|
||||
|
||||
# Colors
|
||||
|
||||
@@ -103,4 +105,6 @@ Below are all the permited colors. Please consult design before considering addi
|
||||
<div class="tw-flex tw-space-x-4">
|
||||
<Table />
|
||||
<Table class="theme_dark tw-bg-background" />
|
||||
<Table class="theme_nord tw-bg-background" />
|
||||
<Table class="theme_solarize tw-bg-background" />
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meta, Story, Source } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Forms" />
|
||||
<Meta title="Component Library/Form" />
|
||||
|
||||
# Forms
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- Iconography.stories.mdx -->
|
||||
{/* Iconography.stories.mdx */}
|
||||
|
||||
import { Meta } from "@storybook/addon-docs/";
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Icons" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/bitInput" />
|
||||
<Meta title="Component Library/Form/Input" />
|
||||
|
||||
# `bitInput`
|
||||
# Input
|
||||
|
||||
`bitInput` is an Angular directive to be used on `<input>`, `<select>`, and `<textarea>` tags in
|
||||
order to provide standardized TailwindCss styling, error handling, and more. It is meant to be used
|
||||
@@ -2,7 +2,8 @@ import { Meta } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Introduction" />
|
||||
|
||||
<style>{`
|
||||
<style>
|
||||
{`
|
||||
.subheading {
|
||||
--mediumdark: '#999999';
|
||||
font-weight: 900;
|
||||
@@ -77,7 +78,8 @@ import { Meta } from "@storybook/addon-docs";
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
`}</style>
|
||||
`}
|
||||
</style>
|
||||
|
||||
# Bitwarden Component Library
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Meta, Story, Source } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Typography" />
|
||||
|
||||
# Typography
|
||||
|
||||
<Story id="component-library-typography--h-1" />
|
||||
|
||||
```html
|
||||
<h1 bitTypography="h1">H1</h1>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--h-2" />
|
||||
|
||||
```html
|
||||
<h2 bitTypography="h2">H2</h2>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--h-3" />
|
||||
|
||||
```html
|
||||
<h3 bitTypography="h3">H3</h3>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--h-4" />
|
||||
|
||||
```html
|
||||
<h4 bitTypography="h4">H4</h4>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--h-5" />
|
||||
|
||||
```html
|
||||
<h5 bitTypography="h5">H5</h5>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--h-6" />
|
||||
|
||||
```html
|
||||
<h6 bitTypography="h6">H6</h6>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--body-1" />
|
||||
|
||||
```html
|
||||
<p bitTypography="body1">Body 1</p>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--body-2" />
|
||||
|
||||
```html
|
||||
<p bitTypography="body2">Body 2</h1>
|
||||
```
|
||||
|
||||
<Story id="component-library-typography--helper" />
|
||||
|
||||
```html
|
||||
<p bitTypography="helper">Helper Text</h1>
|
||||
```
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Meta, Story, Source } from "@storybook/addon-docs";
|
||||
import { Meta, Story, Source, Primary, Controls } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Table" />
|
||||
import * as stories from "./table.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
# Table
|
||||
|
||||
## Overview
|
||||
|
||||
The table component provides a comprehensive way to display, sort and filter data. It consists of
|
||||
two portions, a UI component called `bit-table` and the underlying data source `TableDataSource`.
|
||||
This documentation will initially focus on the UI portion before covering the data source.
|
||||
|
||||
<Primary />
|
||||
|
||||
<Controls />
|
||||
|
||||
## UI Component
|
||||
|
||||
The UI component consists of a couple of elements.
|
||||
@@ -29,10 +33,6 @@ The UI component consists of a couple of elements.
|
||||
Netflix” for an edit option for a Netflix item.
|
||||
- Use [Virtual Scrolling](#virtual-scrolling) for large data sets.
|
||||
|
||||
### Example
|
||||
|
||||
<Story id="component-library-table--default" />
|
||||
|
||||
### Usage
|
||||
|
||||
The below code is the minimum required to create a table. However we strongly advise you to use the
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { countries } from "../form/countries";
|
||||
|
||||
@@ -27,41 +27,43 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>Header 1</th>
|
||||
<th bitCell>Header 2</th>
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 3</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 4</td>
|
||||
<td bitCell>Cell 5</td>
|
||||
<td bitCell>Cell 6</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 7 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 8</td>
|
||||
<td bitCell>Cell 9</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
alignRowContent: "baseline",
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>Header 1</th>
|
||||
<th bitCell>Header 2</th>
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 3</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 4</td>
|
||||
<td bitCell>Cell 5</td>
|
||||
<td bitCell>Cell 6</td>
|
||||
</tr>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 7 <br> Multiline Cell</td>
|
||||
<td bitCell>Cell 8</td>
|
||||
<td bitCell>Cell 9</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
alignRowContent: "baseline",
|
||||
},
|
||||
};
|
||||
|
||||
const data = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
@@ -72,48 +74,13 @@ data.data = [...Array(5).keys()].map((i) => ({
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
const DataSourceTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name">Name</th>
|
||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const DataSource = DataSourceTemplate.bind({});
|
||||
|
||||
const data2 = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
|
||||
data2.data = [...Array(100).keys()].map((i) => ({
|
||||
id: i,
|
||||
name: `name-${i}`,
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
const ScrollableTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data2,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
export const DataSource: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
@@ -123,51 +90,86 @@ const ScrollableTemplate: Story = (args) => ({
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
`,
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
export const Scrollable = ScrollableTemplate.bind({});
|
||||
const data2 = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
|
||||
data2.data = [...Array(100).keys()].map((i) => ({
|
||||
id: i,
|
||||
name: `name-${i}`,
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
export const Scrollable: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data2,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name">Name</th>
|
||||
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
const data3 = new TableDataSource<{ value: string; name: string }>();
|
||||
|
||||
// Chromatic has a max page size, lowering the number of entries to ensure we don't hit it
|
||||
data3.data = countries.slice(0, 100);
|
||||
|
||||
const FilterableTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data3,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
<th bitCell bitSortable="value" width="120px">Value</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.value }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
export const Filterable: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data3,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
|
||||
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
<th bitCell bitSortable="value" width="120px">Value</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *cdkVirtualFor="let r of rows$">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.value }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Filterable = FilterableTemplate.bind({});
|
||||
}),
|
||||
};
|
||||
|
||||
const data4 = new TableDataSource<{ name: string }>();
|
||||
|
||||
@@ -175,24 +177,24 @@ data4.data = [...Array(5).keys()].map((i) => ({
|
||||
name: i % 2 == 0 ? `name-${i}`.toUpperCase() : `name-${i}`.toLowerCase(),
|
||||
}));
|
||||
|
||||
const VariableCaseTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data4,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
export const VariableCase: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
dataSource: data4,
|
||||
},
|
||||
template: `
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const VariableCase = VariableCaseTemplate.bind({});
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../button";
|
||||
import { FormFieldModule } from "../form-field";
|
||||
@@ -44,20 +44,21 @@ export default {
|
||||
ItemThreeDummyComponent,
|
||||
DisabledDummyComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
TabsModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{ path: "", redirectTo: "active", pathMatch: "full" },
|
||||
{ path: "active", component: ActiveDummyComponent },
|
||||
{ path: "item-2", component: ItemTwoDummyComponent },
|
||||
{ path: "item-3", component: ItemThreeDummyComponent },
|
||||
{ path: "disabled", component: DisabledDummyComponent },
|
||||
],
|
||||
{ useHash: true }
|
||||
imports: [CommonModule, TabsModule, ButtonModule, FormFieldModule, RouterModule],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
importProvidersFrom(
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{ path: "", redirectTo: "active", pathMatch: "full" },
|
||||
{ path: "active", component: ActiveDummyComponent },
|
||||
{ path: "item-2", component: ItemTwoDummyComponent },
|
||||
{ path: "item-3", component: ItemThreeDummyComponent },
|
||||
{ path: "disabled", component: DisabledDummyComponent },
|
||||
],
|
||||
{ useHash: true }
|
||||
)
|
||||
),
|
||||
],
|
||||
}),
|
||||
@@ -70,10 +71,13 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const ContentTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Main Content Tabs" class="tw-text-main">
|
||||
type Story = StoryObj<TabGroupComponent>;
|
||||
|
||||
export const ContentTabs: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Main Content Tabs" class="tw-text-main">
|
||||
<bit-tab label="First Tab">First Tab Content</bit-tab>
|
||||
<bit-tab label="Second Tab">Second Tab Content</bit-tab>
|
||||
<bit-tab>
|
||||
@@ -85,56 +89,54 @@ const ContentTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
|
||||
<bit-tab [disabled]="true" label="Disabled">
|
||||
Disabled Content
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
`,
|
||||
});
|
||||
</bit-tab-group>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ContentTabs = ContentTabGroupTemplate.bind({});
|
||||
export const NavigationTabs: Story = {
|
||||
render: (args: TabGroupComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-nav-bar label="Main">
|
||||
<bit-tab-link [route]="['active']">Active</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-2']">Item 2</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-3']">Item 3</bit-tab-link>
|
||||
<bit-tab-link [route]="['disable']" [disabled]="true">Disabled</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
<div class="tw-bg-transparent tw-text-semibold tw-text-center tw-text-main tw-py-10">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
const NavTabGroupTemplate: Story<TabGroupComponent> = (args: TabGroupComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-nav-bar label="Main">
|
||||
<bit-tab-link [route]="['active']">Active</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-2']">Item 2</bit-tab-link>
|
||||
<bit-tab-link [route]="['item-3']">Item 3</bit-tab-link>
|
||||
<bit-tab-link [route]="['disable']" [disabled]="true">Disabled</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
<div class="tw-bg-transparent tw-text-semibold tw-text-center tw-text-main tw-py-10">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const NavigationTabs = NavTabGroupTemplate.bind({});
|
||||
|
||||
const PreserveContentTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Preserve Content Tabs" [preserveContent]="true" class="tw-text-main">
|
||||
export const PreserveContentTabs: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Preserve Content Tabs" [preserveContent]="true" class="tw-text-main">
|
||||
<bit-tab label="Text Tab">
|
||||
<p>
|
||||
Play the video in the other tab and switch back to hear the video is still playing.
|
||||
</p>
|
||||
<p>Play the video in the other tab and switch back to hear the video is still playing.</p>
|
||||
</bit-tab>
|
||||
<bit-tab label="Video Tab">
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/H0-yWbe5XG4"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/H0-yWbe5XG4"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen></iframe>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
`,
|
||||
});
|
||||
</bit-tab-group>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const PreserveContentTabs = PreserveContentTabGroupTemplate.bind({});
|
||||
|
||||
const KeyboardNavTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Keyboard Navigation Tabs" class="tw-text-main">
|
||||
export const KeyboardNavigation: Story = {
|
||||
render: (args: any) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-tab-group label="Keyboard Navigation Tabs" class="tw-text-main">
|
||||
<bit-tab label="Form Tab">
|
||||
<p>
|
||||
You can navigate through all tab labels, form inputs, and the button that is outside the tab group via
|
||||
@@ -153,9 +155,8 @@ const KeyboardNavTabGroupTemplate: Story<TabGroupComponent> = (args: any) => ({
|
||||
<bit-tab label="No Focusable Content Tab" [contentTabIndex]="0">
|
||||
<p>This tab has no focusable content, but the panel should still be focusable</p>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
<button bitButton buttonType="primary" class="tw-mt-5">External Button</button>
|
||||
`,
|
||||
});
|
||||
|
||||
export const KeyboardNavigation = KeyboardNavTabGroupTemplate.bind({});
|
||||
</bit-tab-group>
|
||||
<button bitButton buttonType="primary" class="tw-mt-5">External Button</button>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { BadgeModule } from "../badge";
|
||||
|
||||
@@ -25,30 +25,32 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ToggleGroupComponent> = (args: ToggleGroupComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-toggle-group [(selected)]="selected" aria-label="People list filter">
|
||||
<bit-toggle value="all">
|
||||
All <span bitBadge badgeType="info">3</span>
|
||||
</bit-toggle>
|
||||
type Story = StoryObj<ToggleGroupComponent>;
|
||||
|
||||
<bit-toggle value="invited">
|
||||
Invited
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle value="accepted">
|
||||
Accepted <span bitBadge badgeType="info">2</span>
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle value="deactivated">
|
||||
Deactivated
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
selected: "all",
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<bit-toggle-group [(selected)]="selected" aria-label="People list filter">
|
||||
<bit-toggle value="all">
|
||||
All <span bitBadge badgeType="info">3</span>
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle value="invited">
|
||||
Invited
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle value="accepted">
|
||||
Accepted <span bitBadge badgeType="info">2</span>
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle value="deactivated">
|
||||
Deactivated
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
selected: "all",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -79,3 +79,83 @@
|
||||
|
||||
--tw-ring-offset-color: #1f242e;
|
||||
}
|
||||
|
||||
.theme_nord {
|
||||
--color-transparent-hover: rgb(255 255 255 / 0.12);
|
||||
|
||||
--color-background: 67 76 94;
|
||||
--color-background-alt: 59 66 82;
|
||||
--color-background-alt2: 76 86 106;
|
||||
--color-background-alt3: 76 86 106;
|
||||
--color-background-alt4: 67 76 94;
|
||||
|
||||
--color-primary-300: 108 153 166;
|
||||
--color-primary-500: 136 192 208;
|
||||
--color-primary-700: 160 224 242;
|
||||
|
||||
--color-secondary-100: 76 86 106;
|
||||
--color-secondary-300: 94 105 125;
|
||||
--color-secondary-500: 216 222 233;
|
||||
--color-secondary-700: 255 255 255;
|
||||
|
||||
--color-success-500: 163 190 140;
|
||||
--color-success-700: 144 170 122;
|
||||
|
||||
--color-danger-500: 228 129 139;
|
||||
--color-danger-700: 191 97 106;
|
||||
|
||||
--color-warning-500: 235 203 139;
|
||||
--color-warning-700: 210 181 121;
|
||||
|
||||
--color-info-500: 129 161 193;
|
||||
--color-info-700: 94 129 172;
|
||||
|
||||
--color-text-main: 229 233 240;
|
||||
--color-text-muted: 216 222 233;
|
||||
--color-text-contrast: 46 52 64;
|
||||
--color-text-alt2: 255 255 255;
|
||||
--color-text-code: 219 177 211;
|
||||
|
||||
--tw-ring-offset-color: #434c5e;
|
||||
}
|
||||
|
||||
.theme_solarized {
|
||||
--color-transparent-hover: rgb(255 255 255 / 0.12);
|
||||
|
||||
--color-background: 0 43 54;
|
||||
--color-background-alt: 7 54 66;
|
||||
--color-background-alt2: 31 72 87;
|
||||
--color-background-alt3: 31 72 87;
|
||||
--color-background-alt4: 0 43 54;
|
||||
|
||||
--color-primary-300: 42 161 152;
|
||||
--color-primary-500: 133 153 0;
|
||||
--color-primary-700: 192 203 123;
|
||||
|
||||
--color-secondary-100: 31 72 87;
|
||||
--color-secondary-300: 101 123 131;
|
||||
--color-secondary-500: 131 148 150;
|
||||
--color-secondary-700: 238 232 213;
|
||||
|
||||
--color-success-500: 133 153 0;
|
||||
--color-success-700: 192 203 123;
|
||||
|
||||
--color-danger-500: 220 50 47;
|
||||
--color-danger-700: 223 135 134;
|
||||
|
||||
--color-warning-500: 181 137 0;
|
||||
--color-warning-700: 220 189 92;
|
||||
|
||||
--color-info-500: 133 153 0;
|
||||
--color-info-700: 192 203 123;
|
||||
|
||||
--color-text-main: 253 246 227;
|
||||
--color-text-muted: 147 161 161;
|
||||
--color-text-contrast: 0 43 54;
|
||||
--color-text-alt2: 255 255 255;
|
||||
--color-text-code: 240 141 199;
|
||||
|
||||
--tw-ring-offset-color: #002b36;
|
||||
}
|
||||
|
||||
@import "./search/search.component.css";
|
||||
|
||||
67
libs/components/src/typography/typography.mdx
Normal file
67
libs/components/src/typography/typography.mdx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Meta, Story, Source, Primary, Controls } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./typography.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
# Typography
|
||||
|
||||
<Primary />
|
||||
|
||||
<Controls />
|
||||
|
||||
## Stories
|
||||
|
||||
<Story of={stories.H1} />
|
||||
|
||||
```html
|
||||
<h1 bitTypography="h1">H1</h1>
|
||||
```
|
||||
|
||||
<Story of={stories.H2} />
|
||||
|
||||
```html
|
||||
<h2 bitTypography="h2">H2</h2>
|
||||
```
|
||||
|
||||
<Story of={stories.H3} />
|
||||
|
||||
```html
|
||||
<h3 bitTypography="h3">H3</h3>
|
||||
```
|
||||
|
||||
<Story of={stories.H4} />
|
||||
|
||||
```html
|
||||
<h4 bitTypography="h4">H4</h4>
|
||||
```
|
||||
|
||||
<Story of={stories.H5} />
|
||||
|
||||
```html
|
||||
<h5 bitTypography="h5">H5</h5>
|
||||
```
|
||||
|
||||
<Story of={stories.H6} />
|
||||
|
||||
```html
|
||||
<h6 bitTypography="h6">H6</h6>
|
||||
```
|
||||
|
||||
<Story of={stories.Body1} />
|
||||
|
||||
```html
|
||||
<p bitTypography="body1">Body 1</p>
|
||||
```
|
||||
|
||||
<Story of={stories.Body2} />
|
||||
|
||||
```html
|
||||
<p bitTypography="body2">Body 2</h1>
|
||||
```
|
||||
|
||||
<Story of={stories.Helper} />
|
||||
|
||||
```html
|
||||
<p bitTypography="helper">Helper Text</h1>
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
import { Meta, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { TypographyDirective } from "./typography.directive";
|
||||
|
||||
@@ -10,61 +10,81 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => ({
|
||||
props: args,
|
||||
template: `<span [bitTypography]="bitTypography" class="tw-text-main">{{text}}</span>`,
|
||||
});
|
||||
type Story = StoryObj<TypographyDirective & { text: string }>;
|
||||
|
||||
export const H1 = Template.bind({});
|
||||
H1.args = {
|
||||
bitTypography: "h1",
|
||||
text: "h1. Page Title",
|
||||
export const H1: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<span [bitTypography]="bitTypography" class="tw-text-main">{{text}}</span>`,
|
||||
}),
|
||||
args: {
|
||||
bitTypography: "h1",
|
||||
text: "h1. Page Title",
|
||||
},
|
||||
};
|
||||
|
||||
export const H2 = Template.bind({});
|
||||
H2.args = {
|
||||
bitTypography: "h2",
|
||||
text: "h2. Page Section",
|
||||
export const H2: Story = {
|
||||
...H1,
|
||||
args: {
|
||||
bitTypography: "h2",
|
||||
text: "h2. Page Section",
|
||||
},
|
||||
};
|
||||
|
||||
export const H3 = Template.bind({});
|
||||
H3.args = {
|
||||
bitTypography: "h3",
|
||||
text: "h3. Page Section",
|
||||
export const H3: Story = {
|
||||
...H1,
|
||||
args: {
|
||||
bitTypography: "h3",
|
||||
text: "h3. Page Section",
|
||||
},
|
||||
};
|
||||
|
||||
export const H4 = Template.bind({});
|
||||
H4.args = {
|
||||
bitTypography: "h4",
|
||||
text: "h4. Page Section",
|
||||
export const H4: Story = {
|
||||
...H1,
|
||||
args: {
|
||||
bitTypography: "h4",
|
||||
text: "h4. Page Section",
|
||||
},
|
||||
};
|
||||
|
||||
export const H5 = Template.bind({});
|
||||
H5.args = {
|
||||
bitTypography: "h5",
|
||||
text: "h5. Page Section",
|
||||
export const H5: Story = {
|
||||
...H1,
|
||||
|
||||
args: {
|
||||
bitTypography: "h5",
|
||||
text: "h5. Page Section",
|
||||
},
|
||||
};
|
||||
|
||||
export const H6 = Template.bind({});
|
||||
H6.args = {
|
||||
bitTypography: "h6",
|
||||
text: "h6. Page Section",
|
||||
export const H6: Story = {
|
||||
...H1,
|
||||
|
||||
args: {
|
||||
bitTypography: "h6",
|
||||
text: "h6. Page Section",
|
||||
},
|
||||
};
|
||||
|
||||
export const Body1 = Template.bind({});
|
||||
Body1.args = {
|
||||
bitTypography: "body1",
|
||||
text: "Body 1",
|
||||
export const Body1: Story = {
|
||||
...H1,
|
||||
args: {
|
||||
bitTypography: "body1",
|
||||
text: "Body 1",
|
||||
},
|
||||
};
|
||||
|
||||
export const Body2 = Template.bind({});
|
||||
Body2.args = {
|
||||
bitTypography: "body2",
|
||||
text: "Body 2",
|
||||
export const Body2: Story = {
|
||||
...H1,
|
||||
args: {
|
||||
bitTypography: "body2",
|
||||
text: "Body 2",
|
||||
},
|
||||
};
|
||||
|
||||
export const Helper = Template.bind({});
|
||||
Helper.args = {
|
||||
bitTypography: "helper",
|
||||
text: "Helper Text",
|
||||
export const Helper: Story = {
|
||||
...H1,
|
||||
args: {
|
||||
bitTypography: "helper",
|
||||
text: "Helper Text",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/* eslint-disable */
|
||||
const config = require("./tailwind.config.base");
|
||||
|
||||
config.content = ["./libs/components/src/**/*.{html,ts,mdx}", "./.storybook/preview.js"];
|
||||
config.content = [
|
||||
"libs/components/src/**/*.{html,ts,mdx}",
|
||||
"apps/web/src/**/*.{html,ts,mdx}",
|
||||
"bitwarden_license/bit-web/src/**/*.{html,ts,mdx}",
|
||||
".storybook/preview.tsx",
|
||||
];
|
||||
config.safelist = [
|
||||
{
|
||||
pattern: /tw-bg-(.*)/,
|
||||
|
||||
@@ -2,8 +2,9 @@ const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../shared/tsconfig.libs");
|
||||
|
||||
const sharedConfig = require("../shared/jest.config.base");
|
||||
const sharedConfig = require("../shared/jest.config.ts");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
preset: "ts-jest",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
},
|
||||
|
||||
@@ -2,8 +2,9 @@ const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("../shared/tsconfig.libs");
|
||||
|
||||
const sharedConfig = require("../shared/jest.config.base");
|
||||
const sharedConfig = require("../shared/jest.config.ts");
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
...sharedConfig,
|
||||
preset: "ts-jest",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
},
|
||||
|
||||
107
libs/importer/spec/base-importer.spec.ts
Normal file
107
libs/importer/spec/base-importer.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { BaseImporter } from "../src/importers/base-importer";
|
||||
|
||||
class FakeBaseImporter extends BaseImporter {
|
||||
initLoginCipher(): CipherView {
|
||||
return super.initLoginCipher();
|
||||
}
|
||||
|
||||
setCardExpiration(cipher: CipherView, expiration: string): boolean {
|
||||
return super.setCardExpiration(cipher, expiration);
|
||||
}
|
||||
}
|
||||
|
||||
describe("BaseImporter class", () => {
|
||||
const importer = new FakeBaseImporter();
|
||||
let cipher: CipherView;
|
||||
|
||||
describe("setCardExpiration method", () => {
|
||||
beforeEach(() => {
|
||||
cipher = importer.initLoginCipher();
|
||||
cipher.card = new CardView();
|
||||
cipher.type = CipherType.Card;
|
||||
});
|
||||
|
||||
it.each([
|
||||
["01/2025", "1", "2025"],
|
||||
["5/21", "5", "2021"],
|
||||
["10/2100", "10", "2100"],
|
||||
])(
|
||||
"sets ciper card expYear & expMonth and returns true",
|
||||
(expiration, expectedMonth, expectedYear) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(cipher.card.expMonth).toBe(expectedMonth);
|
||||
expect(cipher.card.expYear).toBe(expectedYear);
|
||||
expect(result).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
["01/2032", "1"],
|
||||
["09/2032", "9"],
|
||||
["10/2032", "10"],
|
||||
])("removes leading zero from month", (expiration, expectedMonth) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(cipher.card.expMonth).toBe(expectedMonth);
|
||||
expect(cipher.card.expYear).toBe("2032");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["12/00", "2000"],
|
||||
["12/99", "2099"],
|
||||
["12/32", "2032"],
|
||||
["12/2042", "2042"],
|
||||
])("prefixes '20' to year if only two digits long", (expiration, expectedYear) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(cipher.card.expYear).toHaveLength(4);
|
||||
expect(cipher.card.expYear).toBe(expectedYear);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([["01 / 2025"], ["01 / 2025"], [" 01/2025 "], [" 01/2025 "]])(
|
||||
"removes any whitespace in expiration string",
|
||||
(expiration) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(cipher.card.expMonth).toBe("1");
|
||||
expect(cipher.card.expYear).toBe("2025");
|
||||
expect(result).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([[""], [" "], [null]])(
|
||||
"returns false if expiration is null or empty ",
|
||||
(expiration) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([["0123"], ["01/03/23"]])(
|
||||
"returns false if invalid card expiration string",
|
||||
(expiration) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([["5/"], ["03/231"], ["12/1"], ["2/20221"]])(
|
||||
"returns false if year is not 2 or 4 digits long",
|
||||
(expiration) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([["/2023"], ["003/2023"], ["111/32"]])(
|
||||
"returns false if month is not 1 or 2 digits long",
|
||||
(expiration) => {
|
||||
const result = importer.setCardExpiration(cipher, expiration);
|
||||
expect(result).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -100,7 +100,7 @@ describe("Enpass JSON Importer", () => {
|
||||
expect(cipher.card.brand).toEqual("Amex");
|
||||
expect(cipher.card.code).toEqual("1234");
|
||||
expect(cipher.card.expMonth).toEqual("3");
|
||||
expect(cipher.card.expYear).toEqual("23");
|
||||
expect(cipher.card.expYear).toEqual("2023");
|
||||
|
||||
// remaining fields as custom fields
|
||||
expect(cipher.fields.length).toEqual(9);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user