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:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ describe("ORGANIZATIONS state", () => {
|
||||
isAdminInitiated: false,
|
||||
ssoEnabled: false,
|
||||
ssoMemberDecryptionType: undefined,
|
||||
usePhishingBlocker: false,
|
||||
},
|
||||
};
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -33,4 +33,6 @@ export enum NotificationType {
|
||||
|
||||
OrganizationBankAccountVerified = 23,
|
||||
ProviderBankAccountVerified = 24,
|
||||
|
||||
SyncPolicy = 25,
|
||||
}
|
||||
|
||||
@@ -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=";
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Component Library/Async Actions/Overview" />
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Controls,
|
||||
Title,
|
||||
Description,
|
||||
} from "@storybook/addon-docs";
|
||||
} from "@storybook/addon-docs/blocks";
|
||||
|
||||
import * as stories from "./button.stories";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Component Library/Form/Input Directive" />
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Documentation/Colors" />
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Documentation/Introduction" />
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Documentation/Migration" />
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Documentation/Responsive Design" />
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/addon-docs";
|
||||
import { Meta } from "@storybook/addon-docs/blocks";
|
||||
|
||||
<Meta title="Documentation/Virtual Scrolling" />
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user