1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build

This commit is contained in:
John Harrington
2025-12-02 07:18:26 -07:00
committed by GitHub
229 changed files with 5088 additions and 5185 deletions

View File

@@ -1,4 +1,4 @@
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(
@@ -12,8 +12,8 @@ export class ModalInjector implements Injector {
options: InjectOptions & { optional?: false },
): T;
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | null): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: null): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any, flags?: any): any {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);

View File

@@ -1026,6 +1026,7 @@ const safeProviders: SafeProvider[] = [
WebPushConnectionService,
AuthRequestAnsweringServiceAbstraction,
ConfigService,
InternalPolicyService,
],
}),
safeProvider({
@@ -1064,7 +1065,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: InternalPolicyService,
useClass: DefaultPolicyService,
deps: [StateProvider, OrganizationServiceAbstraction],
deps: [StateProvider, OrganizationServiceAbstraction, AccountServiceAbstraction],
}),
safeProvider({
provide: PolicyServiceAbstraction,

View File

@@ -1,4 +1,4 @@
import { Meta, Story } from "@storybook/addon-docs";
import { Meta, Story } from "@storybook/addon-docs/blocks";
import * as stories from "./input-password.stories.ts";

View File

@@ -1,8 +1,8 @@
import { importProvidersFrom } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { action } from "@storybook/addon-actions";
import { Meta, StoryObj, applicationConfig } from "@storybook/angular";
import { of } from "rxjs";
import { action } from "storybook/actions";
import { ZXCVBNResult } from "zxcvbn";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Controls } from "@storybook/addon-docs";
import { Meta, Story, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./registration-start.stories";

View File

@@ -101,4 +101,9 @@ export abstract class InternalPolicyService extends PolicyService {
* Replace a policy in the local sync data. This does not update any policies on the server.
*/
abstract replace: (policies: { [id: string]: PolicyData }, userId: UserId) => Promise<void>;
/**
* Wrapper around upsert that uses account service to sync policies for the logged in user. This comes from
* the server push notification to update local policies.
*/
abstract syncPolicy: (payload: PolicyData) => Promise<void>;
}

View File

@@ -64,6 +64,7 @@ describe("ORGANIZATIONS state", () => {
isAdminInitiated: false,
ssoEnabled: false,
ssoMemberDecryptionType: undefined,
usePhishingBlocker: false,
},
};
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));

View File

@@ -67,6 +67,7 @@ export class OrganizationData {
isAdminInitiated: boolean;
ssoEnabled: boolean;
ssoMemberDecryptionType?: MemberDecryptionType;
usePhishingBlocker: boolean;
constructor(
response?: ProfileOrganizationResponse,
@@ -135,6 +136,7 @@ export class OrganizationData {
this.isAdminInitiated = response.isAdminInitiated;
this.ssoEnabled = response.ssoEnabled;
this.ssoMemberDecryptionType = response.ssoMemberDecryptionType;
this.usePhishingBlocker = response.usePhishingBlocker;
this.isMember = options.isMember;
this.isProviderUser = options.isProviderUser;

View File

@@ -98,6 +98,7 @@ export class Organization {
isAdminInitiated: boolean;
ssoEnabled: boolean;
ssoMemberDecryptionType?: MemberDecryptionType;
usePhishingBlocker: boolean;
constructor(obj?: OrganizationData) {
if (obj == null) {
@@ -162,6 +163,7 @@ export class Organization {
this.isAdminInitiated = obj.isAdminInitiated;
this.ssoEnabled = obj.ssoEnabled;
this.ssoMemberDecryptionType = obj.ssoMemberDecryptionType;
this.usePhishingBlocker = obj.usePhishingBlocker;
}
get canAccess() {

View File

@@ -39,6 +39,7 @@ export class OrganizationResponse extends BaseResponse {
limitItemDeletion: boolean;
allowAdminAccessToAllCollectionItems: boolean;
useAccessIntelligence: boolean;
usePhishingBlocker: boolean;
constructor(response: any) {
super(response);
@@ -82,5 +83,6 @@ export class OrganizationResponse extends BaseResponse {
);
// Map from backend API property (UseRiskInsights) to domain model property (useAccessIntelligence)
this.useAccessIntelligence = this.getResponseProperty("UseRiskInsights");
this.usePhishingBlocker = this.getResponseProperty("UsePhishingBlocker") ?? false;
}
}

View File

@@ -62,6 +62,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
isAdminInitiated: boolean;
ssoEnabled: boolean;
ssoMemberDecryptionType?: MemberDecryptionType;
usePhishingBlocker: boolean;
constructor(response: any) {
super(response);
@@ -135,5 +136,6 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.isAdminInitiated = this.getResponseProperty("IsAdminInitiated");
this.ssoEnabled = this.getResponseProperty("SsoEnabled") ?? false;
this.ssoMemberDecryptionType = this.getResponseProperty("SsoMemberDecryptionType");
this.usePhishingBlocker = this.getResponseProperty("UsePhishingBlocker") ?? false;
}
}

View File

@@ -1,6 +1,8 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { newGuid } from "@bitwarden/guid";
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
import { FakeSingleUserState } from "../../../../spec/fake-state";
import {
@@ -22,15 +24,15 @@ import { DefaultPolicyService, getFirstPolicy } from "./default-policy.service";
import { POLICIES } from "./policy-state";
describe("PolicyService", () => {
const userId = "userId" as UserId;
const userId = newGuid() as UserId;
let stateProvider: FakeStateProvider;
let organizationService: MockProxy<OrganizationService>;
let singleUserState: FakeSingleUserState<Record<PolicyId, PolicyData>>;
const accountService = mockAccountServiceWith(userId);
let policyService: DefaultPolicyService;
beforeEach(() => {
const accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
organizationService = mock<OrganizationService>();
singleUserState = stateProvider.singleUser.getFake(userId, POLICIES);
@@ -59,7 +61,7 @@ describe("PolicyService", () => {
organizationService.organizations$.calledWith(userId).mockReturnValue(organizations$);
policyService = new DefaultPolicyService(stateProvider, organizationService);
policyService = new DefaultPolicyService(stateProvider, organizationService, accountService);
});
it("upsert", async () => {
@@ -635,7 +637,7 @@ describe("PolicyService", () => {
beforeEach(() => {
stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
organizationService = mock<OrganizationService>();
policyService = new DefaultPolicyService(stateProvider, organizationService);
policyService = new DefaultPolicyService(stateProvider, organizationService, accountService);
});
it("returns undefined when there are no policies", () => {

View File

@@ -1,4 +1,7 @@
import { combineLatest, map, Observable, of } from "rxjs";
import { combineLatest, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { StateProvider } from "../../../platform/state";
import { UserId } from "../../../types/guid";
@@ -25,6 +28,7 @@ export class DefaultPolicyService implements PolicyService {
constructor(
private stateProvider: StateProvider,
private organizationService: OrganizationService,
private accountService: AccountService,
) {}
private policyState(userId: UserId) {
@@ -326,4 +330,13 @@ export class DefaultPolicyService implements PolicyService {
target.enforceOnLogin = Boolean(target.enforceOnLogin || source.enforceOnLogin);
}
}
async syncPolicy(policyData: PolicyData) {
await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.upsert(policyData, userId)),
),
);
}
}

View File

@@ -40,7 +40,6 @@ export enum FeatureFlag {
EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation",
ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings",
PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption",
WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2",
LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2",
UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data",
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
@@ -73,6 +72,9 @@ export enum FeatureFlag {
/* Innovation */
PM19148_InnovationArchive = "pm-19148-innovation-archive",
/* Desktop */
DesktopUiMigrationMilestone1 = "desktop-ui-migration-milestone-1",
/* UIF */
RouterFocusManagement = "router-focus-management",
}
@@ -140,7 +142,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EnrollAeadOnKeyRotation]: FALSE,
[FeatureFlag.ForceUpdateKDFSettings]: FALSE,
[FeatureFlag.PM25174_DisableType0Decryption]: FALSE,
[FeatureFlag.WindowsBiometricsV2]: FALSE,
[FeatureFlag.LinuxBiometricsV2]: FALSE,
[FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE,
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
@@ -154,6 +155,9 @@ export const DefaultFeatureFlagValue = {
/* Innovation */
[FeatureFlag.PM19148_InnovationArchive]: FALSE,
/* Desktop */
[FeatureFlag.DesktopUiMigrationMilestone1]: FALSE,
/* UIF */
[FeatureFlag.RouterFocusManagement]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

View File

@@ -33,4 +33,6 @@ export enum NotificationType {
OrganizationBankAccountVerified = 23,
ProviderBankAccountVerified = 24,
SyncPolicy = 25,
}

View File

@@ -1,12 +1,20 @@
import { mock } from "jest-mock-extended";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { Utils } from "../../../platform/misc/utils";
import { EcbDecryptParameters } from "../../../platform/models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { WebCryptoFunctionService } from "./web-crypto-function.service";
class TestSdkLoadService extends SdkLoadService {
protected override load(): Promise<void> {
// Simulate successful WASM load
return Promise.resolve();
}
}
const RsaPublicKey =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" +
"4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" +
@@ -40,6 +48,10 @@ const Sha512Mac =
"5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca";
describe("WebCrypto Function Service", () => {
beforeAll(async () => {
await new TestSdkLoadService().loadAndInit();
});
describe("pbkdf2", () => {
const regular256Key = "pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I=";
const utf8256Key = "yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I=";

View File

@@ -1,5 +1,8 @@
import * as forge from "node-forge";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { EncryptionType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import {
@@ -289,28 +292,9 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return new Uint8Array(buffer);
}
async rsaExtractPublicKey(privateKey: Uint8Array): Promise<Uint8Array> {
const rsaParams = {
name: "RSA-OAEP",
// Have to specify some algorithm
hash: { name: this.toWebCryptoAlgorithm("sha1") },
};
const impPrivateKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, true, [
"decrypt",
]);
const jwkPrivateKey = await this.subtle.exportKey("jwk", impPrivateKey);
const jwkPublicKeyParams = {
kty: "RSA",
e: jwkPrivateKey.e,
n: jwkPrivateKey.n,
alg: "RSA-OAEP",
ext: true,
};
const impPublicKey = await this.subtle.importKey("jwk", jwkPublicKeyParams, rsaParams, true, [
"encrypt",
]);
const buffer = await this.subtle.exportKey("spki", impPublicKey);
return new Uint8Array(buffer) as UnsignedPublicKey;
async rsaExtractPublicKey(privateKey: Uint8Array): Promise<UnsignedPublicKey> {
await SdkLoadService.Ready;
return PureCrypto.rsa_extract_public_key(privateKey) as UnsignedPublicKey;
}
async aesGenerateKey(bitLength = 128 | 192 | 256 | 512): Promise<CsprngArray> {

View File

@@ -1,3 +1,5 @@
import { UriMatchType } from "@bitwarden/sdk-internal";
/*
See full documentation at:
https://bitwarden.com/help/uri-match-detection/#match-detection-options
@@ -23,3 +25,28 @@ export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof Uri
// using uniqueness properties of object shape over Set for ease of state storability
export type NeverDomains = { [id: string]: null | { bannerIsDismissed?: boolean } };
export type EquivalentDomains = string[][];
/**
* Normalizes UriMatchStrategySetting for SDK mapping.
* @param value - The URI match strategy from user data
* @returns Valid UriMatchType or undefined if invalid
*/
export function normalizeUriMatchStrategyForSdk(
value: UriMatchStrategySetting | undefined,
): UriMatchType | undefined {
if (value == null) {
return undefined;
}
switch (value) {
case 0: // Domain
case 1: // Host
case 2: // StartsWith
case 3: // Exact
case 4: // RegularExpression
case 5: // Never
return value;
default:
return undefined;
}
}

View File

@@ -1,3 +1,4 @@
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { NotificationViewResponse as EndUserNotificationResponse } from "@bitwarden/common/vault/notifications/models";
import { NotificationType, PushNotificationLogOutReasonType } from "../../enums";
@@ -71,6 +72,9 @@ export class NotificationResponse extends BaseResponse {
case NotificationType.ProviderBankAccountVerified:
this.payload = new ProviderBankAccountVerifiedPushNotification(payload);
break;
case NotificationType.SyncPolicy:
this.payload = new SyncPolicyNotification(payload);
break;
default:
break;
}
@@ -187,6 +191,15 @@ export class ProviderBankAccountVerifiedPushNotification extends BaseResponse {
}
}
export class SyncPolicyNotification extends BaseResponse {
policy: Policy;
constructor(response: any) {
super(response);
this.policy = this.getResponseProperty("Policy");
}
}
export class LogOutNotification extends BaseResponse {
userId: string;
reason?: PushNotificationLogOutReasonType;

View File

@@ -3,6 +3,7 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf
// eslint-disable-next-line no-restricted-imports
import { LogoutReason } from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -34,6 +35,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
let webPushNotificationConnectionService: MockProxy<WebPushConnectionService>;
let authRequestAnsweringService: MockProxy<AuthRequestAnsweringServiceAbstraction>;
let configService: MockProxy<ConfigService>;
let policyService: MockProxy<InternalPolicyService>;
let activeUserAccount$: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
let userAccounts$: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
@@ -136,6 +138,8 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
});
policyService = mock<InternalPolicyService>();
defaultServerNotificationsService = new DefaultServerNotificationsService(
mock<LogService>(),
syncService,
@@ -149,6 +153,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
webPushNotificationConnectionService,
authRequestAnsweringService,
configService,
policyService,
);
});

View File

@@ -4,6 +4,8 @@ import { BehaviorSubject, bufferCount, firstValueFrom, ObservedValueOf, of, Subj
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { LogoutReason } from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { awaitAsync } from "../../../../spec";
@@ -42,6 +44,7 @@ describe("NotificationsService", () => {
let webPushNotificationConnectionService: MockProxy<WebPushConnectionService>;
let authRequestAnsweringService: MockProxy<AuthRequestAnsweringServiceAbstraction>;
let configService: MockProxy<ConfigService>;
let policyService: MockProxy<InternalPolicyService>;
let activeAccount: BehaviorSubject<ObservedValueOf<AccountService["activeAccount$"]>>;
let accounts: BehaviorSubject<ObservedValueOf<AccountService["accounts$"]>>;
@@ -71,6 +74,7 @@ describe("NotificationsService", () => {
webPushNotificationConnectionService = mock<WorkerWebPushConnectionService>();
authRequestAnsweringService = mock<AuthRequestAnsweringServiceAbstraction>();
configService = mock<ConfigService>();
policyService = mock<InternalPolicyService>();
// For these tests, use the active-user implementation (feature flag disabled)
configService.getFeatureFlag$.mockImplementation(() => of(true));
@@ -123,6 +127,7 @@ describe("NotificationsService", () => {
webPushNotificationConnectionService,
authRequestAnsweringService,
configService,
policyService,
);
});
@@ -391,5 +396,67 @@ describe("NotificationsService", () => {
expect(logoutCallback).not.toHaveBeenCalled();
});
});
describe("NotificationType.SyncPolicy", () => {
it("should call policyService.syncPolicy with the policy from the notification", async () => {
const mockPolicy = {
id: "policy-id",
organizationId: "org-id",
type: PolicyType.TwoFactorAuthentication,
enabled: true,
data: { test: "data" },
};
policyService.syncPolicy.mockResolvedValue();
const notification = new NotificationResponse({
type: NotificationType.SyncPolicy,
payload: { policy: mockPolicy },
contextId: "different-app-id",
});
await sut["processNotification"](notification, mockUser1);
expect(policyService.syncPolicy).toHaveBeenCalledTimes(1);
expect(policyService.syncPolicy).toHaveBeenCalledWith(
expect.objectContaining({
id: mockPolicy.id,
organizationId: mockPolicy.organizationId,
type: mockPolicy.type,
enabled: mockPolicy.enabled,
data: mockPolicy.data,
}),
);
});
it("should handle SyncPolicy notification with minimal policy data", async () => {
const mockPolicy = {
id: "policy-id-2",
organizationId: "org-id-2",
type: PolicyType.RequireSso,
enabled: false,
};
policyService.syncPolicy.mockResolvedValue();
const notification = new NotificationResponse({
type: NotificationType.SyncPolicy,
payload: { policy: mockPolicy },
contextId: "different-app-id",
});
await sut["processNotification"](notification, mockUser1);
expect(policyService.syncPolicy).toHaveBeenCalledTimes(1);
expect(policyService.syncPolicy).toHaveBeenCalledWith(
expect.objectContaining({
id: mockPolicy.id,
organizationId: mockPolicy.organizationId,
type: mockPolicy.type,
enabled: mockPolicy.enabled,
}),
);
});
});
});
});

View File

@@ -15,6 +15,8 @@ import {
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { LogoutReason } from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { trackedMerge } from "@bitwarden/common/platform/misc";
@@ -67,6 +69,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
private readonly webPushConnectionService: WebPushConnectionService,
private readonly authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction,
private readonly configService: ConfigService,
private readonly policyService: InternalPolicyService,
) {
this.notifications$ = this.configService
.getFeatureFlag$(FeatureFlag.InactiveUserServerNotification)
@@ -330,6 +333,9 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
adminId: notification.payload.adminId,
});
break;
case NotificationType.SyncPolicy:
await this.policyService.syncPolicy(PolicyData.fromPolicy(notification.payload.policy));
break;
default:
break;
}

View File

@@ -1,3 +1,5 @@
import { CipherRepromptType as SdkCipherRepromptType } from "@bitwarden/sdk-internal";
import { UnionOfValues } from "../types/union-of-values";
export const CipherRepromptType = {
@@ -6,3 +8,20 @@ export const CipherRepromptType = {
} as const;
export type CipherRepromptType = UnionOfValues<typeof CipherRepromptType>;
/**
* Normalizes a CipherRepromptType value to ensure compatibility with the SDK.
* @param value - The cipher reprompt type from user data
* @returns Valid CipherRepromptType, defaults to CipherRepromptType.None if unrecognized
*/
export function normalizeCipherRepromptTypeForSdk(
value: CipherRepromptType,
): SdkCipherRepromptType {
switch (value) {
case CipherRepromptType.None:
case CipherRepromptType.Password:
return value;
default:
return CipherRepromptType.None;
}
}

View File

@@ -1,3 +1,5 @@
import { FieldType as SdkFieldType } from "@bitwarden/sdk-internal";
const _FieldType = Object.freeze({
Text: 0,
Hidden: 1,
@@ -10,3 +12,20 @@ type _FieldType = typeof _FieldType;
export type FieldType = _FieldType[keyof _FieldType];
export const FieldType: Record<keyof _FieldType, FieldType> = _FieldType;
/**
* Normalizes a FieldType value to ensure compatibility with the SDK.
* @param value - The field type from user data
* @returns Valid FieldType, defaults to FieldType.Text if unrecognized
*/
export function normalizeFieldTypeForSdk(value: FieldType): SdkFieldType {
switch (value) {
case FieldType.Text:
case FieldType.Hidden:
case FieldType.Boolean:
case FieldType.Linked:
return value;
default:
return FieldType.Text;
}
}

View File

@@ -1,3 +1,5 @@
import { LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal";
import { UnionOfValues } from "../types/union-of-values";
export type LinkedIdType = LoginLinkedId | CardLinkedId | IdentityLinkedId;
@@ -46,3 +48,25 @@ export const IdentityLinkedId = {
} as const;
export type IdentityLinkedId = UnionOfValues<typeof IdentityLinkedId>;
/**
* Normalizes a LinkedIdType value to ensure compatibility with the SDK.
* @param value - The linked ID type from user data
* @returns Valid LinkedIdType or undefined if unrecognized
*/
export function normalizeLinkedIdTypeForSdk(
value: LinkedIdType | undefined,
): SdkLinkedIdType | undefined {
if (value == null) {
return undefined;
}
// Check all valid LinkedId numeric values (100-418)
const allValidValues = [
...Object.values(LoginLinkedId),
...Object.values(CardLinkedId),
...Object.values(IdentityLinkedId),
];
return allValidValues.includes(value) ? value : undefined;
}

View File

@@ -1,3 +1,5 @@
import { SecureNoteType as SdkSecureNoteType } from "@bitwarden/sdk-internal";
import { UnionOfValues } from "../types/union-of-values";
export const SecureNoteType = {
@@ -5,3 +7,12 @@ export const SecureNoteType = {
} as const;
export type SecureNoteType = UnionOfValues<typeof SecureNoteType>;
/**
* Normalizes a SecureNoteType value to ensure compatibility with the SDK.
* @param value - The secure note type from user data
* @returns Valid SecureNoteType, defaults to SecureNoteType.Generic if unrecognized
*/
export function normalizeSecureNoteTypeForSdk(value: SecureNoteType): SdkSecureNoteType {
return SecureNoteType.Generic;
}

View File

@@ -9,7 +9,10 @@ import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import {
CipherRepromptType,
normalizeCipherRepromptTypeForSdk,
} from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { conditionalEncString, encStringFrom } from "../../utils/domain-utils";
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
@@ -414,10 +417,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
creationDate: this.creationDate.toISOString(),
deletedDate: this.deletedDate?.toISOString(),
archivedDate: this.archivedDate?.toISOString(),
reprompt:
this.reprompt === CipherRepromptType.Password
? CipherRepromptType.Password
: CipherRepromptType.None,
reprompt: normalizeCipherRepromptTypeForSdk(this.reprompt),
// Initialize all cipher-type-specific properties as undefined
login: undefined,
identity: undefined,

View File

@@ -1,11 +1,16 @@
import { Jsonify } from "type-fest";
import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal";
import { Field as SdkField } from "@bitwarden/sdk-internal";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { FieldType, LinkedIdType } from "../../enums";
import {
FieldType,
LinkedIdType,
normalizeFieldTypeForSdk,
normalizeLinkedIdTypeForSdk,
} from "../../enums";
import { conditionalEncString, encStringFrom } from "../../utils/domain-utils";
import { FieldData } from "../data/field.data";
import { FieldView } from "../view/field.view";
@@ -77,9 +82,8 @@ export class Field extends Domain {
return {
name: this.name?.toSdk(),
value: this.value?.toSdk(),
type: this.type,
// Safe type cast: client and SDK LinkedIdType enums have identical values
linkedId: this.linkedId as unknown as SdkLinkedIdType,
type: normalizeFieldTypeForSdk(this.type),
linkedId: normalizeLinkedIdTypeForSdk(this.linkedId),
};
}

View File

@@ -3,7 +3,10 @@ import { Jsonify } from "type-fest";
import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import {
normalizeUriMatchStrategyForSdk,
UriMatchStrategySetting,
} from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -91,7 +94,7 @@ export class LoginUri extends Domain {
return {
uri: this.uri?.toSdk(),
uriChecksum: this.uriChecksum?.toSdk(),
match: this.match,
match: normalizeUriMatchStrategyForSdk(this.match),
};
}

View File

@@ -3,7 +3,7 @@ import { Jsonify } from "type-fest";
import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { SecureNoteType } from "../../enums";
import { normalizeSecureNoteTypeForSdk, SecureNoteType } from "../../enums";
import { SecureNoteData } from "../data/secure-note.data";
import { SecureNoteView } from "../view/secure-note.view";
@@ -46,7 +46,7 @@ export class SecureNote extends Domain {
*/
toSdkSecureNote(): SdkSecureNote {
return {
type: this.type,
type: normalizeSecureNoteTypeForSdk(this.type),
};
}

View File

@@ -1,23 +1,21 @@
import { Directive, effect, ElementRef, input } from "@angular/core";
import { Directive } from "@angular/core";
import { setA11yTitleAndAriaLabel } from "./set-a11y-title-and-aria-label";
import { TooltipDirective } from "../tooltip/tooltip.directive";
/**
* @deprecated This function is deprecated in favor of `bitTooltip`.
* Please use `bitTooltip` instead.
*
* Directive that provides accessible tooltips by internally using TooltipDirective.
* This maintains the appA11yTitle API while leveraging the enhanced tooltip functionality.
*/
@Directive({
selector: "[appA11yTitle]",
hostDirectives: [
{
directive: TooltipDirective,
inputs: ["bitTooltip: appA11yTitle", "tooltipPosition"],
},
],
})
export class A11yTitleDirective {
readonly title = input.required<string>({ alias: "appA11yTitle" });
constructor(private el: ElementRef) {
const originalTitle = this.el.nativeElement.getAttribute("title");
const originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
effect(() => {
setA11yTitleAndAriaLabel({
element: this.el.nativeElement,
title: originalTitle ?? this.title(),
label: originalAriaLabel ?? this.title(),
});
});
}
}
export class A11yTitleDirective {}

View File

@@ -1,5 +1,4 @@
import { DOCUMENT } from "@angular/common";
import { Injectable, Inject, NgZone, OnDestroy } from "@angular/core";
import { Injectable, Inject, NgZone, OnDestroy, DOCUMENT } from "@angular/core";
@Injectable({ providedIn: "root" })
export class AriaDisabledClickCaptureService implements OnDestroy {

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Controls } from "@storybook/addon-docs";
import { Meta, Story, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./anon-layout-wrapper.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Controls } from "@storybook/addon-docs";
import { Meta, Story, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./anon-layout.stories";

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Component Library/Async Actions/In Forms/Documentation" />

View File

@@ -1,8 +1,8 @@
import { Component } from "@angular/core";
import { FormsModule, ReactiveFormsModule, Validators, FormBuilder } from "@angular/forms";
import { action } from "@storybook/addon-actions";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { delay, of } from "rxjs";
import { action } from "storybook/actions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Component Library/Async Actions/Overview" />

View File

@@ -1,4 +1,4 @@
import { Meta, Story } from "@storybook/addon-docs";
import { Meta, Story } from "@storybook/addon-docs/blocks";
import * as stories from "./standalone.stories.ts";
<Meta of={stories} />

View File

@@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { action } from "@storybook/addon-actions";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { delay, of } from "rxjs";
import { action } from "storybook/actions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";

View File

@@ -1,4 +1,4 @@
import { Description, Meta, Canvas, Primary, Controls, Title } from "@storybook/addon-docs";
import { Meta, Description, Canvas, Primary, Controls, Title } from "@storybook/addon-docs/blocks";
import * as stories from "./avatar.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./badge.stories";

View File

@@ -1,4 +1,4 @@
import { Canvas, Controls, Description, Meta, Primary, Title } from "@storybook/addon-docs";
import { Canvas, Controls, Description, Meta, Primary, Title } from "@storybook/addon-docs/blocks";
import * as stories from "./banner.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./breadcrumbs.stories";

View File

@@ -6,7 +6,7 @@ import {
Controls,
Title,
Description,
} from "@storybook/addon-docs";
} from "@storybook/addon-docs/blocks";
import * as stories from "./button.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./callout.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs";
import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./base-card.stories";

View File

@@ -1,4 +1,12 @@
import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import {
Meta,
Canvas,
Source,
Primary,
Controls,
Title,
Description,
} from "@storybook/addon-docs/blocks";
import * as stories from "./checkbox.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs";
import { Meta, Primary, Controls, Canvas, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./chip-select.stories";

View File

@@ -1,6 +1,6 @@
import { FormsModule } from "@angular/forms";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { getAllByRole, userEvent } from "@storybook/test";
import { getAllByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./color-password.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Primary, Title, Description } from "@storybook/addon-docs";
import { Meta, Primary, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./container.stories";

View File

@@ -1,9 +1,9 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Component, inject } from "@angular/core";
import { NoopAnimationsModule, provideAnimations } from "@angular/platform-browser/animations";
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { getAllByRole, userEvent } from "@storybook/test";
import { getAllByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -36,7 +36,7 @@ interface Animal {
imports: [ButtonModule, LayoutComponent],
})
class StoryDialogComponent {
constructor(public dialogService: DialogService) {}
dialogService = inject(DialogService);
openDialog() {
this.dialogService.open(StoryDialogContentComponent, {
@@ -85,10 +85,8 @@ class StoryDialogComponent {
imports: [DialogModule, ButtonModule],
})
class StoryDialogContentComponent {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: Animal,
) {}
dialogRef = inject(DialogRef);
private data = inject<Animal>(DIALOG_DATA);
get animal() {
return this.data?.animal;
@@ -118,10 +116,8 @@ class StoryDialogContentComponent {
imports: [DialogModule, ButtonModule],
})
class NonDismissableContentComponent {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: Animal,
) {}
dialogRef = inject(DialogRef);
private data = inject<Animal>(DIALOG_DATA);
get animal() {
return this.data?.animal;

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./dialog.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Source } from "@storybook/addon-docs";
import { Meta, Canvas, Source } from "@storybook/addon-docs/blocks";
import * as stories from "./dialog.service.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./simple-dialog.stories";

View File

@@ -1,8 +1,8 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Component, inject } from "@angular/core";
import { provideAnimations } from "@angular/platform-browser/animations";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { getAllByRole, userEvent } from "@storybook/test";
import { getAllByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -30,7 +30,7 @@ interface Animal {
imports: [ButtonModule],
})
class StoryDialogComponent {
constructor(public dialogService: DialogService) {}
dialogService = inject(DialogService);
openSimpleDialog() {
this.dialogService.open(SimpleDialogContentComponent, {
@@ -84,10 +84,8 @@ class StoryDialogComponent {
imports: [ButtonModule, DialogModule],
})
class SimpleDialogContentComponent {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: Animal,
) {}
dialogRef = inject(DialogRef);
private data = inject<Animal>(DIALOG_DATA);
get animal() {
return this.data?.animal;
@@ -115,10 +113,8 @@ class SimpleDialogContentComponent {
imports: [ButtonModule, DialogModule],
})
class NonDismissableWithPrimaryButtonContentComponent {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: Animal,
) {}
dialogRef = inject(DialogRef);
private data = inject<Animal>(DIALOG_DATA);
get animal() {
return this.data?.animal;
@@ -141,10 +137,8 @@ class NonDismissableWithPrimaryButtonContentComponent {
imports: [ButtonModule, DialogModule],
})
class NonDismissableWithNoButtonsContentComponent {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: Animal,
) {}
dialogRef = inject(DialogRef);
private data = inject<Animal>(DIALOG_DATA);
get animal() {
return this.data?.animal;

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./disclosure.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./drawer.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Source, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Source, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./form-field.stories";

View File

@@ -6,8 +6,8 @@ import {
FormGroup,
} from "@angular/forms";
import { NgSelectModule } from "@ng-select/ng-select";
import { action } from "@storybook/addon-actions";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { action } from "storybook/actions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,6 +1,6 @@
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { userEvent, getByText } from "@storybook/test";
import { userEvent, getByText } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Source } from "@storybook/addon-docs";
import { Meta, Canvas, Source } from "@storybook/addon-docs/blocks";
import * as formStories from "./form.stories";
import * as fieldStories from "../form-field/form-field.stories";

View File

@@ -120,7 +120,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
* label input will be used to set the `aria-label` attributes on the button.
* This is for accessibility purposes, as it provides a text alternative for the icon button.
*
* NOTE: It will also be used to set the `title` attribute on the button if no `title` is provided.
* NOTE: It will also be used to set the content of the tooltip on the button if no `title` is provided.
*/
readonly label = input<string>();

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./icon-button.stories";
@@ -81,10 +81,5 @@ with less padding around the icon, such as in the navigation component.
Follow guidelines outlined in the [Button docs](?path=/docs/component-library-button--doc)
Always use the `appA11yTitle` directive set to a string that describes the action of the
icon-button. This will auto assign the same string to the `title` and `aria-label` attributes.
`aria-label` allows assistive technology to announce the action the button takes to the users.
`title` attribute provides a user with the browser tool tip if they do not understand what the icon
is indicating.
label input will be used to set the `aria-label` attributes on the button. This is for accessibility
purposes, as it provides a text alternative for the icon button.

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Controls } from "@storybook/addon-docs";
import { Meta, Story, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./icon.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./autofocus.stories";

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Component Library/Form/Input Directive" />

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./item.stories";

View File

@@ -1,6 +1,6 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { userEvent } from "@storybook/test";
import { userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Story, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./link.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./menu.stories";

View File

@@ -108,11 +108,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
/** Needs to be arrow function to retain `this` scope. */
keyDown = (event: KeyboardEvent) => {
const select = this.select();
if (!select.isOpen && event.key === "Enter" && !hasModifierKey(event)) {
if (!select.isOpen() && event.key === "Enter" && !hasModifierKey(event)) {
return false;
}
if (select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
if (select.isOpen() && event.key === "Escape" && !hasModifierKey(event)) {
this.selectedItems = [];
select.close();
event.stopPropagation();
@@ -198,7 +198,9 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
}
set ariaDescribedBy(value: string | undefined) {
this._ariaDescribedBy = value;
this.select()?.searchInput.nativeElement.setAttribute("aria-describedby", value ?? "");
this.select()
?.searchInput()
.nativeElement.setAttribute("aria-describedby", value ?? "");
}
private _ariaDescribedBy?: string;

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./popover.stories";

View File

@@ -1,5 +1,5 @@
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { getByRole, userEvent } from "@storybook/test";
import { getByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs/blocks";
import * as stories from "./progress.stories";

View File

@@ -1,4 +1,12 @@
import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import {
Meta,
Canvas,
Source,
Primary,
Controls,
Title,
Description,
} from "@storybook/addon-docs/blocks";
import * as stories from "./search.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Primary, Controls, Canvas } from "@storybook/addon-docs";
import { Meta, Primary, Controls, Canvas } from "@storybook/addon-docs/blocks";
import * as stories from "./section.stories";

View File

@@ -158,7 +158,9 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
}
set ariaDescribedBy(value: string | undefined) {
this._ariaDescribedBy = value;
this.select()?.searchInput.nativeElement.setAttribute("aria-describedby", value ?? "");
this.select()
?.searchInput()
.nativeElement.setAttribute("aria-describedby", value ?? "");
}
private _ariaDescribedBy?: string;
@@ -218,7 +220,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
* Needs to be arrow function to retain `this` scope.
*/
protected onKeyDown = (event: KeyboardEvent) => {
if (this.select().isOpen && event.key === "Escape" && !hasModifierKey(event)) {
if (this.select().isOpen() && event.key === "Escape" && !hasModifierKey(event)) {
event.stopPropagation();
}

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Source, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Source, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./select.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Source } from "@storybook/addon-docs";
import { Meta, Canvas, Source } from "@storybook/addon-docs/blocks";
import * as skeletonStories from "./skeleton.stories";
import * as skeletonTextStories from "./skeleton-text.stories";

View File

@@ -1,4 +1,12 @@
import { Meta, Story, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import {
Meta,
Story,
Source,
Primary,
Controls,
Title,
Description,
} from "@storybook/addon-docs/blocks";
import * as stories from "./stepper.stories";

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Documentation/Colors" />

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Canvas } from "@storybook/addon-docs";
import { Meta, Story, Canvas } from "@storybook/addon-docs/blocks";
import * as itemStories from "../item/item.stories";
import * as popupLayoutStories from "../../../../apps/browser/src/platform/popup/layout/popup-layout.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas } from "@storybook/addon-docs";
import { Meta, Canvas } from "@storybook/addon-docs/blocks";
import * as stories from "./icons.stories";

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Documentation/Introduction" />

View File

@@ -1,4 +1,4 @@
import { Meta, Story } from "@storybook/addon-docs";
import { Meta, Story } from "@storybook/addon-docs/blocks";
import * as stories from "./kitchen-sink.stories";

View File

@@ -9,7 +9,7 @@ import {
fireEvent,
getByText,
getAllByLabelText,
} from "@storybook/test";
} from "storybook/test";
import { PasswordManagerLogo } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Documentation/Migration" />

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Documentation/Responsive Design" />

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/addon-docs";
import { Meta } from "@storybook/addon-docs/blocks";
<Meta title="Documentation/Virtual Scrolling" />

View File

@@ -1,4 +1,12 @@
import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import {
Meta,
Canvas,
Source,
Primary,
Controls,
Title,
Description,
} from "@storybook/addon-docs/blocks";
import * as stories from "./switch.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Source, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Story, Source, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./table.stories";

View File

@@ -1,4 +1,4 @@
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs";
import { Meta, Canvas, Primary, Controls } from "@storybook/addon-docs/blocks";
import * as stories from "./tabs.stories";
import * as dialogStories from "../dialog/dialog/dialog.stories";

View File

@@ -1,4 +1,12 @@
import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import {
Meta,
Canvas,
Source,
Primary,
Controls,
Title,
Description,
} from "@storybook/addon-docs/blocks";
import * as stories from "./toast.stories";

View File

@@ -1,8 +1,8 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { action } from "@storybook/addon-actions";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { action } from "storybook/actions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs/blocks";
import * as stories from "./toggle-group.stories";

View File

@@ -16,6 +16,7 @@ import {
import { TooltipPositionIdentifier, tooltipPositions } from "./tooltip-positions";
import { TooltipComponent, TOOLTIP_DATA } from "./tooltip.component";
export const TOOLTIP_DELAY_MS = 800;
/**
* Directive to add a tooltip to any element. The tooltip content is provided via the `bitTooltip` input.
* The position of the tooltip can be set via the `tooltipPosition` input. Default position is "above-center".
@@ -85,7 +86,7 @@ export class TooltipDirective implements OnInit {
this.isVisible.set(false);
};
private showTooltip = () => {
protected showTooltip = () => {
if (!this.overlayRef) {
this.overlayRef = this.overlay.create({
...this.defaultPopoverConfig,
@@ -94,14 +95,17 @@ export class TooltipDirective implements OnInit {
this.overlayRef.attach(this.tooltipPortal);
}
this.isVisible.set(true);
setTimeout(() => {
this.isVisible.set(true);
}, TOOLTIP_DELAY_MS);
};
private hideTooltip = () => {
protected hideTooltip = () => {
this.destroyTooltip();
};
private readonly resolvedDescribedByIds = computed(() => {
protected readonly resolvedDescribedByIds = computed(() => {
if (this.addTooltipToDescribedby()) {
if (this.currentDescribedByIds) {
return `${this.currentDescribedByIds || ""} ${this.tooltipId}`;

View File

@@ -1,4 +1,12 @@
import { Meta, Canvas, Source, Primary, Controls, Title, Description } from "@storybook/addon-docs";
import {
Meta,
Canvas,
Source,
Primary,
Controls,
Title,
Description,
} from "@storybook/addon-docs/blocks";
import * as stories from "./tooltip.stories";

View File

@@ -6,11 +6,11 @@ import {
} from "@angular/cdk/overlay";
import { ComponentPortal } from "@angular/cdk/portal";
import { Component } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { Observable, Subject } from "rxjs";
import { TooltipDirective } from "./tooltip.directive";
import { TooltipDirective, TOOLTIP_DELAY_MS } from "./tooltip.directive";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -90,23 +90,25 @@ describe("TooltipDirective (visibility only)", () => {
return hostDE.injector.get(TooltipDirective);
}
it("sets isVisible to true on mouseenter", () => {
it("sets isVisible to true on mouseenter", fakeAsync(() => {
const button: HTMLButtonElement = fixture.debugElement.query(By.css("button")).nativeElement;
const directive = getDirective();
const isVisible = (directive as unknown as { isVisible: () => boolean }).isVisible;
button.dispatchEvent(new Event("mouseenter"));
tick(TOOLTIP_DELAY_MS);
expect(isVisible()).toBe(true);
});
}));
it("sets isVisible to true on focus", () => {
it("sets isVisible to true on focus", fakeAsync(() => {
const button: HTMLButtonElement = fixture.debugElement.query(By.css("button")).nativeElement;
const directive = getDirective();
const isVisible = (directive as unknown as { isVisible: () => boolean }).isVisible;
button.dispatchEvent(new Event("focus"));
tick(TOOLTIP_DELAY_MS);
expect(isVisible()).toBe(true);
});
}));
});

View File

@@ -1,6 +1,6 @@
import { signal } from "@angular/core";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { getByRole, userEvent } from "@storybook/test";
import { getByRole, userEvent } from "storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

Some files were not shown because too many files have changed in this diff Show More