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

[PM-15847] libs/components strict migration (#15738)

This PR migrates `libs/components` to use strict TypeScript.

- Remove `@ts-strict-ignore` from each file in `libs/components` and resolved any new compilation errors
- Converted ViewChild and ContentChild decorators to use the new signal-based queries using the [Angular signal queries migration](https://angular.dev/reference/migrations/signal-queries)
  - Made view/content children `required` where appropriate, eliminating the need for additional null checking. This helped simplify the strict migration.

---

Co-authored-by: Vicki League <vleague@bitwarden.com>
This commit is contained in:
Will Martin
2025-08-18 15:36:45 -04:00
committed by GitHub
parent f2d2d0a767
commit 827c4c0301
77 changed files with 450 additions and 612 deletions

View File

@@ -1,39 +1,24 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
import { Directive, effect, ElementRef, input, Renderer2 } from "@angular/core";
@Directive({
selector: "[appA11yTitle]",
})
export class A11yTitleDirective implements OnInit {
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input() set appA11yTitle(title: string) {
this.title = title;
this.setAttributes();
}
private title: string;
private originalTitle: string | null;
private originalAriaLabel: string | null;
export class A11yTitleDirective {
title = input.required<string>({ alias: "appA11yTitle" });
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {}
ngOnInit() {
this.originalTitle = this.el.nativeElement.getAttribute("title");
this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
this.setAttributes();
}
private setAttributes() {
if (this.originalTitle === null) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
}
if (this.originalAriaLabel === null) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
}
) {
const originalTitle = this.el.nativeElement.getAttribute("title");
const originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
effect(() => {
if (originalTitle === null) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title());
}
if (originalAriaLabel === null) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title());
}
});
}
}

View File

@@ -1,9 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, OnInit, inject, DestroyRef } from "@angular/core";
import { ChangeDetectorRef, Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
import { filter, switchMap, tap } from "rxjs";
import { Subject, filter, of, switchMap, tap } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -53,13 +51,15 @@ export interface AnonLayoutWrapperData {
imports: [AnonLayoutComponent, RouterModule],
})
export class AnonLayoutWrapperComponent implements OnInit {
protected pageTitle: string;
protected pageSubtitle: string;
protected pageIcon: Icon;
protected showReadonlyHostname: boolean;
protected maxWidth: AnonLayoutMaxWidth;
protected hideCardWrapper: boolean;
protected hideIcon: boolean = false;
private destroy$ = new Subject<void>();
protected pageTitle?: string | null;
protected pageSubtitle?: string | null;
protected pageIcon?: Icon | null;
protected showReadonlyHostname?: boolean | null;
protected maxWidth?: AnonLayoutMaxWidth | null;
protected hideCardWrapper?: boolean | null;
protected hideIcon?: boolean | null;
constructor(
private router: Router,
@@ -85,7 +85,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
filter((event) => event instanceof NavigationEnd),
// reset page data on page changes
tap(() => this.resetPageData()),
switchMap(() => this.route.firstChild?.data || null),
switchMap(() => this.route.firstChild?.data || of(null)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((firstChildRouteData: Data | null) => {
@@ -93,7 +93,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
});
}
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData: Data | null) {
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData?: Data | null) {
if (!firstChildRouteData) {
return;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import {
Component,
@@ -56,8 +54,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
protected logo = BitwardenLogo;
protected year: string;
protected clientType: ClientType;
protected hostname: string;
protected version: string;
protected hostname?: string;
protected version?: string;
protected hideYearAndVersion = false;

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, HostListener, model, Optional, inject, DestroyRef } from "@angular/core";
import { DestroyRef, Directive, HostListener, inject, model, Optional } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, finalize, tap } from "rxjs";
@@ -38,7 +36,7 @@ export class BitActionDirective {
disabled = false;
readonly handler = model<FunctionReturningAwaitable>(undefined, { alias: "bitAction" });
readonly handler = model.required<FunctionReturningAwaitable>({ alias: "bitAction" });
private readonly destroyRef = inject(DestroyRef);

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit, Optional, input, inject, DestroyRef } from "@angular/core";
import { DestroyRef, Directive, OnInit, Optional, inject, input } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormGroupDirective } from "@angular/forms";
import { BehaviorSubject, catchError, filter, of, switchMap } from "rxjs";
@@ -22,7 +20,7 @@ export class BitSubmitDirective implements OnInit {
private _loading$ = new BehaviorSubject<boolean>(false);
private _disabled$ = new BehaviorSubject<boolean>(false);
readonly handler = input<FunctionReturningAwaitable>(undefined, { alias: "bitSubmit" });
readonly handler = input.required<FunctionReturningAwaitable>({ alias: "bitSubmit" });
readonly allowDisabledFormSubmit = input<boolean>(false);
@@ -63,7 +61,7 @@ export class BitSubmitDirective implements OnInit {
ngOnInit(): void {
this.formGroupDirective.statusChanges
.pipe(takeUntilDestroyed(this.destroyRef))
?.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((c) => {
if (this.allowDisabledFormSubmit()) {
this._disabled$.next(false);

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, Optional, input } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgClass } from "@angular/common";
import { Component, OnChanges, input } from "@angular/core";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
@@ -41,7 +39,7 @@ export class AvatarComponent implements OnChanges {
private svgFontSize = 20;
private svgFontWeight = 300;
private svgSize = 48;
src: SafeResourceUrl;
src?: SafeResourceUrl;
constructor(public sanitizer: DomSanitizer) {}
@@ -56,8 +54,14 @@ export class AvatarComponent implements OnChanges {
}
private generate() {
let chars: string = null;
const upperCaseText = this.text()?.toUpperCase() ?? "";
const color = this.color();
const text = this.text();
const id = this.id();
if (!text && !color && !id) {
throw new Error("Must supply `text`, `color`, or `id` input.");
}
let chars: string | null = null;
const upperCaseText = text?.toUpperCase() ?? "";
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
@@ -66,18 +70,17 @@ export class AvatarComponent implements OnChanges {
}
// If the chars contain an emoji, only show it.
if (chars.match(Utils.regexpEmojiPresentation)) {
chars = chars.match(Utils.regexpEmojiPresentation)[0];
const emojiMatch = chars.match(Utils.regexpEmojiPresentation);
if (emojiMatch) {
chars = emojiMatch[0];
}
let svg: HTMLElement;
let hexColor = this.color();
const id = this.id();
if (!Utils.isNullOrWhitespace(this.color())) {
let hexColor = color ?? "";
if (!Utils.isNullOrWhitespace(hexColor)) {
svg = this.createSvgElement(this.svgSize, hexColor);
} else if (!Utils.isNullOrWhitespace(id)) {
hexColor = Utils.stringToColor(id.toString());
} else if (!Utils.isNullOrWhitespace(id ?? "")) {
hexColor = Utils.stringToColor(id!.toString());
svg = this.createSvgElement(this.svgSize, hexColor);
} else {
hexColor = Utils.stringToColor(upperCaseText);
@@ -95,7 +98,7 @@ export class AvatarComponent implements OnChanges {
);
}
private getFirstLetters(data: string, count: number): string {
private getFirstLetters(data: string, count: number): string | null {
const parts = data.split(" ");
if (parts.length > 1) {
let text = "";

View File

@@ -1,7 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
import { Component, EventEmitter, Output, TemplateRef, input, viewChild } from "@angular/core";
import { QueryParamsHandling } from "@angular/router";
@Component({
@@ -20,7 +17,7 @@ export class BreadcrumbComponent {
@Output()
click = new EventEmitter();
@ViewChild(TemplateRef, { static: true }) content: TemplateRef<unknown>;
readonly content = viewChild(TemplateRef);
onClick(args: unknown) {
this.click.next(args);

View File

@@ -8,7 +8,7 @@
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</a>
} @else {
<button
@@ -18,7 +18,7 @@
class="tw-my-2 tw-inline-block"
(click)="breadcrumb.onClick($event)"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</button>
}
@if (!last) {
@@ -46,11 +46,11 @@
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</a>
} @else {
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</button>
}
}
@@ -66,7 +66,7 @@
[queryParams]="breadcrumb.queryParams()"
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</a>
} @else {
<button
@@ -76,7 +76,7 @@
class="tw-my-2 tw-inline-block"
(click)="breadcrumb.onClick($event)"
>
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
</button>
}
@if (!last) {

View File

@@ -1,6 +1,6 @@
<aside
class="tw-mb-4 tw-box-border tw-rounded-lg tw-bg-background tw-ps-3 tw-pe-3 tw-py-2 tw-leading-5 tw-text-main"
[ngClass]="calloutClass"
[ngClass]="calloutClass()"
[attr.aria-labelledby]="titleId"
>
@if (titleComputed(); as title) {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, computed, input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -50,11 +48,11 @@ export class CalloutComponent {
return title;
});
protected titleId = `bit-callout-title-${nextId++}`;
protected readonly titleId = `bit-callout-title-${nextId++}`;
constructor(private i18nService: I18nService) {}
get calloutClass() {
protected readonly calloutClass = computed(() => {
switch (this.type()) {
case "danger":
return "tw-bg-danger-100";
@@ -65,5 +63,5 @@ export class CalloutComponent {
case "warning":
return "tw-bg-warning-100";
}
}
});
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, HostBinding, Input, Optional, Self } from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
@@ -114,7 +112,7 @@ export class CheckboxComponent implements BitFormControlAbstraction {
set disabled(value: any) {
this._disabled = value != null && value !== false;
}
private _disabled: boolean;
private _disabled?: boolean;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@@ -127,14 +125,15 @@ export class CheckboxComponent implements BitFormControlAbstraction {
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
private _required?: boolean;
get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
}
get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0];
return [key, this.ngControl.errors[key]];
const errors = this.ngControl?.errors ?? {};
const key = Object.keys(errors)[0];
return [key, errors[key]];
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
AfterViewInit,
Component,
@@ -9,12 +7,12 @@ import {
HostListener,
Input,
QueryList,
ViewChild,
ViewChildren,
booleanAttribute,
inject,
signal,
input,
viewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
@@ -50,9 +48,9 @@ export type ChipSelectOption<T> = Option<T> & {
],
})
export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit {
@ViewChild(MenuComponent) menu: MenuComponent;
@ViewChildren(MenuItemDirective) menuItems: QueryList<MenuItemDirective>;
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>;
readonly menu = viewChild(MenuComponent);
@ViewChildren(MenuItemDirective) menuItems?: QueryList<MenuItemDirective>;
readonly chipSelectButton = viewChild<ElementRef<HTMLButtonElement>>("chipSelectButton");
/** Text to show when there is no selected option */
readonly placeholderText = input.required<string>();
@@ -60,7 +58,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
/** Icon to show when there is no selected option or the selected option does not have an icon */
readonly placeholderIcon = input<string>();
private _options: ChipSelectOption<T>[];
private _options: ChipSelectOption<T>[] = [];
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@@ -103,13 +101,13 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
private destroyRef = inject(DestroyRef);
/** Tree constructed from `this.options` */
private rootTree: ChipSelectOption<T>;
private rootTree?: ChipSelectOption<T> | null;
/** Options that are currently displayed in the menu */
protected renderedOptions: ChipSelectOption<T>;
protected renderedOptions?: ChipSelectOption<T> | null;
/** The option that is currently selected by the user */
protected selectedOption: ChipSelectOption<T>;
protected selectedOption?: ChipSelectOption<T> | null;
/**
* The initial calculated width of the menu when it opens, which is used to
@@ -123,7 +121,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
}
/** The icon to show in the chip button */
protected get icon(): string {
protected get icon(): string | undefined {
return this.selectedOption?.icon || this.placeholderIcon();
}
@@ -133,7 +131,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
*/
protected setOrResetRenderedOptions(): void {
this.renderedOptions = this.selectedOption
? this.selectedOption.children?.length > 0
? (this.selectedOption.children?.length ?? 0) > 0
? this.selectedOption
: this.getParent(this.selectedOption)
: this.rootTree;
@@ -171,7 +169,14 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
* @param value the option value to look for
* @returns the `ChipSelectOption` associated with the provided value, or null if not found
*/
private findOption(tree: ChipSelectOption<T>, value: T): ChipSelectOption<T> | null {
private findOption(
tree: ChipSelectOption<T> | null | undefined,
value: T,
): ChipSelectOption<T> | null {
if (!tree) {
return null;
}
let result = null;
if (tree.value !== null && compareValues(tree.value, value)) {
return tree;
@@ -197,7 +202,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
});
}
protected getParent(option: ChipSelectOption<T>): ChipSelectOption<T> | null {
protected getParent(option: ChipSelectOption<T>): ChipSelectOption<T> | null | undefined {
return this.childParentMap.get(option);
}
@@ -217,8 +222,8 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
* menuItems will change when the user navigates into or out of a submenu. when that happens, we want to
* direct their focus to the first item in the new menu
*/
this.menuItems.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.menu.keyManager.setFirstItemActive();
this.menuItems?.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.menu()?.keyManager?.setFirstItemActive();
});
}
@@ -227,17 +232,17 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
* the initially rendered options
*/
protected setMenuWidth() {
const chipWidth = this.chipSelectButton.nativeElement.getBoundingClientRect().width;
const chipWidth = this.chipSelectButton()?.nativeElement.getBoundingClientRect().width ?? 0;
const firstMenuItemWidth =
this.menu.menuItems.first.elementRef.nativeElement.getBoundingClientRect().width;
this.menu()?.menuItems().at(0)?.elementRef.nativeElement.getBoundingClientRect().width ?? 0;
this.menuWidth = Math.max(chipWidth, firstMenuItemWidth);
}
/** Control Value Accessor */
private notifyOnChange?: (value: T) => void;
private notifyOnChange?: (value: T | null) => void;
private notifyOnTouched?: () => void;
/** Implemented as part of NG_VALUE_ACCESSOR */
@@ -247,7 +252,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
}
/** Implemented as part of NG_VALUE_ACCESSOR */
registerOnChange(fn: (value: T) => void): void {
registerOnChange(fn: (value: T | null) => void): void {
this.notifyOnChange = fn;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { FormGroup, ReactiveFormsModule } from "@angular/forms";
@@ -47,10 +45,10 @@ export class SimpleConfigurableDialogComponent {
];
}
protected title: string;
protected content: string;
protected acceptButtonText: string;
protected cancelButtonText: string;
protected title?: string;
protected content?: string;
protected acceptButtonText?: string;
protected cancelButtonText?: string;
protected formGroup = new FormGroup({});
protected showCancelButton = this.simpleDialogOpts.cancelButtonText !== null;
@@ -58,7 +56,7 @@ export class SimpleConfigurableDialogComponent {
constructor(
public dialogRef: DialogRef,
private i18nService: I18nService,
@Inject(DIALOG_DATA) public simpleDialogOpts?: SimpleDialogOptions,
@Inject(DIALOG_DATA) public simpleDialogOpts: SimpleDialogOptions,
) {
this.localizeText();
}
@@ -76,24 +74,27 @@ export class SimpleConfigurableDialogComponent {
private localizeText() {
this.title = this.translate(this.simpleDialogOpts.title);
this.content = this.translate(this.simpleDialogOpts.content);
this.acceptButtonText = this.translate(this.simpleDialogOpts.acceptButtonText, "yes");
this.acceptButtonText = this.translate(
this.simpleDialogOpts.acceptButtonText ?? { key: "yes" },
);
if (this.showCancelButton) {
// If accept text is overridden, use cancel, otherwise no
this.cancelButtonText = this.translate(
this.simpleDialogOpts.cancelButtonText,
this.simpleDialogOpts.acceptButtonText !== undefined ? "cancel" : "no",
this.simpleDialogOpts.cancelButtonText ??
(this.simpleDialogOpts.acceptButtonText !== undefined
? { key: "cancel" }
: { key: "no" }),
);
}
}
private translate(translation: string | Translation, defaultKey?: string): string {
// Translation interface use implies we must localize.
private translate(translation: string | Translation): string {
// Object implies we must localize.
if (typeof translation === "object") {
return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
}
// Use string that is already translated or use default key post translate
return translation ?? this.i18nService.t(defaultKey);
return translation;
}
}

View File

@@ -1,5 +1,3 @@
// 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 { DisclosureComponent } from "./disclosure.component";
@@ -12,7 +10,7 @@ export class DisclosureTriggerForDirective {
/**
* Accepts template reference for a bit-disclosure component instance
*/
readonly disclosure = input<DisclosureComponent>(undefined, { alias: "bitDisclosureTriggerFor" });
readonly disclosure = input.required<DisclosureComponent>({ alias: "bitDisclosureTriggerFor" });
@HostBinding("attr.aria-expanded") get ariaExpanded() {
return this.disclosure().open;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Component,
EventEmitter,
@@ -40,11 +38,10 @@ let nextId = 0;
template: `<ng-content></ng-content>`,
})
export class DisclosureComponent {
private _open: boolean;
/** Emits the visibility of the disclosure content */
@Output() openChange = new EventEmitter<boolean>();
private _open?: boolean;
/**
* Optionally init the disclosure in its opened state
*/
@@ -54,14 +51,13 @@ export class DisclosureComponent {
this._open = isOpen;
this.openChange.emit(isOpen);
}
get open(): boolean {
return !!this._open;
}
@HostBinding("class") get classList() {
return this.open ? "" : "tw-hidden";
}
@HostBinding("id") id = `bit-disclosure-${nextId++}`;
get open(): boolean {
return this._open;
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";

View File

@@ -1,8 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export abstract class BitFormControlAbstraction {
disabled: boolean;
required: boolean;
hasError: boolean;
error: [string, any];
abstract disabled: boolean;
abstract required: boolean;
abstract hasError: boolean;
abstract error: [string, any];
}

View File

@@ -1,11 +1,11 @@
<label
class="tw-transition tw-items-start [&:has(input[type='checkbox'])]:tw-gap-[.25rem] [&:has(input[type='radio'])]:tw-gap-1.5 tw-select-none tw-mb-0 tw-inline-flex tw-rounded has-[:focus-visible]:tw-ring has-[:focus-visible]:tw-ring-primary-600"
[ngClass]="[formControl.disabled ? 'tw-cursor-auto' : 'tw-cursor-pointer']"
[ngClass]="[formControl().disabled ? 'tw-cursor-auto' : 'tw-cursor-pointer']"
>
<ng-content></ng-content>
<span
class="tw-inline-flex tw-flex-col"
[ngClass]="formControl.disabled ? 'tw-text-muted' : 'tw-text-main'"
[ngClass]="formControl().disabled ? 'tw-text-muted' : 'tw-text-main'"
>
<span bitTypography="body2">
<ng-content select="bit-label"></ng-content>

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgClass } from "@angular/common";
import { booleanAttribute, Component, ContentChild, HostBinding, input } from "@angular/core";
import { booleanAttribute, Component, HostBinding, input, contentChild } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -22,10 +20,10 @@ export class FormControlComponent {
readonly disableMargin = input(false, { transform: booleanAttribute });
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction;
protected readonly formControl = contentChild.required(BitFormControlAbstraction);
@HostBinding("class") get classes() {
return []
return ([] as string[])
.concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
.concat(this.disableMargin() ? [] : ["tw-mb-4"]);
}
@@ -33,15 +31,15 @@ export class FormControlComponent {
constructor(private i18nService: I18nService) {}
get required() {
return this.formControl.required;
return this.formControl().required;
}
get hasError() {
return this.formControl.hasError;
return this.formControl().hasError;
}
get error() {
return this.formControl.error;
return this.formControl().error;
}
get displayError() {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, ElementRef, HostBinding, input, Optional } from "@angular/core";
@@ -32,7 +30,7 @@ export class BitLabel {
];
@HostBinding("title") get title() {
return this.elementRef.nativeElement.textContent.trim();
return this.elementRef.nativeElement.textContent?.trim() ?? "";
}
readonly id = input(`bit-label-${nextId++}`);

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FormControl } from "@angular/forms";
import { forbiddenCharacters } from "./forbidden-characters.validator";
@@ -42,6 +40,6 @@ describe("forbiddenCharacters", () => {
});
});
function createControl(input: string) {
function createControl(input: string | null) {
return new FormControl(input);
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FormControl } from "@angular/forms";
import { trimValidator as validate } from "./trim.validator";
@@ -58,6 +56,6 @@ describe("trimValidator", () => {
});
});
function createControl(input: string) {
function createControl(input: string | null) {
return new FormControl(input);
}

View File

@@ -1,6 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, input } from "@angular/core";
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
@@ -21,7 +18,8 @@ export class BitErrorSummary {
readonly formGroup = input<UntypedFormGroup>();
get errorCount(): number {
return this.getErrorCount(this.formGroup());
const form = this.formGroup();
return form ? this.getErrorCount(form) : 0;
}
get errorString() {

View File

@@ -1,5 +1,3 @@
// 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -24,6 +22,10 @@ export class BitErrorComponent {
get displayError() {
const error = this.error();
if (!error) {
return "";
}
switch (error[0]) {
case "required":
return this.i18nService.t("inputRequired");

View File

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

View File

@@ -29,10 +29,10 @@
>
<label
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-[0.675rem] tw-mb-0 tw-max-w-full tw-pointer-events-auto"
[attr.for]="input.labelForId"
[attr.for]="input().labelForId"
>
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
@if (input.required) {
@if (input().required) {
<span class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]">
({{ "required" | i18n }})</span
>
@@ -78,7 +78,7 @@
<div class="tw-w-full tw-relative">
<label
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted tw-mb-0 tw-max-w-full"
[attr.for]="input.labelForId"
[attr.for]="input().labelForId"
>
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
</label>
@@ -109,11 +109,11 @@
</div>
}
@switch (input.hasError) {
@switch (input().hasError) {
@case (false) {
<ng-content select="bit-hint"></ng-content>
}
@case (true) {
<bit-error [error]="input.error"></bit-error>
<bit-error [error]="input().error"></bit-error>
}
}

View File

@@ -1,18 +1,16 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import {
AfterContentChecked,
booleanAttribute,
Component,
ContentChild,
ElementRef,
HostBinding,
HostListener,
ViewChild,
signal,
input,
Input,
contentChild,
viewChild,
} from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -30,14 +28,14 @@ import { BitFormFieldControl } from "./form-field-control";
imports: [CommonModule, BitErrorComponent, I18nPipe],
})
export class BitFormFieldComponent implements AfterContentChecked {
@ContentChild(BitFormFieldControl) input: BitFormFieldControl;
@ContentChild(BitHintComponent) hint: BitHintComponent;
@ContentChild(BitLabel) label: BitLabel;
readonly input = contentChild.required(BitFormFieldControl);
readonly hint = contentChild(BitHintComponent);
readonly label = contentChild(BitLabel);
@ViewChild("prefixContainer") prefixContainer: ElementRef<HTMLDivElement>;
@ViewChild("suffixContainer") suffixContainer: ElementRef<HTMLDivElement>;
readonly prefixContainer = viewChild<ElementRef<HTMLDivElement>>("prefixContainer");
readonly suffixContainer = viewChild<ElementRef<HTMLDivElement>>("suffixContainer");
@ViewChild(BitErrorComponent) error: BitErrorComponent;
readonly error = viewChild(BitErrorComponent);
readonly disableMargin = input(false, { transform: booleanAttribute });
@@ -54,7 +52,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
const shouldFocusBorderAppear = this.defaultContentIsFocused();
const groupClasses = [
this.input.hasError
this.input().hasError
? "group-hover/bit-form-field:tw-border-danger-700"
: "group-hover/bit-form-field:tw-border-primary-600",
// the next 2 selectors override the above hover selectors when the input (or text area) is non-interactive (i.e. readonly, disabled)
@@ -68,7 +66,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
: "",
];
const baseInputBorderClasses = inputBorderClasses(this.input.hasError);
const baseInputBorderClasses = inputBorderClasses(this.input().hasError);
const borderClasses = baseInputBorderClasses.concat(groupClasses);
@@ -100,19 +98,21 @@ export class BitFormFieldComponent implements AfterContentChecked {
}
protected get readOnly(): boolean {
return this.input.readOnly;
return !!this.input().readOnly;
}
ngAfterContentChecked(): void {
if (this.error) {
this.input.ariaDescribedBy = this.error.id;
} else if (this.hint) {
this.input.ariaDescribedBy = this.hint.id;
const error = this.error();
const hint = this.hint();
if (error) {
this.input().ariaDescribedBy = error.id;
} else if (hint) {
this.input().ariaDescribedBy = hint.id;
} else {
this.input.ariaDescribedBy = undefined;
this.input().ariaDescribedBy = undefined;
}
this.prefixHasChildren.set(this.prefixContainer?.nativeElement.childElementCount > 0);
this.suffixHasChildren.set(this.suffixContainer?.nativeElement.childElementCount > 0);
this.prefixHasChildren.set((this.prefixContainer()?.nativeElement.childElementCount ?? 0) > 0);
this.suffixHasChildren.set((this.suffixContainer()?.nativeElement.childElementCount ?? 0) > 0);
}
}

View File

@@ -1,7 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TextFieldModule } from "@angular/cdk/text-field";
import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
import {
AbstractControl,
UntypedFormBuilder,
@@ -15,6 +12,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { A11yTitleDirective } from "../a11y/a11y-title.directive";
import { AsyncActionsModule } from "../async-actions";
import { BadgeModule } from "../badge";
import { ButtonModule } from "../button";
@@ -31,41 +29,6 @@ import { I18nMockService } from "../utils/i18n-mock.service";
import { BitFormFieldComponent } from "./form-field.component";
import { FormFieldModule } from "./form-field.module";
// TOOD: This solves a circular dependency between components and angular.
@Directive({
selector: "[appA11yTitle]",
})
export class A11yTitleDirective implements OnInit {
@Input() set appA11yTitle(title: string) {
this.title = title;
this.setAttributes();
}
private title: string;
private originalTitle: string | null;
private originalAriaLabel: string | null;
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {}
ngOnInit() {
this.originalTitle = this.el.nativeElement.getAttribute("title");
this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
this.setAttributes();
}
private setAttributes() {
if (this.originalTitle === null) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
}
if (this.originalAriaLabel === null) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
}
}
}
export default {
title: "Component Library/Form/Field",
component: BitFormFieldComponent,

View File

@@ -57,17 +57,19 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
}
ngAfterContentInit(): void {
if (this.formField.input?.type) {
this.toggled.set(this.formField.input.type() !== "password");
const input = this.formField.input();
if (input?.type) {
this.toggled.set(input.type() !== "password");
}
this.button.icon.set(this.icon);
}
private update() {
this.button.icon.set(this.icon);
if (this.formField.input?.type != null) {
this.formField.input.type.set(this.toggled() ? "text" : "password");
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined);
const input = this.formField.input();
if (input?.type != null) {
input.type.set(this.toggled() ? "text" : "password");
input?.spellcheck?.set(this.toggled() ? false : undefined);
}
}
}

View File

@@ -55,7 +55,7 @@ describe("PasswordInputToggle", () => {
button = buttonEl.componentInstance;
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
const formField: BitFormFieldComponent = formFieldEl.componentInstance;
input = formField.input;
input = formField.input();
});
describe("initial state", () => {

View File

@@ -1,5 +1,3 @@
// 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 { toObservable, toSignal } from "@angular/core/rxjs-interop";
@@ -90,7 +88,7 @@ const sizes: Record<IconButtonSize, string[]> = {
},
})
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
readonly icon = model<string>(undefined, { alias: "bitIconButton" });
readonly icon = model.required<string>({ alias: "bitIconButton" });
readonly buttonType = input<IconButtonType>("main");

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
AfterContentChecked,
booleanAttribute,
@@ -73,11 +71,13 @@ export class AutofocusDirective implements AfterContentChecked {
private focus() {
const el = this.getElement();
el.focus();
this.focused = el === document.activeElement;
if (el) {
el.focus();
this.focused = el === document.activeElement;
}
}
private getElement() {
private getElement(): HTMLElement | undefined {
if (this.focusableElement) {
return this.focusableElement.getFocusTarget();
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
Directive,
ElementRef,
@@ -63,7 +61,7 @@ export class BitInputDirective implements BitFormFieldControl {
readonly id = input(`bit-input-${nextId++}`);
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
@HostBinding("attr.aria-describedby") ariaDescribedBy?: string;
@HostBinding("attr.aria-invalid") get ariaInvalid() {
return this.hasError ? true : undefined;
@@ -83,7 +81,7 @@ export class BitInputDirective implements BitFormFieldControl {
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
private _required?: boolean;
readonly hasPrefix = input(false);
readonly hasSuffix = input(false);
@@ -101,19 +99,20 @@ export class BitInputDirective implements BitFormFieldControl {
get hasError() {
if (this.showErrorsWhenDisabled()) {
return (
return !!(
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
this.ngControl?.touched &&
this.ngControl?.errors != null
);
} else {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
}
}
get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0];
return [key, this.ngControl.errors[key]];
const errors = this.ngControl.errors ?? {};
const key = Object.keys(errors)[0];
return [key, errors[key]];
}
constructor(

View File

@@ -1,6 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgClass } from "@angular/common";
import {
AfterContentChecked,
@@ -8,8 +5,8 @@ import {
Component,
ElementRef,
signal,
ViewChild,
input,
viewChild,
} from "@angular/core";
import { TypographyModule } from "../typography";
@@ -30,7 +27,7 @@ import { TypographyModule } from "../typography";
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemContentComponent implements AfterContentChecked {
@ViewChild("endSlot") endSlot: ElementRef<HTMLDivElement>;
readonly endSlot = viewChild<ElementRef<HTMLDivElement>>("endSlot");
protected endSlotHasChildren = signal(false);
@@ -42,6 +39,6 @@ export class ItemContentComponent implements AfterContentChecked {
readonly truncate = input(true);
ngAfterContentChecked(): void {
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
this.endSlotHasChildren.set((this.endSlot()?.nativeElement.childElementCount ?? 0) > 0);
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { hasModifierKey } from "@angular/cdk/keycodes";
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
@@ -31,9 +29,9 @@ export class MenuTriggerForDirective implements OnDestroy {
readonly role = input("button");
readonly menu = input<MenuComponent>(undefined, { alias: "bitMenuTriggerFor" });
readonly menu = input.required<MenuComponent>({ alias: "bitMenuTriggerFor" });
private overlayRef: OverlayRef;
private overlayRef: OverlayRef | null = null;
private defaultMenuConfig: OverlayConfig = {
panelClass: "bit-menu-panel",
hasBackdrop: true,
@@ -52,8 +50,8 @@ export class MenuTriggerForDirective implements OnDestroy {
.withFlexibleDimensions(false)
.withPush(true),
};
private closedEventsSub: Subscription;
private keyDownEventsSub: Subscription;
private closedEventsSub: Subscription | null = null;
private keyDownEventsSub: Subscription | null = null;
constructor(
private elementRef: ElementRef<HTMLElement>,
@@ -78,28 +76,30 @@ export class MenuTriggerForDirective implements OnDestroy {
this.isOpen = true;
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
const templatePortal = new TemplatePortal(menu.templateRef, this.viewContainerRef);
const templatePortal = new TemplatePortal(menu.templateRef(), this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
// Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key
// from doing its normal default action, which would otherwise cause a parent component
// (like a dialog) or extension window to close
if (event?.key === "Escape" && !hasModifierKey(event)) {
event.preventDefault();
}
this.closedEventsSub =
this.getClosedEvents()?.subscribe((event: KeyboardEvent | undefined) => {
// Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key
// from doing its normal default action, which would otherwise cause a parent component
// (like a dialog) or extension window to close
if (event?.key === "Escape" && !hasModifierKey(event)) {
event.preventDefault();
}
if (event?.key && ["Tab", "Escape"].includes(event.key)) {
// Required to ensure tab order resumes correctly
this.elementRef.nativeElement.focus();
}
this.destroyMenu();
}) ?? null;
if (["Tab", "Escape"].includes(event?.key)) {
// Required to ensure tab order resumes correctly
this.elementRef.nativeElement.focus();
}
this.destroyMenu();
});
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));
}
}
@@ -113,7 +113,10 @@ export class MenuTriggerForDirective implements OnDestroy {
this.menu().closed.emit();
}
private getClosedEvents(): Observable<any> {
private getClosedEvents(): Observable<any> | null {
if (!this.overlayRef) {
return null;
}
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef.keydownEvents().pipe(
filter((event: KeyboardEvent) => {

View File

@@ -1,16 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FocusKeyManager, CdkTrapFocus } from "@angular/cdk/a11y";
import {
Component,
Output,
TemplateRef,
ViewChild,
EventEmitter,
ContentChildren,
QueryList,
AfterContentInit,
input,
viewChild,
contentChildren,
} from "@angular/core";
import { MenuItemDirective } from "./menu-item.directive";
@@ -22,10 +19,9 @@ import { MenuItemDirective } from "./menu-item.directive";
imports: [CdkTrapFocus],
})
export class MenuComponent implements AfterContentInit {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
readonly templateRef = viewChild.required(TemplateRef);
@Output() closed = new EventEmitter<void>();
@ContentChildren(MenuItemDirective, { descendants: true })
menuItems: QueryList<MenuItemDirective>;
readonly menuItems = contentChildren(MenuItemDirective, { descendants: true });
keyManager?: FocusKeyManager<MenuItemDirective>;
readonly ariaRole = input<"menu" | "dialog">("menu");
@@ -34,9 +30,9 @@ export class MenuComponent implements AfterContentInit {
ngAfterContentInit() {
if (this.ariaRole() === "menu") {
this.keyManager = new FocusKeyManager(this.menuItems)
this.keyManager = new FocusKeyManager(this.menuItems())
.withWrap()
.skipPredicate((item) => item.disabled);
.skipPredicate((item) => !!item.disabled);
}
}
}

View File

@@ -46,7 +46,7 @@ export const OpenMenu: Story = {
<div class="tw-h-40">
<div class="cdk-overlay-pane bit-menu-panel">
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
<ng-container *ngTemplateOutlet="myMenu.templateRef()"></ng-container>
</div>
</div>
`,

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { hasModifierKey } from "@angular/cdk/keycodes";
import {
Component,
Input,
OnInit,
Output,
ViewChild,
EventEmitter,
HostBinding,
Optional,
@@ -14,6 +11,7 @@ import {
input,
model,
booleanAttribute,
viewChild,
} from "@angular/core";
import {
ControlValueAccessor,
@@ -48,10 +46,10 @@ let nextId = 0;
* This component has been implemented to only support Multi-select list events
*/
export class MultiSelectComponent implements OnInit, BitFormFieldControl, ControlValueAccessor {
@ViewChild(NgSelectComponent) select: NgSelectComponent;
readonly select = viewChild.required(NgSelectComponent);
// Parent component should only pass selectable items (complete list - selected items = baseItems)
readonly baseItems = model<SelectItemView[]>();
readonly baseItems = model.required<SelectItemView[]>();
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
readonly removeSelectedItems = input(false);
readonly placeholder = model<string>();
@@ -61,10 +59,10 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
@Input({ transform: booleanAttribute }) disabled?: boolean;
// Internal tracking of selected items
protected selectedItems: SelectItemView[];
protected selectedItems: SelectItemView[] | null = null;
// Default values for our implementation
loadingText: string;
loadingText?: string;
protected searchInputId = `search-input-${nextId++}`;
@@ -95,13 +93,14 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
/** Function for customizing keyboard navigation */
/** Needs to be arrow function to retain `this` scope. */
keyDown = (event: KeyboardEvent) => {
if (!this.select.isOpen && event.key === "Enter" && !hasModifierKey(event)) {
const select = this.select();
if (!select.isOpen && event.key === "Enter" && !hasModifierKey(event)) {
return false;
}
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
if (select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
this.selectedItems = [];
this.select.close();
select.close();
event.stopPropagation();
return false;
}
@@ -183,11 +182,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
get ariaDescribedBy() {
return this._ariaDescribedBy;
}
set ariaDescribedBy(value: string) {
set ariaDescribedBy(value: string | undefined) {
this._ariaDescribedBy = value;
this.select?.searchInput.nativeElement.setAttribute("aria-describedby", value);
this.select()?.searchInput.nativeElement.setAttribute("aria-describedby", value ?? "");
}
private _ariaDescribedBy: string;
private _ariaDescribedBy?: string;
/**Implemented as part of BitFormFieldControl */
get labelForId() {
@@ -208,16 +207,17 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
private _required?: boolean;
/**Implemented as part of BitFormFieldControl */
get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
}
/**Implemented as part of BitFormFieldControl */
get error(): [string, any] {
const key = Object.keys(this.ngControl?.errors)[0];
return [key, this.ngControl?.errors[key]];
const errors = this.ngControl?.errors ?? {};
const key = Object.keys(errors)[0];
return [key, errors[key]];
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Output, input } from "@angular/core";
import { RouterLink, RouterLinkActive } from "@angular/router";

View File

@@ -1,5 +1,5 @@
<!-- This a higher order component that composes `NavItemComponent` -->
@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
@if (!hideIfEmpty() || nestedNavComponents().length > 0) {
<bit-nav-item
[text]="text()"
[icon]="icon()"

View File

@@ -2,14 +2,13 @@ import { CommonModule } from "@angular/common";
import {
booleanAttribute,
Component,
ContentChildren,
EventEmitter,
Optional,
Output,
QueryList,
SkipSelf,
input,
model,
contentChildren,
} from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -30,10 +29,7 @@ import { SideNavService } from "./side-nav.service";
imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe],
})
export class NavGroupComponent extends NavBaseComponent {
@ContentChildren(NavBaseComponent, {
descendants: true,
})
nestedNavComponents!: QueryList<NavBaseComponent>;
readonly nestedNavComponents = contentChildren(NavBaseComponent, { descendants: true });
/** When the side nav is open, the parent nav item should not show active styles when open. */
protected get parentHideActiveStyles(): boolean {

View File

@@ -1,6 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, input } from "@angular/core";
import { RouterLinkActive, RouterLink } from "@angular/router";

View File

@@ -1,8 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CdkTrapFocus } from "@angular/cdk/a11y";
import { CommonModule } from "@angular/common";
import { Component, ElementRef, ViewChild, input } from "@angular/core";
import { Component, ElementRef, input, viewChild } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -21,15 +19,14 @@ export type SideNavVariant = "primary" | "secondary";
export class SideNavComponent {
readonly variant = input<SideNavVariant>("primary");
@ViewChild("toggleButton", { read: ElementRef, static: true })
private toggleButton: ElementRef<HTMLButtonElement>;
private readonly toggleButton = viewChild("toggleButton", { read: ElementRef });
constructor(protected sideNavService: SideNavService) {}
protected handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
this.sideNavService.setClose();
this.toggleButton?.nativeElement.focus();
this.toggleButton()?.nativeElement.focus();
return false;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
@@ -27,12 +25,12 @@ import { PopoverComponent } from "./popover.component";
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
readonly popoverOpen = model(false);
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" });
readonly popover = input.required<PopoverComponent>({ alias: "bitPopoverTriggerFor" });
readonly position = input<string>();
private overlayRef: OverlayRef;
private closedEventsSub: Subscription;
private overlayRef: OverlayRef | null = null;
private closedEventsSub: Subscription | null = null;
get positions() {
if (!this.position()) {
@@ -82,7 +80,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
this.popoverOpen.set(true);
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
const templatePortal = new TemplatePortal(this.popover().templateRef, this.viewContainerRef);
const templatePortal = new TemplatePortal(this.popover().templateRef(), this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
@@ -91,6 +89,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
}
private getClosedEvents(): Observable<any> {
if (!this.overlayRef) {
throw new Error("Overlay reference is not available");
}
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef
.keydownEvents()
@@ -102,7 +104,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
}
private destroyPopover() {
if (this.overlayRef == null || !this.popoverOpen()) {
if (!this.overlayRef || !this.popoverOpen()) {
return;
}
@@ -112,7 +114,9 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
private disposeAll() {
this.closedEventsSub?.unsubscribe();
this.closedEventsSub = null;
this.overlayRef?.dispose();
this.overlayRef = null;
}
ngAfterViewInit() {

View File

@@ -1,7 +1,5 @@
// 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, Output, TemplateRef, ViewChild, input } from "@angular/core";
import { Component, EventEmitter, Output, TemplateRef, input, viewChild } from "@angular/core";
import { IconButtonModule } from "../icon-button/icon-button.module";
import { SharedModule } from "../shared/shared.module";
@@ -14,7 +12,7 @@ import { TypographyModule } from "../typography";
exportAs: "popoverComponent",
})
export class PopoverComponent {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
readonly templateRef = viewChild.required(TemplateRef);
readonly title = input("");
@Output() closed = new EventEmitter();
}

View File

@@ -1,4 +1,4 @@
@if (label) {
@if (label()) {
<fieldset>
<legend class="tw-mb-1 tw-block tw-text-sm tw-font-semibold tw-text-main">
<ng-content select="bit-label"></ng-content>
@@ -10,7 +10,7 @@
</fieldset>
}
@if (!label) {
@if (!label()) {
<ng-container *ngTemplateOutlet="content"></ng-container>
}

View File

@@ -1,7 +1,5 @@
// 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, Optional, Input, Self, input } from "@angular/core";
import { Component, HostBinding, Optional, Self, input, contentChild } from "@angular/core";
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -22,14 +20,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
selected: unknown;
disabled = false;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
private _name?: string;
@Input() get name() {
return this._name ?? this.ngControl?.name?.toString();
}
set name(value: string) {
this._name = value;
get name() {
return this.ngControl?.name?.toString();
}
readonly block = input(false);
@@ -38,7 +30,7 @@ export class RadioGroupComponent implements ControlValueAccessor {
readonly id = input(`bit-radio-group-${nextId++}`);
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];
@ContentChild(BitLabel) protected label: BitLabel;
protected readonly label = contentChild(BitLabel);
constructor(@Optional() @Self() private ngControl?: NgControl) {
if (ngControl != null) {
@@ -51,8 +43,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
}
// ControlValueAccessor
onChange: (value: unknown) => void;
onTouched: () => void;
onChange?: (value: unknown) => void;
onTouched?: () => void;
writeValue(value: boolean): void {
this.selected = value;
@@ -72,10 +64,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
onInputChange(value: unknown) {
this.selected = value;
this.onChange(this.selected);
this.onChange?.(this.selected);
}
onBlur() {
this.onTouched();
this.onTouched?.();
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, HostBinding, input, Input, Optional, Self } from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
@@ -86,7 +84,7 @@ export class RadioInputComponent implements BitFormControlAbstraction {
set disabled(value: any) {
this._disabled = value != null && value !== false;
}
private _disabled: boolean;
private _disabled?: boolean;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@@ -99,14 +97,15 @@ export class RadioInputComponent implements BitFormControlAbstraction {
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
private _required?: boolean;
get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
}
get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0];
return [key, this.ngControl.errors[key]];
const errors = this.ngControl?.errors ?? {};
const key = Object.keys(errors)[0];
return [key, errors[key]];
}
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgIf, NgClass } from "@angular/common";
import { Component, ElementRef, ViewChild, input, model, signal, computed } from "@angular/core";
import { Component, ElementRef, input, model, signal, computed, viewChild } from "@angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
@@ -37,13 +35,13 @@ let nextId = 0;
imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass],
})
export class SearchComponent implements ControlValueAccessor, FocusableElement {
private notifyOnChange: (v: string) => void;
private notifyOnTouch: () => void;
private notifyOnChange?: (v: string) => void;
private notifyOnTouch?: () => void;
@ViewChild("input") private input: ElementRef<HTMLInputElement>;
private readonly input = viewChild<ElementRef<HTMLInputElement>>("input");
protected id = `search-id-${nextId++}`;
protected searchText: string;
protected searchText?: string;
// Use `type="text"` for Safari to improve rendering performance
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
@@ -57,7 +55,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
readonly autocomplete = input<string>();
getFocusTarget() {
return this.input?.nativeElement;
return this.input()?.nativeElement;
}
onChange(searchText: string) {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, booleanAttribute, input } from "@angular/core";
import { MappedOptionComponent } from "./option";

View File

@@ -1,6 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { hasModifierKey } from "@angular/cdk/keycodes";
import {
Component,
@@ -10,7 +7,6 @@ import {
Optional,
QueryList,
Self,
ViewChild,
Output,
EventEmitter,
input,
@@ -18,6 +14,7 @@ import {
computed,
model,
signal,
viewChild,
} from "@angular/core";
import {
ControlValueAccessor,
@@ -47,7 +44,7 @@ let nextId = 0;
},
})
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
@ViewChild(NgSelectComponent) select: NgSelectComponent;
readonly select = viewChild.required(NgSelectComponent);
/** Optional: Options can be provided using an array input or using `bit-option` */
readonly items = model<Option<T>[] | undefined>();
@@ -55,13 +52,13 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
@Output() closed = new EventEmitter();
protected selectedValue = signal<T>(undefined);
selectedOption: Signal<Option<T>> = computed(() =>
protected selectedValue = signal<T | undefined | null>(undefined);
selectedOption: Signal<Option<T> | null | undefined> = computed(() =>
this.findSelectedOption(this.items(), this.selectedValue()),
);
protected searchInputId = `bit-select-search-input-${nextId++}`;
private notifyOnChange?: (value: T) => void;
private notifyOnChange?: (value?: T | null) => void;
private notifyOnTouched?: () => void;
constructor(
@@ -104,7 +101,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
set disabled(value: any) {
this._disabled = value != null && value !== false;
}
private _disabled: boolean;
private _disabled?: boolean;
/**Implemented as part of NG_VALUE_ACCESSOR */
writeValue(obj: T): void {
@@ -112,7 +109,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
}
/**Implemented as part of NG_VALUE_ACCESSOR */
registerOnChange(fn: (value: T) => void): void {
registerOnChange(fn: (value?: T | null) => void): void {
this.notifyOnChange = fn;
}
@@ -151,11 +148,11 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
get ariaDescribedBy() {
return this._ariaDescribedBy;
}
set ariaDescribedBy(value: string) {
set ariaDescribedBy(value: string | undefined) {
this._ariaDescribedBy = value;
this.select?.searchInput.nativeElement.setAttribute("aria-describedby", value);
this.select()?.searchInput.nativeElement.setAttribute("aria-describedby", value ?? "");
}
private _ariaDescribedBy: string;
private _ariaDescribedBy?: string;
/**Implemented as part of BitFormFieldControl */
get labelForId() {
@@ -176,20 +173,24 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
private _required?: boolean;
/**Implemented as part of BitFormFieldControl */
get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
}
/**Implemented as part of BitFormFieldControl */
get error(): [string, any] {
const key = Object.keys(this.ngControl?.errors)[0];
return [key, this.ngControl?.errors[key]];
const errors = this.ngControl?.errors ?? {};
const key = Object.keys(errors)[0];
return [key, errors[key]];
}
private findSelectedOption(items: Option<T>[] | undefined, value: T): Option<T> | undefined {
private findSelectedOption(
items: Option<T>[] | undefined,
value: T | null | undefined,
): Option<T> | undefined {
return items?.find((item) => item.value === value);
}
@@ -207,7 +208,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
* Needs to be arrow function to retain `this` scope.
*/
protected onKeyDown = (event: KeyboardEvent) => {
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
if (this.select().isOpen && event.key === "Escape" && !hasModifierKey(event)) {
event.stopPropagation();
}

View File

@@ -1,12 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
import { ModelSignal } from "@angular/core";
// @ts-strict-ignore
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
export type ButtonSize = "default" | "small";
export abstract class ButtonLikeAbstraction {
loading: ModelSignal<boolean>;
disabled: ModelSignal<boolean>;
abstract loading: ModelSignal<boolean>;
abstract disabled: ModelSignal<boolean>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
/** Global config for the Bitwarden Design System */
@@ -9,5 +7,5 @@ export abstract class CompactModeService {
*
* Component authors can also hook into compact mode with the `bit-compact:` Tailwind variant.
**/
enabled$: Observable<boolean>;
abstract enabled$: Observable<boolean>;
}

View File

@@ -1,10 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
/**
* Interface for implementing focusable components.
*
* Used by the `AutofocusDirective`.
*/
export abstract class FocusableElement {
getFocusTarget: () => HTMLElement | undefined;
abstract getFocusTarget(): HTMLElement | undefined;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { NgClass } from "@angular/common";
import { Component, HostBinding, OnInit, input } from "@angular/core";
@@ -26,7 +24,7 @@ export class SortableComponent implements OnInit {
/**
* Mark the column as sortable and specify the key to sort by
*/
readonly bitSortable = input<string>();
readonly bitSortable = input.required<string>();
readonly default = input(false, {
transform: (value: SortDirection | boolean | "") => {
@@ -63,7 +61,7 @@ export class SortableComponent implements OnInit {
if (!this.isActive) {
return undefined;
}
return this.sort.direction === "asc" ? "ascending" : "descending";
return this.sort?.direction === "asc" ? "ascending" : "descending";
}
protected setActive() {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { _isNumberValue } from "@angular/cdk/coercion";
import { DataSource } from "@angular/cdk/collections";
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs";
@@ -19,7 +17,7 @@ export type FilterFn<T> = (data: T) => boolean;
export class TableDataSource<T> extends DataSource<T> {
private readonly _data: BehaviorSubject<T[]>;
private readonly _sort: BehaviorSubject<Sort>;
private readonly _filter = new BehaviorSubject<string | FilterFn<T>>(null);
private readonly _filter = new BehaviorSubject<string | FilterFn<T>>(() => true);
private readonly _renderData = new BehaviorSubject<T[]>([]);
private _renderChangesSubscription: Subscription | null = null;
@@ -29,12 +27,12 @@ export class TableDataSource<T> extends DataSource<T> {
* For example, a 'selectAll()' function would likely want to select the set of filtered data
* shown to the user rather than all the data.
*/
filteredData: T[];
filteredData?: T[];
constructor() {
super();
this._data = new BehaviorSubject([]);
this._sort = new BehaviorSubject({ direction: "asc" });
this._data = new BehaviorSubject([] as T[]);
this._sort = new BehaviorSubject<Sort>({ direction: "asc" } as Sort);
}
get data() {

View File

@@ -13,7 +13,9 @@
</thead>
<tbody>
<tr *cdkVirtualFor="let r of rows$; trackBy: trackBy(); templateCacheSize: 0" bitRow>
<ng-container *ngTemplateOutlet="rowDef.template; context: { $implicit: r }"></ng-container>
<ng-container
*ngTemplateOutlet="rowDef().template; context: { $implicit: r }"
></ng-container>
</tr>
</tbody>
</table>

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
CdkVirtualScrollViewport,
CdkFixedSizeVirtualScroll,
@@ -9,7 +7,6 @@ import { CommonModule } from "@angular/common";
import {
AfterContentChecked,
Component,
ContentChild,
OnDestroy,
TemplateRef,
Directive,
@@ -18,6 +15,7 @@ import {
ElementRef,
TrackByFunction,
input,
contentChild,
} from "@angular/core";
import { ScrollLayoutDirective } from "../layout";
@@ -69,7 +67,7 @@ export class TableScrollComponent
/** Optional trackBy function. */
readonly trackBy = input<TrackByFunction<any> | undefined>();
@ContentChild(BitRowDef) protected rowDef: BitRowDef;
protected readonly rowDef = contentChild(BitRowDef);
/**
* Height of the thead element (in pixels).
@@ -81,7 +79,7 @@ export class TableScrollComponent
/**
* Observer for table header, applies padding on resize.
*/
private headerObserver: ResizeObserver;
private headerObserver?: ResizeObserver;
constructor(
private zone: NgZone,

View File

@@ -6,7 +6,7 @@
</thead>
<tbody>
<ng-container
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows$ }"
*ngTemplateOutlet="templateVariable().template; context: { $implicit: rows$ }"
></ng-container>
</tbody>
</table>

View File

@@ -1,15 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { isDataSource } from "@angular/cdk/collections";
import { CommonModule } from "@angular/common";
import {
AfterContentChecked,
Component,
ContentChild,
Directive,
OnDestroy,
TemplateRef,
input,
contentChild,
} from "@angular/core";
import { Observable } from "rxjs";
@@ -32,9 +30,9 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
readonly dataSource = input<TableDataSource<any>>();
readonly layout = input<"auto" | "fixed">("auto");
@ContentChild(TableBodyDirective) templateVariable: TableBodyDirective;
readonly templateVariable = contentChild(TableBodyDirective);
protected rows$: Observable<any[]>;
protected rows$?: Observable<any[]>;
private _initialized = false;

View File

@@ -1,5 +1,3 @@
// 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, input } from "@angular/core";
@@ -15,7 +13,7 @@ export class TabListItemDirective implements FocusableOption {
// TODO: Skipped for signal migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input() disabled: boolean;
@Input() disabled = false;
@HostBinding("attr.disabled")
get disabledAttr() {

View File

@@ -1,5 +1,3 @@
// 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, effect, HostBinding, input } from "@angular/core";
@@ -9,7 +7,7 @@ import { Component, effect, HostBinding, input } from "@angular/core";
imports: [CdkPortalOutlet],
})
export class TabBodyComponent {
private _firstRender: boolean;
private _firstRender = false;
readonly content = input<TemplatePortal>();
readonly preserveContent = input(false);

View File

@@ -5,7 +5,7 @@
[attr.aria-label]="label()"
(keydown)="keyManager.onKeydown($event)"
>
@for (tab of tabs; track tab; let i = $index) {
@for (tab of tabs(); track tab; let i = $index) {
<button
bitTabListItem
type="button"
@@ -30,7 +30,7 @@
</div>
</bit-tab-header>
<div class="tw-px-6 tw-pt-5">
@for (tab of tabs; track tab; let i = $index) {
@for (tab of tabs(); track tab; let i = $index) {
<bit-tab-body
role="tabpanel"
[id]="getTabContentId(i)"

View File

@@ -1,24 +1,21 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FocusKeyManager } from "@angular/cdk/a11y";
import { coerceNumberProperty } from "@angular/cdk/coercion";
import { NgTemplateOutlet } from "@angular/common";
import {
AfterContentChecked,
AfterContentInit,
AfterViewInit,
Component,
ContentChildren,
EventEmitter,
Input,
Output,
QueryList,
ViewChildren,
contentChild,
contentChildren,
effect,
input,
viewChildren,
inject,
DestroyRef,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { TabHeaderComponent } from "../shared/tab-header.component";
import { TabListContainerDirective } from "../shared/tab-list-container.directive";
@@ -41,7 +38,7 @@ let nextId = 0;
TabBodyComponent,
],
})
export class TabGroupComponent implements AfterContentChecked, AfterContentInit, AfterViewInit {
export class TabGroupComponent implements AfterContentChecked, AfterViewInit {
private readonly destroyRef = inject(DestroyRef);
private readonly _groupId: number;
@@ -59,8 +56,11 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
*/
readonly preserveContent = input(false);
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
@ViewChildren(TabListItemDirective) tabLabels: QueryList<TabListItemDirective>;
/** Error if no `TabComponent` is supplied. (`contentChildren`, used to query for all the tabs, doesn't support `required`) */
private _tab = contentChild.required(TabComponent);
protected tabs = contentChildren(TabComponent);
readonly tabLabels = viewChildren(TabListItemDirective);
/** The index of the active tab. */
// TODO: Skipped for signal migration because:
@@ -85,78 +85,18 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
* Focus key manager for keeping tab controls accessible.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
*/
keyManager: FocusKeyManager<TabListItemDirective>;
keyManager?: FocusKeyManager<TabListItemDirective>;
constructor() {
this._groupId = nextId++;
}
protected getTabContentId(id: number): string {
return `bit-tab-content-${this._groupId}-${id}`;
}
protected getTabLabelId(id: number): string {
return `bit-tab-label-${this._groupId}-${id}`;
}
selectTab(index: number) {
this.selectedIndex = index;
}
/**
* After content is checked, the tab group knows what tabs are defined and which index
* should be currently selected.
*/
ngAfterContentChecked(): void {
const indexToSelect = (this._indexToSelect = this._clampTabIndex(this._indexToSelect));
if (this._selectedIndex != indexToSelect) {
const isFirstRun = this._selectedIndex == null;
if (!isFirstRun) {
this.selectedTabChange.emit({
index: indexToSelect,
tab: this.tabs.toArray()[indexToSelect],
});
}
// These values need to be updated after change detection as
// the checked content may have references to them.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Promise.resolve().then(() => {
this.tabs.forEach((tab, index) => (tab.isActive = index === indexToSelect));
if (!isFirstRun) {
this.selectedIndexChange.emit(indexToSelect);
}
});
// Manually update the _selectedIndex and keyManager active item
this._selectedIndex = indexToSelect;
if (this.keyManager) {
this.keyManager.setActiveItem(indexToSelect);
}
}
}
ngAfterViewInit(): void {
this.keyManager = new FocusKeyManager(this.tabLabels)
.withHorizontalOrientation("ltr")
.withWrap()
.withHomeAndEnd();
}
ngAfterContentInit() {
// Subscribe to any changes in the number of tabs, in order to be able
// to re-render content when new tabs are added or removed.
this.tabs.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
const indexToSelect = this._clampTabIndex(this._indexToSelect);
effect(() => {
const indexToSelect = this._clampTabIndex(this._indexToSelect ?? 0);
// If the selected tab didn't explicitly change, keep the previously
// selected tab selected/active
if (indexToSelect === this._selectedIndex) {
const tabs = this.tabs.toArray();
const tabs = this.tabs();
let selectedTab: TabComponent | undefined;
for (let i = 0; i < tabs.length; i++) {
@@ -183,12 +123,66 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
});
}
protected getTabContentId(id: number): string {
return `bit-tab-content-${this._groupId}-${id}`;
}
protected getTabLabelId(id: number): string {
return `bit-tab-label-${this._groupId}-${id}`;
}
selectTab(index: number) {
this.selectedIndex = index;
}
/**
* After content is checked, the tab group knows what tabs are defined and which index
* should be currently selected.
*/
ngAfterContentChecked(): void {
const indexToSelect = (this._indexToSelect = this._clampTabIndex(this._indexToSelect ?? 0));
if (this._selectedIndex != indexToSelect) {
const isFirstRun = this._selectedIndex == null;
if (!isFirstRun) {
this.selectedTabChange.emit({
index: indexToSelect,
tab: this.tabs()[indexToSelect],
});
}
// These values need to be updated after change detection as
// the checked content may have references to them.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Promise.resolve().then(() => {
this.tabs().forEach((tab, index) => (tab.isActive = index === indexToSelect));
if (!isFirstRun) {
this.selectedIndexChange.emit(indexToSelect);
}
});
// Manually update the _selectedIndex and keyManager active item
this._selectedIndex = indexToSelect;
this.keyManager?.setActiveItem(indexToSelect);
}
}
ngAfterViewInit(): void {
this.keyManager = new FocusKeyManager(this.tabLabels())
.withHorizontalOrientation("ltr")
.withWrap()
.withHomeAndEnd();
}
private _clampTabIndex(index: number): number {
return Math.min(this.tabs.length - 1, Math.max(index || 0, 0));
return Math.min(this.tabs().length - 1, Math.max(index || 0, 0));
}
}
export class BitTabChangeEvent {
export interface BitTabChangeEvent {
/**
* The currently selected tab index
*/

View File

@@ -1,14 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TemplatePortal } from "@angular/cdk/portal";
import {
Component,
ContentChild,
OnInit,
TemplateRef,
ViewChild,
ViewContainerRef,
input,
viewChild,
} from "@angular/core";
import { TabLabelDirective } from "./tab-label.directive";
@@ -34,8 +32,8 @@ export class TabComponent implements OnInit {
*/
readonly contentTabIndex = input<number | undefined>();
@ViewChild(TemplateRef, { static: true }) implicitContent: TemplateRef<unknown>;
@ContentChild(TabLabelDirective) templateLabel: TabLabelDirective;
readonly implicitContent = viewChild.required(TemplateRef);
@ContentChild(TabLabelDirective) templateLabel?: TabLabelDirective;
private _contentPortal: TemplatePortal | null = null;
@@ -43,11 +41,11 @@ export class TabComponent implements OnInit {
return this._contentPortal;
}
isActive: boolean;
isActive?: boolean;
constructor(private _viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
this._contentPortal = new TemplatePortal(this.implicitContent, this._viewContainerRef);
this._contentPortal = new TemplatePortal(this.implicitContent(), this._viewContainerRef);
}
}

View File

@@ -1,15 +1,13 @@
// 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,
DestroyRef,
HostListener,
Input,
ViewChild,
input,
inject,
DestroyRef,
input,
viewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router";
@@ -24,9 +22,10 @@ import { TabNavBarComponent } from "./tab-nav-bar.component";
imports: [TabListItemDirective, RouterModule],
})
export class TabLinkComponent implements FocusableOption, AfterViewInit {
private readonly destroyRef = inject(DestroyRef);
@ViewChild(TabListItemDirective) tabItem: TabListItemDirective;
@ViewChild("rla") routerLinkActive: RouterLinkActive;
private destroyRef = inject(DestroyRef);
readonly tabItem = viewChild.required(TabListItemDirective);
readonly routerLinkActive = viewChild.required<RouterLinkActive>("rla");
readonly routerLinkMatchOptions: IsActiveMatchOptions = {
queryParams: "ignored",
@@ -43,25 +42,25 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit {
@HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) {
if (event.code === "Space") {
this.tabItem.click();
this.tabItem().click();
}
}
get active() {
return this.routerLinkActive?.isActive ?? false;
return this.routerLinkActive()?.isActive ?? false;
}
constructor(private _tabNavBar: TabNavBarComponent) {}
focus(): void {
this.tabItem.focus();
this.tabItem().focus();
}
ngAfterViewInit() {
// The active state of tab links are tracked via the routerLinkActive directive
// We need to watch for changes to tell the parent nav group when the tab is active
this.routerLinkActive.isActiveChange
.pipe(takeUntilDestroyed(this.destroyRef))
this.routerLinkActive()
.isActiveChange.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((_) => this._tabNavBar.updateActiveLink());
}
}

View File

@@ -1,14 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FocusKeyManager } from "@angular/cdk/a11y";
import {
AfterContentInit,
Component,
ContentChildren,
forwardRef,
QueryList,
input,
} from "@angular/core";
import { AfterContentInit, Component, forwardRef, input, contentChildren } from "@angular/core";
import { TabHeaderComponent } from "../shared/tab-header.component";
import { TabListContainerDirective } from "../shared/tab-list-container.directive";
@@ -24,17 +15,17 @@ import { TabLinkComponent } from "./tab-link.component";
imports: [TabHeaderComponent, TabListContainerDirective],
})
export class TabNavBarComponent implements AfterContentInit {
@ContentChildren(forwardRef(() => TabLinkComponent)) tabLabels: QueryList<TabLinkComponent>;
readonly tabLabels = contentChildren(forwardRef(() => TabLinkComponent));
readonly label = input("");
/**
* Focus key manager for keeping tab controls accessible.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
*/
keyManager: FocusKeyManager<TabLinkComponent>;
keyManager?: FocusKeyManager<TabLinkComponent>;
ngAfterContentInit(): void {
this.keyManager = new FocusKeyManager(this.tabLabels)
this.keyManager = new FocusKeyManager(this.tabLabels())
.withHorizontalOrientation("ltr")
.withWrap()
.withHomeAndEnd();
@@ -42,10 +33,10 @@ export class TabNavBarComponent implements AfterContentInit {
updateActiveLink() {
// Keep the keyManager in sync with active tabs
const items = this.tabLabels.toArray();
const items = this.tabLabels();
for (let i = 0; i < items.length; i++) {
if (items[i].active) {
this.keyManager.updateActiveItem(i);
this.keyManager?.updateActiveItem(i);
}
}
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { Component, OnInit, viewChild } from "@angular/core";
import { ToastContainerDirective, ToastrService } from "ngx-toastr";
@Component({
@@ -7,12 +7,11 @@ import { ToastContainerDirective, ToastrService } from "ngx-toastr";
imports: [ToastContainerDirective],
})
export class ToastContainerComponent implements OnInit {
@ViewChild(ToastContainerDirective, { static: true })
toastContainer?: ToastContainerDirective;
readonly toastContainer = viewChild(ToastContainerDirective);
constructor(private toastrService: ToastrService) {}
ngOnInit(): void {
this.toastrService.overlayContainer = this.toastContainer;
this.toastrService.overlayContainer = this.toastContainer();
}
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { IndividualConfig, ToastrService } from "ngx-toastr";
@@ -36,7 +34,7 @@ export class ToastService {
: calculateToastTimeout(options.message),
};
this.toastrService.show(null, options.title, toastrConfig);
this.toastrService.show(undefined, options.title, toastrConfig);
}
/**

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgClass } from "@angular/common";
import {
AfterContentChecked,
@@ -8,8 +6,8 @@ import {
ElementRef,
HostBinding,
signal,
ViewChild,
input,
viewChild,
} from "@angular/core";
import { ToggleGroupComponent } from "./toggle-group.component";
@@ -24,9 +22,9 @@ let nextId = 0;
export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewInit {
id = nextId++;
readonly value = input<TValue>();
@ViewChild("labelContent") labelContent: ElementRef<HTMLSpanElement>;
@ViewChild("bitBadgeContainer") bitBadgeContainer: ElementRef<HTMLSpanElement>;
readonly value = input.required<TValue>();
readonly labelContent = viewChild<ElementRef<HTMLSpanElement>>("labelContent");
readonly bitBadgeContainer = viewChild<ElementRef<HTMLSpanElement>>("bitBadgeContainer");
constructor(private groupComponent: ToggleGroupComponent<TValue>) {}
@@ -34,7 +32,7 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
@HostBinding("class") classList = ["tw-group/toggle", "tw-flex", "tw-min-w-16"];
protected bitBadgeContainerHasChidlren = signal(false);
protected labelTitle = signal<string>(null);
protected labelTitle = signal<string | null>(null);
get name() {
return this.groupComponent.name;
@@ -100,12 +98,12 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
ngAfterContentChecked() {
this.bitBadgeContainerHasChidlren.set(
this.bitBadgeContainer?.nativeElement.childElementCount > 0,
(this.bitBadgeContainer()?.nativeElement.childElementCount ?? 0) > 0,
);
}
ngAfterViewInit() {
const labelText = this.labelContent?.nativeElement.innerText;
const labelText = this.labelContent()?.nativeElement.innerText;
if (labelText) {
this.labelTitle.set(labelText);
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { booleanAttribute, Directive, HostBinding, input } from "@angular/core";
type TypographyType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "body1" | "body2" | "helper";
@@ -32,7 +30,7 @@ const margins: Record<TypographyType, string[]> = {
selector: "[bitTypography]",
})
export class TypographyDirective {
readonly bitTypography = input<TypographyType>();
readonly bitTypography = input.required<TypographyType>();
readonly noMargin = input(false, { transform: booleanAttribute });

View File

@@ -1,18 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
export class I18nMockService implements I18nService {
userSetLocale$: Observable<string | undefined>;
locale$: Observable<string>;
supportedTranslationLocales: string[];
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
constructor(private lookupTable: Record<string, string | ((...args: string[]) => string)>) {}
export class I18nMockService implements Pick<I18nService, "t" | "translate"> {
constructor(
private lookupTable: Record<string, string | ((...args: (string | undefined)[]) => string)>,
) {}
t(id: string, p1?: string, p2?: string, p3?: string) {
let value = this.lookupTable[id];

View File

@@ -136,7 +136,7 @@ export class UriOptionComponent implements ControlValueAccessor {
protected toggleMatchDetection() {
this.showMatchDetection = !this.showMatchDetection;
if (this.showMatchDetection) {
setTimeout(() => this.matchDetectionSelect?.select?.focus(), 0);
setTimeout(() => this.matchDetectionSelect?.select()?.focus(), 0);
}
}