mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[CL-707] Migrate CL codebase to signals (#15340)
This commit is contained in:
@@ -20,6 +20,6 @@ export class PopupBackBrowserDirective extends BitActionDirective {
|
|||||||
super(buttonComponent, validationService, logService);
|
super(buttonComponent, validationService, logService);
|
||||||
|
|
||||||
// override `bitAction` input; the parent handles the rest
|
// override `bitAction` input; the parent handles the rest
|
||||||
this.handler = () => this.router.back();
|
this.handler.set(() => this.router.back());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ describe("NavigationProductSwitcherComponent", () => {
|
|||||||
|
|
||||||
const navItem = fixture.debugElement.query(By.directive(NavItemComponent));
|
const navItem = fixture.debugElement.query(By.directive(NavItemComponent));
|
||||||
|
|
||||||
expect(navItem.componentInstance.forceActiveStyles).toBe(true);
|
expect(navItem.componentInstance.forceActiveStyles()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ describe("VaultBannersComponent", () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||||
expect(banner.componentInstance.bannerType).toBe("premium");
|
expect(banner.componentInstance.bannerType()).toBe("premium");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dismisses premium banner", async () => {
|
it("dismisses premium banner", async () => {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
|
|||||||
selector: "[appA11yTitle]",
|
selector: "[appA11yTitle]",
|
||||||
})
|
})
|
||||||
export class A11yTitleDirective implements OnInit {
|
export class A11yTitleDirective implements OnInit {
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input() set appA11yTitle(title: string) {
|
@Input() set appA11yTitle(title: string) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.setAttributes();
|
this.setAttributes();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="!hideLogo"
|
*ngIf="!hideLogo()"
|
||||||
[routerLink]="['/']"
|
[routerLink]="['/']"
|
||||||
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
|
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
|
||||||
>
|
>
|
||||||
@@ -14,29 +14,29 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
|
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
|
||||||
<div *ngIf="!hideIcon" class="tw-w-24 sm:tw-w-28 md:tw-w-32 tw-mx-auto">
|
<div *ngIf="!hideIcon()" class="tw-w-24 sm:tw-w-28 md:tw-w-32 tw-mx-auto">
|
||||||
<bit-icon [icon]="icon"></bit-icon>
|
<bit-icon [icon]="icon()"></bit-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="title">
|
<ng-container *ngIf="title()">
|
||||||
<!-- Small screens -->
|
<!-- Small screens -->
|
||||||
<h1 bitTypography="h3" class="tw-mt-2 sm:tw-hidden">
|
<h1 bitTypography="h3" class="tw-mt-2 sm:tw-hidden">
|
||||||
{{ title }}
|
{{ title() }}
|
||||||
</h1>
|
</h1>
|
||||||
<!-- Medium to Larger screens -->
|
<!-- Medium to Larger screens -->
|
||||||
<h1 bitTypography="h2" class="tw-mt-2 tw-hidden sm:tw-block">
|
<h1 bitTypography="h2" class="tw-mt-2 tw-hidden sm:tw-block">
|
||||||
{{ title }}
|
{{ title() }}
|
||||||
</h1>
|
</h1>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div *ngIf="subtitle" class="tw-text-sm sm:tw-text-base">{{ subtitle }}</div>
|
<div *ngIf="subtitle()" class="tw-text-sm sm:tw-text-base">{{ subtitle() }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
|
class="tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
|
||||||
[ngClass]="maxWidthClass"
|
[ngClass]="maxWidthClass"
|
||||||
>
|
>
|
||||||
@if (hideCardWrapper) {
|
@if (hideCardWrapper()) {
|
||||||
<div class="tw-mb-6 sm:tw-mb-10">
|
<div class="tw-mb-6 sm:tw-mb-10">
|
||||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,11 +50,11 @@
|
|||||||
<ng-content select="[slot=secondary]"></ng-content>
|
<ng-content select="[slot=secondary]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer *ngIf="!hideFooter" class="tw-text-center tw-mt-4 sm:tw-mt-6">
|
<footer *ngIf="!hideFooter()" class="tw-text-center tw-mt-4 sm:tw-mt-6">
|
||||||
<div *ngIf="showReadonlyHostname" bitTypography="body2">
|
<div *ngIf="showReadonlyHostname()" bitTypography="body2">
|
||||||
{{ "accessing" | i18n }} {{ hostname }}
|
{{ "accessing" | i18n }} {{ hostname }}
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="!showReadonlyHostname">
|
<ng-container *ngIf="!showReadonlyHostname()">
|
||||||
<ng-content select="[slot=environment-selector]"></ng-content>
|
<ng-content select="[slot=environment-selector]"></ng-content>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!hideYearAndVersion">
|
<ng-container *ngIf="!hideYearAndVersion">
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, HostBinding, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
|
import {
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
SimpleChanges,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
|
} from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
@@ -29,21 +37,21 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
|||||||
return ["tw-h-full"];
|
return ["tw-h-full"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() title: string;
|
readonly title = input<string>();
|
||||||
@Input() subtitle: string;
|
readonly subtitle = input<string>();
|
||||||
@Input() icon: Icon;
|
readonly icon = model<Icon>();
|
||||||
@Input() showReadonlyHostname: boolean;
|
readonly showReadonlyHostname = input<boolean>(false);
|
||||||
@Input() hideLogo: boolean = false;
|
readonly hideLogo = input<boolean>(false);
|
||||||
@Input() hideFooter: boolean = false;
|
readonly hideFooter = input<boolean>(false);
|
||||||
@Input() hideIcon: boolean = false;
|
readonly hideIcon = input<boolean>(false);
|
||||||
@Input() hideCardWrapper: boolean = false;
|
readonly hideCardWrapper = input<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max width of the anon layout title, subtitle, and content areas.
|
* Max width of the anon layout title, subtitle, and content areas.
|
||||||
*
|
*
|
||||||
* @default 'md'
|
* @default 'md'
|
||||||
*/
|
*/
|
||||||
@Input() maxWidth: AnonLayoutMaxWidth = "md";
|
readonly maxWidth = model<AnonLayoutMaxWidth>("md");
|
||||||
|
|
||||||
protected logo = BitwardenLogo;
|
protected logo = BitwardenLogo;
|
||||||
protected year: string;
|
protected year: string;
|
||||||
@@ -54,7 +62,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
|||||||
protected hideYearAndVersion = false;
|
protected hideYearAndVersion = false;
|
||||||
|
|
||||||
get maxWidthClass(): string {
|
get maxWidthClass(): string {
|
||||||
switch (this.maxWidth) {
|
const maxWidth = this.maxWidth();
|
||||||
|
switch (maxWidth) {
|
||||||
case "md":
|
case "md":
|
||||||
return "tw-max-w-md";
|
return "tw-max-w-md";
|
||||||
case "lg":
|
case "lg":
|
||||||
@@ -78,19 +87,19 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.maxWidth = this.maxWidth ?? "md";
|
this.maxWidth.set(this.maxWidth() ?? "md");
|
||||||
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
|
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
|
||||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||||
|
|
||||||
// If there is no icon input, then use the default icon
|
// If there is no icon input, then use the default icon
|
||||||
if (this.icon == null) {
|
if (this.icon() == null) {
|
||||||
this.icon = AnonLayoutBitwardenShield;
|
this.icon.set(AnonLayoutBitwardenShield);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnChanges(changes: SimpleChanges) {
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
if (changes.maxWidth) {
|
if (changes.maxWidth) {
|
||||||
this.maxWidth = changes.maxWidth.currentValue ?? "md";
|
this.maxWidth.set(changes.maxWidth.currentValue ?? "md");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,7 @@ class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
|
|||||||
getClientType = () => ClientType.Web;
|
getClientType = () => ClientType.Web;
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoryArgs = Pick<
|
type StoryArgs = AnonLayoutComponent & {
|
||||||
AnonLayoutComponent,
|
|
||||||
| "title"
|
|
||||||
| "subtitle"
|
|
||||||
| "showReadonlyHostname"
|
|
||||||
| "hideCardWrapper"
|
|
||||||
| "hideIcon"
|
|
||||||
| "hideLogo"
|
|
||||||
| "hideFooter"
|
|
||||||
| "maxWidth"
|
|
||||||
> & {
|
|
||||||
contentLength: "normal" | "long" | "thin";
|
contentLength: "normal" | "long" | "thin";
|
||||||
showSecondary: boolean;
|
showSecondary: boolean;
|
||||||
useDefaultIcon: boolean;
|
useDefaultIcon: boolean;
|
||||||
@@ -72,15 +62,14 @@ export default {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
render: (args) => {
|
||||||
render: (args: StoryArgs) => {
|
|
||||||
const { useDefaultIcon, icon, ...rest } = args;
|
const { useDefaultIcon, icon, ...rest } = args;
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...rest,
|
...rest,
|
||||||
icon: useDefaultIcon ? null : icon,
|
icon: useDefaultIcon ? null : icon,
|
||||||
},
|
},
|
||||||
template: `
|
template: /*html*/ `
|
||||||
<auth-anon-layout
|
<auth-anon-layout
|
||||||
[title]="title"
|
[title]="title"
|
||||||
[subtitle]="subtitle"
|
[subtitle]="subtitle"
|
||||||
@@ -160,7 +149,7 @@ export default {
|
|||||||
contentLength: "normal",
|
contentLength: "normal",
|
||||||
showSecondary: false,
|
showSecondary: false,
|
||||||
},
|
},
|
||||||
} as Meta<StoryArgs>;
|
} satisfies Meta<StoryArgs>;
|
||||||
|
|
||||||
type Story = StoryObj<StoryArgs>;
|
type Story = StoryObj<StoryArgs>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, HostListener, Input, OnDestroy, Optional } from "@angular/core";
|
import { Directive, HostListener, model, OnDestroy, Optional } from "@angular/core";
|
||||||
import { BehaviorSubject, finalize, Subject, takeUntil, tap } from "rxjs";
|
import { BehaviorSubject, finalize, Subject, takeUntil, tap } from "rxjs";
|
||||||
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -38,7 +38,7 @@ export class BitActionDirective implements OnDestroy {
|
|||||||
|
|
||||||
disabled = false;
|
disabled = false;
|
||||||
|
|
||||||
@Input("bitAction") handler: FunctionReturningAwaitable;
|
readonly handler = model<FunctionReturningAwaitable>(undefined, { alias: "bitAction" });
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private buttonComponent: ButtonLikeAbstraction,
|
private buttonComponent: ButtonLikeAbstraction,
|
||||||
@@ -48,12 +48,12 @@ export class BitActionDirective implements OnDestroy {
|
|||||||
|
|
||||||
@HostListener("click")
|
@HostListener("click")
|
||||||
protected async onClick() {
|
protected async onClick() {
|
||||||
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled()) {
|
if (!this.handler() || this.loading || this.disabled || this.buttonComponent.disabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
functionToObservable(this.handler)
|
functionToObservable(this.handler())
|
||||||
.pipe(
|
.pipe(
|
||||||
tap({
|
tap({
|
||||||
error: (err: unknown) => {
|
error: (err: unknown) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, Input, OnDestroy, OnInit, Optional } from "@angular/core";
|
import { Directive, OnDestroy, OnInit, Optional, input } from "@angular/core";
|
||||||
import { FormGroupDirective } from "@angular/forms";
|
import { FormGroupDirective } from "@angular/forms";
|
||||||
import { BehaviorSubject, catchError, filter, of, Subject, switchMap, takeUntil } from "rxjs";
|
import { BehaviorSubject, catchError, filter, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
@@ -20,9 +20,9 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
|
|||||||
private _loading$ = new BehaviorSubject<boolean>(false);
|
private _loading$ = new BehaviorSubject<boolean>(false);
|
||||||
private _disabled$ = new BehaviorSubject<boolean>(false);
|
private _disabled$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
@Input("bitSubmit") handler: FunctionReturningAwaitable;
|
readonly handler = input<FunctionReturningAwaitable>(undefined, { alias: "bitSubmit" });
|
||||||
|
|
||||||
@Input() allowDisabledFormSubmit?: boolean = false;
|
readonly allowDisabledFormSubmit = input<boolean>(false);
|
||||||
|
|
||||||
readonly loading$ = this._loading$.asObservable();
|
readonly loading$ = this._loading$.asObservable();
|
||||||
readonly disabled$ = this._disabled$.asObservable();
|
readonly disabled$ = this._disabled$.asObservable();
|
||||||
@@ -38,7 +38,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
|
|||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
// Calling functionToObservable executes the sync part of the handler
|
// Calling functionToObservable executes the sync part of the handler
|
||||||
// allowing the function to check form validity before it gets disabled.
|
// allowing the function to check form validity before it gets disabled.
|
||||||
const awaitable = functionToObservable(this.handler);
|
const awaitable = functionToObservable(this.handler());
|
||||||
|
|
||||||
// Disable form
|
// Disable form
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
@@ -61,7 +61,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.formGroupDirective.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
|
this.formGroupDirective.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
|
||||||
if (this.allowDisabledFormSubmit) {
|
if (this.allowDisabledFormSubmit()) {
|
||||||
this._disabled$.next(false);
|
this._disabled$.next(false);
|
||||||
} else {
|
} else {
|
||||||
this._disabled$.next(c === "DISABLED");
|
this._disabled$.next(c === "DISABLED");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, Input, OnDestroy, Optional } from "@angular/core";
|
import { Directive, OnDestroy, Optional, input } from "@angular/core";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||||
@@ -29,8 +29,8 @@ import { BitSubmitDirective } from "./bit-submit.directive";
|
|||||||
export class BitFormButtonDirective implements OnDestroy {
|
export class BitFormButtonDirective implements OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@Input() type: string;
|
readonly type = input<string>();
|
||||||
@Input() disabled?: boolean;
|
readonly disabled = input<boolean>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
buttonComponent: ButtonLikeAbstraction,
|
buttonComponent: ButtonLikeAbstraction,
|
||||||
@@ -39,16 +39,17 @@ export class BitFormButtonDirective implements OnDestroy {
|
|||||||
) {
|
) {
|
||||||
if (submitDirective && buttonComponent) {
|
if (submitDirective && buttonComponent) {
|
||||||
submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
|
submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
|
||||||
if (this.type === "submit") {
|
if (this.type() === "submit") {
|
||||||
buttonComponent.loading.set(loading);
|
buttonComponent.loading.set(loading);
|
||||||
} else {
|
} else {
|
||||||
buttonComponent.disabled.set(this.disabled || loading);
|
buttonComponent.disabled.set(this.disabled() || loading);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
||||||
if (this.disabled !== false) {
|
const disabledValue = this.disabled();
|
||||||
buttonComponent.disabled.set(this.disabled || disabled);
|
if (disabledValue !== false) {
|
||||||
|
buttonComponent.disabled.set(disabledValue || disabled);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const template = /*html*/ `
|
|||||||
template,
|
template,
|
||||||
selector: "app-promise-example",
|
selector: "app-promise-example",
|
||||||
imports: [AsyncActionsModule, ButtonModule, IconButtonModule],
|
imports: [AsyncActionsModule, ButtonModule, IconButtonModule],
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
class PromiseExampleComponent {
|
class PromiseExampleComponent {
|
||||||
statusEmoji = "🟡";
|
statusEmoji = "🟡";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { NgClass } from "@angular/common";
|
import { NgClass } from "@angular/common";
|
||||||
import { Component, Input, OnChanges } from "@angular/core";
|
import { Component, OnChanges, input } from "@angular/core";
|
||||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||||
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@@ -25,17 +25,17 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "bit-avatar",
|
selector: "bit-avatar",
|
||||||
template: `@if (src) {
|
template: `@if (src) {
|
||||||
<img [src]="src" title="{{ title || text }}" [ngClass]="classList" />
|
<img [src]="src" title="{{ title() || text() }}" [ngClass]="classList" />
|
||||||
}`,
|
}`,
|
||||||
imports: [NgClass],
|
imports: [NgClass],
|
||||||
})
|
})
|
||||||
export class AvatarComponent implements OnChanges {
|
export class AvatarComponent implements OnChanges {
|
||||||
@Input() border = false;
|
readonly border = input(false);
|
||||||
@Input() color?: string;
|
readonly color = input<string>();
|
||||||
@Input() id?: string;
|
readonly id = input<string>();
|
||||||
@Input() text?: string;
|
readonly text = input<string>();
|
||||||
@Input() title: string;
|
readonly title = input<string>();
|
||||||
@Input() size: SizeTypes = "default";
|
readonly size = input<SizeTypes>("default");
|
||||||
|
|
||||||
private svgCharCount = 2;
|
private svgCharCount = 2;
|
||||||
private svgFontSize = 20;
|
private svgFontSize = 20;
|
||||||
@@ -51,13 +51,13 @@ export class AvatarComponent implements OnChanges {
|
|||||||
|
|
||||||
get classList() {
|
get classList() {
|
||||||
return ["tw-rounded-full"]
|
return ["tw-rounded-full"]
|
||||||
.concat(SizeClasses[this.size] ?? [])
|
.concat(SizeClasses[this.size()] ?? [])
|
||||||
.concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
|
.concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generate() {
|
private generate() {
|
||||||
let chars: string = null;
|
let chars: string = null;
|
||||||
const upperCaseText = this.text?.toUpperCase() ?? "";
|
const upperCaseText = this.text()?.toUpperCase() ?? "";
|
||||||
|
|
||||||
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
||||||
|
|
||||||
@@ -71,12 +71,13 @@ export class AvatarComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let svg: HTMLElement;
|
let svg: HTMLElement;
|
||||||
let hexColor = this.color;
|
let hexColor = this.color();
|
||||||
|
|
||||||
if (!Utils.isNullOrWhitespace(this.color)) {
|
const id = this.id();
|
||||||
|
if (!Utils.isNullOrWhitespace(this.color())) {
|
||||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||||
} else if (!Utils.isNullOrWhitespace(this.id)) {
|
} else if (!Utils.isNullOrWhitespace(id)) {
|
||||||
hexColor = Utils.stringToColor(this.id.toString());
|
hexColor = Utils.stringToColor(id.toString());
|
||||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||||
} else {
|
} else {
|
||||||
hexColor = Utils.stringToColor(upperCaseText);
|
hexColor = Utils.stringToColor(upperCaseText);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tw-inline-flex tw-flex-wrap tw-gap-2">
|
<div class="tw-inline-flex tw-flex-wrap tw-gap-2">
|
||||||
@for (item of filteredItems; track item; let last = $last) {
|
@for (item of filteredItems; track item; let last = $last) {
|
||||||
<span bitBadge [variant]="variant" [truncate]="truncate">
|
<span bitBadge [variant]="variant()" [truncate]="truncate()">
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</span>
|
</span>
|
||||||
@if (!last || isFiltered) {
|
@if (!last || isFiltered) {
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (isFiltered) {
|
@if (isFiltered) {
|
||||||
<span bitBadge [variant]="variant">
|
<span bitBadge [variant]="variant()">
|
||||||
{{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }}
|
{{ "plusNMore" | i18n: (items().length - filteredItems.length).toString() }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,42 +1,36 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
import { Component, OnChanges, input } from "@angular/core";
|
||||||
// @ts-strict-ignore
|
|
||||||
|
|
||||||
import { Component, Input, OnChanges } from "@angular/core";
|
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
import { BadgeModule, BadgeVariant } from "../badge";
|
import { BadgeModule, BadgeVariant } from "../badge";
|
||||||
|
|
||||||
|
function transformMaxItems(value: number | undefined) {
|
||||||
|
return value == undefined ? undefined : Math.max(1, value);
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "bit-badge-list",
|
selector: "bit-badge-list",
|
||||||
templateUrl: "badge-list.component.html",
|
templateUrl: "badge-list.component.html",
|
||||||
imports: [BadgeModule, I18nPipe],
|
imports: [BadgeModule, I18nPipe],
|
||||||
})
|
})
|
||||||
export class BadgeListComponent implements OnChanges {
|
export class BadgeListComponent implements OnChanges {
|
||||||
private _maxItems: number;
|
|
||||||
|
|
||||||
protected filteredItems: string[] = [];
|
protected filteredItems: string[] = [];
|
||||||
protected isFiltered = false;
|
protected isFiltered = false;
|
||||||
|
|
||||||
@Input() variant: BadgeVariant = "primary";
|
readonly variant = input<BadgeVariant>("primary");
|
||||||
@Input() items: string[] = [];
|
readonly items = input<string[]>([]);
|
||||||
@Input() truncate = true;
|
readonly truncate = input(true);
|
||||||
|
|
||||||
@Input()
|
readonly maxItems = input(undefined, { transform: transformMaxItems });
|
||||||
get maxItems(): number | undefined {
|
|
||||||
return this._maxItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
set maxItems(value: number | undefined) {
|
|
||||||
this._maxItems = value == undefined ? undefined : Math.max(1, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
if (this.maxItems == undefined || this.items.length <= this.maxItems) {
|
const maxItems = this.maxItems();
|
||||||
this.filteredItems = this.items;
|
|
||||||
|
if (maxItems == undefined || this.items().length <= maxItems) {
|
||||||
|
this.filteredItems = this.items();
|
||||||
} else {
|
} else {
|
||||||
this.filteredItems = this.items.slice(0, this.maxItems - 1);
|
this.filteredItems = this.items().slice(0, maxItems - 1);
|
||||||
}
|
}
|
||||||
this.isFiltered = this.items.length > this.filteredItems.length;
|
this.isFiltered = this.items().length > this.filteredItems.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<span [ngClass]="{ 'tw-truncate tw-block': truncate }">
|
<span [ngClass]="{ 'tw-truncate tw-block': truncate() }">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
|
import { Component, ElementRef, HostBinding, input } from "@angular/core";
|
||||||
|
|
||||||
import { FocusableElement } from "../shared/focusable-element";
|
import { FocusableElement } from "../shared/focusable-element";
|
||||||
|
|
||||||
@@ -89,33 +89,34 @@ export class BadgeComponent implements FocusableElement {
|
|||||||
"disabled:hover:!tw-text-muted",
|
"disabled:hover:!tw-text-muted",
|
||||||
"disabled:tw-cursor-not-allowed",
|
"disabled:tw-cursor-not-allowed",
|
||||||
]
|
]
|
||||||
.concat(styles[this.variant])
|
.concat(styles[this.variant()])
|
||||||
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant], "tw-min-w-10"] : [])
|
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant()], "tw-min-w-10"] : [])
|
||||||
.concat(this.truncate ? this.maxWidthClass : []);
|
.concat(this.truncate() ? this.maxWidthClass() : []);
|
||||||
}
|
}
|
||||||
@HostBinding("attr.title") get titleAttr() {
|
@HostBinding("attr.title") get titleAttr() {
|
||||||
if (this.title !== undefined) {
|
const title = this.title();
|
||||||
return this.title;
|
if (title !== undefined) {
|
||||||
|
return title;
|
||||||
}
|
}
|
||||||
return this.truncate ? this?.el?.nativeElement?.textContent?.trim() : null;
|
return this.truncate() ? this?.el?.nativeElement?.textContent?.trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional override for the automatic badge title when truncating.
|
* Optional override for the automatic badge title when truncating.
|
||||||
*/
|
*/
|
||||||
@Input() title?: string;
|
readonly title = input<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant, sets the background color of the badge.
|
* Variant, sets the background color of the badge.
|
||||||
*/
|
*/
|
||||||
@Input() variant: BadgeVariant = "primary";
|
readonly variant = input<BadgeVariant>("primary");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate long text
|
* Truncate long text
|
||||||
*/
|
*/
|
||||||
@Input() truncate = true;
|
readonly truncate = input(true);
|
||||||
|
|
||||||
@Input() maxWidthClass: `tw-max-w-${string}` = "tw-max-w-40";
|
readonly maxWidthClass = input<`tw-max-w-${string}`>("tw-max-w-40");
|
||||||
|
|
||||||
getFocusTarget() {
|
getFocusTarget() {
|
||||||
return this.el.nativeElement;
|
return this.el.nativeElement;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div
|
<div
|
||||||
class="tw-flex tw-items-center tw-gap-2 tw-p-2 tw-ps-4 tw-text-main tw-border-transparent tw-bg-clip-padding tw-border-solid tw-border-b tw-border-0"
|
class="tw-flex tw-items-center tw-gap-2 tw-p-2 tw-ps-4 tw-text-main tw-border-transparent tw-bg-clip-padding tw-border-solid tw-border-b tw-border-0"
|
||||||
[ngClass]="bannerClass"
|
[ngClass]="bannerClass"
|
||||||
[attr.role]="useAlertRole ? 'status' : null"
|
[attr.role]="useAlertRole() ? 'status' : null"
|
||||||
[attr.aria-live]="useAlertRole ? 'polite' : null"
|
[attr.aria-live]="useAlertRole() ? 'polite' : null"
|
||||||
>
|
>
|
||||||
@if (icon) {
|
@if (icon(); as icon) {
|
||||||
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" aria-hidden="true"></i>
|
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" aria-hidden="true"></i>
|
||||||
}
|
}
|
||||||
<!-- Overriding focus-visible color for link buttons for a11y against colored background -->
|
<!-- Overriding focus-visible color for link buttons for a11y against colored background -->
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</span>
|
</span>
|
||||||
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
|
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
|
||||||
@if (showClose) {
|
@if (showClose()) {
|
||||||
<button
|
<button
|
||||||
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
|
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -30,17 +30,17 @@ describe("BannerComponent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should create with alert", () => {
|
it("should create with alert", () => {
|
||||||
expect(component.useAlertRole).toBe(true);
|
expect(component.useAlertRole()).toBe(true);
|
||||||
const el = fixture.nativeElement.children[0];
|
const el = fixture.nativeElement.children[0];
|
||||||
expect(el.getAttribute("role")).toEqual("status");
|
expect(el.getAttribute("role")).toEqual("status");
|
||||||
expect(el.getAttribute("aria-live")).toEqual("polite");
|
expect(el.getAttribute("aria-live")).toEqual("polite");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("useAlertRole=false", () => {
|
it("useAlertRole=false", () => {
|
||||||
component.useAlertRole = false;
|
fixture.componentRef.setInput("useAlertRole", false);
|
||||||
fixture.autoDetectChanges();
|
fixture.autoDetectChanges();
|
||||||
|
|
||||||
expect(component.useAlertRole).toBe(false);
|
expect(component.useAlertRole()).toBe(false);
|
||||||
const el = fixture.nativeElement.children[0];
|
const el = fixture.nativeElement.children[0];
|
||||||
expect(el.getAttribute("role")).toBeNull();
|
expect(el.getAttribute("role")).toBeNull();
|
||||||
expect(el.getAttribute("aria-live")).toBeNull();
|
expect(el.getAttribute("aria-live")).toBeNull();
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
|
import { Component, OnInit, Output, EventEmitter, input, model } from "@angular/core";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
@@ -31,19 +29,22 @@ const defaultIcon: Record<BannerType, string> = {
|
|||||||
imports: [CommonModule, IconButtonModule, I18nPipe],
|
imports: [CommonModule, IconButtonModule, I18nPipe],
|
||||||
})
|
})
|
||||||
export class BannerComponent implements OnInit {
|
export class BannerComponent implements OnInit {
|
||||||
@Input("bannerType") bannerType: BannerType = "info";
|
readonly bannerType = input<BannerType>("info");
|
||||||
@Input() icon: string;
|
|
||||||
@Input() useAlertRole = true;
|
readonly icon = model<string>();
|
||||||
@Input() showClose = true;
|
readonly useAlertRole = input(true);
|
||||||
|
readonly showClose = input(true);
|
||||||
|
|
||||||
@Output() onClose = new EventEmitter<void>();
|
@Output() onClose = new EventEmitter<void>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.icon ??= defaultIcon[this.bannerType];
|
if (!this.icon()) {
|
||||||
|
this.icon.set(defaultIcon[this.bannerType()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get bannerClass() {
|
get bannerClass() {
|
||||||
switch (this.bannerType) {
|
switch (this.bannerType()) {
|
||||||
case "danger":
|
case "danger":
|
||||||
return "tw-bg-danger-100 tw-border-b-danger-700";
|
return "tw-bg-danger-100 tw-border-b-danger-700";
|
||||||
case "info":
|
case "info":
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<ng-template>
|
<ng-template>
|
||||||
@if (icon) {
|
@if (icon(); as icon) {
|
||||||
<i class="bwi {{ icon }} !tw-me-2" aria-hidden="true"></i>
|
<i class="bwi {{ icon }} !tw-me-2" aria-hidden="true"></i>
|
||||||
}
|
}
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
|
||||||
import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core";
|
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
|
||||||
import { QueryParamsHandling } from "@angular/router";
|
import { QueryParamsHandling } from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -9,17 +9,13 @@ import { QueryParamsHandling } from "@angular/router";
|
|||||||
templateUrl: "./breadcrumb.component.html",
|
templateUrl: "./breadcrumb.component.html",
|
||||||
})
|
})
|
||||||
export class BreadcrumbComponent {
|
export class BreadcrumbComponent {
|
||||||
@Input()
|
readonly icon = input<string>();
|
||||||
icon?: string;
|
|
||||||
|
|
||||||
@Input()
|
readonly route = input<string | any[]>();
|
||||||
route?: string | any[] = undefined;
|
|
||||||
|
|
||||||
@Input()
|
readonly queryParams = input<Record<string, string>>({});
|
||||||
queryParams?: Record<string, string> = {};
|
|
||||||
|
|
||||||
@Input()
|
readonly queryParamsHandling = input<QueryParamsHandling>();
|
||||||
queryParamsHandling?: QueryParamsHandling;
|
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
click = new EventEmitter();
|
click = new EventEmitter();
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) {
|
@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) {
|
||||||
@if (breadcrumb.route) {
|
@if (breadcrumb.route(); as route) {
|
||||||
<a
|
<a
|
||||||
bitLink
|
bitLink
|
||||||
linkType="primary"
|
linkType="primary"
|
||||||
class="tw-my-2 tw-inline-block"
|
class="tw-my-2 tw-inline-block"
|
||||||
[routerLink]="breadcrumb.route"
|
[routerLink]="route"
|
||||||
[queryParams]="breadcrumb.queryParams"
|
[queryParams]="breadcrumb.queryParams()"
|
||||||
[queryParamsHandling]="breadcrumb.queryParamsHandling"
|
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||||
>
|
>
|
||||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
}
|
} @else {
|
||||||
@if (!breadcrumb.route) {
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitLink
|
bitLink
|
||||||
@@ -39,18 +38,17 @@
|
|||||||
></button>
|
></button>
|
||||||
<bit-menu #overflowMenu>
|
<bit-menu #overflowMenu>
|
||||||
@for (breadcrumb of overflow; track breadcrumb) {
|
@for (breadcrumb of overflow; track breadcrumb) {
|
||||||
@if (breadcrumb.route) {
|
@if (breadcrumb.route(); as route) {
|
||||||
<a
|
<a
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
linkType="primary"
|
linkType="primary"
|
||||||
[routerLink]="breadcrumb.route"
|
[routerLink]="route"
|
||||||
[queryParams]="breadcrumb.queryParams"
|
[queryParams]="breadcrumb.queryParams()"
|
||||||
[queryParamsHandling]="breadcrumb.queryParamsHandling"
|
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||||
>
|
>
|
||||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
}
|
} @else {
|
||||||
@if (!breadcrumb.route) {
|
|
||||||
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
|
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
|
||||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||||
</button>
|
</button>
|
||||||
@@ -59,19 +57,18 @@
|
|||||||
</bit-menu>
|
</bit-menu>
|
||||||
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||||
@for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) {
|
@for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) {
|
||||||
@if (breadcrumb.route) {
|
@if (breadcrumb.route(); as route) {
|
||||||
<a
|
<a
|
||||||
bitLink
|
bitLink
|
||||||
linkType="primary"
|
linkType="primary"
|
||||||
class="tw-my-2 tw-inline-block"
|
class="tw-my-2 tw-inline-block"
|
||||||
[routerLink]="breadcrumb.route"
|
[routerLink]="route"
|
||||||
[queryParams]="breadcrumb.queryParams"
|
[queryParams]="breadcrumb.queryParams()"
|
||||||
[queryParamsHandling]="breadcrumb.queryParamsHandling"
|
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||||
>
|
>
|
||||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
}
|
} @else {
|
||||||
@if (!breadcrumb.route) {
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitLink
|
bitLink
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, ContentChildren, Input, QueryList } from "@angular/core";
|
import { Component, ContentChildren, QueryList, input } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
import { IconButtonModule } from "../icon-button";
|
import { IconButtonModule } from "../icon-button";
|
||||||
@@ -19,8 +19,7 @@ import { BreadcrumbComponent } from "./breadcrumb.component";
|
|||||||
imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule],
|
imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule],
|
||||||
})
|
})
|
||||||
export class BreadcrumbsComponent {
|
export class BreadcrumbsComponent {
|
||||||
@Input()
|
readonly show = input(3);
|
||||||
show = 3;
|
|
||||||
|
|
||||||
private breadcrumbs: BreadcrumbComponent[] = [];
|
private breadcrumbs: BreadcrumbComponent[] = [];
|
||||||
|
|
||||||
@@ -31,14 +30,14 @@ export class BreadcrumbsComponent {
|
|||||||
|
|
||||||
protected get beforeOverflow() {
|
protected get beforeOverflow() {
|
||||||
if (this.hasOverflow) {
|
if (this.hasOverflow) {
|
||||||
return this.breadcrumbs.slice(0, this.show - 1);
|
return this.breadcrumbs.slice(0, this.show() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.breadcrumbs;
|
return this.breadcrumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get overflow() {
|
protected get overflow() {
|
||||||
return this.breadcrumbs.slice(this.show - 1, -1);
|
return this.breadcrumbs.slice(this.show() - 1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get afterOverflow() {
|
protected get afterOverflow() {
|
||||||
@@ -46,6 +45,6 @@ export class BreadcrumbsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected get hasOverflow() {
|
protected get hasOverflow() {
|
||||||
return this.breadcrumbs.length > this.show;
|
return this.breadcrumbs.length > this.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
|
||||||
import { NgClass } from "@angular/common";
|
import { NgClass } from "@angular/common";
|
||||||
import {
|
import {
|
||||||
Input,
|
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Component,
|
Component,
|
||||||
model,
|
model,
|
||||||
@@ -10,6 +8,7 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
inject,
|
inject,
|
||||||
Signal,
|
Signal,
|
||||||
|
booleanAttribute,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
|
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
|
||||||
import { debounce, interval } from "rxjs";
|
import { debounce, interval } from "rxjs";
|
||||||
@@ -79,7 +78,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
|||||||
"hover:tw-no-underline",
|
"hover:tw-no-underline",
|
||||||
"focus:tw-outline-none",
|
"focus:tw-outline-none",
|
||||||
]
|
]
|
||||||
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
|
.concat(this.block() ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
|
||||||
.concat(
|
.concat(
|
||||||
this.showDisabledStyles() || this.disabled()
|
this.showDisabledStyles() || this.disabled()
|
||||||
? [
|
? [
|
||||||
@@ -95,7 +94,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
)
|
)
|
||||||
.concat(buttonStyles[this.buttonType ?? "secondary"])
|
.concat(buttonStyles[this.buttonType() ?? "secondary"])
|
||||||
.concat(buttonSizeStyles[this.size() || "default"]);
|
.concat(buttonSizeStyles[this.size() || "default"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,22 +115,13 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
|||||||
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@Input() buttonType: ButtonType = "secondary";
|
readonly buttonType = input<ButtonType>("secondary");
|
||||||
|
|
||||||
size = input<ButtonSize>("default");
|
readonly size = input<ButtonSize>("default");
|
||||||
|
|
||||||
private _block = false;
|
readonly block = input(false, { transform: booleanAttribute });
|
||||||
|
|
||||||
@Input()
|
readonly loading = model<boolean>(false);
|
||||||
get block(): boolean {
|
|
||||||
return this._block;
|
|
||||||
}
|
|
||||||
|
|
||||||
set block(value: boolean | "") {
|
|
||||||
this._block = coerceBooleanProperty(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
loading = model<boolean>(false);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether it is appropriate to display a loading spinner. We only want to show
|
* Determine whether it is appropriate to display a loading spinner. We only want to show
|
||||||
@@ -149,7 +139,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
|||||||
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
|
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
|
||||||
);
|
);
|
||||||
|
|
||||||
disabled = model<boolean>(false);
|
readonly disabled = model<boolean>(false);
|
||||||
private el = inject(ElementRef<HTMLButtonElement>);
|
private el = inject(ElementRef<HTMLButtonElement>);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
[ngClass]="calloutClass"
|
[ngClass]="calloutClass"
|
||||||
[attr.aria-labelledby]="titleId"
|
[attr.aria-labelledby]="titleId"
|
||||||
>
|
>
|
||||||
@if (title) {
|
@if (titleComputed(); as title) {
|
||||||
<header
|
<header
|
||||||
id="{{ titleId }}"
|
id="{{ titleId }}"
|
||||||
class="tw-mb-1 tw-mt-0 tw-text-base tw-font-semibold tw-flex tw-gap-2 tw-items-start"
|
class="tw-mb-1 tw-mt-0 tw-text-base tw-font-semibold tw-flex tw-gap-2 tw-items-start"
|
||||||
>
|
>
|
||||||
@if (icon) {
|
@if (iconComputed(); as icon) {
|
||||||
<i
|
<i
|
||||||
class="bwi !tw-text-main tw-relative tw-top-[3px]"
|
class="bwi !tw-text-main tw-relative tw-top-[3px]"
|
||||||
[ngClass]="[icon]"
|
[ngClass]="[icon]"
|
||||||
|
|||||||
@@ -30,31 +30,31 @@ describe("Callout", () => {
|
|||||||
|
|
||||||
describe("default state", () => {
|
describe("default state", () => {
|
||||||
it("success", () => {
|
it("success", () => {
|
||||||
component.type = "success";
|
fixture.componentRef.setInput("type", "success");
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.title).toBeUndefined();
|
expect(component.titleComputed()).toBeUndefined();
|
||||||
expect(component.icon).toBe("bwi-check-circle");
|
expect(component.iconComputed()).toBe("bwi-check-circle");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("info", () => {
|
it("info", () => {
|
||||||
component.type = "info";
|
fixture.componentRef.setInput("type", "info");
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.title).toBeUndefined();
|
expect(component.titleComputed()).toBeUndefined();
|
||||||
expect(component.icon).toBe("bwi-info-circle");
|
expect(component.iconComputed()).toBe("bwi-info-circle");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("warning", () => {
|
it("warning", () => {
|
||||||
component.type = "warning";
|
fixture.componentRef.setInput("type", "warning");
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.title).toBe("Warning");
|
expect(component.titleComputed()).toBe("Warning");
|
||||||
expect(component.icon).toBe("bwi-exclamation-triangle");
|
expect(component.iconComputed()).toBe("bwi-exclamation-triangle");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("danger", () => {
|
it("danger", () => {
|
||||||
component.type = "danger";
|
fixture.componentRef.setInput("type", "danger");
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.title).toBe("Error");
|
expect(component.titleComputed()).toBe("Error");
|
||||||
expect(component.icon).toBe("bwi-error");
|
expect(component.iconComputed()).toBe("bwi-error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, Input, OnInit } from "@angular/core";
|
import { Component, computed, input } from "@angular/core";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
@@ -34,24 +34,28 @@ let nextId = 0;
|
|||||||
templateUrl: "callout.component.html",
|
templateUrl: "callout.component.html",
|
||||||
imports: [SharedModule, TypographyModule],
|
imports: [SharedModule, TypographyModule],
|
||||||
})
|
})
|
||||||
export class CalloutComponent implements OnInit {
|
export class CalloutComponent {
|
||||||
@Input() type: CalloutTypes = "info";
|
readonly type = input<CalloutTypes>("info");
|
||||||
@Input() icon: string;
|
readonly icon = input<string>();
|
||||||
@Input() title: string;
|
readonly title = input<string>();
|
||||||
@Input() useAlertRole = false;
|
readonly useAlertRole = input(false);
|
||||||
|
readonly iconComputed = computed(() => this.icon() ?? defaultIcon[this.type()]);
|
||||||
|
readonly titleComputed = computed(() => {
|
||||||
|
const title = this.title();
|
||||||
|
const type = this.type();
|
||||||
|
if (title == null && defaultI18n[type] != null) {
|
||||||
|
return this.i18nService.t(defaultI18n[type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
});
|
||||||
|
|
||||||
protected titleId = `bit-callout-title-${nextId++}`;
|
protected titleId = `bit-callout-title-${nextId++}`;
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.icon ??= defaultIcon[this.type];
|
|
||||||
if (this.title == null && defaultI18n[this.type] != null) {
|
|
||||||
this.title = this.i18nService.t(defaultI18n[this.type]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get calloutClass() {
|
get calloutClass() {
|
||||||
switch (this.type) {
|
switch (this.type()) {
|
||||||
case "danger":
|
case "danger":
|
||||||
return "tw-bg-danger-100";
|
return "tw-bg-danger-100";
|
||||||
case "info":
|
case "info":
|
||||||
|
|||||||
@@ -68,3 +68,18 @@ export const Danger: Story = {
|
|||||||
type: "danger",
|
type: "danger",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CustomIcon: Story = {
|
||||||
|
...Info,
|
||||||
|
args: {
|
||||||
|
...Info.args,
|
||||||
|
icon: "bwi-star",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoTitle: Story = {
|
||||||
|
...Info,
|
||||||
|
args: {
|
||||||
|
icon: "bwi-star",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
|||||||
protected indeterminateImage =
|
protected indeterminateImage =
|
||||||
`url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`;
|
`url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`;
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@HostBinding()
|
@HostBinding()
|
||||||
@Input()
|
@Input()
|
||||||
get disabled() {
|
get disabled() {
|
||||||
@@ -114,6 +116,8 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
|||||||
}
|
}
|
||||||
private _disabled: boolean;
|
private _disabled: boolean;
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input()
|
@Input()
|
||||||
get required() {
|
get required() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const template = /*html*/ `
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-example",
|
selector: "app-example",
|
||||||
template,
|
template,
|
||||||
imports: [CheckboxModule, FormFieldModule, ReactiveFormsModule],
|
imports: [FormControlModule, CheckboxModule, FormsModule, FormFieldModule, ReactiveFormsModule],
|
||||||
})
|
})
|
||||||
class ExampleComponent {
|
class ExampleComponent {
|
||||||
protected formObj = this.formBuilder.group({
|
protected formObj = this.formBuilder.group({
|
||||||
|
|||||||
@@ -69,10 +69,10 @@
|
|||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="viewOption(parent, $event)"
|
(click)="viewOption(parent, $event)"
|
||||||
class="tw-text-[length:inherit]"
|
class="tw-text-[length:inherit]"
|
||||||
[title]="'backTo' | i18n: parent.label ?? placeholderText"
|
[title]="'backTo' | i18n: parent.label ?? placeholderText()"
|
||||||
>
|
>
|
||||||
<i slot="start" class="bwi bwi-angle-left" aria-hidden="true"></i>
|
<i slot="start" class="bwi bwi-angle-left" aria-hidden="true"></i>
|
||||||
{{ "backTo" | i18n: parent.label ?? placeholderText }}
|
{{ "backTo" | i18n: parent.label ?? placeholderText() }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
booleanAttribute,
|
booleanAttribute,
|
||||||
inject,
|
inject,
|
||||||
signal,
|
signal,
|
||||||
|
input,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||||
@@ -54,12 +55,15 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
|||||||
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>;
|
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>;
|
||||||
|
|
||||||
/** Text to show when there is no selected option */
|
/** Text to show when there is no selected option */
|
||||||
@Input({ required: true }) placeholderText: string;
|
readonly placeholderText = input.required<string>();
|
||||||
|
|
||||||
/** Icon to show when there is no selected option or the selected option does not have an icon */
|
/** Icon to show when there is no selected option or the selected option does not have an icon */
|
||||||
@Input() placeholderIcon: string;
|
readonly placeholderIcon = input<string>();
|
||||||
|
|
||||||
private _options: ChipSelectOption<T>[];
|
private _options: ChipSelectOption<T>[];
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
/** The select options to render */
|
/** The select options to render */
|
||||||
@Input({ required: true })
|
@Input({ required: true })
|
||||||
get options(): ChipSelectOption<T>[] {
|
get options(): ChipSelectOption<T>[] {
|
||||||
@@ -71,10 +75,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Disables the entire chip */
|
/** Disables the entire chip */
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Your application code writes to the input. This prevents migration.
|
||||||
@Input({ transform: booleanAttribute }) disabled = false;
|
@Input({ transform: booleanAttribute }) disabled = false;
|
||||||
|
|
||||||
/** Chip will stretch to full width of its container */
|
/** Chip will stretch to full width of its container */
|
||||||
@Input({ transform: booleanAttribute }) fullWidth?: boolean;
|
readonly fullWidth = input<boolean, unknown>(undefined, { transform: booleanAttribute });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We have `:focus-within` and `:focus-visible` but no `:focus-visible-within`
|
* We have `:focus-within` and `:focus-visible` but no `:focus-visible-within`
|
||||||
@@ -91,7 +97,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
|||||||
|
|
||||||
@HostBinding("class")
|
@HostBinding("class")
|
||||||
get classList() {
|
get classList() {
|
||||||
return ["tw-inline-block", this.fullWidth ? "tw-w-full" : "tw-max-w-52"];
|
return ["tw-inline-block", this.fullWidth() ? "tw-w-full" : "tw-max-w-52"];
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
@@ -113,12 +119,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
|||||||
|
|
||||||
/** The label to show in the chip button */
|
/** The label to show in the chip button */
|
||||||
protected get label(): string {
|
protected get label(): string {
|
||||||
return this.selectedOption?.label || this.placeholderText;
|
return this.selectedOption?.label || this.placeholderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The icon to show in the chip button */
|
/** The icon to show in the chip button */
|
||||||
protected get icon(): string {
|
protected get icon(): string {
|
||||||
return this.selectedOption?.icon || this.placeholderIcon;
|
return this.selectedOption?.icon || this.placeholderIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Directive, HostListener, Input, InjectionToken, Inject, Optional } from "@angular/core";
|
import {
|
||||||
|
Directive,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
InjectionToken,
|
||||||
|
Inject,
|
||||||
|
Optional,
|
||||||
|
input,
|
||||||
|
} from "@angular/core";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -28,13 +36,13 @@ export class CopyClickDirective {
|
|||||||
@Optional() @Inject(COPY_CLICK_LISTENER) private copyListener?: CopyClickListener,
|
@Optional() @Inject(COPY_CLICK_LISTENER) private copyListener?: CopyClickListener,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Input("appCopyClick") valueToCopy = "";
|
readonly valueToCopy = input("", { alias: "appCopyClick" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When set, the toast displayed will show `<valueLabel> copied`
|
* When set, the toast displayed will show `<valueLabel> copied`
|
||||||
* instead of the default messaging.
|
* instead of the default messaging.
|
||||||
*/
|
*/
|
||||||
@Input() valueLabel?: string;
|
readonly valueLabel = input<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When set without a value, a success toast will be shown when the value is copied
|
* When set without a value, a success toast will be shown when the value is copied
|
||||||
@@ -49,6 +57,8 @@ export class CopyClickDirective {
|
|||||||
* <app-component [appCopyClick]="value to copy" showToast="info"/></app-component>
|
* <app-component [appCopyClick]="value to copy" showToast="info"/></app-component>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input() set showToast(value: ToastVariant | "") {
|
@Input() set showToast(value: ToastVariant | "") {
|
||||||
// When the `showToast` is set without a value, an empty string will be passed
|
// When the `showToast` is set without a value, an empty string will be passed
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@@ -60,15 +70,17 @@ export class CopyClickDirective {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@HostListener("click") onClick() {
|
@HostListener("click") onClick() {
|
||||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
const valueToCopy = this.valueToCopy();
|
||||||
|
this.platformUtilsService.copyToClipboard(valueToCopy);
|
||||||
|
|
||||||
if (this.copyListener) {
|
if (this.copyListener) {
|
||||||
this.copyListener.onCopy(this.valueToCopy);
|
this.copyListener.onCopy(valueToCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._showToast) {
|
if (this._showToast) {
|
||||||
const message = this.valueLabel
|
const valueLabel = this.valueLabel();
|
||||||
? this.i18nService.t("valueCopied", this.valueLabel)
|
const message = valueLabel
|
||||||
|
? this.i18nService.t("valueCopied", valueLabel)
|
||||||
: this.i18nService.t("copySuccessful");
|
: this.i18nService.t("copySuccessful");
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
cdkTrapFocus
|
cdkTrapFocus
|
||||||
cdkTrapFocusAutoCapture
|
cdkTrapFocusAutoCapture
|
||||||
>
|
>
|
||||||
@let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
|
@let showHeaderBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().top;
|
||||||
<header
|
<header
|
||||||
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid"
|
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
noMargin
|
noMargin
|
||||||
class="tw-text-main tw-mb-0 tw-truncate"
|
class="tw-text-main tw-mb-0 tw-truncate"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title() }}
|
||||||
@if (subtitle) {
|
@if (subtitle(); as subtitleText) {
|
||||||
<span class="tw-text-muted tw-font-normal tw-text-sm">
|
<span class="tw-text-muted tw-font-normal tw-text-sm">
|
||||||
{{ subtitle }}
|
{{ subtitleText }}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||||
@@ -46,12 +46,12 @@
|
|||||||
<div
|
<div
|
||||||
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
|
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-min-h-60': loading,
|
'tw-min-h-60': loading(),
|
||||||
'tw-bg-background': background === 'default',
|
'tw-bg-background': background() === 'default',
|
||||||
'tw-bg-background-alt': background === 'alt',
|
'tw-bg-background-alt': background() === 'alt',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@if (loading) {
|
@if (loading()) {
|
||||||
<div class="tw-absolute tw-flex tw-size-full tw-items-center tw-justify-center">
|
<div class="tw-absolute tw-flex tw-size-full tw-items-center tw-justify-center">
|
||||||
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
|
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,17 +59,17 @@
|
|||||||
<div
|
<div
|
||||||
cdkScrollable
|
cdkScrollable
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-p-4': !disablePadding && !isDrawer,
|
'tw-p-4': !disablePadding() && !isDrawer,
|
||||||
'tw-px-6 tw-py-4': !disablePadding && isDrawer,
|
'tw-px-6 tw-py-4': !disablePadding() && isDrawer,
|
||||||
'tw-overflow-y-auto': !loading,
|
'tw-overflow-y-auto': !loading(),
|
||||||
'tw-invisible tw-overflow-y-hidden': loading,
|
'tw-invisible tw-overflow-y-hidden': loading(),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ng-content select="[bitDialogContent]"></ng-content>
|
<ng-content select="[bitDialogContent]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
|
@let showFooterBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().bottom;
|
||||||
<footer
|
<footer
|
||||||
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background"
|
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background"
|
||||||
[ngClass]="[isDrawer ? 'tw-px-6 tw-py-4' : 'tw-p-4']"
|
[ngClass]="[isDrawer ? 'tw-px-6 tw-py-4' : 'tw-p-4']"
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
|
||||||
// @ts-strict-ignore
|
|
||||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
|
||||||
import { CdkScrollable } from "@angular/cdk/scrolling";
|
import { CdkScrollable } from "@angular/cdk/scrolling";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, HostBinding, Input, inject, viewChild } from "@angular/core";
|
import { Component, HostBinding, inject, viewChild, input, booleanAttribute } from "@angular/core";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
@@ -40,39 +37,32 @@ export class DialogComponent {
|
|||||||
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
|
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
|
||||||
|
|
||||||
/** Background color */
|
/** Background color */
|
||||||
@Input()
|
readonly background = input<"default" | "alt">("default");
|
||||||
background: "default" | "alt" = "default";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog size, more complex dialogs should use large, otherwise default is fine.
|
* Dialog size, more complex dialogs should use large, otherwise default is fine.
|
||||||
*/
|
*/
|
||||||
@Input() dialogSize: "small" | "default" | "large" = "default";
|
readonly dialogSize = input<"small" | "default" | "large">("default");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title to show in the dialog's header
|
* Title to show in the dialog's header
|
||||||
*/
|
*/
|
||||||
@Input() title: string;
|
readonly title = input<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subtitle to show in the dialog's header
|
* Subtitle to show in the dialog's header
|
||||||
*/
|
*/
|
||||||
@Input() subtitle: string;
|
readonly subtitle = input<string>();
|
||||||
|
|
||||||
private _disablePadding = false;
|
|
||||||
/**
|
/**
|
||||||
* Disable the built-in padding on the dialog, for use with tabbed dialogs.
|
* Disable the built-in padding on the dialog, for use with tabbed dialogs.
|
||||||
*/
|
*/
|
||||||
@Input() set disablePadding(value: boolean | "") {
|
readonly disablePadding = input(false, { transform: booleanAttribute });
|
||||||
this._disablePadding = coerceBooleanProperty(value);
|
|
||||||
}
|
|
||||||
get disablePadding() {
|
|
||||||
return this._disablePadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the dialog as loading which replaces the content with a spinner.
|
* Mark the dialog as loading which replaces the content with a spinner.
|
||||||
*/
|
*/
|
||||||
@Input() loading = false;
|
readonly loading = input(false);
|
||||||
|
|
||||||
@HostBinding("class") get classes() {
|
@HostBinding("class") get classes() {
|
||||||
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
|
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
|
||||||
@@ -94,7 +84,7 @@ export class DialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get width() {
|
get width() {
|
||||||
switch (this.dialogSize) {
|
switch (this.dialogSize()) {
|
||||||
case "small": {
|
case "small": {
|
||||||
return "md:tw-max-w-sm";
|
return "md:tw-max-w-sm";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { DialogRef } from "@angular/cdk/dialog";
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Directive, HostBinding, HostListener, Input, Optional } from "@angular/core";
|
import { Directive, HostBinding, HostListener, Optional, input } from "@angular/core";
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitDialogClose]",
|
selector: "[bitDialogClose]",
|
||||||
})
|
})
|
||||||
export class DialogCloseDirective {
|
export class DialogCloseDirective {
|
||||||
@Input("bitDialogClose") dialogResult: any;
|
readonly dialogResult = input<any>(undefined, { alias: "bitDialogClose" });
|
||||||
|
|
||||||
constructor(@Optional() public dialogRef: DialogRef) {}
|
constructor(@Optional() public dialogRef: DialogRef) {}
|
||||||
|
|
||||||
@@ -20,6 +20,6 @@ export class DialogCloseDirective {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogRef.close(this.dialogResult);
|
this.dialogRef.close(this.dialogResult());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CdkDialogContainer, DialogRef } from "@angular/cdk/dialog";
|
import { CdkDialogContainer, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
import { Directive, HostBinding, OnInit, Optional, input } from "@angular/core";
|
||||||
|
|
||||||
// Increments for each instance of this component
|
// Increments for each instance of this component
|
||||||
let nextId = 0;
|
let nextId = 0;
|
||||||
@@ -10,7 +10,7 @@ let nextId = 0;
|
|||||||
export class DialogTitleContainerDirective implements OnInit {
|
export class DialogTitleContainerDirective implements OnInit {
|
||||||
@HostBinding("id") id = `bit-dialog-title-${nextId++}`;
|
@HostBinding("id") id = `bit-dialog-title-${nextId++}`;
|
||||||
|
|
||||||
@Input() simple = false;
|
readonly simple = input(false);
|
||||||
|
|
||||||
constructor(@Optional() private dialogRef: DialogRef<any>) {}
|
constructor(@Optional() private dialogRef: DialogRef<any>) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, HostBinding, HostListener, Input } from "@angular/core";
|
import { Directive, HostBinding, HostListener, input } from "@angular/core";
|
||||||
|
|
||||||
import { DisclosureComponent } from "./disclosure.component";
|
import { DisclosureComponent } from "./disclosure.component";
|
||||||
|
|
||||||
@@ -12,17 +12,17 @@ export class DisclosureTriggerForDirective {
|
|||||||
/**
|
/**
|
||||||
* Accepts template reference for a bit-disclosure component instance
|
* Accepts template reference for a bit-disclosure component instance
|
||||||
*/
|
*/
|
||||||
@Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent;
|
readonly disclosure = input<DisclosureComponent>(undefined, { alias: "bitDisclosureTriggerFor" });
|
||||||
|
|
||||||
@HostBinding("attr.aria-expanded") get ariaExpanded() {
|
@HostBinding("attr.aria-expanded") get ariaExpanded() {
|
||||||
return this.disclosure.open;
|
return this.disclosure().open;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding("attr.aria-controls") get ariaControls() {
|
@HostBinding("attr.aria-controls") get ariaControls() {
|
||||||
return this.disclosure.id;
|
return this.disclosure().id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener("click") click() {
|
@HostListener("click") click() {
|
||||||
this.disclosure.open = !this.disclosure.open;
|
this.disclosure().open = !this.disclosure().open;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export class DisclosureComponent {
|
|||||||
/**
|
/**
|
||||||
* Optionally init the disclosure in its opened state
|
* Optionally init the disclosure in its opened state
|
||||||
*/
|
*/
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input({ transform: booleanAttribute }) set open(isOpen: boolean) {
|
@Input({ transform: booleanAttribute }) set open(isOpen: boolean) {
|
||||||
this._open = isOpen;
|
this._open = isOpen;
|
||||||
this.openChange.emit(isOpen);
|
this.openChange.emit(isOpen);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
|
||||||
import { NgClass } from "@angular/common";
|
import { NgClass } from "@angular/common";
|
||||||
import { Component, ContentChild, HostBinding, Input } from "@angular/core";
|
import { booleanAttribute, Component, ContentChild, HostBinding, input } from "@angular/core";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
@@ -17,30 +16,18 @@ import { BitFormControlAbstraction } from "./form-control.abstraction";
|
|||||||
imports: [NgClass, TypographyDirective, I18nPipe],
|
imports: [NgClass, TypographyDirective, I18nPipe],
|
||||||
})
|
})
|
||||||
export class FormControlComponent {
|
export class FormControlComponent {
|
||||||
@Input() label: string;
|
readonly label = input<string>();
|
||||||
|
|
||||||
private _inline = false;
|
readonly inline = input(false, { transform: booleanAttribute });
|
||||||
@Input() get inline() {
|
|
||||||
return this._inline;
|
|
||||||
}
|
|
||||||
set inline(value: boolean | "") {
|
|
||||||
this._inline = coerceBooleanProperty(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _disableMargin = false;
|
readonly disableMargin = input(false, { transform: booleanAttribute });
|
||||||
@Input() set disableMargin(value: boolean | "") {
|
|
||||||
this._disableMargin = coerceBooleanProperty(value);
|
|
||||||
}
|
|
||||||
get disableMargin() {
|
|
||||||
return this._disableMargin;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction;
|
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction;
|
||||||
|
|
||||||
@HostBinding("class") get classes() {
|
@HostBinding("class") get classes() {
|
||||||
return []
|
return []
|
||||||
.concat(this.inline ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
|
.concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
|
||||||
.concat(this.disableMargin ? [] : ["tw-mb-4"]);
|
.concat(this.disableMargin() ? [] : ["tw-mb-4"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, ElementRef, HostBinding, Input, Optional } from "@angular/core";
|
import { Component, ElementRef, HostBinding, input, Optional } from "@angular/core";
|
||||||
|
|
||||||
import { FormControlComponent } from "./form-control.component";
|
import { FormControlComponent } from "./form-control.component";
|
||||||
|
|
||||||
@@ -12,6 +12,10 @@ let nextId = 0;
|
|||||||
selector: "bit-label",
|
selector: "bit-label",
|
||||||
templateUrl: "label.component.html",
|
templateUrl: "label.component.html",
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
|
host: {
|
||||||
|
"[class]": "classList",
|
||||||
|
"[id]": "id()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class BitLabel {
|
export class BitLabel {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -19,15 +23,19 @@ export class BitLabel {
|
|||||||
@Optional() private parentFormControl: FormControlComponent,
|
@Optional() private parentFormControl: FormControlComponent,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@HostBinding("class") @Input() get classList() {
|
readonly classList = [
|
||||||
return ["tw-inline-flex", "tw-gap-1", "tw-items-baseline", "tw-flex-row", "tw-min-w-0"];
|
"tw-inline-flex",
|
||||||
}
|
"tw-gap-1",
|
||||||
|
"tw-items-baseline",
|
||||||
|
"tw-flex-row",
|
||||||
|
"tw-min-w-0",
|
||||||
|
];
|
||||||
|
|
||||||
@HostBinding("title") get title() {
|
@HostBinding("title") get title() {
|
||||||
return this.elementRef.nativeElement.textContent.trim();
|
return this.elementRef.nativeElement.textContent.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding() @Input() id = `bit-label-${nextId++}`;
|
readonly id = input(`bit-label-${nextId++}`);
|
||||||
|
|
||||||
get isInsideFormControl() {
|
get isInsideFormControl() {
|
||||||
return !!this.parentFormControl;
|
return !!this.parentFormControl;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, input } from "@angular/core";
|
||||||
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
|
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
@@ -18,11 +18,10 @@ import { I18nPipe } from "@bitwarden/ui-common";
|
|||||||
imports: [I18nPipe],
|
imports: [I18nPipe],
|
||||||
})
|
})
|
||||||
export class BitErrorSummary {
|
export class BitErrorSummary {
|
||||||
@Input()
|
readonly formGroup = input<UntypedFormGroup>();
|
||||||
formGroup: UntypedFormGroup;
|
|
||||||
|
|
||||||
get errorCount(): number {
|
get errorCount(): number {
|
||||||
return this.getErrorCount(this.formGroup);
|
return this.getErrorCount(this.formGroup());
|
||||||
}
|
}
|
||||||
|
|
||||||
get errorString() {
|
get errorString() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, HostBinding, Input } from "@angular/core";
|
import { Component, HostBinding, input } from "@angular/core";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
@@ -18,37 +18,38 @@ let nextId = 0;
|
|||||||
export class BitErrorComponent {
|
export class BitErrorComponent {
|
||||||
@HostBinding() id = `bit-error-${nextId++}`;
|
@HostBinding() id = `bit-error-${nextId++}`;
|
||||||
|
|
||||||
@Input() error: [string, any];
|
readonly error = input<[string, any]>();
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
get displayError() {
|
get displayError() {
|
||||||
switch (this.error[0]) {
|
const error = this.error();
|
||||||
|
switch (error[0]) {
|
||||||
case "required":
|
case "required":
|
||||||
return this.i18nService.t("inputRequired");
|
return this.i18nService.t("inputRequired");
|
||||||
case "email":
|
case "email":
|
||||||
return this.i18nService.t("inputEmail");
|
return this.i18nService.t("inputEmail");
|
||||||
case "minlength":
|
case "minlength":
|
||||||
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
|
return this.i18nService.t("inputMinLength", error[1]?.requiredLength);
|
||||||
case "maxlength":
|
case "maxlength":
|
||||||
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
|
return this.i18nService.t("inputMaxLength", error[1]?.requiredLength);
|
||||||
case "min":
|
case "min":
|
||||||
return this.i18nService.t("inputMinValue", this.error[1]?.min);
|
return this.i18nService.t("inputMinValue", error[1]?.min);
|
||||||
case "max":
|
case "max":
|
||||||
return this.i18nService.t("inputMaxValue", this.error[1]?.max);
|
return this.i18nService.t("inputMaxValue", error[1]?.max);
|
||||||
case "forbiddenCharacters":
|
case "forbiddenCharacters":
|
||||||
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
|
return this.i18nService.t("inputForbiddenCharacters", error[1]?.characters.join(", "));
|
||||||
case "multipleEmails":
|
case "multipleEmails":
|
||||||
return this.i18nService.t("multipleInputEmails");
|
return this.i18nService.t("multipleInputEmails");
|
||||||
case "trim":
|
case "trim":
|
||||||
return this.i18nService.t("inputTrimValidator");
|
return this.i18nService.t("inputTrimValidator");
|
||||||
default:
|
default:
|
||||||
// Attempt to show a custom error message.
|
// Attempt to show a custom error message.
|
||||||
if (this.error[1]?.message) {
|
if (error[1]?.message) {
|
||||||
return this.error[1]?.message;
|
return error[1]?.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
|
|
||||||
|
import { ModelSignal, Signal } from "@angular/core";
|
||||||
|
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
export type InputTypes =
|
export type InputTypes =
|
||||||
| "text"
|
| "text"
|
||||||
@@ -14,13 +17,13 @@ export type InputTypes =
|
|||||||
|
|
||||||
export abstract class BitFormFieldControl {
|
export abstract class BitFormFieldControl {
|
||||||
ariaDescribedBy: string;
|
ariaDescribedBy: string;
|
||||||
id: string;
|
id: Signal<string>;
|
||||||
labelForId: string;
|
labelForId: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
error: [string, any];
|
error: [string, any];
|
||||||
type?: InputTypes;
|
type?: ModelSignal<InputTypes>;
|
||||||
spellcheck?: boolean;
|
spellcheck?: ModelSignal<boolean | undefined>;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
focus?: () => void;
|
focus?: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
|
||||||
ViewChild,
|
ViewChild,
|
||||||
signal,
|
signal,
|
||||||
|
input,
|
||||||
|
Input,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
@@ -38,10 +39,11 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
|||||||
|
|
||||||
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
||||||
|
|
||||||
@Input({ transform: booleanAttribute })
|
readonly disableMargin = input(false, { transform: booleanAttribute });
|
||||||
disableMargin = false;
|
|
||||||
|
|
||||||
/** If `true`, remove the bottom border for `readonly` inputs */
|
/** If `true`, remove the bottom border for `readonly` inputs */
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Your application code writes to the input. This prevents migration.
|
||||||
@Input({ transform: booleanAttribute })
|
@Input({ transform: booleanAttribute })
|
||||||
disableReadOnlyBorder = false;
|
disableReadOnlyBorder = false;
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
|||||||
@HostBinding("class")
|
@HostBinding("class")
|
||||||
get classList() {
|
get classList() {
|
||||||
return ["tw-block"]
|
return ["tw-block"]
|
||||||
.concat(this.disableMargin ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
|
.concat(this.disableMargin() ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
|
||||||
.concat(this.readOnly ? [] : "tw-pt-2");
|
.concat(this.readOnly ? [] : "tw-pt-2");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -414,13 +414,18 @@ export const Select: Story = {
|
|||||||
|
|
||||||
export const AdvancedSelect: Story = {
|
export const AdvancedSelect: Story = {
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: {
|
||||||
|
formObj: fb.group({
|
||||||
|
select: "value1",
|
||||||
|
}),
|
||||||
|
...args,
|
||||||
|
},
|
||||||
template: /*html*/ `
|
template: /*html*/ `
|
||||||
<bit-form-field>
|
<bit-form-field [formGroup]="formObj">
|
||||||
<bit-label>Label</bit-label>
|
<bit-label>Label</bit-label>
|
||||||
<bit-select>
|
<bit-select formControlName="select">
|
||||||
<bit-option label="Select"></bit-option>
|
<bit-option label="Select" value="value1"></bit-option>
|
||||||
<bit-option label="Other"></bit-option>
|
<bit-option label="Other" value="value2"></bit-option>
|
||||||
</bit-select>
|
</bit-select>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
Host,
|
Host,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
model,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
Output,
|
Output,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
@@ -18,12 +18,15 @@ import { BitFormFieldComponent } from "./form-field.component";
|
|||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitPasswordInputToggle]",
|
selector: "[bitPasswordInputToggle]",
|
||||||
|
host: {
|
||||||
|
"[attr.aria-pressed]": "toggled()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
|
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
|
||||||
/**
|
/**
|
||||||
* Whether the input is toggled to show the password.
|
* Whether the input is toggled to show the password.
|
||||||
*/
|
*/
|
||||||
@HostBinding("attr.aria-pressed") @Input() toggled = false;
|
readonly toggled = model(false);
|
||||||
@Output() toggledChange = new EventEmitter<boolean>();
|
@Output() toggledChange = new EventEmitter<boolean>();
|
||||||
|
|
||||||
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
|
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
|
||||||
@@ -33,8 +36,8 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
|||||||
* Click handler to toggle the state of the input type.
|
* Click handler to toggle the state of the input type.
|
||||||
*/
|
*/
|
||||||
@HostListener("click") onClick() {
|
@HostListener("click") onClick() {
|
||||||
this.toggled = !this.toggled;
|
this.toggled.update((toggled) => !toggled);
|
||||||
this.toggledChange.emit(this.toggled);
|
this.toggledChange.emit(this.toggled());
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
@@ -46,7 +49,7 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
get icon() {
|
get icon() {
|
||||||
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
|
return this.toggled() ? "bwi-eye-slash" : "bwi-eye";
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
@@ -55,16 +58,16 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
|||||||
|
|
||||||
ngAfterContentInit(): void {
|
ngAfterContentInit(): void {
|
||||||
if (this.formField.input?.type) {
|
if (this.formField.input?.type) {
|
||||||
this.toggled = this.formField.input.type !== "password";
|
this.toggled.set(this.formField.input.type() !== "password");
|
||||||
}
|
}
|
||||||
this.button.icon = this.icon;
|
this.button.icon.set(this.icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.button.icon = this.icon;
|
this.button.icon.set(this.icon);
|
||||||
if (this.formField.input?.type != null) {
|
if (this.formField.input?.type != null) {
|
||||||
this.formField.input.type = this.toggled ? "text" : "password";
|
this.formField.input.type.set(this.toggled() ? "text" : "password");
|
||||||
this.formField.input.spellcheck = this.toggled ? false : undefined;
|
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,15 +60,15 @@ describe("PasswordInputToggle", () => {
|
|||||||
|
|
||||||
describe("initial state", () => {
|
describe("initial state", () => {
|
||||||
it("has correct icon", () => {
|
it("has correct icon", () => {
|
||||||
expect(button.icon).toBe("bwi-eye");
|
expect(button.icon()).toBe("bwi-eye");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("input is type password", () => {
|
it("input is type password", () => {
|
||||||
expect(input.type).toBe("password");
|
expect(input.type!()).toBe("password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("spellcheck is disabled", () => {
|
it("spellcheck is disabled", () => {
|
||||||
expect(input.spellcheck).toBe(undefined);
|
expect(input.spellcheck!()).toBe(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,15 +78,15 @@ describe("PasswordInputToggle", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("has correct icon", () => {
|
it("has correct icon", () => {
|
||||||
expect(button.icon).toBe("bwi-eye-slash");
|
expect(button.icon()).toBe("bwi-eye-slash");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("input is type text", () => {
|
it("input is type text", () => {
|
||||||
expect(input.type).toBe("text");
|
expect(input.type!()).toBe("text");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("spellcheck is disabled", () => {
|
it("spellcheck is disabled", () => {
|
||||||
expect(input.spellcheck).toBe(false);
|
expect(input.spellcheck!()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,15 +97,15 @@ describe("PasswordInputToggle", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("has correct icon", () => {
|
it("has correct icon", () => {
|
||||||
expect(button.icon).toBe("bwi-eye");
|
expect(button.icon()).toBe("bwi-eye");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("input is type password", () => {
|
it("input is type password", () => {
|
||||||
expect(input.type).toBe("password");
|
expect(input.type!()).toBe("password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("spellcheck is disabled", () => {
|
it("spellcheck is disabled", () => {
|
||||||
expect(input.spellcheck).toBe(undefined);
|
expect(input.spellcheck!()).toBe(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
import { Directive, OnInit, Optional } from "@angular/core";
|
||||||
|
|
||||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitPrefix]",
|
selector: "[bitPrefix]",
|
||||||
|
host: {
|
||||||
|
"[class]": "classList",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class BitPrefixDirective implements OnInit {
|
export class BitPrefixDirective implements OnInit {
|
||||||
@HostBinding("class") @Input() get classList() {
|
readonly classList = ["tw-text-muted"];
|
||||||
return ["tw-text-muted"];
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
|
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.iconButtonComponent) {
|
if (this.iconButtonComponent) {
|
||||||
this.iconButtonComponent.size = "small";
|
this.iconButtonComponent.size.set("small");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
import { Directive, OnInit, Optional } from "@angular/core";
|
||||||
|
|
||||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitSuffix]",
|
selector: "[bitSuffix]",
|
||||||
|
host: {
|
||||||
|
"[class]": "classList",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class BitSuffixDirective implements OnInit {
|
export class BitSuffixDirective implements OnInit {
|
||||||
@HostBinding("class") @Input() get classList() {
|
readonly classList = ["tw-text-muted"];
|
||||||
return ["tw-text-muted"];
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
|
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.iconButtonComponent) {
|
if (this.iconButtonComponent) {
|
||||||
this.iconButtonComponent.size = "small";
|
this.iconButtonComponent.size.set("small");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin"
|
class="bwi bwi-spinner bwi-spin"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
[ngClass]="{ 'bwi-lg': size === 'default' }"
|
[ngClass]="{ 'bwi-lg': size() === 'default' }"
|
||||||
></i>
|
></i>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
inject,
|
inject,
|
||||||
Input,
|
input,
|
||||||
model,
|
model,
|
||||||
Signal,
|
Signal,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
@@ -177,11 +177,11 @@ const sizes: Record<IconButtonSize, string[]> = {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
|
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
|
||||||
@Input("bitIconButton") icon: string;
|
readonly icon = model<string>(undefined, { alias: "bitIconButton" });
|
||||||
|
|
||||||
@Input() buttonType: IconButtonType = "main";
|
readonly buttonType = input<IconButtonType>("main");
|
||||||
|
|
||||||
@Input() size: IconButtonSize = "default";
|
readonly size = model<IconButtonSize>("default");
|
||||||
|
|
||||||
@HostBinding("class") get classList() {
|
@HostBinding("class") get classList() {
|
||||||
return [
|
return [
|
||||||
@@ -193,13 +193,15 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
|||||||
"hover:tw-no-underline",
|
"hover:tw-no-underline",
|
||||||
"focus:tw-outline-none",
|
"focus:tw-outline-none",
|
||||||
]
|
]
|
||||||
.concat(styles[this.buttonType])
|
.concat(styles[this.buttonType()])
|
||||||
.concat(sizes[this.size])
|
.concat(sizes[this.size()])
|
||||||
.concat(this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType] : []);
|
.concat(
|
||||||
|
this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType()] : [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get iconClass() {
|
get iconClass() {
|
||||||
return [this.icon, "!tw-m-0"];
|
return [this.icon(), "!tw-m-0"];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected disabledAttr = computed(() => {
|
protected disabledAttr = computed(() => {
|
||||||
@@ -219,7 +221,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
|||||||
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
||||||
});
|
});
|
||||||
|
|
||||||
loading = model(false);
|
readonly loading = model(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether it is appropriate to display a loading spinner. We only want to show
|
* Determine whether it is appropriate to display a loading spinner. We only want to show
|
||||||
@@ -237,7 +239,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
|||||||
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
|
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
|
||||||
);
|
);
|
||||||
|
|
||||||
disabled = model<boolean>(false);
|
readonly disabled = model<boolean>(false);
|
||||||
|
|
||||||
getFocusTarget() {
|
getFocusTarget() {
|
||||||
return this.elementRef.nativeElement;
|
return this.elementRef.nativeElement;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import { Component, effect, input } from "@angular/core";
|
||||||
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
|
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
|
||||||
|
|
||||||
import { Icon, isIcon } from "./icon";
|
import { Icon, isIcon } from "./icon";
|
||||||
@@ -6,8 +6,8 @@ import { Icon, isIcon } from "./icon";
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "bit-icon",
|
selector: "bit-icon",
|
||||||
host: {
|
host: {
|
||||||
"[attr.aria-hidden]": "!ariaLabel",
|
"[attr.aria-hidden]": "!ariaLabel()",
|
||||||
"[attr.aria-label]": "ariaLabel",
|
"[attr.aria-label]": "ariaLabel()",
|
||||||
"[innerHtml]": "innerHtml",
|
"[innerHtml]": "innerHtml",
|
||||||
},
|
},
|
||||||
template: ``,
|
template: ``,
|
||||||
@@ -15,16 +15,18 @@ import { Icon, isIcon } from "./icon";
|
|||||||
export class BitIconComponent {
|
export class BitIconComponent {
|
||||||
innerHtml: SafeHtml | null = null;
|
innerHtml: SafeHtml | null = null;
|
||||||
|
|
||||||
@Input() set icon(icon: Icon) {
|
readonly icon = input<Icon>();
|
||||||
if (!isIcon(icon)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = icon.svg;
|
readonly ariaLabel = input<string>();
|
||||||
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
|
|
||||||
|
constructor(private domSanitizer: DomSanitizer) {
|
||||||
|
effect(() => {
|
||||||
|
const icon = this.icon();
|
||||||
|
if (!isIcon(icon)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const svg = icon.svg;
|
||||||
|
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() ariaLabel: string | undefined = undefined;
|
|
||||||
|
|
||||||
constructor(private domSanitizer: DomSanitizer) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Icon, svgIcon } from "./icon";
|
|||||||
import { BitIconComponent } from "./icon.component";
|
import { BitIconComponent } from "./icon.component";
|
||||||
|
|
||||||
describe("IconComponent", () => {
|
describe("IconComponent", () => {
|
||||||
let component: BitIconComponent;
|
|
||||||
let fixture: ComponentFixture<BitIconComponent>;
|
let fixture: ComponentFixture<BitIconComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -13,14 +12,13 @@ describe("IconComponent", () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(BitIconComponent);
|
fixture = TestBed.createComponent(BitIconComponent);
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have empty innerHtml when input is not an Icon", () => {
|
it("should have empty innerHtml when input is not an Icon", () => {
|
||||||
const fakeIcon = { svg: "harmful user input" } as Icon;
|
const fakeIcon = { svg: "harmful user input" } as Icon;
|
||||||
|
|
||||||
component.icon = fakeIcon;
|
fixture.componentRef.setInput("icon", fakeIcon);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const el = fixture.nativeElement as HTMLElement;
|
const el = fixture.nativeElement as HTMLElement;
|
||||||
@@ -30,7 +28,7 @@ describe("IconComponent", () => {
|
|||||||
it("should contain icon when input is a safe Icon", () => {
|
it("should contain icon when input is a safe Icon", () => {
|
||||||
const icon = svgIcon`<svg><text x="0" y="15">safe icon</text></svg>`;
|
const icon = svgIcon`<svg><text x="0" y="15">safe icon</text></svg>`;
|
||||||
|
|
||||||
component.icon = icon;
|
fixture.componentRef.setInput("icon", icon);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const el = fixture.nativeElement as HTMLElement;
|
const el = fixture.nativeElement as HTMLElement;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { AfterContentChecked, Directive, ElementRef, Input, NgZone, Optional } from "@angular/core";
|
import {
|
||||||
|
AfterContentChecked,
|
||||||
|
booleanAttribute,
|
||||||
|
Directive,
|
||||||
|
ElementRef,
|
||||||
|
input,
|
||||||
|
NgZone,
|
||||||
|
Optional,
|
||||||
|
} from "@angular/core";
|
||||||
import { take } from "rxjs/operators";
|
import { take } from "rxjs/operators";
|
||||||
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@@ -21,11 +29,7 @@ import { FocusableElement } from "../shared/focusable-element";
|
|||||||
selector: "[appAutofocus], [bitAutofocus]",
|
selector: "[appAutofocus], [bitAutofocus]",
|
||||||
})
|
})
|
||||||
export class AutofocusDirective implements AfterContentChecked {
|
export class AutofocusDirective implements AfterContentChecked {
|
||||||
@Input() set appAutofocus(condition: boolean | string) {
|
readonly appAutofocus = input(undefined, { transform: booleanAttribute });
|
||||||
this.autofocus = condition === "" || condition === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private autofocus: boolean;
|
|
||||||
|
|
||||||
// Track if we have already focused the element.
|
// Track if we have already focused the element.
|
||||||
private focused = false;
|
private focused = false;
|
||||||
@@ -46,7 +50,7 @@ export class AutofocusDirective implements AfterContentChecked {
|
|||||||
*/
|
*/
|
||||||
ngAfterContentChecked() {
|
ngAfterContentChecked() {
|
||||||
// We only want to focus the element on initial render and it's not a mobile browser
|
// We only want to focus the element on initial render and it's not a mobile browser
|
||||||
if (this.focused || !this.autofocus || Utils.isMobileBrowser) {
|
if (this.focused || !this.appAutofocus() || Utils.isMobileBrowser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
libs/components/src/input/autofocus.mdx
Normal file
21
libs/components/src/input/autofocus.mdx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import * as stories from "./autofocus.stories";
|
||||||
|
|
||||||
|
<Meta of={stories} />
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { AutofocusDirective } from "@bitwarden/components";
|
||||||
|
```
|
||||||
|
|
||||||
|
<Title />
|
||||||
|
<Description />
|
||||||
|
|
||||||
|
<Primary />
|
||||||
|
<Controls />
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
The autofocus directive has accessibility implications, because it will steal focus from wherever it
|
||||||
|
would naturally be placed on page load. Please consider whether or not the user truly needs the
|
||||||
|
element truly needs to be manually focused on their behalf.
|
||||||
26
libs/components/src/input/autofocus.stories.ts
Normal file
26
libs/components/src/input/autofocus.stories.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { FormFieldModule } from "../form-field";
|
||||||
|
|
||||||
|
import { AutofocusDirective } from "./autofocus.directive";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Component Library/Form/Autofocus Directive",
|
||||||
|
component: AutofocusDirective,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [AutofocusDirective, FormFieldModule],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
export const AutofocusField: StoryObj = {
|
||||||
|
render: (args) => ({
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>Email</bit-label>
|
||||||
|
<input bitInput formControlName="email" appAutofocus />
|
||||||
|
</bit-form-field>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
NgZone,
|
NgZone,
|
||||||
Optional,
|
Optional,
|
||||||
Self,
|
Self,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { NgControl, Validators } from "@angular/forms";
|
import { NgControl, Validators } from "@angular/forms";
|
||||||
|
|
||||||
@@ -30,9 +32,15 @@ export function inputBorderClasses(error: boolean) {
|
|||||||
@Directive({
|
@Directive({
|
||||||
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
||||||
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
|
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
|
||||||
|
host: {
|
||||||
|
"[class]": "classList()",
|
||||||
|
"[id]": "id()",
|
||||||
|
"[attr.type]": "type()",
|
||||||
|
"[attr.spellcheck]": "spellcheck()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class BitInputDirective implements BitFormFieldControl {
|
export class BitInputDirective implements BitFormFieldControl {
|
||||||
@HostBinding("class") @Input() get classList() {
|
classList() {
|
||||||
const classes = [
|
const classes = [
|
||||||
"tw-block",
|
"tw-block",
|
||||||
"tw-w-full",
|
"tw-w-full",
|
||||||
@@ -52,7 +60,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
|||||||
return classes.filter((s) => s != "");
|
return classes.filter((s) => s != "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding() @Input() id = `bit-input-${nextId++}`;
|
readonly id = input(`bit-input-${nextId++}`);
|
||||||
|
|
||||||
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
||||||
|
|
||||||
@@ -60,10 +68,12 @@ export class BitInputDirective implements BitFormFieldControl {
|
|||||||
return this.hasError ? true : undefined;
|
return this.hasError ? true : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding("attr.type") @Input() type?: InputTypes;
|
readonly type = model<InputTypes>();
|
||||||
|
|
||||||
@HostBinding("attr.spellcheck") @Input() spellcheck?: boolean;
|
readonly spellcheck = model<boolean>();
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@HostBinding()
|
@HostBinding()
|
||||||
@Input()
|
@Input()
|
||||||
get required() {
|
get required() {
|
||||||
@@ -74,13 +84,13 @@ export class BitInputDirective implements BitFormFieldControl {
|
|||||||
}
|
}
|
||||||
private _required: boolean;
|
private _required: boolean;
|
||||||
|
|
||||||
@Input() hasPrefix = false;
|
readonly hasPrefix = input(false);
|
||||||
@Input() hasSuffix = false;
|
readonly hasSuffix = input(false);
|
||||||
|
|
||||||
@Input() showErrorsWhenDisabled? = false;
|
readonly showErrorsWhenDisabled = input<boolean>(false);
|
||||||
|
|
||||||
get labelForId(): string {
|
get labelForId(): string {
|
||||||
return this.id;
|
return this.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener("input")
|
@HostListener("input")
|
||||||
@@ -89,7 +99,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get hasError() {
|
get hasError() {
|
||||||
if (this.showErrorsWhenDisabled) {
|
if (this.showErrorsWhenDisabled()) {
|
||||||
return (
|
return (
|
||||||
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
|
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
|
||||||
this.ngControl?.touched &&
|
this.ngControl?.touched &&
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-truncate': truncate,
|
'tw-truncate': truncate(),
|
||||||
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
|
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
bitTypography="helper"
|
bitTypography="helper"
|
||||||
class="tw-text-muted tw-w-full"
|
class="tw-text-muted tw-w-full"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-truncate': truncate,
|
'tw-truncate': truncate(),
|
||||||
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
|
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ng-content select="[slot=secondary]"></ng-content>
|
<ng-content select="[slot=secondary]"></ng-content>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
Input,
|
|
||||||
signal,
|
signal,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
|
input,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { TypographyModule } from "../typography";
|
import { TypographyModule } from "../typography";
|
||||||
@@ -39,7 +39,7 @@ export class ItemContentComponent implements AfterContentChecked {
|
|||||||
*
|
*
|
||||||
* Default behavior is truncation.
|
* Default behavior is truncation.
|
||||||
*/
|
*/
|
||||||
@Input() truncate = true;
|
readonly truncate = input(true);
|
||||||
|
|
||||||
ngAfterContentChecked(): void {
|
ngAfterContentChecked(): void {
|
||||||
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
|
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { HostBinding, Directive, inject, ElementRef, input, booleanAttribute } from "@angular/core";
|
||||||
Input,
|
|
||||||
HostBinding,
|
|
||||||
Directive,
|
|
||||||
inject,
|
|
||||||
ElementRef,
|
|
||||||
input,
|
|
||||||
booleanAttribute,
|
|
||||||
} from "@angular/core";
|
|
||||||
|
|
||||||
import { ariaDisableElement } from "../utils";
|
import { ariaDisableElement } from "../utils";
|
||||||
|
|
||||||
@@ -77,8 +69,7 @@ const commonStyles = [
|
|||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
abstract class LinkDirective {
|
abstract class LinkDirective {
|
||||||
@Input()
|
readonly linkType = input<LinkType>("primary");
|
||||||
linkType: LinkType = "primary";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,7 +87,7 @@ export class AnchorLinkDirective extends LinkDirective {
|
|||||||
@HostBinding("class") get classList() {
|
@HostBinding("class") get classList() {
|
||||||
return ["before:-tw-inset-y-[0.125rem]"]
|
return ["before:-tw-inset-y-[0.125rem]"]
|
||||||
.concat(commonStyles)
|
.concat(commonStyles)
|
||||||
.concat(linkStyles[this.linkType] ?? []);
|
.concat(linkStyles[this.linkType()] ?? []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +102,7 @@ export class ButtonLinkDirective extends LinkDirective {
|
|||||||
@HostBinding("class") get classList() {
|
@HostBinding("class") get classList() {
|
||||||
return ["before:-tw-inset-y-[0.25rem]"]
|
return ["before:-tw-inset-y-[0.25rem]"]
|
||||||
.concat(commonStyles)
|
.concat(commonStyles)
|
||||||
.concat(linkStyles[this.linkType] ?? []);
|
.concat(linkStyles[this.linkType()] ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export class MenuItemDirective implements FocusableOption {
|
|||||||
return this.disabled || null; // native disabled attr must be null when false
|
return this.disabled || null; // native disabled attr must be null when false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// This input overrides a field from a superclass, while the superclass field
|
||||||
|
// is not migrated.
|
||||||
@Input({ transform: coerceBooleanProperty }) disabled?: boolean = false;
|
@Input({ transform: coerceBooleanProperty }) disabled?: boolean = false;
|
||||||
|
|
||||||
constructor(public elementRef: ElementRef<HTMLButtonElement>) {}
|
constructor(public elementRef: ElementRef<HTMLButtonElement>) {}
|
||||||
|
|||||||
@@ -8,26 +8,30 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
|
input,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { Observable, Subscription } from "rxjs";
|
import { Observable, Subscription } from "rxjs";
|
||||||
import { filter, mergeWith } from "rxjs/operators";
|
import { filter, mergeWith } from "rxjs/operators";
|
||||||
|
|
||||||
import { MenuComponent } from "./menu.component";
|
import { MenuComponent } from "./menu.component";
|
||||||
|
|
||||||
@Directive({ selector: "[bitMenuTriggerFor]", exportAs: "menuTrigger", standalone: true })
|
@Directive({
|
||||||
|
selector: "[bitMenuTriggerFor]",
|
||||||
|
exportAs: "menuTrigger",
|
||||||
|
standalone: true,
|
||||||
|
host: { "[attr.role]": "this.role()" },
|
||||||
|
})
|
||||||
export class MenuTriggerForDirective implements OnDestroy {
|
export class MenuTriggerForDirective implements OnDestroy {
|
||||||
@HostBinding("attr.aria-expanded") isOpen = false;
|
@HostBinding("attr.aria-expanded") isOpen = false;
|
||||||
@HostBinding("attr.aria-haspopup") get hasPopup(): "menu" | "dialog" {
|
@HostBinding("attr.aria-haspopup") get hasPopup(): "menu" | "dialog" {
|
||||||
return this.menu?.ariaRole || "menu";
|
return this.menu()?.ariaRole() || "menu";
|
||||||
}
|
}
|
||||||
@HostBinding("attr.role")
|
|
||||||
@Input()
|
|
||||||
role = "button";
|
|
||||||
|
|
||||||
@Input("bitMenuTriggerFor") menu: MenuComponent;
|
readonly role = input("button");
|
||||||
|
|
||||||
|
readonly menu = input<MenuComponent>(undefined, { alias: "bitMenuTriggerFor" });
|
||||||
|
|
||||||
private overlayRef: OverlayRef;
|
private overlayRef: OverlayRef;
|
||||||
private defaultMenuConfig: OverlayConfig = {
|
private defaultMenuConfig: OverlayConfig = {
|
||||||
@@ -66,14 +70,15 @@ export class MenuTriggerForDirective implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private openMenu() {
|
private openMenu() {
|
||||||
if (this.menu == null) {
|
const menu = this.menu();
|
||||||
|
if (menu == null) {
|
||||||
throw new Error("Cannot find bit-menu element");
|
throw new Error("Cannot find bit-menu element");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
|
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
|
||||||
|
|
||||||
const templatePortal = new TemplatePortal(this.menu.templateRef, this.viewContainerRef);
|
const templatePortal = new TemplatePortal(menu.templateRef, this.viewContainerRef);
|
||||||
this.overlayRef.attach(templatePortal);
|
this.overlayRef.attach(templatePortal);
|
||||||
|
|
||||||
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
|
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
|
||||||
@@ -90,11 +95,11 @@ export class MenuTriggerForDirective implements OnDestroy {
|
|||||||
}
|
}
|
||||||
this.destroyMenu();
|
this.destroyMenu();
|
||||||
});
|
});
|
||||||
if (this.menu.keyManager) {
|
if (menu.keyManager) {
|
||||||
this.menu.keyManager.setFirstItemActive();
|
menu.keyManager.setFirstItemActive();
|
||||||
this.keyDownEventsSub = this.overlayRef
|
this.keyDownEventsSub = this.overlayRef
|
||||||
.keydownEvents()
|
.keydownEvents()
|
||||||
.subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event));
|
.subscribe((event: KeyboardEvent) => this.menu().keyManager.onKeydown(event));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,19 +110,19 @@ export class MenuTriggerForDirective implements OnDestroy {
|
|||||||
|
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
this.disposeAll();
|
this.disposeAll();
|
||||||
this.menu.closed.emit();
|
this.menu().closed.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getClosedEvents(): Observable<any> {
|
private getClosedEvents(): Observable<any> {
|
||||||
const detachments = this.overlayRef.detachments();
|
const detachments = this.overlayRef.detachments();
|
||||||
const escKey = this.overlayRef.keydownEvents().pipe(
|
const escKey = this.overlayRef.keydownEvents().pipe(
|
||||||
filter((event: KeyboardEvent) => {
|
filter((event: KeyboardEvent) => {
|
||||||
const keys = this.menu.ariaRole === "menu" ? ["Escape", "Tab"] : ["Escape"];
|
const keys = this.menu().ariaRole() === "menu" ? ["Escape", "Tab"] : ["Escape"];
|
||||||
return keys.includes(event.key);
|
return keys.includes(event.key);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const backdrop = this.overlayRef.backdropClick();
|
const backdrop = this.overlayRef.backdropClick();
|
||||||
const menuClosed = this.menu.closed;
|
const menuClosed = this.menu().closed;
|
||||||
|
|
||||||
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
|
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<div
|
<div
|
||||||
(click)="closed.emit()"
|
(click)="closed.emit()"
|
||||||
class="tw-flex tw-shrink-0 tw-flex-col tw-rounded-lg tw-border tw-border-solid tw-border-secondary-500 tw-bg-background tw-bg-clip-padding tw-py-1 tw-overflow-y-auto"
|
class="tw-flex tw-shrink-0 tw-flex-col tw-rounded-lg tw-border tw-border-solid tw-border-secondary-500 tw-bg-background tw-bg-clip-padding tw-py-1 tw-overflow-y-auto"
|
||||||
[attr.role]="ariaRole"
|
[attr.role]="ariaRole()"
|
||||||
[attr.aria-label]="ariaLabel"
|
[attr.aria-label]="ariaLabel()"
|
||||||
cdkTrapFocus
|
cdkTrapFocus
|
||||||
[cdkTrapFocusAutoCapture]="ariaRole === 'dialog'"
|
[cdkTrapFocusAutoCapture]="ariaRole() === 'dialog'"
|
||||||
>
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ContentChildren,
|
ContentChildren,
|
||||||
QueryList,
|
QueryList,
|
||||||
AfterContentInit,
|
AfterContentInit,
|
||||||
Input,
|
input,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { MenuItemDirective } from "./menu-item.directive";
|
import { MenuItemDirective } from "./menu-item.directive";
|
||||||
@@ -28,12 +28,12 @@ export class MenuComponent implements AfterContentInit {
|
|||||||
menuItems: QueryList<MenuItemDirective>;
|
menuItems: QueryList<MenuItemDirective>;
|
||||||
keyManager?: FocusKeyManager<MenuItemDirective>;
|
keyManager?: FocusKeyManager<MenuItemDirective>;
|
||||||
|
|
||||||
@Input() ariaRole: "menu" | "dialog" = "menu";
|
readonly ariaRole = input<"menu" | "dialog">("menu");
|
||||||
|
|
||||||
@Input() ariaLabel: string;
|
readonly ariaLabel = input<string>();
|
||||||
|
|
||||||
ngAfterContentInit() {
|
ngAfterContentInit() {
|
||||||
if (this.ariaRole === "menu") {
|
if (this.ariaRole() === "menu") {
|
||||||
this.keyManager = new FocusKeyManager(this.menuItems)
|
this.keyManager = new FocusKeyManager(this.menuItems)
|
||||||
.withWrap()
|
.withWrap()
|
||||||
.skipPredicate((item) => item.disabled);
|
.skipPredicate((item) => item.disabled);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<ng-select
|
<ng-select
|
||||||
[items]="baseItems"
|
[items]="baseItems()"
|
||||||
[(ngModel)]="selectedItems"
|
[(ngModel)]="selectedItems"
|
||||||
(ngModelChange)="onChange($event)"
|
(ngModelChange)="onChange($event)"
|
||||||
(blur)="onBlur()"
|
(blur)="onBlur()"
|
||||||
bindLabel="listName"
|
bindLabel="listName"
|
||||||
groupBy="parentGrouping"
|
groupBy="parentGrouping"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder()"
|
||||||
[loading]="loading"
|
[loading]="loading()"
|
||||||
[loadingText]="loadingText"
|
[loadingText]="loadingText"
|
||||||
notFoundText="{{ 'multiSelectNotFound' | i18n }}"
|
notFoundText="{{ 'multiSelectNotFound' | i18n }}"
|
||||||
clearAllText="{{ 'multiSelectClearAll' | i18n }}"
|
clearAllText="{{ 'multiSelectClearAll' | i18n }}"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
|
||||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -12,6 +11,9 @@ import {
|
|||||||
HostBinding,
|
HostBinding,
|
||||||
Optional,
|
Optional,
|
||||||
Self,
|
Self,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
|
booleanAttribute,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import {
|
import {
|
||||||
ControlValueAccessor,
|
ControlValueAccessor,
|
||||||
@@ -38,6 +40,9 @@ let nextId = 0;
|
|||||||
templateUrl: "./multi-select.component.html",
|
templateUrl: "./multi-select.component.html",
|
||||||
providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }],
|
providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }],
|
||||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe],
|
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe],
|
||||||
|
host: {
|
||||||
|
"[id]": "this.id()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This component has been implemented to only support Multi-select list events
|
* This component has been implemented to only support Multi-select list events
|
||||||
@@ -46,12 +51,14 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
|||||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||||
|
|
||||||
// Parent component should only pass selectable items (complete list - selected items = baseItems)
|
// Parent component should only pass selectable items (complete list - selected items = baseItems)
|
||||||
@Input() baseItems: SelectItemView[];
|
readonly baseItems = model<SelectItemView[]>();
|
||||||
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
|
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
|
||||||
@Input() removeSelectedItems = false;
|
readonly removeSelectedItems = input(false);
|
||||||
@Input() placeholder: string;
|
readonly placeholder = model<string>();
|
||||||
@Input() loading = false;
|
readonly loading = input(false);
|
||||||
@Input({ transform: coerceBooleanProperty }) disabled?: boolean;
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Your application code writes to the input. This prevents migration.
|
||||||
|
@Input({ transform: booleanAttribute }) disabled?: boolean;
|
||||||
|
|
||||||
// Internal tracking of selected items
|
// Internal tracking of selected items
|
||||||
protected selectedItems: SelectItemView[];
|
protected selectedItems: SelectItemView[];
|
||||||
@@ -79,7 +86,9 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Default Text Values
|
// Default Text Values
|
||||||
this.placeholder = this.placeholder ?? this.i18nService.t("multiSelectPlaceholder");
|
this.placeholder.update(
|
||||||
|
(placeholder) => placeholder ?? this.i18nService.t("multiSelectPlaceholder"),
|
||||||
|
);
|
||||||
this.loadingText = this.i18nService.t("multiSelectLoading");
|
this.loadingText = this.i18nService.t("multiSelectLoading");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,15 +128,15 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
|||||||
this.onItemsConfirmed.emit(this.selectedItems);
|
this.onItemsConfirmed.emit(this.selectedItems);
|
||||||
|
|
||||||
// Remove selected items from base list based on input property
|
// Remove selected items from base list based on input property
|
||||||
if (this.removeSelectedItems) {
|
if (this.removeSelectedItems()) {
|
||||||
let updatedBaseItems = this.baseItems;
|
let updatedBaseItems = this.baseItems();
|
||||||
this.selectedItems.forEach((selectedItem) => {
|
this.selectedItems.forEach((selectedItem) => {
|
||||||
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
|
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset Lists
|
// Reset Lists
|
||||||
this.selectedItems = null;
|
this.selectedItems = null;
|
||||||
this.baseItems = updatedBaseItems;
|
this.baseItems.set(updatedBaseItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,9 +195,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**Implemented as part of BitFormFieldControl */
|
/**Implemented as part of BitFormFieldControl */
|
||||||
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
|
readonly id = input(`bit-multi-select-${nextId++}`);
|
||||||
|
|
||||||
/**Implemented as part of BitFormFieldControl */
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@HostBinding("attr.required")
|
@HostBinding("attr.required")
|
||||||
@Input()
|
@Input()
|
||||||
get required() {
|
get required() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
import { Directive, EventEmitter, Output, input } from "@angular/core";
|
||||||
import { RouterLink, RouterLinkActive } from "@angular/router";
|
import { RouterLink, RouterLinkActive } from "@angular/router";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,17 +11,17 @@ export abstract class NavBaseComponent {
|
|||||||
/**
|
/**
|
||||||
* Text to display in main content
|
* Text to display in main content
|
||||||
*/
|
*/
|
||||||
@Input() text: string;
|
readonly text = input<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `aria-label` for main content
|
* `aria-label` for main content
|
||||||
*/
|
*/
|
||||||
@Input() ariaLabel: string;
|
readonly ariaLabel = input<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional icon, e.g. `"bwi-collection-shared"`
|
* Optional icon, e.g. `"bwi-collection-shared"`
|
||||||
*/
|
*/
|
||||||
@Input() icon: string;
|
readonly icon = input<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional route to be passed to internal `routerLink`. If not provided, the nav component will render as a button.
|
* Optional route to be passed to internal `routerLink`. If not provided, the nav component will render as a button.
|
||||||
@@ -34,31 +34,31 @@ export abstract class NavBaseComponent {
|
|||||||
*
|
*
|
||||||
* See: {@link https://github.com/angular/angular/issues/24482}
|
* See: {@link https://github.com/angular/angular/issues/24482}
|
||||||
*/
|
*/
|
||||||
@Input() route?: RouterLink["routerLink"];
|
readonly route = input<RouterLink["routerLink"]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passed to internal `routerLink`
|
* Passed to internal `routerLink`
|
||||||
*
|
*
|
||||||
* See {@link RouterLink.relativeTo}
|
* See {@link RouterLink.relativeTo}
|
||||||
*/
|
*/
|
||||||
@Input() relativeTo?: RouterLink["relativeTo"];
|
readonly relativeTo = input<RouterLink["relativeTo"]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passed to internal `routerLink`
|
* Passed to internal `routerLink`
|
||||||
*
|
*
|
||||||
* See {@link RouterLinkActive.routerLinkActiveOptions}
|
* See {@link RouterLinkActive.routerLinkActiveOptions}
|
||||||
*/
|
*/
|
||||||
@Input() routerLinkActiveOptions?: RouterLinkActive["routerLinkActiveOptions"] = {
|
readonly routerLinkActiveOptions = input<RouterLinkActive["routerLinkActiveOptions"]>({
|
||||||
paths: "subset",
|
paths: "subset",
|
||||||
queryParams: "ignored",
|
queryParams: "ignored",
|
||||||
fragment: "ignored",
|
fragment: "ignored",
|
||||||
matrixParams: "ignored",
|
matrixParams: "ignored",
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `true`, do not change styles when nav item is active.
|
* If `true`, do not change styles when nav item is active.
|
||||||
*/
|
*/
|
||||||
@Input() hideActiveStyles = false;
|
readonly hideActiveStyles = input(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires when main content is clicked
|
* Fires when main content is clicked
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
<!-- This a higher order component that composes `NavItemComponent` -->
|
<!-- This a higher order component that composes `NavItemComponent` -->
|
||||||
@if (!hideIfEmpty || nestedNavComponents.length > 0) {
|
@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
|
||||||
<bit-nav-item
|
<bit-nav-item
|
||||||
[text]="text"
|
[text]="text()"
|
||||||
[icon]="icon"
|
[icon]="icon()"
|
||||||
[route]="route"
|
[route]="route()"
|
||||||
[relativeTo]="relativeTo"
|
[relativeTo]="relativeTo()"
|
||||||
[routerLinkActiveOptions]="routerLinkActiveOptions"
|
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
||||||
(mainContentClicked)="handleMainContentClicked()"
|
(mainContentClicked)="handleMainContentClicked()"
|
||||||
[ariaLabel]="ariaLabel"
|
[ariaLabel]="ariaLabel()"
|
||||||
[hideActiveStyles]="parentHideActiveStyles"
|
[hideActiveStyles]="parentHideActiveStyles"
|
||||||
>
|
>
|
||||||
<ng-template #button>
|
<ng-template #button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tw-ms-auto"
|
class="tw-ms-auto"
|
||||||
[bitIconButton]="open ? 'bwi-angle-up' : 'bwi-angle-down'"
|
[bitIconButton]="open() ? 'bwi-angle-up' : 'bwi-angle-down'"
|
||||||
[buttonType]="'light'"
|
[buttonType]="'light'"
|
||||||
(click)="toggle($event)"
|
(click)="toggle($event)"
|
||||||
size="small"
|
size="small"
|
||||||
[title]="'toggleCollapse' | i18n"
|
[title]="'toggleCollapse' | i18n"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
[attr.aria-expanded]="open.toString()"
|
[attr.aria-expanded]="open().toString()"
|
||||||
[attr.aria-controls]="contentId"
|
[attr.aria-controls]="contentId"
|
||||||
[attr.aria-label]="['toggleCollapse' | i18n, text].join(' ')"
|
[attr.aria-label]="['toggleCollapse' | i18n, text()].join(' ')"
|
||||||
></button>
|
></button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-container slot="end">
|
<ng-container slot="end">
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
</bit-nav-item>
|
</bit-nav-item>
|
||||||
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
||||||
@if (sideNavService.open$ | async) {
|
@if (sideNavService.open$ | async) {
|
||||||
@if (open) {
|
@if (open()) {
|
||||||
<div
|
<div
|
||||||
[attr.id]="contentId"
|
[attr.id]="contentId"
|
||||||
[attr.aria-label]="[text, 'submenu' | i18n].join(' ')"
|
[attr.aria-label]="[text(), 'submenu' | i18n].join(' ')"
|
||||||
role="group"
|
role="group"
|
||||||
>
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ContentChildren,
|
ContentChildren,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
|
||||||
Optional,
|
Optional,
|
||||||
Output,
|
Output,
|
||||||
QueryList,
|
QueryList,
|
||||||
SkipSelf,
|
SkipSelf,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
@@ -36,7 +37,7 @@ export class NavGroupComponent extends NavBaseComponent {
|
|||||||
|
|
||||||
/** When the side nav is open, the parent nav item should not show active styles when open. */
|
/** When the side nav is open, the parent nav item should not show active styles when open. */
|
||||||
protected get parentHideActiveStyles(): boolean {
|
protected get parentHideActiveStyles(): boolean {
|
||||||
return this.hideActiveStyles || (this.open && this.sideNavService.open);
|
return this.hideActiveStyles() || (this.open() && this.sideNavService.open);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,14 +48,12 @@ export class NavGroupComponent extends NavBaseComponent {
|
|||||||
/**
|
/**
|
||||||
* Is `true` if the expanded content is visible
|
* Is `true` if the expanded content is visible
|
||||||
*/
|
*/
|
||||||
@Input()
|
readonly open = model(false);
|
||||||
open = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically hide the nav group if there are no child buttons
|
* Automatically hide the nav group if there are no child buttons
|
||||||
*/
|
*/
|
||||||
@Input({ transform: booleanAttribute })
|
readonly hideIfEmpty = input(false, { transform: booleanAttribute });
|
||||||
hideIfEmpty = false;
|
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
openChange = new EventEmitter<boolean>();
|
openChange = new EventEmitter<boolean>();
|
||||||
@@ -67,24 +66,24 @@ export class NavGroupComponent extends NavBaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setOpen(isOpen: boolean) {
|
setOpen(isOpen: boolean) {
|
||||||
this.open = isOpen;
|
this.open.set(isOpen);
|
||||||
this.openChange.emit(this.open);
|
this.openChange.emit(this.open());
|
||||||
// FIXME: Remove when updating file. Eslint update
|
// FIXME: Remove when updating file. Eslint update
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
this.open && this.parentNavGroup?.setOpen(this.open);
|
this.open() && this.parentNavGroup?.setOpen(this.open());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toggle(event?: MouseEvent) {
|
protected toggle(event?: MouseEvent) {
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
this.setOpen(!this.open);
|
this.setOpen(!this.open());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleMainContentClicked() {
|
protected handleMainContentClicked() {
|
||||||
if (!this.sideNavService.open) {
|
if (!this.sideNavService.open) {
|
||||||
if (!this.route) {
|
if (!this.route()) {
|
||||||
this.sideNavService.setOpen();
|
this.sideNavService.setOpen();
|
||||||
}
|
}
|
||||||
this.open = true;
|
this.open.set(true);
|
||||||
} else {
|
} else {
|
||||||
this.toggle();
|
this.toggle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tw-ps-2 tw-pe-2">
|
<div class="tw-ps-2 tw-pe-2">
|
||||||
@let open = sideNavService.open$ | async;
|
@let open = sideNavService.open$ | async;
|
||||||
@if (open || icon) {
|
@if (open || icon()) {
|
||||||
<div
|
<div
|
||||||
class="tw-relative tw-rounded-md tw-h-10"
|
class="tw-relative tw-rounded-md tw-h-10"
|
||||||
[ngClass]="[
|
[ngClass]="[
|
||||||
@@ -16,17 +16,17 @@
|
|||||||
<!-- Main content of `NavItem` -->
|
<!-- Main content of `NavItem` -->
|
||||||
<ng-template #anchorAndButtonContent>
|
<ng-template #anchorAndButtonContent>
|
||||||
<div
|
<div
|
||||||
[title]="text"
|
[title]="text()"
|
||||||
class="tw-gap-2 tw-items-center tw-font-bold tw-h-full tw-content-center"
|
class="tw-gap-2 tw-items-center tw-font-bold tw-h-full tw-content-center"
|
||||||
[ngClass]="{ 'tw-text-center': !open, 'tw-flex': open }"
|
[ngClass]="{ 'tw-text-center': !open, 'tw-flex': open }"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-alt2 {{ icon }}"
|
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-alt2 {{ icon() }}"
|
||||||
[attr.aria-hidden]="open"
|
[attr.aria-hidden]="open"
|
||||||
[attr.aria-label]="text"
|
[attr.aria-label]="text()"
|
||||||
></i>
|
></i>
|
||||||
@if (open) {
|
@if (open) {
|
||||||
<span class="tw-truncate">{{ text }}</span>
|
<span class="tw-truncate">{{ text() }}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -38,11 +38,11 @@
|
|||||||
<a
|
<a
|
||||||
class="tw-size-full tw-px-3 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
|
class="tw-size-full tw-px-3 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
|
||||||
data-fvw
|
data-fvw
|
||||||
[routerLink]="route"
|
[routerLink]="route()"
|
||||||
[relativeTo]="relativeTo"
|
[relativeTo]="relativeTo()"
|
||||||
[attr.aria-label]="ariaLabel || text"
|
[attr.aria-label]="ariaLabel() || text()"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
[routerLinkActiveOptions]="routerLinkActiveOptions"
|
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
||||||
[ariaCurrentWhenActive]="'page'"
|
[ariaCurrentWhenActive]="'page'"
|
||||||
(isActiveChange)="setIsActive($event)"
|
(isActiveChange)="setIsActive($event)"
|
||||||
(click)="mainContentClicked.emit()"
|
(click)="mainContentClicked.emit()"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, HostListener, Input, Optional } from "@angular/core";
|
import { Component, HostListener, Optional, input } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
import { BehaviorSubject, map } from "rxjs";
|
import { BehaviorSubject, map } from "rxjs";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ export abstract class NavGroupAbstraction {
|
|||||||
})
|
})
|
||||||
export class NavItemComponent extends NavBaseComponent {
|
export class NavItemComponent extends NavBaseComponent {
|
||||||
/** Forces active styles to be shown, regardless of the `routerLinkActiveOptions` */
|
/** Forces active styles to be shown, regardless of the `routerLinkActiveOptions` */
|
||||||
@Input() forceActiveStyles? = false;
|
readonly forceActiveStyles = input<boolean>(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is `true` if `to` matches the current route
|
* Is `true` if `to` matches the current route
|
||||||
@@ -34,7 +34,7 @@ export class NavItemComponent extends NavBaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected get showActiveStyles() {
|
protected get showActiveStyles() {
|
||||||
return this.forceActiveStyles || (this._isActive && !this.hideActiveStyles);
|
return this.forceActiveStyles() || (this._isActive && !this.hideActiveStyles());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
class="tw-px-2 tw-pt-5"
|
class="tw-px-2 tw-pt-5"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
[routerLink]="route"
|
[routerLink]="route()"
|
||||||
class="tw-p-3 tw-block tw-rounded-md tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2 hover:tw-bg-hover-contrast [&_path]:tw-fill-text-alt2"
|
class="tw-p-3 tw-block tw-rounded-md tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2 hover:tw-bg-hover-contrast [&_path]:tw-fill-text-alt2"
|
||||||
[ngClass]="{ '[&_svg]:tw-w-[1.687rem]': !sideNavService.open }"
|
[ngClass]="{ '[&_svg]:tw-w-[1.687rem]': !sideNavService.open }"
|
||||||
[attr.aria-label]="label"
|
[attr.aria-label]="label()"
|
||||||
[title]="label"
|
[title]="label()"
|
||||||
routerLinkActive
|
routerLinkActive
|
||||||
[ariaCurrentWhenActive]="'page'"
|
[ariaCurrentWhenActive]="'page'"
|
||||||
>
|
>
|
||||||
<bit-icon [icon]="sideNavService.open ? openIcon : closedIcon"></bit-icon>
|
<bit-icon [icon]="sideNavService.open ? openIcon() : closedIcon()"></bit-icon>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, input } from "@angular/core";
|
||||||
import { RouterLinkActive, RouterLink } from "@angular/router";
|
import { RouterLinkActive, RouterLink } from "@angular/router";
|
||||||
|
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@@ -17,18 +18,18 @@ import { SideNavService } from "./side-nav.service";
|
|||||||
})
|
})
|
||||||
export class NavLogoComponent {
|
export class NavLogoComponent {
|
||||||
/** Icon that is displayed when the side nav is closed */
|
/** Icon that is displayed when the side nav is closed */
|
||||||
@Input() closedIcon = BitwardenShield;
|
readonly closedIcon = input(BitwardenShield);
|
||||||
|
|
||||||
/** Icon that is displayed when the side nav is open */
|
/** Icon that is displayed when the side nav is open */
|
||||||
@Input({ required: true }) openIcon: Icon;
|
readonly openIcon = input.required<Icon>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to be passed to internal `routerLink`
|
* Route to be passed to internal `routerLink`
|
||||||
*/
|
*/
|
||||||
@Input({ required: true }) route: string | any[];
|
readonly route = input.required<string | any[]>();
|
||||||
|
|
||||||
/** Passed to `attr.aria-label` and `attr.title` */
|
/** Passed to `attr.aria-label` and `attr.title` */
|
||||||
@Input({ required: true }) label: string;
|
readonly label = input.required<string>();
|
||||||
|
|
||||||
constructor(protected sideNavService: SideNavService) {}
|
constructor(protected sideNavService: SideNavService) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
class="tw-fixed md:tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
|
class="tw-fixed md:tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
|
||||||
[ngClass]="{ 'tw-w-60': data.open }"
|
[ngClass]="{ 'tw-w-60': data.open }"
|
||||||
[ngStyle]="
|
[ngStyle]="
|
||||||
variant === 'secondary' && {
|
variant() === 'secondary' && {
|
||||||
'--color-text-alt2': 'var(--color-text-main)',
|
'--color-text-alt2': 'var(--color-text-main)',
|
||||||
'--color-background-alt3': 'var(--color-secondary-100)',
|
'--color-background-alt3': 'var(--color-secondary-100)',
|
||||||
'--color-background-alt4': 'var(--color-secondary-300)',
|
'--color-background-alt4': 'var(--color-secondary-300)',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
import { Component, ElementRef, ViewChild, input } from "@angular/core";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export type SideNavVariant = "primary" | "secondary";
|
|||||||
imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe],
|
imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe],
|
||||||
})
|
})
|
||||||
export class SideNavComponent {
|
export class SideNavComponent {
|
||||||
@Input() variant: SideNavVariant = "primary";
|
readonly variant = input<SideNavVariant>("primary");
|
||||||
|
|
||||||
@ViewChild("toggleButton", { read: ElementRef, static: true })
|
@ViewChild("toggleButton", { read: ElementRef, static: true })
|
||||||
private toggleButton: ElementRef<HTMLButtonElement>;
|
private toggleButton: ElementRef<HTMLButtonElement>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-6">
|
<div class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-6">
|
||||||
<div class="tw-max-w-sm tw-flex tw-flex-col tw-items-center">
|
<div class="tw-max-w-sm tw-flex tw-flex-col tw-items-center">
|
||||||
<bit-icon [icon]="icon" aria-hidden="true"></bit-icon>
|
<bit-icon [icon]="icon()" aria-hidden="true"></bit-icon>
|
||||||
<h3 class="tw-font-semibold tw-text-center tw-mt-4">
|
<h3 class="tw-font-semibold tw-text-center tw-mt-4">
|
||||||
<ng-content select="[slot=title]"></ng-content>
|
<ng-content select="[slot=title]"></ng-content>
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import { Component, input } from "@angular/core";
|
||||||
|
|
||||||
import { Icons } from "..";
|
import { Icons } from "..";
|
||||||
import { BitIconComponent } from "../icon/icon.component";
|
import { BitIconComponent } from "../icon/icon.component";
|
||||||
@@ -12,5 +12,5 @@ import { BitIconComponent } from "../icon/icon.component";
|
|||||||
imports: [BitIconComponent],
|
imports: [BitIconComponent],
|
||||||
})
|
})
|
||||||
export class NoItemsComponent {
|
export class NoItemsComponent {
|
||||||
@Input() icon = Icons.Search;
|
readonly icon = input(Icons.Search);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import {
|
|||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
Directive,
|
Directive,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
|
||||||
HostListener,
|
HostListener,
|
||||||
Input,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
|
input,
|
||||||
|
model,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { Observable, Subscription, filter, mergeWith } from "rxjs";
|
import { Observable, Subscription, filter, mergeWith } from "rxjs";
|
||||||
|
|
||||||
@@ -20,27 +20,26 @@ import { PopoverComponent } from "./popover.component";
|
|||||||
@Directive({
|
@Directive({
|
||||||
selector: "[bitPopoverTriggerFor]",
|
selector: "[bitPopoverTriggerFor]",
|
||||||
exportAs: "popoverTrigger",
|
exportAs: "popoverTrigger",
|
||||||
|
host: {
|
||||||
|
"[attr.aria-expanded]": "this.popoverOpen()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||||
@Input()
|
readonly popoverOpen = model(false);
|
||||||
@HostBinding("attr.aria-expanded")
|
|
||||||
popoverOpen = false;
|
|
||||||
|
|
||||||
@Input("bitPopoverTriggerFor")
|
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" });
|
||||||
popover: PopoverComponent;
|
|
||||||
|
|
||||||
@Input("position")
|
readonly position = input<string>();
|
||||||
position: string;
|
|
||||||
|
|
||||||
private overlayRef: OverlayRef;
|
private overlayRef: OverlayRef;
|
||||||
private closedEventsSub: Subscription;
|
private closedEventsSub: Subscription;
|
||||||
|
|
||||||
get positions() {
|
get positions() {
|
||||||
if (!this.position) {
|
if (!this.position()) {
|
||||||
return defaultPositions;
|
return defaultPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferredPosition = defaultPositions.find((position) => position.id === this.position);
|
const preferredPosition = defaultPositions.find((position) => position.id === this.position());
|
||||||
|
|
||||||
if (preferredPosition) {
|
if (preferredPosition) {
|
||||||
return [preferredPosition, ...defaultPositions];
|
return [preferredPosition, ...defaultPositions];
|
||||||
@@ -72,7 +71,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
@HostListener("click")
|
@HostListener("click")
|
||||||
togglePopover() {
|
togglePopover() {
|
||||||
if (this.popoverOpen) {
|
if (this.popoverOpen()) {
|
||||||
this.closePopover();
|
this.closePopover();
|
||||||
} else {
|
} else {
|
||||||
this.openPopover();
|
this.openPopover();
|
||||||
@@ -80,10 +79,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private openPopover() {
|
private openPopover() {
|
||||||
this.popoverOpen = true;
|
this.popoverOpen.set(true);
|
||||||
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
|
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
|
||||||
|
|
||||||
const templatePortal = new TemplatePortal(this.popover.templateRef, this.viewContainerRef);
|
const templatePortal = new TemplatePortal(this.popover().templateRef, this.viewContainerRef);
|
||||||
|
|
||||||
this.overlayRef.attach(templatePortal);
|
this.overlayRef.attach(templatePortal);
|
||||||
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
|
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
|
||||||
@@ -97,17 +96,17 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
|||||||
.keydownEvents()
|
.keydownEvents()
|
||||||
.pipe(filter((event: KeyboardEvent) => event.key === "Escape"));
|
.pipe(filter((event: KeyboardEvent) => event.key === "Escape"));
|
||||||
const backdrop = this.overlayRef.backdropClick();
|
const backdrop = this.overlayRef.backdropClick();
|
||||||
const popoverClosed = this.popover.closed;
|
const popoverClosed = this.popover().closed;
|
||||||
|
|
||||||
return detachments.pipe(mergeWith(escKey, backdrop, popoverClosed));
|
return detachments.pipe(mergeWith(escKey, backdrop, popoverClosed));
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroyPopover() {
|
private destroyPopover() {
|
||||||
if (this.overlayRef == null || !this.popoverOpen) {
|
if (this.overlayRef == null || !this.popoverOpen()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popoverOpen = false;
|
this.popoverOpen.set(false);
|
||||||
this.disposeAll();
|
this.disposeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
if (this.popoverOpen) {
|
if (this.popoverOpen()) {
|
||||||
this.openPopover();
|
this.openPopover();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
>
|
>
|
||||||
<div class="tw-mb-1 tw-me-2 tw-flex tw-items-start tw-justify-between tw-gap-4 tw-ps-4">
|
<div class="tw-mb-1 tw-me-2 tw-flex tw-items-start tw-justify-between tw-gap-4 tw-ps-4">
|
||||||
<h2 bitTypography="h5" class="tw-mt-1 tw-font-semibold">
|
<h2 bitTypography="h5" class="tw-mt-1 tw-font-semibold">
|
||||||
{{ title }}
|
{{ title() }}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { A11yModule } from "@angular/cdk/a11y";
|
import { A11yModule } from "@angular/cdk/a11y";
|
||||||
import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core";
|
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
|
||||||
|
|
||||||
import { IconButtonModule } from "../icon-button/icon-button.module";
|
import { IconButtonModule } from "../icon-button/icon-button.module";
|
||||||
import { SharedModule } from "../shared/shared.module";
|
import { SharedModule } from "../shared/shared.module";
|
||||||
@@ -15,6 +15,6 @@ import { TypographyModule } from "../typography";
|
|||||||
})
|
})
|
||||||
export class PopoverComponent {
|
export class PopoverComponent {
|
||||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||||
@Input() title = "";
|
readonly title = input("");
|
||||||
@Output() closed = new EventEmitter();
|
@Output() closed = new EventEmitter();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
attr.aria-valuenow="{{ barWidth }}"
|
attr.aria-valuenow="{{ barWidth() }}"
|
||||||
[ngStyle]="{ width: barWidth + '%' }"
|
[ngStyle]="{ width: barWidth() + '%' }"
|
||||||
>
|
>
|
||||||
@if (displayText) {
|
@if (displayText) {
|
||||||
<div class="tw-flex tw-h-full tw-flex-wrap tw-items-center tw-overflow-hidden">
|
<div class="tw-flex tw-h-full tw-flex-wrap tw-items-center tw-overflow-hidden">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, input } from "@angular/core";
|
||||||
|
|
||||||
type ProgressSizeType = "small" | "default" | "large";
|
type ProgressSizeType = "small" | "default" | "large";
|
||||||
type BackgroundType = "danger" | "primary" | "success" | "warning";
|
type BackgroundType = "danger" | "primary" | "success" | "warning";
|
||||||
@@ -26,19 +26,19 @@ const BackgroundClasses: Record<BackgroundType, string[]> = {
|
|||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
})
|
})
|
||||||
export class ProgressComponent {
|
export class ProgressComponent {
|
||||||
@Input() barWidth = 0;
|
readonly barWidth = input(0);
|
||||||
@Input() bgColor: BackgroundType = "primary";
|
readonly bgColor = input<BackgroundType>("primary");
|
||||||
@Input() showText = true;
|
readonly showText = input(true);
|
||||||
@Input() size: ProgressSizeType = "default";
|
readonly size = input<ProgressSizeType>("default");
|
||||||
@Input() text?: string;
|
readonly text = input<string>();
|
||||||
|
|
||||||
get displayText() {
|
get displayText() {
|
||||||
return this.showText && this.size !== "small";
|
return this.showText() && this.size() !== "small";
|
||||||
}
|
}
|
||||||
|
|
||||||
get outerBarStyles() {
|
get outerBarStyles() {
|
||||||
return ["tw-overflow-hidden", "tw-rounded", "tw-bg-secondary-100"].concat(
|
return ["tw-overflow-hidden", "tw-rounded", "tw-bg-secondary-100"].concat(
|
||||||
SizeClasses[this.size],
|
SizeClasses[this.size()],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +53,11 @@ export class ProgressComponent {
|
|||||||
"tw-text-contrast",
|
"tw-text-contrast",
|
||||||
"tw-transition-all",
|
"tw-transition-all",
|
||||||
]
|
]
|
||||||
.concat(SizeClasses[this.size])
|
.concat(SizeClasses[this.size()])
|
||||||
.concat(BackgroundClasses[this.bgColor]);
|
.concat(BackgroundClasses[this.bgColor()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
get textContent() {
|
get textContent() {
|
||||||
return this.text || this.barWidth + "%";
|
return this.text() || this.barWidth() + "%";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
type="radio"
|
type="radio"
|
||||||
bitRadio
|
bitRadio
|
||||||
[id]="inputId"
|
[id]="inputId"
|
||||||
[disabled]="groupDisabled || disabled"
|
[disabled]="groupDisabled || disabled()"
|
||||||
[value]="value"
|
[value]="value()"
|
||||||
[checked]="selected"
|
[checked]="selected"
|
||||||
(change)="onInputChange()"
|
(change)="onInputChange()"
|
||||||
(blur)="onBlur()"
|
(blur)="onBlur()"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, DebugElement } from "@angular/core";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
import { By } from "@angular/platform-browser";
|
import { By } from "@angular/platform-browser";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -10,70 +11,62 @@ import { RadioButtonModule } from "./radio-button.module";
|
|||||||
import { RadioGroupComponent } from "./radio-group.component";
|
import { RadioGroupComponent } from "./radio-group.component";
|
||||||
|
|
||||||
describe("RadioButton", () => {
|
describe("RadioButton", () => {
|
||||||
let mockGroupComponent: MockedButtonGroupComponent;
|
let fixture: ComponentFixture<TestComponent>;
|
||||||
let fixture: ComponentFixture<TestApp>;
|
let radioButtonGroup: RadioGroupComponent;
|
||||||
let testAppComponent: TestApp;
|
let radioButtons: DebugElement[];
|
||||||
let radioButton: HTMLInputElement;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockGroupComponent = new MockedButtonGroupComponent();
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TestApp],
|
imports: [TestComponent],
|
||||||
providers: [
|
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
|
||||||
{ provide: RadioGroupComponent, useValue: mockGroupComponent },
|
|
||||||
{ provide: I18nService, useValue: new I18nMockService({}) },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await TestBed.compileComponents();
|
await TestBed.compileComponents();
|
||||||
fixture = TestBed.createComponent(TestApp);
|
fixture = TestBed.createComponent(TestComponent);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
testAppComponent = fixture.debugElement.componentInstance;
|
radioButtonGroup = fixture.debugElement.query(
|
||||||
radioButton = fixture.debugElement.query(By.css("input[type=radio]")).nativeElement;
|
By.directive(RadioGroupComponent),
|
||||||
|
).componentInstance;
|
||||||
|
radioButtons = fixture.debugElement.queryAll(By.css("input[type=radio]"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit value when clicking on radio button", () => {
|
it("should emit value when clicking on radio button", () => {
|
||||||
testAppComponent.value = "value";
|
const spyFn = jest.spyOn(radioButtonGroup, "onInputChange");
|
||||||
|
|
||||||
|
radioButtons[1].triggerEventHandler("change");
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
radioButton.click();
|
expect(spyFn).toHaveBeenCalledWith(1);
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect(mockGroupComponent.onInputChange).toHaveBeenCalledWith("value");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should check radio button when selected matches value", () => {
|
it("should check radio button only when selected matches value", () => {
|
||||||
testAppComponent.value = "value";
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
mockGroupComponent.selected = "value";
|
expect(radioButtons[0].nativeElement.checked).toBe(true);
|
||||||
|
expect(radioButtons[1].nativeElement.checked).toBe(false);
|
||||||
|
|
||||||
|
radioButtons[1].triggerEventHandler("change");
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(radioButton.checked).toBe(true);
|
expect(radioButtons[0].nativeElement.checked).toBe(false);
|
||||||
});
|
expect(radioButtons[1].nativeElement.checked).toBe(true);
|
||||||
|
|
||||||
it("should not check radio button when selected does not match value", () => {
|
|
||||||
testAppComponent.value = "value";
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
mockGroupComponent.selected = "nonMatchingValue";
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect(radioButton.checked).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class MockedButtonGroupComponent implements Partial<RadioGroupComponent> {
|
|
||||||
onInputChange = jest.fn();
|
|
||||||
selected: unknown = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "test-app",
|
selector: "test-component",
|
||||||
template: `<bit-radio-button [value]="value"><bit-label>Element</bit-label></bit-radio-button>`,
|
template: `
|
||||||
imports: [RadioButtonModule],
|
<form [formGroup]="formObj">
|
||||||
|
<bit-radio-group formControlName="radio">
|
||||||
|
<bit-radio-button [value]="0"><bit-label>Element</bit-label></bit-radio-button>
|
||||||
|
<bit-radio-button [value]="1"><bit-label>Element</bit-label></bit-radio-button>
|
||||||
|
</bit-radio-group>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
imports: [FormsModule, ReactiveFormsModule, RadioButtonModule],
|
||||||
})
|
})
|
||||||
class TestApp {
|
class TestComponent {
|
||||||
value?: string;
|
formObj = new FormGroup({
|
||||||
|
radio: new FormControl(0),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, HostBinding, Input } from "@angular/core";
|
import { Component, HostBinding, input } from "@angular/core";
|
||||||
|
|
||||||
import { FormControlModule } from "../form-control/form-control.module";
|
import { FormControlModule } from "../form-control/form-control.module";
|
||||||
|
|
||||||
@@ -11,20 +11,23 @@ let nextId = 0;
|
|||||||
selector: "bit-radio-button",
|
selector: "bit-radio-button",
|
||||||
templateUrl: "radio-button.component.html",
|
templateUrl: "radio-button.component.html",
|
||||||
imports: [FormControlModule, RadioInputComponent],
|
imports: [FormControlModule, RadioInputComponent],
|
||||||
|
host: {
|
||||||
|
"[id]": "this.id()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class RadioButtonComponent {
|
export class RadioButtonComponent {
|
||||||
@HostBinding("attr.id") @Input() id = `bit-radio-button-${nextId++}`;
|
readonly id = input(`bit-radio-button-${nextId++}`);
|
||||||
@HostBinding("class") get classList() {
|
@HostBinding("class") get classList() {
|
||||||
return [this.block ? "tw-block" : "tw-inline-block", "tw-mb-1", "[&_bit-hint]:tw-mt-0"];
|
return [this.block ? "tw-block" : "tw-inline-block", "tw-mb-1", "[&_bit-hint]:tw-mt-0"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() value: unknown;
|
readonly value = input<unknown>();
|
||||||
@Input() disabled = false;
|
readonly disabled = input(false);
|
||||||
|
|
||||||
constructor(private groupComponent: RadioGroupComponent) {}
|
constructor(private groupComponent: RadioGroupComponent) {}
|
||||||
|
|
||||||
get inputId() {
|
get inputId() {
|
||||||
return `${this.id}-input`;
|
return `${this.id()}-input`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@@ -32,7 +35,7 @@ export class RadioButtonComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selected() {
|
get selected() {
|
||||||
return this.groupComponent.selected === this.value;
|
return this.groupComponent.selected === this.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
get groupDisabled() {
|
get groupDisabled() {
|
||||||
@@ -40,11 +43,11 @@ export class RadioButtonComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get block() {
|
get block() {
|
||||||
return this.groupComponent.block;
|
return this.groupComponent.block();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onInputChange() {
|
protected onInputChange() {
|
||||||
this.groupComponent.onInputChange(this.value);
|
this.groupComponent.onInputChange(this.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onBlur() {
|
protected onBlur() {
|
||||||
|
|||||||
@@ -177,15 +177,15 @@ export const Disabled: Story = {
|
|||||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||||
<bit-label>Group of radio buttons</bit-label>
|
<bit-label>Group of radio buttons</bit-label>
|
||||||
|
|
||||||
<bit-radio-button id="radio-first" [value]="0" [disabled]="true">
|
<bit-radio-button [value]="0" [disabled]="true">
|
||||||
<bit-label>First</bit-label>
|
<bit-label>First</bit-label>
|
||||||
</bit-radio-button>
|
</bit-radio-button>
|
||||||
|
|
||||||
<bit-radio-button id="radio-second" [value]="1" [disabled]="true">
|
<bit-radio-button [value]="1" [disabled]="true">
|
||||||
<bit-label>Second</bit-label>
|
<bit-label>Second</bit-label>
|
||||||
</bit-radio-button>
|
</bit-radio-button>
|
||||||
|
|
||||||
<bit-radio-button id="radio-third" [value]="2" [disabled]="true">
|
<bit-radio-button [value]="2" [disabled]="true">
|
||||||
<bit-label>Third</bit-label>
|
<bit-label>Third</bit-label>
|
||||||
</bit-radio-button>
|
</bit-radio-button>
|
||||||
</bit-radio-group>
|
</bit-radio-group>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { NgTemplateOutlet } from "@angular/common";
|
import { NgTemplateOutlet } from "@angular/common";
|
||||||
import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core";
|
import { Component, ContentChild, HostBinding, Optional, Input, Self, input } from "@angular/core";
|
||||||
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
||||||
|
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
@@ -14,11 +14,16 @@ let nextId = 0;
|
|||||||
selector: "bit-radio-group",
|
selector: "bit-radio-group",
|
||||||
templateUrl: "radio-group.component.html",
|
templateUrl: "radio-group.component.html",
|
||||||
imports: [NgTemplateOutlet, I18nPipe],
|
imports: [NgTemplateOutlet, I18nPipe],
|
||||||
|
host: {
|
||||||
|
"[id]": "id()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class RadioGroupComponent implements ControlValueAccessor {
|
export class RadioGroupComponent implements ControlValueAccessor {
|
||||||
selected: unknown;
|
selected: unknown;
|
||||||
disabled = false;
|
disabled = false;
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
private _name?: string;
|
private _name?: string;
|
||||||
@Input() get name() {
|
@Input() get name() {
|
||||||
return this._name ?? this.ngControl?.name?.toString();
|
return this._name ?? this.ngControl?.name?.toString();
|
||||||
@@ -27,10 +32,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
|
|||||||
this._name = value;
|
this._name = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() block = false;
|
readonly block = input(false);
|
||||||
|
|
||||||
@HostBinding("attr.role") role = "radiogroup";
|
@HostBinding("attr.role") role = "radiogroup";
|
||||||
@HostBinding("attr.id") @Input() id = `bit-radio-group-${nextId++}`;
|
readonly id = input(`bit-radio-group-${nextId++}`);
|
||||||
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];
|
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];
|
||||||
|
|
||||||
@ContentChild(BitLabel) protected label: BitLabel;
|
@ContentChild(BitLabel) protected label: BitLabel;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, HostBinding, Input, Optional, Self } from "@angular/core";
|
import { Component, HostBinding, input, Input, Optional, Self } from "@angular/core";
|
||||||
import { NgControl, Validators } from "@angular/forms";
|
import { NgControl, Validators } from "@angular/forms";
|
||||||
|
|
||||||
import { BitFormControlAbstraction } from "../form-control";
|
import { BitFormControlAbstraction } from "../form-control";
|
||||||
@@ -11,9 +11,12 @@ let nextId = 0;
|
|||||||
selector: "input[type=radio][bitRadio]",
|
selector: "input[type=radio][bitRadio]",
|
||||||
template: "",
|
template: "",
|
||||||
providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }],
|
providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }],
|
||||||
|
host: {
|
||||||
|
"[id]": "this.id()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class RadioInputComponent implements BitFormControlAbstraction {
|
export class RadioInputComponent implements BitFormControlAbstraction {
|
||||||
@HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`;
|
readonly id = input(`bit-radio-input-${nextId++}`);
|
||||||
|
|
||||||
@HostBinding("class")
|
@HostBinding("class")
|
||||||
protected inputClasses = [
|
protected inputClasses = [
|
||||||
@@ -73,6 +76,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
|
|||||||
|
|
||||||
constructor(@Optional() @Self() private ngControl?: NgControl) {}
|
constructor(@Optional() @Self() private ngControl?: NgControl) {}
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@HostBinding()
|
@HostBinding()
|
||||||
@Input()
|
@Input()
|
||||||
get disabled() {
|
get disabled() {
|
||||||
@@ -83,6 +88,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
|
|||||||
}
|
}
|
||||||
private _disabled: boolean;
|
private _disabled: boolean;
|
||||||
|
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input()
|
@Input()
|
||||||
get required() {
|
get required() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
bitInput
|
bitInput
|
||||||
[type]="inputType"
|
[type]="inputType"
|
||||||
[id]="id"
|
[id]="id"
|
||||||
[placeholder]="placeholder ?? ('search' | i18n)"
|
[placeholder]="placeholder() ?? ('search' | i18n)"
|
||||||
class="tw-ps-9"
|
class="tw-ps-9"
|
||||||
[ngModel]="searchText"
|
[ngModel]="searchText"
|
||||||
(ngModelChange)="onChange($event)"
|
(ngModelChange)="onChange($event)"
|
||||||
(blur)="onTouch()"
|
(blur)="onTouch()"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled()"
|
||||||
[attr.autocomplete]="autocomplete"
|
[attr.autocomplete]="autocomplete()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
import { Component, ElementRef, ViewChild, input, model } from "@angular/core";
|
||||||
import {
|
import {
|
||||||
ControlValueAccessor,
|
ControlValueAccessor,
|
||||||
NG_VALUE_ACCESSOR,
|
NG_VALUE_ACCESSOR,
|
||||||
@@ -43,9 +43,9 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
|||||||
// Use `type="text"` for Safari to improve rendering performance
|
// Use `type="text"` for Safari to improve rendering performance
|
||||||
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
|
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
|
||||||
|
|
||||||
@Input() disabled: boolean;
|
readonly disabled = model<boolean>();
|
||||||
@Input() placeholder: string;
|
readonly placeholder = input<string>();
|
||||||
@Input() autocomplete: string;
|
readonly autocomplete = input<string>();
|
||||||
|
|
||||||
getFocusTarget() {
|
getFocusTarget() {
|
||||||
return this.input?.nativeElement;
|
return this.input?.nativeElement;
|
||||||
@@ -76,6 +76,6 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDisabledState(isDisabled: boolean) {
|
setDisabledState(isDisabled: boolean) {
|
||||||
this.disabled = isDisabled;
|
this.disabled.set(isDisabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, input } from "@angular/core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "bit-section",
|
selector: "bit-section",
|
||||||
@@ -9,7 +9,7 @@ import { Component, Input } from "@angular/core";
|
|||||||
<section
|
<section
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'tw-mb-5 bit-compact:tw-mb-4 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12':
|
'tw-mb-5 bit-compact:tw-mb-4 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12':
|
||||||
!disableMargin,
|
!disableMargin(),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
@@ -17,5 +17,5 @@ import { Component, Input } from "@angular/core";
|
|||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class SectionComponent {
|
export class SectionComponent {
|
||||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
readonly disableMargin = input(false, { transform: coerceBooleanProperty });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Component, Input, booleanAttribute } from "@angular/core";
|
import { Component, booleanAttribute, input } from "@angular/core";
|
||||||
|
|
||||||
import { Option } from "./option";
|
import { MappedOptionComponent } from "./option";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "bit-option",
|
selector: "bit-option",
|
||||||
template: `<ng-template><ng-content></ng-content></ng-template>`,
|
template: `<ng-template><ng-content></ng-content></ng-template>`,
|
||||||
})
|
})
|
||||||
export class OptionComponent<T = unknown> implements Option<T> {
|
export class OptionComponent<T = unknown> implements MappedOptionComponent<T> {
|
||||||
@Input()
|
readonly icon = input<string>();
|
||||||
icon?: string;
|
|
||||||
|
|
||||||
@Input({ required: true })
|
readonly value = input.required<T>();
|
||||||
value: T;
|
|
||||||
|
|
||||||
@Input({ required: true })
|
readonly label = input.required<string>();
|
||||||
label: string;
|
|
||||||
|
|
||||||
@Input({ transform: booleanAttribute })
|
readonly disabled = input(undefined, { transform: booleanAttribute });
|
||||||
disabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import { MappedDataToSignal } from "../shared/data-to-signal-type";
|
||||||
|
|
||||||
export interface Option<T> {
|
export interface Option<T> {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
value: T | null;
|
value: T | null;
|
||||||
label?: string;
|
label?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MappedOptionComponent<T> = MappedDataToSignal<Option<T>>;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<ng-select
|
<ng-select
|
||||||
[(ngModel)]="selectedOption"
|
[ngModel]="selectedOption()"
|
||||||
(ngModelChange)="onChange($event)"
|
(ngModelChange)="onChange($event)"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder()"
|
||||||
[items]="items"
|
[items]="items()"
|
||||||
(blur)="onBlur()"
|
(blur)="onBlur()"
|
||||||
[labelForId]="labelForId"
|
[labelForId]="labelForId"
|
||||||
[clearable]="false"
|
[clearable]="false"
|
||||||
|
|||||||
@@ -37,15 +37,15 @@ describe("Select Component", () => {
|
|||||||
|
|
||||||
describe("initial state", () => {
|
describe("initial state", () => {
|
||||||
it("selected option should update when items input changes", () => {
|
it("selected option should update when items input changes", () => {
|
||||||
expect(select.selectedOption?.value).toBeUndefined();
|
expect(select.selectedOption()?.value).toBeUndefined();
|
||||||
|
|
||||||
select.items = [
|
select.items.set([
|
||||||
{ label: "Apple", value: "apple" },
|
{ label: "Apple", value: "apple" },
|
||||||
{ label: "Pear", value: "pear" },
|
{ label: "Pear", value: "pear" },
|
||||||
{ label: "Banana", value: "banana" },
|
{ label: "Banana", value: "banana" },
|
||||||
];
|
]);
|
||||||
|
|
||||||
expect(select.selectedOption?.value).toBe("apple");
|
expect(select.selectedOption()?.value).toBe("apple");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
Output,
|
Output,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
|
input,
|
||||||
|
Signal,
|
||||||
|
computed,
|
||||||
|
model,
|
||||||
|
signal,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import {
|
import {
|
||||||
ControlValueAccessor,
|
ControlValueAccessor,
|
||||||
@@ -37,29 +42,23 @@ let nextId = 0;
|
|||||||
templateUrl: "select.component.html",
|
templateUrl: "select.component.html",
|
||||||
providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }],
|
providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }],
|
||||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule],
|
imports: [NgSelectModule, ReactiveFormsModule, FormsModule],
|
||||||
|
host: {
|
||||||
|
"[id]": "id()",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
|
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
|
||||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||||
|
|
||||||
private _items: Option<T>[] = [];
|
|
||||||
/** Optional: Options can be provided using an array input or using `bit-option` */
|
/** Optional: Options can be provided using an array input or using `bit-option` */
|
||||||
@Input()
|
readonly items = model<Option<T>[] | undefined>();
|
||||||
get items(): Option<T>[] {
|
|
||||||
return this._items;
|
|
||||||
}
|
|
||||||
set items(next: Option<T>[]) {
|
|
||||||
this._items = next;
|
|
||||||
this._selectedOption = this.findSelectedOption(next, this.selectedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Input() placeholder = this.i18nService.t("selectPlaceholder");
|
readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
|
||||||
@Output() closed = new EventEmitter();
|
@Output() closed = new EventEmitter();
|
||||||
|
|
||||||
protected selectedValue: T;
|
protected selectedValue = signal<T>(undefined);
|
||||||
protected _selectedOption: Option<T>;
|
selectedOption: Signal<Option<T>> = computed(() =>
|
||||||
get selectedOption() {
|
this.findSelectedOption(this.items(), this.selectedValue()),
|
||||||
return this._selectedOption;
|
);
|
||||||
}
|
|
||||||
protected searchInputId = `bit-select-search-input-${nextId++}`;
|
protected searchInputId = `bit-select-search-input-${nextId++}`;
|
||||||
|
|
||||||
private notifyOnChange?: (value: T) => void;
|
private notifyOnChange?: (value: T) => void;
|
||||||
@@ -79,7 +78,14 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
|||||||
if (value == null || value.length == 0) {
|
if (value == null || value.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.items = value.toArray();
|
this.items.set(
|
||||||
|
value.toArray().map((option) => ({
|
||||||
|
icon: option.icon(),
|
||||||
|
value: option.value(),
|
||||||
|
label: option.label(),
|
||||||
|
disabled: option.disabled(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostBinding("class") protected classes = ["tw-block", "tw-w-full", "tw-h-full"];
|
@HostBinding("class") protected classes = ["tw-block", "tw-w-full", "tw-h-full"];
|
||||||
@@ -89,6 +95,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
|||||||
get disabledAttr() {
|
get disabledAttr() {
|
||||||
return this.disabled || null;
|
return this.disabled || null;
|
||||||
}
|
}
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input()
|
@Input()
|
||||||
get disabled() {
|
get disabled() {
|
||||||
return this._disabled ?? this.ngControl?.disabled ?? false;
|
return this._disabled ?? this.ngControl?.disabled ?? false;
|
||||||
@@ -100,8 +108,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
|||||||
|
|
||||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
writeValue(obj: T): void {
|
writeValue(obj: T): void {
|
||||||
this.selectedValue = obj;
|
this.selectedValue.set(obj);
|
||||||
this._selectedOption = this.findSelectedOption(this.items, this.selectedValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
@@ -121,6 +128,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
|||||||
|
|
||||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
protected onChange(option: Option<T> | null) {
|
protected onChange(option: Option<T> | null) {
|
||||||
|
this.selectedValue.set(option?.value);
|
||||||
|
|
||||||
if (!this.notifyOnChange) {
|
if (!this.notifyOnChange) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,9 +163,11 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**Implemented as part of BitFormFieldControl */
|
/**Implemented as part of BitFormFieldControl */
|
||||||
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
|
readonly id = input(`bit-multi-select-${nextId++}`);
|
||||||
|
|
||||||
/**Implemented as part of BitFormFieldControl */
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@HostBinding("attr.required")
|
@HostBinding("attr.required")
|
||||||
@Input()
|
@Input()
|
||||||
get required() {
|
get required() {
|
||||||
@@ -178,8 +189,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
|||||||
return [key, this.ngControl?.errors[key]];
|
return [key, this.ngControl?.errors[key]];
|
||||||
}
|
}
|
||||||
|
|
||||||
private findSelectedOption(items: Option<T>[], value: T): Option<T> | undefined {
|
private findSelectedOption(items: Option<T>[] | undefined, value: T): Option<T> | undefined {
|
||||||
return items.find((item) => item.value === value);
|
return items?.find((item) => item.value === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Emits the closed event. */
|
/**Emits the closed event. */
|
||||||
|
|||||||
5
libs/components/src/shared/data-to-signal-type.ts
Normal file
5
libs/components/src/shared/data-to-signal-type.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Signal } from "@angular/core";
|
||||||
|
|
||||||
|
export type MappedDataToSignal<T> = {
|
||||||
|
[Property in keyof T]: Signal<T[Property]>;
|
||||||
|
};
|
||||||
@@ -42,6 +42,8 @@ export class StepperComponent extends CdkStepper {
|
|||||||
private initialOrientation: StepperOrientation | undefined = undefined;
|
private initialOrientation: StepperOrientation | undefined = undefined;
|
||||||
|
|
||||||
// overriding CdkStepper orientation input so we can default to vertical
|
// overriding CdkStepper orientation input so we can default to vertical
|
||||||
|
// TODO: Skipped for signal migration because:
|
||||||
|
// Accessor inputs cannot be migrated as they are too complex.
|
||||||
@Input()
|
@Input()
|
||||||
override get orientation() {
|
override get orientation() {
|
||||||
return this.internalOrientation || "vertical";
|
return this.internalOrientation || "vertical";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Directive, HostBinding, Input } from "@angular/core";
|
import { Directive, HostBinding, input } from "@angular/core";
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "tr[bitRow]",
|
selector: "tr[bitRow]",
|
||||||
})
|
})
|
||||||
export class RowDirective {
|
export class RowDirective {
|
||||||
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle";
|
readonly alignContent = input<"top" | "middle" | "bottom" | "baseline">("middle");
|
||||||
|
|
||||||
get alignmentClass(): string {
|
get alignmentClass(): string {
|
||||||
switch (this.alignContent) {
|
switch (this.alignContent()) {
|
||||||
case "top":
|
case "top":
|
||||||
return "tw-align-top";
|
return "tw-align-top";
|
||||||
case "middle":
|
case "middle":
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user