1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 19:53:59 +00:00

initial migration run

(cherry picked from commit 4eb5023a30)
This commit is contained in:
Vicki League
2025-06-23 10:56:51 -04:00
committed by William Martin
parent 172623e050
commit b42ecccb39
95 changed files with 602 additions and 375 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 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,8 +14,8 @@
</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">
@@ -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,
Input,
OnChanges,
OnInit,
SimpleChanges,
input,
} from "@angular/core";
import { RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
@@ -28,13 +36,17 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
return ["tw-h-full"];
}
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() title: string;
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() subtitle: string;
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() icon: Icon;
@Input() showReadonlyHostname: boolean;
@Input() hideLogo: boolean = false;
@Input() hideFooter: boolean = false;
@Input() hideIcon: boolean = false;
@Input() hideCardWrapper: boolean = false;
/**
@@ -42,7 +54,22 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
*
* @default 'md'
*/
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() maxWidth: AnonLayoutMaxWidth = "md";
readonly showReadonlyHostname = input<boolean>(undefined);
readonly hideLogo = input<boolean>(false);
readonly hideFooter = input<boolean>(false);
readonly hideIcon = input<boolean>(false);
/**
* Max width of the title area content
*
* @default null
*/
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() titleAreaMaxWidth?: "md";
protected logo = BitwardenLogo;
protected year: string;

View File

@@ -38,6 +38,8 @@ export class BitActionDirective implements OnDestroy {
disabled = false;
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input("bitAction") handler: FunctionReturningAwaitable;
constructor(

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>(undefined);
readonly disabled = input<boolean>(undefined);
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

@@ -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>(undefined);
readonly id = input<string>(undefined);
readonly text = input<string>(undefined);
readonly title = input<string>(undefined);
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,7 +1,7 @@
// 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, Input, OnChanges, input } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -18,10 +18,12 @@ export class BadgeListComponent implements OnChanges {
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);
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get maxItems(): number | undefined {
return this._maxItems;
@@ -32,11 +34,11 @@ export class BadgeListComponent implements OnChanges {
}
ngOnChanges() {
if (this.maxItems == undefined || this.items.length <= this.maxItems) {
this.filteredItems = this.items;
if (this.maxItems == undefined || this.items().length <= this.maxItems) {
this.filteredItems = this.items();
} else {
this.filteredItems = this.items.slice(0, this.maxItems - 1);
this.filteredItems = this.items().slice(0, this.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>(undefined);
/**
* 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,8 +1,8 @@
<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) {
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" aria-hidden="true"></i>
@@ -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

@@ -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, Input, OnInit, Output, EventEmitter } from "@angular/core";
import { Component, Input, OnInit, Output, EventEmitter, input } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -31,19 +31,22 @@ const defaultIcon: Record<BannerType, string> = {
imports: [CommonModule, IconButtonModule, I18nPipe],
})
export class BannerComponent implements OnInit {
@Input("bannerType") bannerType: BannerType = "info";
readonly bannerType = input<BannerType>("info");
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() icon: string;
@Input() useAlertRole = true;
@Input() showClose = true;
readonly useAlertRole = input(true);
readonly showClose = input(true);
@Output() onClose = new EventEmitter<void>();
ngOnInit(): void {
this.icon ??= defaultIcon[this.bannerType];
this.icon ??= 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,7 +1,15 @@
// 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,
Input,
Output,
TemplateRef,
ViewChild,
input,
} from "@angular/core";
import { QueryParamsHandling } from "@angular/router";
@Component({
@@ -9,17 +17,21 @@ import { QueryParamsHandling } from "@angular/router";
templateUrl: "./breadcrumb.component.html",
})
export class BreadcrumbComponent {
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input()
icon?: string;
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input()
route?: string | any[] = undefined;
@Input()
queryParams?: Record<string, string> = {};
readonly queryParams = input<Record<string, string>>({});
@Input()
queryParamsHandling?: QueryParamsHandling;
readonly queryParamsHandling = input<QueryParamsHandling>(undefined);
@Output()
click = new EventEmitter();

View File

@@ -5,8 +5,8 @@
linkType="primary"
class="tw-my-2 tw-inline-block"
[routerLink]="breadcrumb.route"
[queryParams]="breadcrumb.queryParams"
[queryParamsHandling]="breadcrumb.queryParamsHandling"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</a>
@@ -44,8 +44,8 @@
bitMenuItem
linkType="primary"
[routerLink]="breadcrumb.route"
[queryParams]="breadcrumb.queryParams"
[queryParamsHandling]="breadcrumb.queryParamsHandling"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</a>
@@ -65,8 +65,8 @@
linkType="primary"
class="tw-my-2 tw-inline-block"
[routerLink]="breadcrumb.route"
[queryParams]="breadcrumb.queryParams"
[queryParamsHandling]="breadcrumb.queryParamsHandling"
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
</a>

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

@@ -71,7 +71,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
"focus:tw-outline-none",
]
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
.concat(buttonStyles[this.buttonType ?? "secondary"])
.concat(buttonStyles[this.buttonType() ?? "secondary"])
.concat(
this.showDisabledStyles() || this.disabled()
? [
@@ -106,12 +106,14 @@ 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");
private _block = false;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get block(): boolean {
return this._block;

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, Input, OnInit, input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -35,23 +35,29 @@ let nextId = 0;
imports: [SharedModule, TypographyModule],
})
export class CalloutComponent implements OnInit {
@Input() type: CalloutTypes = "info";
readonly type = input<CalloutTypes>("info");
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() icon: string;
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() title: string;
@Input() useAlertRole = false;
readonly useAlertRole = input(false);
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]);
const type = this.type();
this.icon ??= defaultIcon[type];
if (this.title == null && defaultI18n[type] != null) {
this.title = this.i18nService.t(defaultI18n[type]);
}
}
get calloutClass() {
switch (this.type) {
switch (this.type()) {
case "danger":
return "tw-bg-danger-100";
case "info":

View File

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

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,13 +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>(undefined);
private _options: ChipSelectOption<T>[];
/** The select options to render */
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input({ required: true })
get options(): ChipSelectOption<T>[] {
return this._options;
@@ -71,10 +74,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
}
/** Disables the entire chip */
// TODO: Skipped for 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 +96,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 +118,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>(undefined);
/**
* 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 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

@@ -22,7 +22,7 @@
noMargin
class="tw-text-main tw-mb-0 tw-truncate"
>
{{ title }}
{{ title() }}
@if (subtitle) {
<span class="tw-text-muted tw-font-normal tw-text-sm">
{{ subtitle }}
@@ -44,12 +44,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,8 +59,8 @@
[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-overflow-y-auto': !loading(),
'tw-invisible tw-overflow-y-hidden': loading(),
}"
>
<ng-content select="[bitDialogContent]"></ng-content>

View File

@@ -4,7 +4,7 @@ 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, Input, inject, viewChild, input } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -40,28 +40,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>(undefined);
/**
* Subtitle to show in the dialog's header
*/
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() subtitle: string;
private _disablePadding = false;
/**
* Disable the built-in padding on the dialog, for use with tabbed dialogs.
*/
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() set disablePadding(value: boolean | "") {
this._disablePadding = coerceBooleanProperty(value);
}
@@ -72,7 +76,7 @@ export class DialogComponent {
/**
* 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
@@ -92,7 +96,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 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

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { NgClass } from "@angular/common";
import { Component, ContentChild, HostBinding, Input } from "@angular/core";
import { Component, ContentChild, HostBinding, Input, input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -17,9 +17,11 @@ import { BitFormControlAbstraction } from "./form-control.abstraction";
imports: [NgClass, TypographyDirective, I18nPipe],
})
export class FormControlComponent {
@Input() label: string;
readonly label = input<string>(undefined);
private _inline = false;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() get inline() {
return this._inline;
}
@@ -28,6 +30,8 @@ export class FormControlComponent {
}
private _disableMargin = false;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() set disableMargin(value: boolean | "") {
this._disableMargin = coerceBooleanProperty(value);
}

View File

@@ -19,6 +19,8 @@ export class BitLabel {
@Optional() private parentFormControl: FormControlComponent,
) {}
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("class") @Input() get classList() {
return ["tw-inline-flex", "tw-gap-1", "tw-items-baseline", "tw-flex-row", "tw-min-w-0"];
}
@@ -27,6 +29,9 @@ export class BitLabel {
return this.elementRef.nativeElement.textContent.trim();
}
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding() @Input() id = `bit-label-${nextId++}`;
get isInsideFormControl() {

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>(undefined);
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]>(undefined);
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

@@ -12,6 +12,7 @@ import {
Input,
ViewChild,
signal,
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 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

@@ -23,6 +23,8 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
/**
* Whether the input is toggled to show the password.
*/
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@HostBinding("attr.aria-pressed") @Input() toggled = false;
@Output() toggledChange = new EventEmitter<boolean>();

View File

@@ -6,6 +6,8 @@ import { BitIconButtonComponent } from "../icon-button/icon-button.component";
selector: "[bitPrefix]",
})
export class BitPrefixDirective implements OnInit {
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("class") @Input() get classList() {
return ["tw-text-muted"];
}

View File

@@ -6,6 +6,8 @@ import { BitIconButtonComponent } from "../icon-button/icon-button.component";
selector: "[bitSuffix]",
})
export class BitSuffixDirective implements OnInit {
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("class") @Input() get classList() {
return ["tw-text-muted"];
}

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, computed, ElementRef, HostBinding, Input, model } from "@angular/core";
import { Component, computed, ElementRef, HostBinding, Input, model, input } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { debounce, interval } from "rxjs";
@@ -167,10 +167,14 @@ const sizes: Record<IconButtonSize, string[]> = {
},
})
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input("bitIconButton") icon: string;
@Input() buttonType: IconButtonType = "main";
readonly buttonType = input<IconButtonType>("main");
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() size: IconButtonSize = "default";
@HostBinding("class") get classList() {
@@ -183,9 +187,11 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
"hover:tw-no-underline",
"focus:tw-outline-none",
]
.concat(styles[this.buttonType])
.concat(styles[this.buttonType()])
.concat(sizes[this.size])
.concat(this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType] : []);
.concat(
this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType()] : [],
);
}
get iconClass() {

View File

@@ -1,4 +1,4 @@
import { Component, Input } from "@angular/core";
import { Component, Input, 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,6 +15,8 @@ import { Icon, isIcon } from "./icon";
export class BitIconComponent {
innerHtml: SafeHtml | null = null;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() set icon(icon: Icon) {
if (!isIcon(icon)) {
return;
@@ -24,7 +26,7 @@ export class BitIconComponent {
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
}
@Input() ariaLabel: string | undefined = undefined;
readonly ariaLabel = input<string | undefined>(undefined);
constructor(private domSanitizer: DomSanitizer) {}
}

View File

@@ -21,6 +21,8 @@ import { FocusableElement } from "../shared/focusable-element";
selector: "[appAutofocus], [bitAutofocus]",
})
export class AutofocusDirective implements AfterContentChecked {
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() set appAutofocus(condition: boolean | string) {
this.autofocus = condition === "" || condition === true;
}

View File

@@ -9,6 +9,7 @@ import {
NgZone,
Optional,
Self,
input,
} from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
@@ -32,6 +33,8 @@ export function inputBorderClasses(error: boolean) {
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
})
export class BitInputDirective implements BitFormFieldControl {
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("class") @Input() get classList() {
const classes = [
"tw-block",
@@ -52,6 +55,9 @@ export class BitInputDirective implements BitFormFieldControl {
return classes.filter((s) => s != "");
}
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding() @Input() id = `bit-input-${nextId++}`;
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
@@ -60,10 +66,18 @@ export class BitInputDirective implements BitFormFieldControl {
return this.hasError ? true : undefined;
}
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding("attr.type") @Input() type?: InputTypes;
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding("attr.spellcheck") @Input() spellcheck?: boolean;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding()
@Input()
get required() {
@@ -74,10 +88,10 @@ 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;
@@ -89,7 +103,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,4 +1,4 @@
import { Input, HostBinding, Directive } from "@angular/core";
import { HostBinding, Directive, input } from "@angular/core";
export type LinkType = "primary" | "secondary" | "contrast" | "light";
@@ -62,8 +62,7 @@ const commonStyles = [
@Directive()
abstract class LinkDirective {
@Input()
linkType: LinkType = "primary";
readonly linkType = input<LinkType>("primary");
}
/**
@@ -81,7 +80,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()] ?? []);
}
}
@@ -92,6 +91,6 @@ 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()] ?? []);
}
}

View File

@@ -39,6 +39,8 @@ export class MenuItemDirective implements FocusableOption {
return this.disabled || null; // native disabled attr must be null when false
}
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input({ transform: coerceBooleanProperty }) disabled?: boolean = false;
constructor(public elementRef: ElementRef<HTMLButtonElement>) {}

View File

@@ -11,6 +11,7 @@ import {
Input,
OnDestroy,
ViewContainerRef,
input,
} from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { filter, mergeWith } from "rxjs/operators";
@@ -21,13 +22,16 @@ import { MenuComponent } from "./menu.component";
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";
}
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding("attr.role")
@Input()
role = "button";
@Input("bitMenuTriggerFor") menu: MenuComponent;
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>(undefined);
ngAfterContentInit() {
if (this.ariaRole === "menu") {
if (this.ariaRole() === "menu") {
this.keyManager = new FocusKeyManager(this.menuItems)
.withWrap()
.skipPredicate((item) => item.disabled);

View File

@@ -6,7 +6,7 @@
bindLabel="listName"
groupBy="parentGrouping"
[placeholder]="placeholder"
[loading]="loading"
[loading]="loading()"
[loadingText]="loadingText"
notFoundText="{{ 'multiSelectNotFound' | i18n }}"
clearAllText="{{ 'multiSelectClearAll' | i18n }}"

View File

@@ -12,6 +12,7 @@ import {
HostBinding,
Optional,
Self,
input,
} from "@angular/core";
import {
ControlValueAccessor,
@@ -46,11 +47,17 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
@ViewChild(NgSelectComponent) select: NgSelectComponent;
// Parent component should only pass selectable items (complete list - selected items = baseItems)
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() baseItems: SelectItemView[];
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
@Input() removeSelectedItems = false;
readonly removeSelectedItems = input(false);
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() placeholder: string;
@Input() loading = false;
readonly loading = input(false);
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input({ transform: coerceBooleanProperty }) disabled?: boolean;
// Internal tracking of selected items
@@ -119,7 +126,7 @@ 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) {
if (this.removeSelectedItems()) {
let updatedBaseItems = this.baseItems;
this.selectedItems.forEach((selectedItem) => {
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
@@ -186,9 +193,14 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
}
/**Implemented as part of BitFormFieldControl */
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
/**Implemented as part of BitFormFieldControl */
// TODO: Skipped for 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, Input, Output, input } from "@angular/core";
import { RouterLink, RouterLinkActive } from "@angular/router";
/**
@@ -11,16 +11,19 @@ export abstract class NavBaseComponent {
/**
* Text to display in main content
*/
@Input() text: string;
readonly text = input<string>(undefined);
/**
* `aria-label` for main content
*/
@Input() ariaLabel: string;
readonly ariaLabel = input<string>(undefined);
/**
* Optional icon, e.g. `"bwi-collection-shared"`
*/
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() icon: string;
/**
@@ -34,41 +37,43 @@ export abstract class NavBaseComponent {
*
* See: {@link https://github.com/angular/angular/issues/24482}
*/
@Input() route?: RouterLink["routerLink"];
readonly route = input<RouterLink["routerLink"]>(undefined);
/**
* Passed to internal `routerLink`
*
* See {@link RouterLink.relativeTo}
*/
@Input() relativeTo?: RouterLink["relativeTo"];
readonly relativeTo = input<RouterLink["relativeTo"]>(undefined);
/**
* 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 this item is used within a tree, set `variant` to `"tree"`
*/
@Input() variant: "default" | "tree" = "default";
readonly variant = input<"default" | "tree">("default");
/**
* Depth level when nested inside of a `'tree'` variant
*/
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() treeDepth = 0;
/**
* 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,15 +1,15 @@
<!-- This a higher order component that composes `NavItemComponent` -->
@if (!hideIfEmpty || nestedNavComponents.length > 0) {
@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
<bit-nav-item
[text]="text"
[text]="text()"
[icon]="icon"
[route]="route"
[relativeTo]="relativeTo"
[routerLinkActiveOptions]="routerLinkActiveOptions"
[variant]="variant"
[route]="route()"
[relativeTo]="relativeTo()"
[routerLinkActiveOptions]="routerLinkActiveOptions()"
[variant]="variant()"
[treeDepth]="treeDepth"
(mainContentClicked)="handleMainContentClicked()"
[ariaLabel]="ariaLabel"
[ariaLabel]="ariaLabel()"
[hideActiveStyles]="parentHideActiveStyles"
>
<ng-template #button>
@@ -17,7 +17,7 @@
type="button"
class="tw-ms-auto"
[bitIconButton]="
open ? 'bwi-angle-up' : variant === 'tree' ? 'bwi-angle-right' : 'bwi-angle-down'
open ? 'bwi-angle-up' : variant() === 'tree' ? 'bwi-angle-right' : 'bwi-angle-down'
"
[buttonType]="'light'"
(click)="toggle($event)"
@@ -26,18 +26,18 @@
aria-haspopup="true"
[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>
<!-- Show toggle to the left for trees otherwise to the right -->
@if (variant === "tree") {
@if (variant() === "tree") {
<ng-container slot="start">
<ng-container *ngTemplateOutlet="button"></ng-container>
</ng-container>
}
<ng-container slot="end">
<ng-content select="[slot=end]"></ng-content>
@if (variant !== "tree") {
@if (variant() !== "tree") {
<ng-container *ngTemplateOutlet="button"></ng-container>
}
</ng-container>
@@ -47,7 +47,7 @@
@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

@@ -10,6 +10,7 @@ import {
Output,
QueryList,
SkipSelf,
input,
} from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -37,7 +38,7 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI
/** 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);
}
/**
@@ -48,14 +49,15 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI
/**
* Is `true` if the expanded content is visible
*/
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input()
open = 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>();
@@ -84,7 +86,7 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI
* - For any nested NavGroupComponents or NavItemComponents, increment the `treeDepth` by 1.
*/
private initNestedStyles() {
if (this.variant !== "tree") {
if (this.variant() !== "tree") {
return;
}
[...this.nestedNavComponents].forEach((navGroupOrItem) => {
@@ -94,7 +96,7 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI
protected handleMainContentClicked() {
if (!this.sideNavService.open) {
if (!this.route) {
if (!this.route()) {
this.sideNavService.setOpen();
}
this.open = true;

View File

@@ -15,11 +15,13 @@
>
<div
[ngStyle]="{
'padding-left': data.open ? (variant === 'tree' ? 2.5 : 1) + treeDepth * 1.5 + 'rem' : '0',
'padding-left': data.open
? (variant() === 'tree' ? 2.5 : 1) + treeDepth * 1.5 + 'rem'
: '0',
}"
class="tw-relative tw-flex"
>
<div [ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']">
<div [ngClass]="[variant() === 'tree' ? 'tw-py-1' : 'tw-py-2']">
<div
#slotStart
class="[&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:!tw-text-alt2"
@@ -30,7 +32,7 @@
<div
*ngIf="slotStart.childElementCount === 0"
[ngClass]="{
'tw-w-0': variant !== 'tree',
'tw-w-0': variant() !== 'tree',
}"
>
<button
@@ -43,27 +45,27 @@
</div>
</div>
<ng-container *ngIf="route; then isAnchor; else isButton"></ng-container>
<ng-container *ngIf="route(); then isAnchor; else isButton"></ng-container>
<!-- Main content of `NavItem` -->
<ng-template #anchorAndButtonContent>
<div
[title]="text"
[title]="text()"
class="tw-truncate"
[ngClass]="[
variant === 'tree' ? 'tw-py-1' : 'tw-py-2',
variant() === 'tree' ? 'tw-py-1' : 'tw-py-2',
data.open ? 'tw-pe-4' : 'tw-text-center',
]"
>
<i
class="bwi bwi-fw tw-text-alt2 tw-mx-1 {{ icon }}"
[attr.aria-hidden]="data.open"
[attr.aria-label]="text"
[attr.aria-label]="text()"
></i
><span
*ngIf="data.open"
[ngClass]="showActiveStyles ? 'tw-font-bold' : 'tw-font-semibold'"
>{{ text }}</span
>{{ text() }}</span
>
</div>
</ng-template>
@@ -75,11 +77,11 @@
<a
class="tw-w-full tw-truncate tw-border-none tw-bg-transparent tw-p-0 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()"
@@ -104,7 +106,7 @@
<div
*ngIf="data.open"
class="tw-flex -tw-ms-3 tw-pe-4 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
[ngClass]="[variant === 'tree' ? 'tw-py-1' : 'tw-py-2']"
[ngClass]="[variant() === 'tree' ? 'tw-py-1' : 'tw-py-2']"
>
<ng-content select="[slot=end]"></ng-content>
</div>

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

@@ -1,14 +1,14 @@
@if (sideNavService.open) {
<div class="tw-sticky tw-top-0 tw-z-50">
<a
[routerLink]="route"
[routerLink]="route()"
class="tw-px-5 tw-pb-5 tw-pt-7 tw-block tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2"
[attr.aria-label]="label"
[title]="label"
[attr.aria-label]="label()"
[title]="label()"
routerLinkActive
[ariaCurrentWhenActive]="'page'"
>
<bit-icon [icon]="openIcon"></bit-icon>
<bit-icon [icon]="openIcon()"></bit-icon>
</a>
</div>
}
@@ -16,8 +16,8 @@
<bit-nav-item
class="tw-block tw-pt-7"
[hideActiveStyles]="true"
[route]="route"
[icon]="closedIcon"
[text]="label"
[route]="route()"
[icon]="closedIcon()"
[text]="label()"
></bit-nav-item>
}

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 { RouterLinkActive, RouterLink } from "@angular/router";
import { Icon } from "../icon";
@@ -17,18 +17,18 @@ import { SideNavService } from "./side-nav.service";
})
export class NavLogoComponent {
/** Icon that is displayed when the side nav is closed */
@Input() closedIcon = "bwi-shield";
readonly closedIcon = input("bwi-shield");
/** 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

@@ -11,6 +11,7 @@ import {
Input,
OnDestroy,
ViewContainerRef,
input,
} from "@angular/core";
import { Observable, Subscription, filter, mergeWith } from "rxjs";
@@ -22,25 +23,25 @@ import { PopoverComponent } from "./popover.component";
exportAs: "popoverTrigger",
})
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input()
@HostBinding("attr.aria-expanded")
popoverOpen = false;
@Input("bitPopoverTriggerFor")
popover: PopoverComponent;
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" });
@Input("position")
position: string;
readonly position = input<string>(undefined);
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];
@@ -83,7 +84,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
this.popoverOpen = 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,7 +98,7 @@ 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));
}

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>(undefined);
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,4 +1,4 @@
import { Component, HostBinding, Input } from "@angular/core";
import { Component, HostBinding, Input, input } from "@angular/core";
import { FormControlModule } from "../form-control/form-control.module";
@@ -13,13 +13,16 @@ let nextId = 0;
imports: [FormControlModule, RadioInputComponent],
})
export class RadioButtonComponent {
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding("attr.id") @Input() id = `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>(undefined);
readonly disabled = input(false);
constructor(private groupComponent: RadioGroupComponent) {}
@@ -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

@@ -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, Input, Optional, Self, input } from "@angular/core";
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -20,6 +20,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
disabled = false;
private _name?: string;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() get name() {
return this._name ?? this.ngControl?.name?.toString();
}
@@ -27,9 +29,12 @@ export class RadioGroupComponent implements ControlValueAccessor {
this._name = value;
}
@Input() block = false;
readonly block = input(false);
@HostBinding("attr.role") role = "radiogroup";
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding("attr.id") @Input() id = `bit-radio-group-${nextId++}`;
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];

View File

@@ -13,6 +13,9 @@ let nextId = 0;
providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }],
})
export class RadioInputComponent implements BitFormControlAbstraction {
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`;
@HostBinding("class")
@@ -74,6 +77,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
constructor(@Optional() @Self() private ngControl?: NgControl) {}
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding()
@Input()
get disabled() {
@@ -84,6 +89,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
}
private _disabled: boolean;
// TODO: Skipped for 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"
[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, Input, ViewChild, input } from "@angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
@@ -43,9 +43,11 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
// Use `type="text"` for Safari to improve rendering performance
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() disabled: boolean;
@Input() placeholder: string;
@Input() autocomplete: string;
readonly placeholder = input<string>(undefined);
readonly autocomplete = input<string>(undefined);
getFocusTarget() {
return this.input?.nativeElement;

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

@@ -9,15 +9,27 @@ import { Option } from "./option";
template: `<ng-template><ng-content></ng-content></ng-template>`,
})
export class OptionComponent<T = unknown> implements Option<T> {
// TODO: Skipped for migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input()
icon?: string;
// TODO: Skipped for migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input({ required: true })
value: T;
// TODO: Skipped for migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input({ required: true })
label: string;
// TODO: Skipped for migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input({ transform: booleanAttribute })
disabled: boolean;
}

View File

@@ -2,7 +2,7 @@
[(ngModel)]="selectedOption"
(ngModelChange)="onChange($event)"
[disabled]="disabled"
[placeholder]="placeholder"
[placeholder]="placeholder()"
[items]="items"
(blur)="onBlur()"
[labelForId]="labelForId"

View File

@@ -13,6 +13,7 @@ import {
ViewChild,
Output,
EventEmitter,
input,
} from "@angular/core";
import {
ControlValueAccessor,
@@ -43,6 +44,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
private _items: Option<T>[] = [];
/** Optional: Options can be provided using an array input or using `bit-option` */
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get items(): Option<T>[] {
return this._items;
@@ -52,7 +55,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
this._selectedOption = this.findSelectedOption(next, this.selectedValue);
}
@Input() placeholder = this.i18nService.t("selectPlaceholder");
readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
@Output() closed = new EventEmitter();
protected selectedValue: T;
@@ -89,6 +92,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
get disabledAttr() {
return this.disabled || null;
}
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get disabled() {
return this._disabled ?? this.ngControl?.disabled ?? false;
@@ -154,9 +159,14 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
}
/**Implemented as part of BitFormFieldControl */
// TODO: Skipped for migration because:
// This input is used in combination with `@HostBinding` and migrating would
// break.
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
/**Implemented as part of BitFormFieldControl */
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding("attr.required")
@Input()
get required() {

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 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, Input, OnInit, input } from "@angular/core";
import type { SortDirection, SortFn } from "./table-data-source";
import { TableComponent } from "./table.component";
@@ -26,12 +26,14 @@ export class SortableComponent implements OnInit {
/**
* Mark the column as sortable and specify the key to sort by
*/
@Input() bitSortable: string;
readonly bitSortable = input<string>(undefined);
private _default: SortDirection | boolean = false;
/**
* Mark the column as the default sort column
*/
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() set default(value: SortDirection | boolean | "") {
if (value === "desc" || value === "asc") {
this._default = value;
@@ -51,7 +53,7 @@ export class SortableComponent implements OnInit {
* return direction === 'asc' ? result : -result;
* }
*/
@Input() fn: SortFn;
readonly fn = input<SortFn>(undefined);
constructor(private table: TableComponent) {}
@@ -69,7 +71,8 @@ export class SortableComponent implements OnInit {
}
protected setActive() {
if (this.table.dataSource) {
const dataSource = this.table.dataSource();
if (dataSource) {
const defaultDirection = this._default === "desc" ? "desc" : "asc";
const direction = this.isActive
? this.direction === "asc"
@@ -77,20 +80,20 @@ export class SortableComponent implements OnInit {
: "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>(undefined);
@ContentChild(BitRowDef) protected rowDef: BitRowDef;

View File

@@ -7,9 +7,9 @@ import {
Component,
ContentChild,
Directive,
Input,
OnDestroy,
TemplateRef,
input,
} from "@angular/core";
import { Observable } from "rxjs";
@@ -29,8 +29,8 @@ export class TableBodyDirective {
imports: [CommonModule],
})
export class TableComponent implements OnDestroy, AfterContentChecked {
@Input() dataSource: TableDataSource<any>;
@Input() layout: "auto" | "fixed" = "auto";
readonly dataSource = input<TableDataSource<any>>(undefined);
readonly layout = input<"auto" | "fixed">("auto");
@ContentChild(TableBodyDirective) templateVariable: TableBodyDirective;
@@ -45,22 +45,24 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
"tw-text-main",
"tw-border-collapse",
"tw-text-start",
this.layout === "auto" ? "tw-table-auto" : "tw-table-fixed",
this.layout() === "auto" ? "tw-table-auto" : "tw-table-fixed",
];
}
ngAfterContentChecked(): void {
if (!this._initialized && isDataSource(this.dataSource)) {
const dataSource = this.dataSource();
if (!this._initialized && isDataSource(dataSource)) {
this._initialized = true;
const dataStream = this.dataSource.connect();
const dataStream = dataSource.connect();
this.rows$ = dataStream;
}
}
ngOnDestroy(): void {
if (isDataSource(this.dataSource)) {
this.dataSource.disconnect();
const dataSource = this.dataSource();
if (isDataSource(dataSource)) {
dataSource.disconnect();
}
}
}

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FocusableOption } from "@angular/cdk/a11y";
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
import { Directive, ElementRef, HostBinding, Input, input } from "@angular/core";
/**
* Directive used for styling tab header items for both nav links (anchor tags)
@@ -11,7 +11,10 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
selector: "[bitTabListItem]",
})
export class TabListItemDirective implements FocusableOption {
@Input() active: boolean;
readonly active = input<boolean>(undefined);
// TODO: Skipped for migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input() disabled: boolean;
@HostBinding("attr.disabled")
@@ -32,7 +35,7 @@ export class TabListItemDirective implements FocusableOption {
@HostBinding("class")
get classList(): string[] {
return this.baseClassList
.concat(this.active ? this.activeClassList : [])
.concat(this.active() ? this.activeClassList : [])
.concat(this.disabled ? this.disabledClassList : [])
.concat(this.textColorClassList);
}
@@ -45,7 +48,7 @@ export class TabListItemDirective implements FocusableOption {
if (this.disabled) {
return ["!tw-text-secondary-300", "hover:!tw-text-secondary-300"];
}
if (this.active) {
if (this.active()) {
return ["!tw-text-primary-600", "hover:!tw-text-primary-700"];
}
return ["!tw-text-main", "hover:!tw-text-main"];

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal";
import { Component, HostBinding, Input } from "@angular/core";
import { Component, HostBinding, Input, input } from "@angular/core";
@Component({
selector: "bit-tab-body",
@@ -11,13 +11,15 @@ import { Component, HostBinding, Input } from "@angular/core";
export class TabBodyComponent {
private _firstRender: boolean;
@Input() content: TemplatePortal;
@Input() preserveContent = false;
readonly content = input<TemplatePortal>(undefined);
readonly preserveContent = input(false);
@HostBinding("attr.hidden") get hidden() {
return !this.active || null;
}
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get active() {
return this._active;
@@ -38,10 +40,10 @@ export class TabBodyComponent {
*/
get tabContent() {
if (this.active) {
return this.content;
return this.content();
}
if (this.preserveContent && this._firstRender) {
return this.content;
if (this.preserveContent() && this._firstRender) {
return this.content();
}
return null;
}

View File

@@ -2,7 +2,7 @@
<div
bitTabListContainer
role="tablist"
[attr.aria-label]="label"
[attr.aria-label]="label()"
(keydown)="keyManager.onKeydown($event)"
>
@for (tab of tabs; track tab; let i = $index) {
@@ -12,7 +12,7 @@
role="tab"
[id]="getTabLabelId(i)"
[active]="tab.isActive"
[disabled]="tab.disabled"
[disabled]="tab.disabled()"
[attr.aria-selected]="selectedIndex === i"
[attr.tabindex]="selectedIndex === i ? 0 : -1"
(click)="selectTab(i)"
@@ -22,7 +22,7 @@
@if (tab.templateLabel) {
<ng-container [ngTemplateOutlet]="tab.templateLabel.templateRef"></ng-container>
} @else {
{{ tab.textLabel }}
{{ tab.textLabel() }}
}
</ng-template>
</button>
@@ -34,11 +34,11 @@
<bit-tab-body
role="tabpanel"
[id]="getTabContentId(i)"
[attr.tabindex]="tab.contentTabIndex"
[attr.tabindex]="tab.contentTabIndex()"
[attr.labeledby]="getTabLabelId(i)"
[active]="tab.isActive"
[content]="tab.content"
[preserveContent]="preserveContent"
[preserveContent]="preserveContent()"
>
</bit-tab-body>
}

View File

@@ -15,6 +15,7 @@ import {
Output,
QueryList,
ViewChildren,
input,
} from "@angular/core";
import { Subject, takeUntil } from "rxjs";
@@ -49,19 +50,21 @@ export class TabGroupComponent
/**
* Aria label for the tab list menu
*/
@Input() label = "";
readonly label = input("");
/**
* Keep the content of off-screen tabs in the DOM.
* Useful for keeping <audio> or <video> elements from re-initializing
* after navigating between tabs.
*/
@Input() preserveContent = false;
readonly preserveContent = input(false);
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
@ViewChildren(TabListItemDirective) tabLabels: QueryList<TabListItemDirective>;
/** The index of the active tab. */
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get selectedIndex(): number | null {
return this._selectedIndex;

View File

@@ -4,11 +4,11 @@ import { TemplatePortal } from "@angular/cdk/portal";
import {
Component,
ContentChild,
Input,
OnInit,
TemplateRef,
ViewChild,
ViewContainerRef,
input,
} from "@angular/core";
import { TabLabelDirective } from "./tab-label.directive";
@@ -21,8 +21,8 @@ import { TabLabelDirective } from "./tab-label.directive";
},
})
export class TabComponent implements OnInit {
@Input() disabled = false;
@Input("label") textLabel = "";
readonly disabled = input(false);
readonly textLabel = input("", { alias: "label" });
/**
* Optional tabIndex for the tabPanel that contains this tab's content.
@@ -32,7 +32,7 @@ export class TabComponent implements OnInit {
*
* @remarks See note 4 of https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/
*/
@Input() contentTabIndex: number | undefined;
readonly contentTabIndex = input<number | undefined>(undefined);
@ViewChild(TemplateRef, { static: true }) implicitContent: TemplateRef<unknown>;
@ContentChild(TabLabelDirective) templateLabel: TabLabelDirective;

View File

@@ -1,6 +1,6 @@
<a
bitTabListItem
[routerLink]="disabled ? null : route"
[routerLink]="disabled ? null : route()"
routerLinkActive
[routerLinkActiveOptions]="routerLinkMatchOptions"
#rla="routerLinkActive"

View File

@@ -1,7 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FocusableOption } from "@angular/cdk/a11y";
import { AfterViewInit, Component, HostListener, Input, OnDestroy, ViewChild } from "@angular/core";
import {
AfterViewInit,
Component,
HostListener,
Input,
OnDestroy,
ViewChild,
input,
} from "@angular/core";
import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@@ -27,7 +35,10 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestr
fragment: "ignored",
};
@Input() route: string | any[];
readonly route = input<string | any[]>(undefined);
// TODO: Skipped for migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input() disabled = false;
@HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) {

View File

@@ -1,5 +1,5 @@
<bit-tab-header>
<nav bitTabListContainer [attr.aria-label]="label" (keydown)="keyManager.onKeydown($event)">
<nav bitTabListContainer [attr.aria-label]="label()" (keydown)="keyManager.onKeydown($event)">
<ng-content></ng-content>
</nav>
</bit-tab-header>

View File

@@ -6,8 +6,8 @@ import {
Component,
ContentChildren,
forwardRef,
Input,
QueryList,
input,
} from "@angular/core";
import { TabHeaderComponent } from "../shared/tab-header.component";
@@ -25,7 +25,7 @@ import { TabLinkComponent } from "./tab-link.component";
})
export class TabNavBarComponent implements AfterContentInit {
@ContentChildren(forwardRef(() => TabLinkComponent)) tabLabels: QueryList<TabLinkComponent>;
@Input() label = "";
readonly label = input("");
/**
* Focus key manager for keeping tab controls accessible.

View File

@@ -2,12 +2,12 @@
class="tw-mb-1 tw-min-w-[--bit-toast-width] tw-text-main tw-flex tw-flex-col tw-justify-between tw-rounded-md tw-pointer-events-auto tw-cursor-default tw-overflow-hidden tw-shadow-lg {{
bgColor
}}"
[attr.role]="variant === 'error' ? 'alert' : null"
[attr.role]="variant() === 'error' ? 'alert' : null"
>
<div class="tw-flex tw-items-center tw-gap-4 tw-px-2 tw-pb-1 tw-pt-2">
<i aria-hidden="true" class="bwi tw-text-xl tw-py-1.5 tw-px-2.5 {{ iconClass }}"></i>
<div>
<span class="tw-sr-only">{{ variant | i18n }}</span>
<span class="tw-sr-only">{{ variant() | i18n }}</span>
@if (title) {
<p data-testid="toast-title" class="tw-font-semibold tw-mb-0">{{ title }}</p>
}
@@ -26,5 +26,5 @@
(click)="this.onClose.emit()"
></button>
</div>
<div class="tw-h-1 tw-w-full tw-bg-text-main/30" [style.width]="progressWidth + '%'"></div>
<div class="tw-h-1 tw-w-full tw-bg-text-main/30" [style.width]="progressWidth() + '%'"></div>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, EventEmitter, Input, Output, input } from "@angular/core";
import { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared";
@@ -31,36 +31,39 @@ const variants: Record<ToastVariant, { icon: string; bgColor: string }> = {
imports: [SharedModule, IconButtonModule, TypographyModule],
})
export class ToastComponent {
@Input() variant: ToastVariant = "info";
readonly variant = input<ToastVariant>("info");
/**
* The message to display
*
* Pass an array to render multiple paragraphs.
**/
@Input({ required: true })
message!: string | string[];
readonly message = input.required<string | string[]>();
/** An optional title to display over the message. */
// TODO: Skipped for migration because:
// This input is used in a control flow expression (e.g. `@if` or `*ngIf`)
// and migrating would break narrowing currently.
@Input() title?: string;
/**
* The percent width of the progress bar, from 0-100
**/
@Input() progressWidth = 0;
readonly progressWidth = input(0);
/** Emits when the user presses the close button */
@Output() onClose = new EventEmitter<void>();
protected get iconClass(): string {
return variants[this.variant].icon;
return variants[this.variant()].icon;
}
protected get bgColor(): string {
return variants[this.variant].bgColor;
return variants[this.variant()].bgColor;
}
protected get messageArray(): string[] {
return Array.isArray(this.message) ? this.message : [this.message];
const message = this.message();
return Array.isArray(message) ? message : [message];
}
}

View File

@@ -5,6 +5,7 @@ import {
HostBinding,
Input,
Output,
input,
} from "@angular/core";
let nextId = 0;
@@ -17,14 +18,16 @@ export class ToggleGroupComponent<TValue = unknown> {
private id = nextId++;
name = `bit-toggle-group-${this.id}`;
@Input({ transform: booleanAttribute }) fullWidth?: boolean;
readonly fullWidth = input<boolean, unknown>(undefined, { transform: booleanAttribute });
// TODO: Skipped for migration because:
// Your application code writes to the input. This prevents migration.
@Input() selected?: TValue;
@Output() selectedChange = new EventEmitter<TValue>();
@HostBinding("attr.role") role = "radiogroup";
@HostBinding("class")
get classList() {
return ["tw-flex"].concat(this.fullWidth ? ["tw-w-full", "[&>*]:tw-flex-1"] : []);
return ["tw-flex"].concat(this.fullWidth() ? ["tw-w-full", "[&>*]:tw-flex-1"] : []);
}
onInputInteraction(value: TValue) {

View File

@@ -7,9 +7,9 @@ import {
Component,
ElementRef,
HostBinding,
Input,
signal,
ViewChild,
input,
} from "@angular/core";
import { ToggleGroupComponent } from "./toggle-group.component";
@@ -24,7 +24,7 @@ let nextId = 0;
export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewInit {
id = nextId++;
@Input() value?: TValue;
readonly value = input<TValue>(undefined);
@ViewChild("labelContent") labelContent: ElementRef<HTMLSpanElement>;
@ViewChild("bitBadgeContainer") bitBadgeContainer: ElementRef<HTMLSpanElement>;
@@ -41,7 +41,7 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
}
get selected() {
return this.groupComponent.selected === this.value;
return this.groupComponent.selected === this.value();
}
get inputClasses() {
@@ -95,7 +95,7 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
}
onInputInteraction() {
this.groupComponent.onInputInteraction(this.value);
this.groupComponent.onInputInteraction(this.value());
}
ngAfterContentChecked() {

View File

@@ -1,7 +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 { Directive, HostBinding, Input } from "@angular/core";
import { Directive, HostBinding, Input, input } from "@angular/core";
type TypographyType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "body1" | "body2" | "helper";
@@ -33,15 +33,17 @@ const margins: Record<TypographyType, string[]> = {
selector: "[bitTypography]",
})
export class TypographyDirective {
@Input("bitTypography") bitTypography: TypographyType;
readonly bitTypography = input<TypographyType>(undefined);
private _margin = true;
// TODO: Skipped for migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
set noMargin(value: boolean | "") {
this._margin = !coerceBooleanProperty(value);
}
@HostBinding("class") get classList() {
return styles[this.bitTypography].concat(this._margin ? margins[this.bitTypography] : []);
return styles[this.bitTypography()].concat(this._margin ? margins[this.bitTypography()] : []);
}
}