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

[CL-707] Migrate CL codebase to signals (#15340)

This commit is contained in:
Vicki League
2025-07-16 08:39:37 -04:00
committed by GitHub
parent 97ec9a6339
commit 6811ea4c0b
124 changed files with 944 additions and 809 deletions

View File

@@ -6,6 +6,8 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
selector: "[appA11yTitle]",
})
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) {
this.title = title;
this.setAttributes();

View File

@@ -6,7 +6,7 @@
}"
>
<a
*ngIf="!hideLogo"
*ngIf="!hideLogo()"
[routerLink]="['/']"
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
>
@@ -14,29 +14,29 @@
</a>
<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">
<bit-icon [icon]="icon"></bit-icon>
<div *ngIf="!hideIcon()" class="tw-w-24 sm:tw-w-28 md:tw-w-32 tw-mx-auto">
<bit-icon [icon]="icon()"></bit-icon>
</div>
<ng-container *ngIf="title">
<ng-container *ngIf="title()">
<!-- Small screens -->
<h1 bitTypography="h3" class="tw-mt-2 sm:tw-hidden">
{{ title }}
{{ title() }}
</h1>
<!-- Medium to Larger screens -->
<h1 bitTypography="h2" class="tw-mt-2 tw-hidden sm:tw-block">
{{ title }}
{{ title() }}
</h1>
</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
class="tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
[ngClass]="maxWidthClass"
>
@if (hideCardWrapper) {
@if (hideCardWrapper()) {
<div class="tw-mb-6 sm:tw-mb-10">
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
@@ -50,11 +50,11 @@
<ng-content select="[slot=secondary]"></ng-content>
</div>
<footer *ngIf="!hideFooter" class="tw-text-center tw-mt-4 sm:tw-mt-6">
<div *ngIf="showReadonlyHostname" bitTypography="body2">
<footer *ngIf="!hideFooter()" class="tw-text-center tw-mt-4 sm:tw-mt-6">
<div *ngIf="showReadonlyHostname()" bitTypography="body2">
{{ "accessing" | i18n }} {{ hostname }}
</div>
<ng-container *ngIf="!showReadonlyHostname">
<ng-container *ngIf="!showReadonlyHostname()">
<ng-content select="[slot=environment-selector]"></ng-content>
</ng-container>
<ng-container *ngIf="!hideYearAndVersion">

View File

@@ -1,7 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { firstValueFrom } from "rxjs";
@@ -29,21 +37,21 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
return ["tw-h-full"];
}
@Input() title: string;
@Input() subtitle: string;
@Input() icon: Icon;
@Input() showReadonlyHostname: boolean;
@Input() hideLogo: boolean = false;
@Input() hideFooter: boolean = false;
@Input() hideIcon: boolean = false;
@Input() hideCardWrapper: boolean = false;
readonly title = input<string>();
readonly subtitle = input<string>();
readonly icon = model<Icon>();
readonly showReadonlyHostname = input<boolean>(false);
readonly hideLogo = input<boolean>(false);
readonly hideFooter = input<boolean>(false);
readonly hideIcon = input<boolean>(false);
readonly hideCardWrapper = input<boolean>(false);
/**
* Max width of the anon layout title, subtitle, and content areas.
*
* @default 'md'
*/
@Input() maxWidth: AnonLayoutMaxWidth = "md";
readonly maxWidth = model<AnonLayoutMaxWidth>("md");
protected logo = BitwardenLogo;
protected year: string;
@@ -54,7 +62,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
protected hideYearAndVersion = false;
get maxWidthClass(): string {
switch (this.maxWidth) {
const maxWidth = this.maxWidth();
switch (maxWidth) {
case "md":
return "tw-max-w-md";
case "lg":
@@ -78,19 +87,19 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
}
async ngOnInit() {
this.maxWidth = this.maxWidth ?? "md";
this.maxWidth.set(this.maxWidth() ?? "md");
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
this.version = await this.platformUtilsService.getApplicationVersion();
// If there is no icon input, then use the default icon
if (this.icon == null) {
this.icon = AnonLayoutBitwardenShield;
if (this.icon() == null) {
this.icon.set(AnonLayoutBitwardenShield);
}
}
async ngOnChanges(changes: SimpleChanges) {
if (changes.maxWidth) {
this.maxWidth = changes.maxWidth.currentValue ?? "md";
this.maxWidth.set(changes.maxWidth.currentValue ?? "md");
}
}
}

View File

@@ -19,17 +19,7 @@ class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
getClientType = () => ClientType.Web;
}
type StoryArgs = Pick<
AnonLayoutComponent,
| "title"
| "subtitle"
| "showReadonlyHostname"
| "hideCardWrapper"
| "hideIcon"
| "hideLogo"
| "hideFooter"
| "maxWidth"
> & {
type StoryArgs = AnonLayoutComponent & {
contentLength: "normal" | "long" | "thin";
showSecondary: boolean;
useDefaultIcon: boolean;
@@ -72,15 +62,14 @@ export default {
],
}),
],
render: (args: StoryArgs) => {
render: (args) => {
const { useDefaultIcon, icon, ...rest } = args;
return {
props: {
...rest,
icon: useDefaultIcon ? null : icon,
},
template: `
template: /*html*/ `
<auth-anon-layout
[title]="title"
[subtitle]="subtitle"
@@ -160,7 +149,7 @@ export default {
contentLength: "normal",
showSecondary: false,
},
} as Meta<StoryArgs>;
} satisfies Meta<StoryArgs>;
type Story = StoryObj<StoryArgs>;

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -38,7 +38,7 @@ export class BitActionDirective implements OnDestroy {
disabled = false;
@Input("bitAction") handler: FunctionReturningAwaitable;
readonly handler = model<FunctionReturningAwaitable>(undefined, { alias: "bitAction" });
constructor(
private buttonComponent: ButtonLikeAbstraction,
@@ -48,12 +48,12 @@ export class BitActionDirective implements OnDestroy {
@HostListener("click")
protected async onClick() {
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled()) {
if (!this.handler() || this.loading || this.disabled || this.buttonComponent.disabled()) {
return;
}
this.loading = true;
functionToObservable(this.handler)
functionToObservable(this.handler())
.pipe(
tap({
error: (err: unknown) => {

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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 { 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 _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 disabled$ = this._disabled$.asObservable();
@@ -38,7 +38,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
switchMap(() => {
// Calling functionToObservable executes the sync part of the handler
// allowing the function to check form validity before it gets disabled.
const awaitable = functionToObservable(this.handler);
const awaitable = functionToObservable(this.handler());
// Disable form
this.loading = true;
@@ -61,7 +61,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
ngOnInit(): void {
this.formGroupDirective.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
if (this.allowDisabledFormSubmit) {
if (this.allowDisabledFormSubmit()) {
this._disabled$.next(false);
} else {
this._disabled$.next(c === "DISABLED");

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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 { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
@@ -29,8 +29,8 @@ import { BitSubmitDirective } from "./bit-submit.directive";
export class BitFormButtonDirective implements OnDestroy {
private destroy$ = new Subject<void>();
@Input() type: string;
@Input() disabled?: boolean;
readonly type = input<string>();
readonly disabled = input<boolean>();
constructor(
buttonComponent: ButtonLikeAbstraction,
@@ -39,16 +39,17 @@ export class BitFormButtonDirective implements OnDestroy {
) {
if (submitDirective && buttonComponent) {
submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
if (this.type === "submit") {
if (this.type() === "submit") {
buttonComponent.loading.set(loading);
} else {
buttonComponent.disabled.set(this.disabled || loading);
buttonComponent.disabled.set(this.disabled() || loading);
}
});
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
if (this.disabled !== false) {
buttonComponent.disabled.set(this.disabled || disabled);
const disabledValue = this.disabled();
if (disabledValue !== false) {
buttonComponent.disabled.set(disabledValue || disabled);
}
});
}

View File

@@ -22,7 +22,6 @@ const template = /*html*/ `
template,
selector: "app-promise-example",
imports: [AsyncActionsModule, ButtonModule, IconButtonModule],
standalone: true,
})
class PromiseExampleComponent {
statusEmoji = "🟡";

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -25,17 +25,17 @@ const SizeClasses: Record<SizeTypes, string[]> = {
@Component({
selector: "bit-avatar",
template: `@if (src) {
<img [src]="src" title="{{ title || text }}" [ngClass]="classList" />
<img [src]="src" title="{{ title() || text() }}" [ngClass]="classList" />
}`,
imports: [NgClass],
})
export class AvatarComponent implements OnChanges {
@Input() border = false;
@Input() color?: string;
@Input() id?: string;
@Input() text?: string;
@Input() title: string;
@Input() size: SizeTypes = "default";
readonly border = input(false);
readonly color = input<string>();
readonly id = input<string>();
readonly text = input<string>();
readonly title = input<string>();
readonly size = input<SizeTypes>("default");
private svgCharCount = 2;
private svgFontSize = 20;
@@ -51,13 +51,13 @@ export class AvatarComponent implements OnChanges {
get classList() {
return ["tw-rounded-full"]
.concat(SizeClasses[this.size] ?? [])
.concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
.concat(SizeClasses[this.size()] ?? [])
.concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
}
private generate() {
let chars: string = null;
const upperCaseText = this.text?.toUpperCase() ?? "";
const upperCaseText = this.text()?.toUpperCase() ?? "";
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
@@ -71,12 +71,13 @@ export class AvatarComponent implements OnChanges {
}
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);
} else if (!Utils.isNullOrWhitespace(this.id)) {
hexColor = Utils.stringToColor(this.id.toString());
} else if (!Utils.isNullOrWhitespace(id)) {
hexColor = Utils.stringToColor(id.toString());
svg = this.createSvgElement(this.svgSize, hexColor);
} else {
hexColor = Utils.stringToColor(upperCaseText);

View File

@@ -1,6 +1,6 @@
<div class="tw-inline-flex tw-flex-wrap tw-gap-2">
@for (item of filteredItems; track item; let last = $last) {
<span bitBadge [variant]="variant" [truncate]="truncate">
<span bitBadge [variant]="variant()" [truncate]="truncate()">
{{ item }}
</span>
@if (!last || isFiltered) {
@@ -8,8 +8,8 @@
}
}
@if (isFiltered) {
<span bitBadge [variant]="variant">
{{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }}
<span bitBadge [variant]="variant()">
{{ "plusNMore" | i18n: (items().length - filteredItems.length).toString() }}
</span>
}
</div>

View File

@@ -1,42 +1,36 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input, OnChanges } from "@angular/core";
import { Component, OnChanges, input } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
import { BadgeModule, BadgeVariant } from "../badge";
function transformMaxItems(value: number | undefined) {
return value == undefined ? undefined : Math.max(1, value);
}
@Component({
selector: "bit-badge-list",
templateUrl: "badge-list.component.html",
imports: [BadgeModule, I18nPipe],
})
export class BadgeListComponent implements OnChanges {
private _maxItems: number;
protected filteredItems: string[] = [];
protected isFiltered = false;
@Input() variant: BadgeVariant = "primary";
@Input() items: string[] = [];
@Input() truncate = true;
readonly variant = input<BadgeVariant>("primary");
readonly items = input<string[]>([]);
readonly truncate = input(true);
@Input()
get maxItems(): number | undefined {
return this._maxItems;
}
set maxItems(value: number | undefined) {
this._maxItems = value == undefined ? undefined : Math.max(1, value);
}
readonly maxItems = input(undefined, { transform: transformMaxItems });
ngOnChanges() {
if (this.maxItems == undefined || this.items.length <= this.maxItems) {
this.filteredItems = this.items;
const maxItems = this.maxItems();
if (maxItems == undefined || this.items().length <= maxItems) {
this.filteredItems = this.items();
} 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;
}
}

View File

@@ -1,3 +1,3 @@
<span [ngClass]="{ 'tw-truncate tw-block': truncate }">
<span [ngClass]="{ 'tw-truncate tw-block': truncate() }">
<ng-content></ng-content>
</span>

View File

@@ -1,5 +1,5 @@
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";
@@ -89,33 +89,34 @@ export class BadgeComponent implements FocusableElement {
"disabled:hover:!tw-text-muted",
"disabled:tw-cursor-not-allowed",
]
.concat(styles[this.variant])
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant], "tw-min-w-10"] : [])
.concat(this.truncate ? this.maxWidthClass : []);
.concat(styles[this.variant()])
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant()], "tw-min-w-10"] : [])
.concat(this.truncate() ? this.maxWidthClass() : []);
}
@HostBinding("attr.title") get titleAttr() {
if (this.title !== undefined) {
return this.title;
const title = 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.
*/
@Input() title?: string;
readonly title = input<string>();
/**
* Variant, sets the background color of the badge.
*/
@Input() variant: BadgeVariant = "primary";
readonly variant = input<BadgeVariant>("primary");
/**
* 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() {
return this.el.nativeElement;

View File

@@ -1,10 +1,10 @@
<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"
[ngClass]="bannerClass"
[attr.role]="useAlertRole ? 'status' : null"
[attr.aria-live]="useAlertRole ? 'polite' : null"
[attr.role]="useAlertRole() ? 'status' : 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>
}
<!-- Overriding focus-visible color for link buttons for a11y against colored background -->
@@ -12,7 +12,7 @@
<ng-content></ng-content>
</span>
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
@if (showClose) {
@if (showClose()) {
<button
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
type="button"

View File

@@ -30,17 +30,17 @@ describe("BannerComponent", () => {
});
it("should create with alert", () => {
expect(component.useAlertRole).toBe(true);
expect(component.useAlertRole()).toBe(true);
const el = fixture.nativeElement.children[0];
expect(el.getAttribute("role")).toEqual("status");
expect(el.getAttribute("aria-live")).toEqual("polite");
});
it("useAlertRole=false", () => {
component.useAlertRole = false;
fixture.componentRef.setInput("useAlertRole", false);
fixture.autoDetectChanges();
expect(component.useAlertRole).toBe(false);
expect(component.useAlertRole()).toBe(false);
const el = fixture.nativeElement.children[0];
expect(el.getAttribute("role")).toBeNull();
expect(el.getAttribute("aria-live")).toBeNull();

View File

@@ -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 { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
import { Component, OnInit, Output, EventEmitter, input, model } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -31,19 +29,22 @@ const defaultIcon: Record<BannerType, string> = {
imports: [CommonModule, IconButtonModule, I18nPipe],
})
export class BannerComponent implements OnInit {
@Input("bannerType") bannerType: BannerType = "info";
@Input() icon: string;
@Input() useAlertRole = true;
@Input() showClose = true;
readonly bannerType = input<BannerType>("info");
readonly icon = model<string>();
readonly useAlertRole = input(true);
readonly showClose = input(true);
@Output() onClose = new EventEmitter<void>();
ngOnInit(): void {
this.icon ??= defaultIcon[this.bannerType];
if (!this.icon()) {
this.icon.set(defaultIcon[this.bannerType()]);
}
}
get bannerClass() {
switch (this.bannerType) {
switch (this.bannerType()) {
case "danger":
return "tw-bg-danger-100 tw-border-b-danger-700";
case "info":

View File

@@ -1,5 +1,5 @@
<ng-template>
@if (icon) {
@if (icon(); as icon) {
<i class="bwi {{ icon }} !tw-me-2" aria-hidden="true"></i>
}
<ng-content></ng-content>

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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";
@Component({
@@ -9,17 +9,13 @@ import { QueryParamsHandling } from "@angular/router";
templateUrl: "./breadcrumb.component.html",
})
export class BreadcrumbComponent {
@Input()
icon?: string;
readonly icon = input<string>();
@Input()
route?: string | any[] = undefined;
readonly route = input<string | any[]>();
@Input()
queryParams?: Record<string, string> = {};
readonly queryParams = input<Record<string, string>>({});
@Input()
queryParamsHandling?: QueryParamsHandling;
readonly queryParamsHandling = input<QueryParamsHandling>();
@Output()
click = new EventEmitter();

View File

@@ -1,17 +1,16 @@
@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) {
@if (breadcrumb.route) {
@if (breadcrumb.route(); as route) {
<a
bitLink
linkType="primary"
class="tw-my-2 tw-inline-block"
[routerLink]="breadcrumb.route"
[queryParams]="breadcrumb.queryParams"
[queryParamsHandling]="breadcrumb.queryParamsHandling"
[routerLink]="route"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</a>
}
@if (!breadcrumb.route) {
} @else {
<button
type="button"
bitLink
@@ -39,18 +38,17 @@
></button>
<bit-menu #overflowMenu>
@for (breadcrumb of overflow; track breadcrumb) {
@if (breadcrumb.route) {
@if (breadcrumb.route(); as route) {
<a
bitMenuItem
linkType="primary"
[routerLink]="breadcrumb.route"
[queryParams]="breadcrumb.queryParams"
[queryParamsHandling]="breadcrumb.queryParamsHandling"
[routerLink]="route"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</a>
}
@if (!breadcrumb.route) {
} @else {
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</button>
@@ -59,19 +57,18 @@
</bit-menu>
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
@for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) {
@if (breadcrumb.route) {
@if (breadcrumb.route(); as route) {
<a
bitLink
linkType="primary"
class="tw-my-2 tw-inline-block"
[routerLink]="breadcrumb.route"
[queryParams]="breadcrumb.queryParams"
[queryParamsHandling]="breadcrumb.queryParamsHandling"
[routerLink]="route"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</a>
}
@if (!breadcrumb.route) {
} @else {
<button
type="button"
bitLink

View File

@@ -1,5 +1,5 @@
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 { IconButtonModule } from "../icon-button";
@@ -19,8 +19,7 @@ import { BreadcrumbComponent } from "./breadcrumb.component";
imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule],
})
export class BreadcrumbsComponent {
@Input()
show = 3;
readonly show = input(3);
private breadcrumbs: BreadcrumbComponent[] = [];
@@ -31,14 +30,14 @@ export class BreadcrumbsComponent {
protected get beforeOverflow() {
if (this.hasOverflow) {
return this.breadcrumbs.slice(0, this.show - 1);
return this.breadcrumbs.slice(0, this.show() - 1);
}
return this.breadcrumbs;
}
protected get overflow() {
return this.breadcrumbs.slice(this.show - 1, -1);
return this.breadcrumbs.slice(this.show() - 1, -1);
}
protected get afterOverflow() {
@@ -46,6 +45,6 @@ export class BreadcrumbsComponent {
}
protected get hasOverflow() {
return this.breadcrumbs.length > this.show;
return this.breadcrumbs.length > this.show();
}
}

View File

@@ -1,7 +1,5 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { NgClass } from "@angular/common";
import {
Input,
HostBinding,
Component,
model,
@@ -10,6 +8,7 @@ import {
ElementRef,
inject,
Signal,
booleanAttribute,
} from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { debounce, interval } from "rxjs";
@@ -79,7 +78,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
"hover:tw-no-underline",
"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(
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"]);
}
@@ -116,22 +115,13 @@ export class ButtonComponent implements ButtonLikeAbstraction {
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()
get block(): boolean {
return this._block;
}
set block(value: boolean | "") {
this._block = coerceBooleanProperty(value);
}
loading = model<boolean>(false);
readonly loading = model<boolean>(false);
/**
* 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))),
);
disabled = model<boolean>(false);
readonly disabled = model<boolean>(false);
private el = inject(ElementRef<HTMLButtonElement>);
constructor() {

View File

@@ -3,12 +3,12 @@
[ngClass]="calloutClass"
[attr.aria-labelledby]="titleId"
>
@if (title) {
@if (titleComputed(); as title) {
<header
id="{{ titleId }}"
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
class="bwi !tw-text-main tw-relative tw-top-[3px]"
[ngClass]="[icon]"

View File

@@ -30,31 +30,31 @@ describe("Callout", () => {
describe("default state", () => {
it("success", () => {
component.type = "success";
fixture.componentRef.setInput("type", "success");
fixture.detectChanges();
expect(component.title).toBeUndefined();
expect(component.icon).toBe("bwi-check-circle");
expect(component.titleComputed()).toBeUndefined();
expect(component.iconComputed()).toBe("bwi-check-circle");
});
it("info", () => {
component.type = "info";
fixture.componentRef.setInput("type", "info");
fixture.detectChanges();
expect(component.title).toBeUndefined();
expect(component.icon).toBe("bwi-info-circle");
expect(component.titleComputed()).toBeUndefined();
expect(component.iconComputed()).toBe("bwi-info-circle");
});
it("warning", () => {
component.type = "warning";
fixture.componentRef.setInput("type", "warning");
fixture.detectChanges();
expect(component.title).toBe("Warning");
expect(component.icon).toBe("bwi-exclamation-triangle");
expect(component.titleComputed()).toBe("Warning");
expect(component.iconComputed()).toBe("bwi-exclamation-triangle");
});
it("danger", () => {
component.type = "danger";
fixture.componentRef.setInput("type", "danger");
fixture.detectChanges();
expect(component.title).toBe("Error");
expect(component.icon).toBe("bwi-error");
expect(component.titleComputed()).toBe("Error");
expect(component.iconComputed()).toBe("bwi-error");
});
});
});

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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";
@@ -34,24 +34,28 @@ let nextId = 0;
templateUrl: "callout.component.html",
imports: [SharedModule, TypographyModule],
})
export class CalloutComponent implements OnInit {
@Input() type: CalloutTypes = "info";
@Input() icon: string;
@Input() title: string;
@Input() useAlertRole = false;
export class CalloutComponent {
readonly type = input<CalloutTypes>("info");
readonly icon = input<string>();
readonly title = input<string>();
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++}`;
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() {
switch (this.type) {
switch (this.type()) {
case "danger":
return "tw-bg-danger-100";
case "info":

View File

@@ -68,3 +68,18 @@ export const Danger: Story = {
type: "danger",
},
};
export const CustomIcon: Story = {
...Info,
args: {
...Info.args,
icon: "bwi-star",
},
};
export const NoTitle: Story = {
...Info,
args: {
icon: "bwi-star",
},
};

View File

@@ -104,6 +104,8 @@ export class CheckboxComponent implements BitFormControlAbstraction {
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')`;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding()
@Input()
get disabled() {
@@ -114,6 +116,8 @@ export class CheckboxComponent implements BitFormControlAbstraction {
}
private _disabled: boolean;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get required() {
return (

View File

@@ -31,7 +31,7 @@ const template = /*html*/ `
@Component({
selector: "app-example",
template,
imports: [CheckboxModule, FormFieldModule, ReactiveFormsModule],
imports: [FormControlModule, CheckboxModule, FormsModule, FormFieldModule, ReactiveFormsModule],
})
class ExampleComponent {
protected formObj = this.formBuilder.group({

View File

@@ -69,10 +69,10 @@
bitMenuItem
(click)="viewOption(parent, $event)"
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>
{{ "backTo" | i18n: parent.label ?? placeholderText }}
{{ "backTo" | i18n: parent.label ?? placeholderText() }}
</button>
<button
type="button"

View File

@@ -14,6 +14,7 @@ import {
booleanAttribute,
inject,
signal,
input,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
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>;
/** 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 */
@Input() placeholderIcon: string;
readonly placeholderIcon = input<string>();
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 */
@Input({ required: true })
get options(): ChipSelectOption<T>[] {
@@ -71,10 +75,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
}
/** 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;
/** 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`
@@ -91,7 +97,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
@HostBinding("class")
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);
@@ -113,12 +119,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
/** The label to show in the chip button */
protected get label(): string {
return this.selectedOption?.label || this.placeholderText;
return this.selectedOption?.label || this.placeholderText();
}
/** The icon to show in the chip button */
protected get icon(): string {
return this.selectedOption?.icon || this.placeholderIcon;
return this.selectedOption?.icon || this.placeholderIcon();
}
/**

View File

@@ -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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -28,13 +36,13 @@ export class CopyClickDirective {
@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`
* 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
@@ -49,6 +57,8 @@ export class CopyClickDirective {
* <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 | "") {
// When the `showToast` is set without a value, an empty string will be passed
if (value === "") {
@@ -60,15 +70,17 @@ export class CopyClickDirective {
}
@HostListener("click") onClick() {
this.platformUtilsService.copyToClipboard(this.valueToCopy);
const valueToCopy = this.valueToCopy();
this.platformUtilsService.copyToClipboard(valueToCopy);
if (this.copyListener) {
this.copyListener.onCopy(this.valueToCopy);
this.copyListener.onCopy(valueToCopy);
}
if (this._showToast) {
const message = this.valueLabel
? this.i18nService.t("valueCopied", this.valueLabel)
const valueLabel = this.valueLabel();
const message = valueLabel
? this.i18nService.t("valueCopied", valueLabel)
: this.i18nService.t("copySuccessful");
this.toastService.showToast({

View File

@@ -6,7 +6,7 @@
cdkTrapFocus
cdkTrapFocusAutoCapture
>
@let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
@let showHeaderBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().top;
<header
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid"
[ngClass]="{
@@ -22,10 +22,10 @@
noMargin
class="tw-text-main tw-mb-0 tw-truncate"
>
{{ title }}
@if (subtitle) {
{{ title() }}
@if (subtitle(); as subtitleText) {
<span class="tw-text-muted tw-font-normal tw-text-sm">
{{ subtitle }}
{{ subtitleText }}
</span>
}
<ng-content select="[bitDialogTitle]"></ng-content>
@@ -46,12 +46,12 @@
<div
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
[ngClass]="{
'tw-min-h-60': loading,
'tw-bg-background': background === 'default',
'tw-bg-background-alt': background === 'alt',
'tw-min-h-60': loading(),
'tw-bg-background': background() === 'default',
'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">
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
</div>
@@ -59,17 +59,17 @@
<div
cdkScrollable
[ngClass]="{
'tw-p-4': !disablePadding && !isDrawer,
'tw-px-6 tw-py-4': !disablePadding && isDrawer,
'tw-overflow-y-auto': !loading,
'tw-invisible tw-overflow-y-hidden': loading,
'tw-p-4': !disablePadding() && !isDrawer,
'tw-px-6 tw-py-4': !disablePadding() && isDrawer,
'tw-overflow-y-auto': !loading(),
'tw-invisible tw-overflow-y-hidden': loading(),
}"
>
<ng-content select="[bitDialogContent]"></ng-content>
</div>
</div>
@let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
@let showFooterBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().bottom;
<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"
[ngClass]="[isDrawer ? 'tw-px-6 tw-py-4' : 'tw-p-4']"

View File

@@ -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 { coerceBooleanProperty } from "@angular/cdk/coercion";
import { CdkScrollable } from "@angular/cdk/scrolling";
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";
@@ -40,39 +37,32 @@ export class DialogComponent {
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
/** Background color */
@Input()
background: "default" | "alt" = "default";
readonly background = input<"default" | "alt">("default");
/**
* 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
*/
@Input() title: string;
readonly title = input<string>();
/**
* 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.
*/
@Input() set disablePadding(value: boolean | "") {
this._disablePadding = coerceBooleanProperty(value);
}
get disablePadding() {
return this._disablePadding;
}
readonly disablePadding = input(false, { transform: booleanAttribute });
/**
* Mark the dialog as loading which replaces the content with a spinner.
*/
@Input() loading = false;
readonly loading = input(false);
@HostBinding("class") get classes() {
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
@@ -94,7 +84,7 @@ export class DialogComponent {
}
get width() {
switch (this.dialogSize) {
switch (this.dialogSize()) {
case "small": {
return "md:tw-max-w-sm";
}

View File

@@ -1,11 +1,11 @@
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({
selector: "[bitDialogClose]",
})
export class DialogCloseDirective {
@Input("bitDialogClose") dialogResult: any;
readonly dialogResult = input<any>(undefined, { alias: "bitDialogClose" });
constructor(@Optional() public dialogRef: DialogRef) {}
@@ -20,6 +20,6 @@ export class DialogCloseDirective {
return;
}
this.dialogRef.close(this.dialogResult);
this.dialogRef.close(this.dialogResult());
}
}

View File

@@ -1,5 +1,5 @@
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
let nextId = 0;
@@ -10,7 +10,7 @@ let nextId = 0;
export class DialogTitleContainerDirective implements OnInit {
@HostBinding("id") id = `bit-dialog-title-${nextId++}`;
@Input() simple = false;
readonly simple = input(false);
constructor(@Optional() private dialogRef: DialogRef<any>) {}

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, HostBinding, HostListener, Input } from "@angular/core";
import { Directive, HostBinding, HostListener, input } from "@angular/core";
import { DisclosureComponent } from "./disclosure.component";
@@ -12,17 +12,17 @@ export class DisclosureTriggerForDirective {
/**
* 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() {
return this.disclosure.open;
return this.disclosure().open;
}
@HostBinding("attr.aria-controls") get ariaControls() {
return this.disclosure.id;
return this.disclosure().id;
}
@HostListener("click") click() {
this.disclosure.open = !this.disclosure.open;
this.disclosure().open = !this.disclosure().open;
}
}

View File

@@ -48,6 +48,8 @@ export class DisclosureComponent {
/**
* 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) {
this._open = isOpen;
this.openChange.emit(isOpen);

View File

@@ -1,8 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
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 { I18nPipe } from "@bitwarden/ui-common";
@@ -17,30 +16,18 @@ import { BitFormControlAbstraction } from "./form-control.abstraction";
imports: [NgClass, TypographyDirective, I18nPipe],
})
export class FormControlComponent {
@Input() label: string;
readonly label = input<string>();
private _inline = false;
@Input() get inline() {
return this._inline;
}
set inline(value: boolean | "") {
this._inline = coerceBooleanProperty(value);
}
readonly inline = input(false, { transform: booleanAttribute });
private _disableMargin = false;
@Input() set disableMargin(value: boolean | "") {
this._disableMargin = coerceBooleanProperty(value);
}
get disableMargin() {
return this._disableMargin;
}
readonly disableMargin = input(false, { transform: booleanAttribute });
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction;
@HostBinding("class") get classes() {
return []
.concat(this.inline ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
.concat(this.disableMargin ? [] : ["tw-mb-4"]);
.concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
.concat(this.disableMargin() ? [] : ["tw-mb-4"]);
}
constructor(private i18nService: I18nService) {}

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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";
@@ -12,6 +12,10 @@ let nextId = 0;
selector: "bit-label",
templateUrl: "label.component.html",
imports: [CommonModule],
host: {
"[class]": "classList",
"[id]": "id()",
},
})
export class BitLabel {
constructor(
@@ -19,15 +23,19 @@ export class BitLabel {
@Optional() private parentFormControl: FormControlComponent,
) {}
@HostBinding("class") @Input() get classList() {
return ["tw-inline-flex", "tw-gap-1", "tw-items-baseline", "tw-flex-row", "tw-min-w-0"];
}
readonly classList = [
"tw-inline-flex",
"tw-gap-1",
"tw-items-baseline",
"tw-flex-row",
"tw-min-w-0",
];
@HostBinding("title") get title() {
return this.elementRef.nativeElement.textContent.trim();
}
@HostBinding() @Input() id = `bit-label-${nextId++}`;
readonly id = input(`bit-label-${nextId++}`);
get isInsideFormControl() {
return !!this.parentFormControl;

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
import { Component, input } from "@angular/core";
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -18,11 +18,10 @@ import { I18nPipe } from "@bitwarden/ui-common";
imports: [I18nPipe],
})
export class BitErrorSummary {
@Input()
formGroup: UntypedFormGroup;
readonly formGroup = input<UntypedFormGroup>();
get errorCount(): number {
return this.getErrorCount(this.formGroup);
return this.getErrorCount(this.formGroup());
}
get errorString() {

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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";
@@ -18,37 +18,38 @@ let nextId = 0;
export class BitErrorComponent {
@HostBinding() id = `bit-error-${nextId++}`;
@Input() error: [string, any];
readonly error = input<[string, any]>();
constructor(private i18nService: I18nService) {}
get displayError() {
switch (this.error[0]) {
const error = this.error();
switch (error[0]) {
case "required":
return this.i18nService.t("inputRequired");
case "email":
return this.i18nService.t("inputEmail");
case "minlength":
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
return this.i18nService.t("inputMinLength", error[1]?.requiredLength);
case "maxlength":
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
return this.i18nService.t("inputMaxLength", error[1]?.requiredLength);
case "min":
return this.i18nService.t("inputMinValue", this.error[1]?.min);
return this.i18nService.t("inputMinValue", error[1]?.min);
case "max":
return this.i18nService.t("inputMaxValue", this.error[1]?.max);
return this.i18nService.t("inputMaxValue", error[1]?.max);
case "forbiddenCharacters":
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
return this.i18nService.t("inputForbiddenCharacters", error[1]?.characters.join(", "));
case "multipleEmails":
return this.i18nService.t("multipleInputEmails");
case "trim":
return this.i18nService.t("inputTrimValidator");
default:
// Attempt to show a custom error message.
if (this.error[1]?.message) {
return this.error[1]?.message;
if (error[1]?.message) {
return error[1]?.message;
}
return this.error;
return error;
}
}
}

View File

@@ -1,4 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
import { ModelSignal, Signal } from "@angular/core";
// @ts-strict-ignore
export type InputTypes =
| "text"
@@ -14,13 +17,13 @@ export type InputTypes =
export abstract class BitFormFieldControl {
ariaDescribedBy: string;
id: string;
id: Signal<string>;
labelForId: string;
required: boolean;
hasError: boolean;
error: [string, any];
type?: InputTypes;
spellcheck?: boolean;
type?: ModelSignal<InputTypes>;
spellcheck?: ModelSignal<boolean | undefined>;
readOnly?: boolean;
focus?: () => void;
}

View File

@@ -9,9 +9,10 @@ import {
ElementRef,
HostBinding,
HostListener,
Input,
ViewChild,
signal,
input,
Input,
} from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -38,10 +39,11 @@ export class BitFormFieldComponent implements AfterContentChecked {
@ViewChild(BitErrorComponent) error: BitErrorComponent;
@Input({ transform: booleanAttribute })
disableMargin = false;
readonly disableMargin = input(false, { transform: booleanAttribute });
/** 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 })
disableReadOnlyBorder = false;
@@ -76,7 +78,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
@HostBinding("class")
get classList() {
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");
}

View File

@@ -414,13 +414,18 @@ export const Select: Story = {
export const AdvancedSelect: Story = {
render: (args) => ({
props: args,
props: {
formObj: fb.group({
select: "value1",
}),
...args,
},
template: /*html*/ `
<bit-form-field>
<bit-form-field [formGroup]="formObj">
<bit-label>Label</bit-label>
<bit-select>
<bit-option label="Select"></bit-option>
<bit-option label="Other"></bit-option>
<bit-select formControlName="select">
<bit-option label="Select" value="value1"></bit-option>
<bit-option label="Other" value="value2"></bit-option>
</bit-select>
</bit-form-field>
`,

View File

@@ -5,7 +5,7 @@ import {
Host,
HostBinding,
HostListener,
Input,
model,
OnChanges,
Output,
} from "@angular/core";
@@ -18,12 +18,15 @@ import { BitFormFieldComponent } from "./form-field.component";
@Directive({
selector: "[bitPasswordInputToggle]",
host: {
"[attr.aria-pressed]": "toggled()",
},
})
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
/**
* 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>();
@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.
*/
@HostListener("click") onClick() {
this.toggled = !this.toggled;
this.toggledChange.emit(this.toggled);
this.toggled.update((toggled) => !toggled);
this.toggledChange.emit(this.toggled());
this.update();
}
@@ -46,7 +49,7 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
) {}
get icon() {
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
return this.toggled() ? "bwi-eye-slash" : "bwi-eye";
}
ngOnChanges(): void {
@@ -55,16 +58,16 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
ngAfterContentInit(): void {
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() {
this.button.icon = this.icon;
this.button.icon.set(this.icon);
if (this.formField.input?.type != null) {
this.formField.input.type = this.toggled ? "text" : "password";
this.formField.input.spellcheck = this.toggled ? false : undefined;
this.formField.input.type.set(this.toggled() ? "text" : "password");
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined);
}
}
}

View File

@@ -60,15 +60,15 @@ describe("PasswordInputToggle", () => {
describe("initial state", () => {
it("has correct icon", () => {
expect(button.icon).toBe("bwi-eye");
expect(button.icon()).toBe("bwi-eye");
});
it("input is type password", () => {
expect(input.type).toBe("password");
expect(input.type!()).toBe("password");
});
it("spellcheck is disabled", () => {
expect(input.spellcheck).toBe(undefined);
expect(input.spellcheck!()).toBe(undefined);
});
});
@@ -78,15 +78,15 @@ describe("PasswordInputToggle", () => {
});
it("has correct icon", () => {
expect(button.icon).toBe("bwi-eye-slash");
expect(button.icon()).toBe("bwi-eye-slash");
});
it("input is type text", () => {
expect(input.type).toBe("text");
expect(input.type!()).toBe("text");
});
it("spellcheck is disabled", () => {
expect(input.spellcheck).toBe(false);
expect(input.spellcheck!()).toBe(false);
});
});
@@ -97,15 +97,15 @@ describe("PasswordInputToggle", () => {
});
it("has correct icon", () => {
expect(button.icon).toBe("bwi-eye");
expect(button.icon()).toBe("bwi-eye");
});
it("input is type password", () => {
expect(input.type).toBe("password");
expect(input.type!()).toBe("password");
});
it("spellcheck is disabled", () => {
expect(input.spellcheck).toBe(undefined);
expect(input.spellcheck!()).toBe(undefined);
});
});
});

View File

@@ -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";
@Directive({
selector: "[bitPrefix]",
host: {
"[class]": "classList",
},
})
export class BitPrefixDirective implements OnInit {
@HostBinding("class") @Input() get classList() {
return ["tw-text-muted"];
}
readonly classList = ["tw-text-muted"];
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
ngOnInit() {
if (this.iconButtonComponent) {
this.iconButtonComponent.size = "small";
this.iconButtonComponent.size.set("small");
}
}
}

View File

@@ -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";
@Directive({
selector: "[bitSuffix]",
host: {
"[class]": "classList",
},
})
export class BitSuffixDirective implements OnInit {
@HostBinding("class") @Input() get classList() {
return ["tw-text-muted"];
}
readonly classList = ["tw-text-muted"];
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
ngOnInit() {
if (this.iconButtonComponent) {
this.iconButtonComponent.size = "small";
this.iconButtonComponent.size.set("small");
}
}
}

View File

@@ -9,7 +9,7 @@
<i
class="bwi bwi-spinner bwi-spin"
aria-hidden="true"
[ngClass]="{ 'bwi-lg': size === 'default' }"
[ngClass]="{ 'bwi-lg': size() === 'default' }"
></i>
</span>
</span>

View File

@@ -7,7 +7,7 @@ import {
ElementRef,
HostBinding,
inject,
Input,
input,
model,
Signal,
} from "@angular/core";
@@ -177,11 +177,11 @@ const sizes: Record<IconButtonSize, string[]> = {
},
})
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() {
return [
@@ -193,13 +193,15 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
"hover:tw-no-underline",
"focus:tw-outline-none",
]
.concat(styles[this.buttonType])
.concat(sizes[this.size])
.concat(this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType] : []);
.concat(styles[this.buttonType()])
.concat(sizes[this.size()])
.concat(
this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType()] : [],
);
}
get iconClass() {
return [this.icon, "!tw-m-0"];
return [this.icon(), "!tw-m-0"];
}
protected disabledAttr = computed(() => {
@@ -219,7 +221,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
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
@@ -237,7 +239,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
);
disabled = model<boolean>(false);
readonly disabled = model<boolean>(false);
getFocusTarget() {
return this.elementRef.nativeElement;

View File

@@ -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 { Icon, isIcon } from "./icon";
@@ -6,8 +6,8 @@ import { Icon, isIcon } from "./icon";
@Component({
selector: "bit-icon",
host: {
"[attr.aria-hidden]": "!ariaLabel",
"[attr.aria-label]": "ariaLabel",
"[attr.aria-hidden]": "!ariaLabel()",
"[attr.aria-label]": "ariaLabel()",
"[innerHtml]": "innerHtml",
},
template: ``,
@@ -15,16 +15,18 @@ import { Icon, isIcon } from "./icon";
export class BitIconComponent {
innerHtml: SafeHtml | null = null;
@Input() set icon(icon: Icon) {
if (!isIcon(icon)) {
return;
}
readonly icon = input<Icon>();
const svg = icon.svg;
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
readonly ariaLabel = input<string>();
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) {}
}

View File

@@ -4,7 +4,6 @@ import { Icon, svgIcon } from "./icon";
import { BitIconComponent } from "./icon.component";
describe("IconComponent", () => {
let component: BitIconComponent;
let fixture: ComponentFixture<BitIconComponent>;
beforeEach(async () => {
@@ -13,14 +12,13 @@ describe("IconComponent", () => {
}).compileComponents();
fixture = TestBed.createComponent(BitIconComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should have empty innerHtml when input is not an Icon", () => {
const fakeIcon = { svg: "harmful user input" } as Icon;
component.icon = fakeIcon;
fixture.componentRef.setInput("icon", fakeIcon);
fixture.detectChanges();
const el = fixture.nativeElement as HTMLElement;
@@ -30,7 +28,7 @@ describe("IconComponent", () => {
it("should contain icon when input is a safe Icon", () => {
const icon = svgIcon`<svg><text x="0" y="15">safe icon</text></svg>`;
component.icon = icon;
fixture.componentRef.setInput("icon", icon);
fixture.detectChanges();
const el = fixture.nativeElement as HTMLElement;

View File

@@ -1,6 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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 { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -21,11 +29,7 @@ import { FocusableElement } from "../shared/focusable-element";
selector: "[appAutofocus], [bitAutofocus]",
})
export class AutofocusDirective implements AfterContentChecked {
@Input() set appAutofocus(condition: boolean | string) {
this.autofocus = condition === "" || condition === true;
}
private autofocus: boolean;
readonly appAutofocus = input(undefined, { transform: booleanAttribute });
// Track if we have already focused the element.
private focused = false;
@@ -46,7 +50,7 @@ export class AutofocusDirective implements AfterContentChecked {
*/
ngAfterContentChecked() {
// 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;
}

View 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.

View 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>
`,
}),
};

View File

@@ -9,6 +9,8 @@ import {
NgZone,
Optional,
Self,
input,
model,
} from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
@@ -30,9 +32,15 @@ export function inputBorderClasses(error: boolean) {
@Directive({
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
host: {
"[class]": "classList()",
"[id]": "id()",
"[attr.type]": "type()",
"[attr.spellcheck]": "spellcheck()",
},
})
export class BitInputDirective implements BitFormFieldControl {
@HostBinding("class") @Input() get classList() {
classList() {
const classes = [
"tw-block",
"tw-w-full",
@@ -52,7 +60,7 @@ export class BitInputDirective implements BitFormFieldControl {
return classes.filter((s) => s != "");
}
@HostBinding() @Input() id = `bit-input-${nextId++}`;
readonly id = input(`bit-input-${nextId++}`);
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
@@ -60,10 +68,12 @@ export class BitInputDirective implements BitFormFieldControl {
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()
@Input()
get required() {
@@ -74,13 +84,13 @@ export class BitInputDirective implements BitFormFieldControl {
}
private _required: boolean;
@Input() hasPrefix = false;
@Input() hasSuffix = false;
readonly hasPrefix = input(false);
readonly hasSuffix = input(false);
@Input() showErrorsWhenDisabled? = false;
readonly showErrorsWhenDisabled = input<boolean>(false);
get labelForId(): string {
return this.id;
return this.id();
}
@HostListener("input")
@@ -89,7 +99,7 @@ export class BitInputDirective implements BitFormFieldControl {
}
get hasError() {
if (this.showErrorsWhenDisabled) {
if (this.showErrorsWhenDisabled()) {
return (
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
this.ngControl?.touched &&

View File

@@ -8,8 +8,8 @@
>
<div
[ngClass]="{
'tw-truncate': truncate,
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
'tw-truncate': truncate(),
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
}"
>
<ng-content></ng-content>
@@ -22,8 +22,8 @@
bitTypography="helper"
class="tw-text-muted tw-w-full"
[ngClass]="{
'tw-truncate': truncate,
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
'tw-truncate': truncate(),
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
}"
>
<ng-content select="[slot=secondary]"></ng-content>

View File

@@ -7,9 +7,9 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
signal,
ViewChild,
input,
} from "@angular/core";
import { TypographyModule } from "../typography";
@@ -39,7 +39,7 @@ export class ItemContentComponent implements AfterContentChecked {
*
* Default behavior is truncation.
*/
@Input() truncate = true;
readonly truncate = input(true);
ngAfterContentChecked(): void {
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);

View File

@@ -1,12 +1,4 @@
import {
Input,
HostBinding,
Directive,
inject,
ElementRef,
input,
booleanAttribute,
} from "@angular/core";
import { HostBinding, Directive, inject, ElementRef, input, booleanAttribute } from "@angular/core";
import { ariaDisableElement } from "../utils";
@@ -77,8 +69,7 @@ const commonStyles = [
@Directive()
abstract class LinkDirective {
@Input()
linkType: LinkType = "primary";
readonly linkType = input<LinkType>("primary");
}
/**
@@ -96,7 +87,7 @@ export class AnchorLinkDirective extends LinkDirective {
@HostBinding("class") get classList() {
return ["before:-tw-inset-y-[0.125rem]"]
.concat(commonStyles)
.concat(linkStyles[this.linkType] ?? []);
.concat(linkStyles[this.linkType()] ?? []);
}
}
@@ -111,7 +102,7 @@ export class ButtonLinkDirective extends LinkDirective {
@HostBinding("class") get classList() {
return ["before:-tw-inset-y-[0.25rem]"]
.concat(commonStyles)
.concat(linkStyles[this.linkType] ?? []);
.concat(linkStyles[this.linkType()] ?? []);
}
constructor() {

View File

@@ -39,6 +39,9 @@ export class MenuItemDirective implements FocusableOption {
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;
constructor(public elementRef: ElementRef<HTMLButtonElement>) {}

View File

@@ -8,26 +8,30 @@ import {
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy,
ViewContainerRef,
input,
} from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { filter, mergeWith } from "rxjs/operators";
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 {
@HostBinding("attr.aria-expanded") isOpen = false;
@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 defaultMenuConfig: OverlayConfig = {
@@ -66,14 +70,15 @@ export class MenuTriggerForDirective implements OnDestroy {
}
private openMenu() {
if (this.menu == null) {
const menu = this.menu();
if (menu == null) {
throw new Error("Cannot find bit-menu element");
}
this.isOpen = true;
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.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
@@ -90,11 +95,11 @@ export class MenuTriggerForDirective implements OnDestroy {
}
this.destroyMenu();
});
if (this.menu.keyManager) {
this.menu.keyManager.setFirstItemActive();
if (menu.keyManager) {
menu.keyManager.setFirstItemActive();
this.keyDownEventsSub = this.overlayRef
.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.disposeAll();
this.menu.closed.emit();
this.menu().closed.emit();
}
private getClosedEvents(): Observable<any> {
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef.keydownEvents().pipe(
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);
}),
);
const backdrop = this.overlayRef.backdropClick();
const menuClosed = this.menu.closed;
const menuClosed = this.menu().closed;
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
}

View File

@@ -2,10 +2,10 @@
<div
(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"
[attr.role]="ariaRole"
[attr.aria-label]="ariaLabel"
[attr.role]="ariaRole()"
[attr.aria-label]="ariaLabel()"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="ariaRole === 'dialog'"
[cdkTrapFocusAutoCapture]="ariaRole() === 'dialog'"
>
<ng-content></ng-content>
</div>

View File

@@ -10,7 +10,7 @@ import {
ContentChildren,
QueryList,
AfterContentInit,
Input,
input,
} from "@angular/core";
import { MenuItemDirective } from "./menu-item.directive";
@@ -28,12 +28,12 @@ export class MenuComponent implements AfterContentInit {
menuItems: QueryList<MenuItemDirective>;
keyManager?: FocusKeyManager<MenuItemDirective>;
@Input() ariaRole: "menu" | "dialog" = "menu";
readonly ariaRole = input<"menu" | "dialog">("menu");
@Input() ariaLabel: string;
readonly ariaLabel = input<string>();
ngAfterContentInit() {
if (this.ariaRole === "menu") {
if (this.ariaRole() === "menu") {
this.keyManager = new FocusKeyManager(this.menuItems)
.withWrap()
.skipPredicate((item) => item.disabled);

View File

@@ -1,12 +1,12 @@
<ng-select
[items]="baseItems"
[items]="baseItems()"
[(ngModel)]="selectedItems"
(ngModelChange)="onChange($event)"
(blur)="onBlur()"
bindLabel="listName"
groupBy="parentGrouping"
[placeholder]="placeholder"
[loading]="loading"
[placeholder]="placeholder()"
[loading]="loading()"
[loadingText]="loadingText"
notFoundText="{{ 'multiSelectNotFound' | i18n }}"
clearAllText="{{ 'multiSelectClearAll' | i18n }}"

View File

@@ -1,6 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { hasModifierKey } from "@angular/cdk/keycodes";
import {
Component,
@@ -12,6 +11,9 @@ import {
HostBinding,
Optional,
Self,
input,
model,
booleanAttribute,
} from "@angular/core";
import {
ControlValueAccessor,
@@ -38,6 +40,9 @@ let nextId = 0;
templateUrl: "./multi-select.component.html",
providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }],
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe],
host: {
"[id]": "this.id()",
},
})
/**
* 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;
// 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
@Input() removeSelectedItems = false;
@Input() placeholder: string;
@Input() loading = false;
@Input({ transform: coerceBooleanProperty }) disabled?: boolean;
readonly removeSelectedItems = input(false);
readonly placeholder = model<string>();
readonly loading = input(false);
// 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
protected selectedItems: SelectItemView[];
@@ -79,7 +86,9 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
ngOnInit(): void {
// 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");
}
@@ -119,15 +128,15 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
this.onItemsConfirmed.emit(this.selectedItems);
// Remove selected items from base list based on input property
if (this.removeSelectedItems) {
let updatedBaseItems = this.baseItems;
if (this.removeSelectedItems()) {
let updatedBaseItems = this.baseItems();
this.selectedItems.forEach((selectedItem) => {
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
});
// Reset Lists
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 */
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
readonly id = input(`bit-multi-select-${nextId++}`);
/**Implemented as part of BitFormFieldControl */
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("attr.required")
@Input()
get required() {

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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";
/**
@@ -11,17 +11,17 @@ export abstract class NavBaseComponent {
/**
* Text to display in main content
*/
@Input() text: string;
readonly text = input<string>();
/**
* `aria-label` for main content
*/
@Input() ariaLabel: string;
readonly ariaLabel = input<string>();
/**
* 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.
@@ -34,31 +34,31 @@ export abstract class NavBaseComponent {
*
* See: {@link https://github.com/angular/angular/issues/24482}
*/
@Input() route?: RouterLink["routerLink"];
readonly route = input<RouterLink["routerLink"]>();
/**
* Passed to internal `routerLink`
*
* See {@link RouterLink.relativeTo}
*/
@Input() relativeTo?: RouterLink["relativeTo"];
readonly relativeTo = input<RouterLink["relativeTo"]>();
/**
* Passed to internal `routerLink`
*
* See {@link RouterLinkActive.routerLinkActiveOptions}
*/
@Input() routerLinkActiveOptions?: RouterLinkActive["routerLinkActiveOptions"] = {
readonly routerLinkActiveOptions = input<RouterLinkActive["routerLinkActiveOptions"]>({
paths: "subset",
queryParams: "ignored",
fragment: "ignored",
matrixParams: "ignored",
};
});
/**
* If `true`, do not change styles when nav item is active.
*/
@Input() hideActiveStyles = false;
readonly hideActiveStyles = input(false);
/**
* Fires when main content is clicked

View File

@@ -1,28 +1,28 @@
<!-- This a higher order component that composes `NavItemComponent` -->
@if (!hideIfEmpty || nestedNavComponents.length > 0) {
@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
<bit-nav-item
[text]="text"
[icon]="icon"
[route]="route"
[relativeTo]="relativeTo"
[routerLinkActiveOptions]="routerLinkActiveOptions"
[text]="text()"
[icon]="icon()"
[route]="route()"
[relativeTo]="relativeTo()"
[routerLinkActiveOptions]="routerLinkActiveOptions()"
(mainContentClicked)="handleMainContentClicked()"
[ariaLabel]="ariaLabel"
[ariaLabel]="ariaLabel()"
[hideActiveStyles]="parentHideActiveStyles"
>
<ng-template #button>
<button
type="button"
class="tw-ms-auto"
[bitIconButton]="open ? 'bwi-angle-up' : 'bwi-angle-down'"
[bitIconButton]="open() ? 'bwi-angle-up' : 'bwi-angle-down'"
[buttonType]="'light'"
(click)="toggle($event)"
size="small"
[title]="'toggleCollapse' | i18n"
aria-haspopup="true"
[attr.aria-expanded]="open.toString()"
[attr.aria-expanded]="open().toString()"
[attr.aria-controls]="contentId"
[attr.aria-label]="['toggleCollapse' | i18n, text].join(' ')"
[attr.aria-label]="['toggleCollapse' | i18n, text()].join(' ')"
></button>
</ng-template>
<ng-container slot="end">
@@ -32,10 +32,10 @@
</bit-nav-item>
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
@if (sideNavService.open$ | async) {
@if (open) {
@if (open()) {
<div
[attr.id]="contentId"
[attr.aria-label]="[text, 'submenu' | i18n].join(' ')"
[attr.aria-label]="[text(), 'submenu' | i18n].join(' ')"
role="group"
>
<ng-content></ng-content>

View File

@@ -4,11 +4,12 @@ import {
Component,
ContentChildren,
EventEmitter,
Input,
Optional,
Output,
QueryList,
SkipSelf,
input,
model,
} from "@angular/core";
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. */
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
*/
@Input()
open = false;
readonly open = model(false);
/**
* Automatically hide the nav group if there are no child buttons
*/
@Input({ transform: booleanAttribute })
hideIfEmpty = false;
readonly hideIfEmpty = input(false, { transform: booleanAttribute });
@Output()
openChange = new EventEmitter<boolean>();
@@ -67,24 +66,24 @@ export class NavGroupComponent extends NavBaseComponent {
}
setOpen(isOpen: boolean) {
this.open = isOpen;
this.openChange.emit(this.open);
this.open.set(isOpen);
this.openChange.emit(this.open());
// FIXME: Remove when updating file. Eslint update
// 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) {
event?.stopPropagation();
this.setOpen(!this.open);
this.setOpen(!this.open());
}
protected handleMainContentClicked() {
if (!this.sideNavService.open) {
if (!this.route) {
if (!this.route()) {
this.sideNavService.setOpen();
}
this.open = true;
this.open.set(true);
} else {
this.toggle();
}

View File

@@ -1,6 +1,6 @@
<div class="tw-ps-2 tw-pe-2">
@let open = sideNavService.open$ | async;
@if (open || icon) {
@if (open || icon()) {
<div
class="tw-relative tw-rounded-md tw-h-10"
[ngClass]="[
@@ -16,17 +16,17 @@
<!-- Main content of `NavItem` -->
<ng-template #anchorAndButtonContent>
<div
[title]="text"
[title]="text()"
class="tw-gap-2 tw-items-center tw-font-bold tw-h-full tw-content-center"
[ngClass]="{ 'tw-text-center': !open, 'tw-flex': open }"
>
<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-label]="text"
[attr.aria-label]="text()"
></i>
@if (open) {
<span class="tw-truncate">{{ text }}</span>
<span class="tw-truncate">{{ text() }}</span>
}
</div>
</ng-template>
@@ -38,11 +38,11 @@
<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"
data-fvw
[routerLink]="route"
[relativeTo]="relativeTo"
[attr.aria-label]="ariaLabel || text"
[routerLink]="route()"
[relativeTo]="relativeTo()"
[attr.aria-label]="ariaLabel() || text()"
routerLinkActive
[routerLinkActiveOptions]="routerLinkActiveOptions"
[routerLinkActiveOptions]="routerLinkActiveOptions()"
[ariaCurrentWhenActive]="'page'"
(isActiveChange)="setIsActive($event)"
(click)="mainContentClicked.emit()"

View File

@@ -1,5 +1,5 @@
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 { BehaviorSubject, map } from "rxjs";
@@ -21,7 +21,7 @@ export abstract class NavGroupAbstraction {
})
export class NavItemComponent extends NavBaseComponent {
/** 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
@@ -34,7 +34,7 @@ export class NavItemComponent extends NavBaseComponent {
}
}
protected get showActiveStyles() {
return this.forceActiveStyles || (this._isActive && !this.hideActiveStyles);
return this.forceActiveStyles() || (this._isActive && !this.hideActiveStyles());
}
/**

View File

@@ -6,14 +6,14 @@
class="tw-px-2 tw-pt-5"
>
<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"
[ngClass]="{ '[&_svg]:tw-w-[1.687rem]': !sideNavService.open }"
[attr.aria-label]="label"
[title]="label"
[attr.aria-label]="label()"
[title]="label()"
routerLinkActive
[ariaCurrentWhenActive]="'page'"
>
<bit-icon [icon]="sideNavService.open ? openIcon : closedIcon"></bit-icon>
<bit-icon [icon]="sideNavService.open ? openIcon() : closedIcon()"></bit-icon>
</a>
</div>

View File

@@ -1,7 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Component, input } from "@angular/core";
import { RouterLinkActive, RouterLink } from "@angular/router";
import { Icon } from "../icon";
@@ -17,18 +18,18 @@ import { SideNavService } from "./side-nav.service";
})
export class NavLogoComponent {
/** 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 */
@Input({ required: true }) openIcon: Icon;
readonly openIcon = input.required<Icon>();
/**
* 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` */
@Input({ required: true }) label: string;
readonly label = input.required<string>();
constructor(protected sideNavService: SideNavService) {}
}

View File

@@ -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"
[ngClass]="{ 'tw-w-60': data.open }"
[ngStyle]="
variant === 'secondary' && {
variant() === 'secondary' && {
'--color-text-alt2': 'var(--color-text-main)',
'--color-background-alt3': 'var(--color-secondary-100)',
'--color-background-alt4': 'var(--color-secondary-300)',

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { CdkTrapFocus } from "@angular/cdk/a11y";
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";
@@ -19,7 +19,7 @@ export type SideNavVariant = "primary" | "secondary";
imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe],
})
export class SideNavComponent {
@Input() variant: SideNavVariant = "primary";
readonly variant = input<SideNavVariant>("primary");
@ViewChild("toggleButton", { read: ElementRef, static: true })
private toggleButton: ElementRef<HTMLButtonElement>;

View File

@@ -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-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">
<ng-content select="[slot=title]"></ng-content>
</h3>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from "@angular/core";
import { Component, input } from "@angular/core";
import { Icons } from "..";
import { BitIconComponent } from "../icon/icon.component";
@@ -12,5 +12,5 @@ import { BitIconComponent } from "../icon/icon.component";
imports: [BitIconComponent],
})
export class NoItemsComponent {
@Input() icon = Icons.Search;
readonly icon = input(Icons.Search);
}

View File

@@ -6,11 +6,11 @@ import {
AfterViewInit,
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy,
ViewContainerRef,
input,
model,
} from "@angular/core";
import { Observable, Subscription, filter, mergeWith } from "rxjs";
@@ -20,27 +20,26 @@ import { PopoverComponent } from "./popover.component";
@Directive({
selector: "[bitPopoverTriggerFor]",
exportAs: "popoverTrigger",
host: {
"[attr.aria-expanded]": "this.popoverOpen()",
},
})
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
@Input()
@HostBinding("attr.aria-expanded")
popoverOpen = false;
readonly popoverOpen = model(false);
@Input("bitPopoverTriggerFor")
popover: PopoverComponent;
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" });
@Input("position")
position: string;
readonly position = input<string>();
private overlayRef: OverlayRef;
private closedEventsSub: Subscription;
get positions() {
if (!this.position) {
if (!this.position()) {
return defaultPositions;
}
const preferredPosition = defaultPositions.find((position) => position.id === this.position);
const preferredPosition = defaultPositions.find((position) => position.id === this.position());
if (preferredPosition) {
return [preferredPosition, ...defaultPositions];
@@ -72,7 +71,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
@HostListener("click")
togglePopover() {
if (this.popoverOpen) {
if (this.popoverOpen()) {
this.closePopover();
} else {
this.openPopover();
@@ -80,10 +79,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
}
private openPopover() {
this.popoverOpen = true;
this.popoverOpen.set(true);
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.closedEventsSub = this.getClosedEvents().subscribe(() => {
@@ -97,17 +96,17 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
.keydownEvents()
.pipe(filter((event: KeyboardEvent) => event.key === "Escape"));
const backdrop = this.overlayRef.backdropClick();
const popoverClosed = this.popover.closed;
const popoverClosed = this.popover().closed;
return detachments.pipe(mergeWith(escKey, backdrop, popoverClosed));
}
private destroyPopover() {
if (this.overlayRef == null || !this.popoverOpen) {
if (this.overlayRef == null || !this.popoverOpen()) {
return;
}
this.popoverOpen = false;
this.popoverOpen.set(false);
this.disposeAll();
}
@@ -117,7 +116,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
}
ngAfterViewInit() {
if (this.popoverOpen) {
if (this.popoverOpen()) {
this.openPopover();
}
}

View File

@@ -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">
<h2 bitTypography="h5" class="tw-mt-1 tw-font-semibold">
{{ title }}
{{ title() }}
</h2>
<button
type="button"

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { SharedModule } from "../shared/shared.module";
@@ -15,6 +15,6 @@ import { TypographyModule } from "../typography";
})
export class PopoverComponent {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Input() title = "";
readonly title = input("");
@Output() closed = new EventEmitter();
}

View File

@@ -4,8 +4,8 @@
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
attr.aria-valuenow="{{ barWidth }}"
[ngStyle]="{ width: barWidth + '%' }"
attr.aria-valuenow="{{ barWidth() }}"
[ngStyle]="{ width: barWidth() + '%' }"
>
@if (displayText) {
<div class="tw-flex tw-h-full tw-flex-wrap tw-items-center tw-overflow-hidden">

View File

@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Component, input } from "@angular/core";
type ProgressSizeType = "small" | "default" | "large";
type BackgroundType = "danger" | "primary" | "success" | "warning";
@@ -26,19 +26,19 @@ const BackgroundClasses: Record<BackgroundType, string[]> = {
imports: [CommonModule],
})
export class ProgressComponent {
@Input() barWidth = 0;
@Input() bgColor: BackgroundType = "primary";
@Input() showText = true;
@Input() size: ProgressSizeType = "default";
@Input() text?: string;
readonly barWidth = input(0);
readonly bgColor = input<BackgroundType>("primary");
readonly showText = input(true);
readonly size = input<ProgressSizeType>("default");
readonly text = input<string>();
get displayText() {
return this.showText && this.size !== "small";
return this.showText() && this.size() !== "small";
}
get outerBarStyles() {
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-transition-all",
]
.concat(SizeClasses[this.size])
.concat(BackgroundClasses[this.bgColor]);
.concat(SizeClasses[this.size()])
.concat(BackgroundClasses[this.bgColor()]);
}
get textContent() {
return this.text || this.barWidth + "%";
return this.text() || this.barWidth() + "%";
}
}

View File

@@ -3,8 +3,8 @@
type="radio"
bitRadio
[id]="inputId"
[disabled]="groupDisabled || disabled"
[value]="value"
[disabled]="groupDisabled || disabled()"
[value]="value()"
[checked]="selected"
(change)="onInputChange()"
(blur)="onBlur()"

View File

@@ -1,5 +1,6 @@
import { Component } from "@angular/core";
import { Component, DebugElement } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { By } from "@angular/platform-browser";
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";
describe("RadioButton", () => {
let mockGroupComponent: MockedButtonGroupComponent;
let fixture: ComponentFixture<TestApp>;
let testAppComponent: TestApp;
let radioButton: HTMLInputElement;
let fixture: ComponentFixture<TestComponent>;
let radioButtonGroup: RadioGroupComponent;
let radioButtons: DebugElement[];
beforeEach(async () => {
mockGroupComponent = new MockedButtonGroupComponent();
TestBed.configureTestingModule({
imports: [TestApp],
providers: [
{ provide: RadioGroupComponent, useValue: mockGroupComponent },
{ provide: I18nService, useValue: new I18nMockService({}) },
],
imports: [TestComponent],
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
});
await TestBed.compileComponents();
fixture = TestBed.createComponent(TestApp);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
testAppComponent = fixture.debugElement.componentInstance;
radioButton = fixture.debugElement.query(By.css("input[type=radio]")).nativeElement;
radioButtonGroup = fixture.debugElement.query(
By.directive(RadioGroupComponent),
).componentInstance;
radioButtons = fixture.debugElement.queryAll(By.css("input[type=radio]"));
});
it("should emit value when clicking on radio button", () => {
testAppComponent.value = "value";
const spyFn = jest.spyOn(radioButtonGroup, "onInputChange");
radioButtons[1].triggerEventHandler("change");
fixture.detectChanges();
radioButton.click();
fixture.detectChanges();
expect(mockGroupComponent.onInputChange).toHaveBeenCalledWith("value");
expect(spyFn).toHaveBeenCalledWith(1);
});
it("should check radio button when selected matches value", () => {
testAppComponent.value = "value";
it("should check radio button only when selected matches value", () => {
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();
expect(radioButton.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);
expect(radioButtons[0].nativeElement.checked).toBe(false);
expect(radioButtons[1].nativeElement.checked).toBe(true);
});
});
class MockedButtonGroupComponent implements Partial<RadioGroupComponent> {
onInputChange = jest.fn();
selected: unknown = null;
}
@Component({
selector: "test-app",
template: `<bit-radio-button [value]="value"><bit-label>Element</bit-label></bit-radio-button>`,
imports: [RadioButtonModule],
selector: "test-component",
template: `
<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 {
value?: string;
class TestComponent {
formObj = new FormGroup({
radio: new FormControl(0),
});
}

View File

@@ -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";
@@ -11,20 +11,23 @@ let nextId = 0;
selector: "bit-radio-button",
templateUrl: "radio-button.component.html",
imports: [FormControlModule, RadioInputComponent],
host: {
"[id]": "this.id()",
},
})
export class RadioButtonComponent {
@HostBinding("attr.id") @Input() id = `bit-radio-button-${nextId++}`;
readonly id = input(`bit-radio-button-${nextId++}`);
@HostBinding("class") get classList() {
return [this.block ? "tw-block" : "tw-inline-block", "tw-mb-1", "[&_bit-hint]:tw-mt-0"];
}
@Input() value: unknown;
@Input() disabled = false;
readonly value = input<unknown>();
readonly disabled = input(false);
constructor(private groupComponent: RadioGroupComponent) {}
get inputId() {
return `${this.id}-input`;
return `${this.id()}-input`;
}
get name() {
@@ -32,7 +35,7 @@ export class RadioButtonComponent {
}
get selected() {
return this.groupComponent.selected === this.value;
return this.groupComponent.selected === this.value();
}
get groupDisabled() {
@@ -40,11 +43,11 @@ export class RadioButtonComponent {
}
get block() {
return this.groupComponent.block;
return this.groupComponent.block();
}
protected onInputChange() {
this.groupComponent.onInputChange(this.value);
this.groupComponent.onInputChange(this.value());
}
protected onBlur() {

View File

@@ -177,15 +177,15 @@ export const Disabled: Story = {
<bit-radio-group formControlName="radio" aria-label="Example radio group">
<bit-label>Group of radio buttons</bit-label>
<bit-radio-button id="radio-first" [value]="0" [disabled]="true">
<bit-radio-button [value]="0" [disabled]="true">
<bit-label>First</bit-label>
</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-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-radio-button>
</bit-radio-group>

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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 { I18nPipe } from "@bitwarden/ui-common";
@@ -14,11 +14,16 @@ let nextId = 0;
selector: "bit-radio-group",
templateUrl: "radio-group.component.html",
imports: [NgTemplateOutlet, I18nPipe],
host: {
"[id]": "id()",
},
})
export class RadioGroupComponent implements ControlValueAccessor {
selected: unknown;
disabled = false;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
private _name?: string;
@Input() get name() {
return this._name ?? this.ngControl?.name?.toString();
@@ -27,10 +32,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
this._name = value;
}
@Input() block = false;
readonly block = input(false);
@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"];
@ContentChild(BitLabel) protected label: BitLabel;

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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 { BitFormControlAbstraction } from "../form-control";
@@ -11,9 +11,12 @@ let nextId = 0;
selector: "input[type=radio][bitRadio]",
template: "",
providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }],
host: {
"[id]": "this.id()",
},
})
export class RadioInputComponent implements BitFormControlAbstraction {
@HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`;
readonly id = input(`bit-radio-input-${nextId++}`);
@HostBinding("class")
protected inputClasses = [
@@ -73,6 +76,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
constructor(@Optional() @Self() private ngControl?: NgControl) {}
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding()
@Input()
get disabled() {
@@ -83,6 +88,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
}
private _disabled: boolean;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get required() {
return (

View File

@@ -12,12 +12,12 @@
bitInput
[type]="inputType"
[id]="id"
[placeholder]="placeholder ?? ('search' | i18n)"
[placeholder]="placeholder() ?? ('search' | i18n)"
class="tw-ps-9"
[ngModel]="searchText"
(ngModelChange)="onChange($event)"
(blur)="onTouch()"
[disabled]="disabled"
[attr.autocomplete]="autocomplete"
[disabled]="disabled()"
[attr.autocomplete]="autocomplete()"
/>
</div>

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
import { Component, ElementRef, ViewChild, input, model } from "@angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
@@ -43,9 +43,9 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
// Use `type="text"` for Safari to improve rendering performance
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
@Input() disabled: boolean;
@Input() placeholder: string;
@Input() autocomplete: string;
readonly disabled = model<boolean>();
readonly placeholder = input<string>();
readonly autocomplete = input<string>();
getFocusTarget() {
return this.input?.nativeElement;
@@ -76,6 +76,6 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
this.disabled.set(isDisabled);
}
}

View File

@@ -1,6 +1,6 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { Component, input } from "@angular/core";
@Component({
selector: "bit-section",
@@ -9,7 +9,7 @@ import { Component, Input } from "@angular/core";
<section
[ngClass]="{
'tw-mb-5 bit-compact:tw-mb-4 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12':
!disableMargin,
!disableMargin(),
}"
>
<ng-content></ng-content>
@@ -17,5 +17,5 @@ import { Component, Input } from "@angular/core";
`,
})
export class SectionComponent {
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
readonly disableMargin = input(false, { transform: coerceBooleanProperty });
}

View File

@@ -1,23 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @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({
selector: "bit-option",
template: `<ng-template><ng-content></ng-content></ng-template>`,
})
export class OptionComponent<T = unknown> implements Option<T> {
@Input()
icon?: string;
export class OptionComponent<T = unknown> implements MappedOptionComponent<T> {
readonly icon = input<string>();
@Input({ required: true })
value: T;
readonly value = input.required<T>();
@Input({ required: true })
label: string;
readonly label = input.required<string>();
@Input({ transform: booleanAttribute })
disabled: boolean;
readonly disabled = input(undefined, { transform: booleanAttribute });
}

View File

@@ -1,6 +1,10 @@
import { MappedDataToSignal } from "../shared/data-to-signal-type";
export interface Option<T> {
icon?: string;
value: T | null;
label?: string;
disabled?: boolean;
}
export type MappedOptionComponent<T> = MappedDataToSignal<Option<T>>;

View File

@@ -1,9 +1,9 @@
<ng-select
[(ngModel)]="selectedOption"
[ngModel]="selectedOption()"
(ngModelChange)="onChange($event)"
[disabled]="disabled"
[placeholder]="placeholder"
[items]="items"
[placeholder]="placeholder()"
[items]="items()"
(blur)="onBlur()"
[labelForId]="labelForId"
[clearable]="false"

View File

@@ -37,15 +37,15 @@ describe("Select Component", () => {
describe("initial state", () => {
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: "Pear", value: "pear" },
{ label: "Banana", value: "banana" },
];
]);
expect(select.selectedOption?.value).toBe("apple");
expect(select.selectedOption()?.value).toBe("apple");
});
});
});

View File

@@ -13,6 +13,11 @@ import {
ViewChild,
Output,
EventEmitter,
input,
Signal,
computed,
model,
signal,
} from "@angular/core";
import {
ControlValueAccessor,
@@ -37,29 +42,23 @@ let nextId = 0;
templateUrl: "select.component.html",
providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }],
imports: [NgSelectModule, ReactiveFormsModule, FormsModule],
host: {
"[id]": "id()",
},
})
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
@ViewChild(NgSelectComponent) select: NgSelectComponent;
private _items: Option<T>[] = [];
/** Optional: Options can be provided using an array input or using `bit-option` */
@Input()
get items(): Option<T>[] {
return this._items;
}
set items(next: Option<T>[]) {
this._items = next;
this._selectedOption = this.findSelectedOption(next, this.selectedValue);
}
readonly items = model<Option<T>[] | undefined>();
@Input() placeholder = this.i18nService.t("selectPlaceholder");
readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
@Output() closed = new EventEmitter();
protected selectedValue: T;
protected _selectedOption: Option<T>;
get selectedOption() {
return this._selectedOption;
}
protected selectedValue = signal<T>(undefined);
selectedOption: Signal<Option<T>> = computed(() =>
this.findSelectedOption(this.items(), this.selectedValue()),
);
protected searchInputId = `bit-select-search-input-${nextId++}`;
private notifyOnChange?: (value: T) => void;
@@ -79,7 +78,14 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
if (value == null || value.length == 0) {
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"];
@@ -89,6 +95,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
get disabledAttr() {
return this.disabled || null;
}
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get disabled() {
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 */
writeValue(obj: T): void {
this.selectedValue = obj;
this._selectedOption = this.findSelectedOption(this.items, this.selectedValue);
this.selectedValue.set(obj);
}
/**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 */
protected onChange(option: Option<T> | null) {
this.selectedValue.set(option?.value);
if (!this.notifyOnChange) {
return;
}
@@ -154,9 +163,11 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
}
/**Implemented as part of BitFormFieldControl */
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
readonly id = input(`bit-multi-select-${nextId++}`);
/**Implemented as part of BitFormFieldControl */
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("attr.required")
@Input()
get required() {
@@ -178,8 +189,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
return [key, this.ngControl?.errors[key]];
}
private findSelectedOption(items: Option<T>[], value: T): Option<T> | undefined {
return items.find((item) => item.value === value);
private findSelectedOption(items: Option<T>[] | undefined, value: T): Option<T> | undefined {
return items?.find((item) => item.value === value);
}
/**Emits the closed event. */

View File

@@ -0,0 +1,5 @@
import { Signal } from "@angular/core";
export type MappedDataToSignal<T> = {
[Property in keyof T]: Signal<T[Property]>;
};

View File

@@ -42,6 +42,8 @@ export class StepperComponent extends CdkStepper {
private initialOrientation: StepperOrientation | undefined = undefined;
// 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()
override get orientation() {
return this.internalOrientation || "vertical";

View File

@@ -1,13 +1,13 @@
import { Directive, HostBinding, Input } from "@angular/core";
import { Directive, HostBinding, input } from "@angular/core";
@Directive({
selector: "tr[bitRow]",
})
export class RowDirective {
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle";
readonly alignContent = input<"top" | "middle" | "bottom" | "baseline">("middle");
get alignmentClass(): string {
switch (this.alignContent) {
switch (this.alignContent()) {
case "top":
return "tw-align-top";
case "middle":

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { NgClass } from "@angular/common";
import { Component, HostBinding, Input, OnInit } from "@angular/core";
import { Component, HostBinding, OnInit, input } from "@angular/core";
import type { SortDirection, SortFn } from "./table-data-source";
import { TableComponent } from "./table.component";
@@ -26,19 +26,17 @@ export class SortableComponent implements OnInit {
/**
* Mark the column as sortable and specify the key to sort by
*/
@Input() bitSortable: string;
readonly bitSortable = input<string>();
private _default: SortDirection | boolean = false;
/**
* Mark the column as the default sort column
*/
@Input() set default(value: SortDirection | boolean | "") {
if (value === "desc" || value === "asc") {
this._default = value;
} else {
this._default = coerceBooleanProperty(value) ? "asc" : false;
}
}
readonly default = input(false, {
transform: (value: SortDirection | boolean | "") => {
if (value === "desc" || value === "asc") {
return value as SortDirection;
} else {
return coerceBooleanProperty(value) ? ("asc" as SortDirection) : false;
}
},
});
/**
* Custom sorting function
@@ -51,12 +49,12 @@ export class SortableComponent implements OnInit {
* return direction === 'asc' ? result : -result;
* }
*/
@Input() fn: SortFn;
readonly fn = input<SortFn>();
constructor(private table: TableComponent) {}
ngOnInit(): void {
if (this._default && !this.isActive) {
if (this.default() && !this.isActive) {
this.setActive();
}
}
@@ -69,28 +67,29 @@ export class SortableComponent implements OnInit {
}
protected setActive() {
if (this.table.dataSource) {
const defaultDirection = this._default === "desc" ? "desc" : "asc";
const dataSource = this.table.dataSource();
if (dataSource) {
const defaultDirection = this.default() === "desc" ? "desc" : "asc";
const direction = this.isActive
? this.direction === "asc"
? "desc"
: "asc"
: defaultDirection;
this.table.dataSource.sort = {
column: this.bitSortable,
dataSource.sort = {
column: this.bitSortable(),
direction: direction,
fn: this.fn,
fn: this.fn(),
};
}
}
private get sort() {
return this.table.dataSource?.sort;
return this.table.dataSource()?.sort;
}
get isActive() {
return this.sort?.column === this.bitSortable;
return this.sort?.column === this.bitSortable();
}
get direction() {

View File

@@ -1,6 +1,6 @@
<cdk-virtual-scroll-viewport
bitScrollLayout
[itemSize]="rowSize"
[itemSize]="rowSize()"
[ngStyle]="{ paddingBottom: headerHeight + 'px' }"
>
<table [ngClass]="tableClass">
@@ -12,7 +12,7 @@
</tr>
</thead>
<tbody>
<tr *cdkVirtualFor="let r of rows$; trackBy: trackBy; templateCacheSize: 0" bitRow>
<tr *cdkVirtualFor="let r of rows$; trackBy: trackBy(); templateCacheSize: 0" bitRow>
<ng-container *ngTemplateOutlet="rowDef.template; context: { $implicit: r }"></ng-container>
</tr>
</tbody>

View File

@@ -10,7 +10,6 @@ import {
AfterContentChecked,
Component,
ContentChild,
Input,
OnDestroy,
TemplateRef,
Directive,
@@ -18,6 +17,7 @@ import {
AfterViewInit,
ElementRef,
TrackByFunction,
input,
} from "@angular/core";
import { ScrollLayoutDirective } from "../layout";
@@ -64,10 +64,10 @@ export class TableScrollComponent
implements AfterContentChecked, AfterViewInit, OnDestroy
{
/** The size of the rows in the list (in pixels). */
@Input({ required: true }) rowSize: number;
readonly rowSize = input.required<number>();
/** Optional trackBy function. */
@Input() trackBy: TrackByFunction<any> | undefined;
readonly trackBy = input<TrackByFunction<any> | undefined>();
@ContentChild(BitRowDef) protected rowDef: BitRowDef;

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