1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +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,8 +1,6 @@
import { Directive, Optional } from "@angular/core"; import { Directive, inject, model } from "@angular/core";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { BitActionDirective, FunctionReturningAwaitable } from "@bitwarden/components";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { BitActionDirective, ButtonLikeAbstraction } from "@bitwarden/components";
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
@@ -11,15 +9,10 @@ import { PopupRouterCacheService } from "../view-cache/popup-router-cache.servic
selector: "[popupBackAction]", selector: "[popupBackAction]",
}) })
export class PopupBackBrowserDirective extends BitActionDirective { export class PopupBackBrowserDirective extends BitActionDirective {
constructor( private routerCacheService = inject(PopupRouterCacheService);
buttonComponent: ButtonLikeAbstraction, // Override the required input to make it optional since we set it automatically
private router: PopupRouterCacheService, override readonly handler = model<FunctionReturningAwaitable>(
@Optional() validationService?: ValidationService, () => this.routerCacheService.back(),
@Optional() logService?: LogService, { alias: "popupBackAction" },
) { );
super(buttonComponent, validationService, logService);
// override `bitAction` input; the parent handles the rest
this.handler.set(() => this.router.back());
}
} }

View File

@@ -343,7 +343,7 @@ export default {
generator: "Generator", generator: "Generator",
send: "Send", send: "Send",
settings: "Settings", settings: "Settings",
labelWithNotification: (label: string) => `${label}: New Notification`, labelWithNotification: (label: string | undefined) => `${label}: New Notification`,
}); });
}, },
}, },

View File

@@ -179,7 +179,7 @@ type Story = StoryObj<
const Template: Story = { const Template: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: ` template: /*html*/ `
<router-outlet [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></router-outlet> <router-outlet [mockOrgs]="mockOrgs" [mockProviders]="mockProviders"></router-outlet>
<div class="tw-flex tw-gap-[200px]"> <div class="tw-flex tw-gap-[200px]">
<div> <div>
@@ -191,7 +191,7 @@ const Template: Story = {
<product-switcher-content #content></product-switcher-content> <product-switcher-content #content></product-switcher-content>
<div class="tw-h-40"> <div class="tw-h-40">
<div class="cdk-overlay-pane bit-menu-panel"> <div class="cdk-overlay-pane bit-menu-panel">
<ng-container *ngTemplateOutlet="content?.menu?.templateRef"></ng-container> <ng-container *ngTemplateOutlet="content?.menu?.templateRef()"></ng-container>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line import { DestroyRef, Directive, HostListener, inject, model, Optional } from "@angular/core";
// @ts-strict-ignore
import { Directive, HostListener, model, Optional, inject, DestroyRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, finalize, tap } from "rxjs"; import { BehaviorSubject, finalize, tap } from "rxjs";
@@ -38,7 +36,7 @@ export class BitActionDirective {
disabled = false; disabled = false;
readonly handler = model<FunctionReturningAwaitable>(undefined, { alias: "bitAction" }); readonly handler = model.required<FunctionReturningAwaitable>({ alias: "bitAction" });
private readonly destroyRef = inject(DestroyRef); 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 import { DestroyRef, Directive, OnInit, Optional, inject, input } from "@angular/core";
// @ts-strict-ignore
import { Directive, OnInit, Optional, input, inject, DestroyRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormGroupDirective } from "@angular/forms"; import { FormGroupDirective } from "@angular/forms";
import { BehaviorSubject, catchError, filter, of, switchMap } from "rxjs"; import { BehaviorSubject, catchError, filter, of, switchMap } from "rxjs";
@@ -22,7 +20,7 @@ export class BitSubmitDirective implements OnInit {
private _loading$ = new BehaviorSubject<boolean>(false); private _loading$ = new BehaviorSubject<boolean>(false);
private _disabled$ = 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); readonly allowDisabledFormSubmit = input<boolean>(false);
@@ -63,7 +61,7 @@ export class BitSubmitDirective implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.formGroupDirective.statusChanges this.formGroupDirective.statusChanges
.pipe(takeUntilDestroyed(this.destroyRef)) ?.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((c) => { .subscribe((c) => {
if (this.allowDisabledFormSubmit()) { if (this.allowDisabledFormSubmit()) {
this._disabled$.next(false); 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 { Directive, Optional, input } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; 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 { NgClass } from "@angular/common";
import { Component, OnChanges, input } from "@angular/core"; import { Component, OnChanges, input } from "@angular/core";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
@@ -41,7 +39,7 @@ export class AvatarComponent implements OnChanges {
private svgFontSize = 20; private svgFontSize = 20;
private svgFontWeight = 300; private svgFontWeight = 300;
private svgSize = 48; private svgSize = 48;
src: SafeResourceUrl; src?: SafeResourceUrl;
constructor(public sanitizer: DomSanitizer) {} constructor(public sanitizer: DomSanitizer) {}
@@ -56,8 +54,14 @@ export class AvatarComponent implements OnChanges {
} }
private generate() { private generate() {
let chars: string = null; const color = this.color();
const upperCaseText = this.text()?.toUpperCase() ?? ""; 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); 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 the chars contain an emoji, only show it.
if (chars.match(Utils.regexpEmojiPresentation)) { const emojiMatch = chars.match(Utils.regexpEmojiPresentation);
chars = chars.match(Utils.regexpEmojiPresentation)[0]; if (emojiMatch) {
chars = emojiMatch[0];
} }
let svg: HTMLElement; let svg: HTMLElement;
let hexColor = this.color(); let hexColor = color ?? "";
if (!Utils.isNullOrWhitespace(hexColor)) {
const id = this.id();
if (!Utils.isNullOrWhitespace(this.color())) {
svg = this.createSvgElement(this.svgSize, hexColor); svg = this.createSvgElement(this.svgSize, hexColor);
} else if (!Utils.isNullOrWhitespace(id)) { } else if (!Utils.isNullOrWhitespace(id ?? "")) {
hexColor = Utils.stringToColor(id.toString()); hexColor = Utils.stringToColor(id!.toString());
svg = this.createSvgElement(this.svgSize, hexColor); svg = this.createSvgElement(this.svgSize, hexColor);
} else { } else {
hexColor = Utils.stringToColor(upperCaseText); hexColor = Utils.stringToColor(upperCaseText);
@@ -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(" "); const parts = data.split(" ");
if (parts.length > 1) { if (parts.length > 1) {
let text = ""; let text = "";

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<aside <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" 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" [attr.aria-labelledby]="titleId"
> >
@if (titleComputed(); as title) { @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 { Component, computed, input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -50,11 +48,11 @@ export class CalloutComponent {
return title; return title;
}); });
protected titleId = `bit-callout-title-${nextId++}`; protected readonly titleId = `bit-callout-title-${nextId++}`;
constructor(private i18nService: I18nService) {} constructor(private i18nService: I18nService) {}
get calloutClass() { protected readonly calloutClass = computed(() => {
switch (this.type()) { switch (this.type()) {
case "danger": case "danger":
return "tw-bg-danger-100"; return "tw-bg-danger-100";
@@ -65,5 +63,5 @@ export class CalloutComponent {
case "warning": case "warning":
return "tw-bg-warning-100"; 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 { Component, HostBinding, Input, Optional, Self } from "@angular/core";
import { NgControl, Validators } from "@angular/forms"; import { NgControl, Validators } from "@angular/forms";
@@ -114,7 +112,7 @@ export class CheckboxComponent implements BitFormControlAbstraction {
set disabled(value: any) { set disabled(value: any) {
this._disabled = value != null && value !== false; this._disabled = value != null && value !== false;
} }
private _disabled: boolean; private _disabled?: boolean;
// TODO: Skipped for signal migration because: // TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex. // Accessor inputs cannot be migrated as they are too complex.
@@ -127,14 +125,15 @@ export class CheckboxComponent implements BitFormControlAbstraction {
set required(value: any) { set required(value: any) {
this._required = value != null && value !== false; this._required = value != null && value !== false;
} }
private _required: boolean; private _required?: boolean;
get hasError() { get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched; return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
} }
get error(): [string, any] { get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0]; const errors = this.ngControl?.errors ?? {};
return [key, this.ngControl.errors[key]]; 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 { import {
AfterViewInit, AfterViewInit,
Component, Component,
@@ -9,12 +7,12 @@ import {
HostListener, HostListener,
Input, Input,
QueryList, QueryList,
ViewChild,
ViewChildren, ViewChildren,
booleanAttribute, booleanAttribute,
inject, inject,
signal, signal,
input, input,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
@@ -50,9 +48,9 @@ export type ChipSelectOption<T> = Option<T> & {
], ],
}) })
export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit { export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit {
@ViewChild(MenuComponent) menu: MenuComponent; readonly menu = viewChild(MenuComponent);
@ViewChildren(MenuItemDirective) menuItems: QueryList<MenuItemDirective>; @ViewChildren(MenuItemDirective) menuItems?: QueryList<MenuItemDirective>;
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>; readonly chipSelectButton = viewChild<ElementRef<HTMLButtonElement>>("chipSelectButton");
/** Text to show when there is no selected option */ /** Text to show when there is no selected option */
readonly placeholderText = input.required<string>(); 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 */ /** Icon to show when there is no selected option or the selected option does not have an icon */
readonly placeholderIcon = input<string>(); readonly placeholderIcon = input<string>();
private _options: ChipSelectOption<T>[]; private _options: ChipSelectOption<T>[] = [];
// TODO: Skipped for signal migration because: // TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex. // 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); private destroyRef = inject(DestroyRef);
/** Tree constructed from `this.options` */ /** Tree constructed from `this.options` */
private rootTree: ChipSelectOption<T>; private rootTree?: ChipSelectOption<T> | null;
/** Options that are currently displayed in the menu */ /** 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 */ /** 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 * 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 */ /** The icon to show in the chip button */
protected get icon(): string { protected get icon(): string | undefined {
return this.selectedOption?.icon || this.placeholderIcon(); return this.selectedOption?.icon || this.placeholderIcon();
} }
@@ -133,7 +131,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
*/ */
protected setOrResetRenderedOptions(): void { protected setOrResetRenderedOptions(): void {
this.renderedOptions = this.selectedOption this.renderedOptions = this.selectedOption
? this.selectedOption.children?.length > 0 ? (this.selectedOption.children?.length ?? 0) > 0
? this.selectedOption ? this.selectedOption
: this.getParent(this.selectedOption) : this.getParent(this.selectedOption)
: this.rootTree; : this.rootTree;
@@ -171,7 +169,14 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
* @param value the option value to look for * @param value the option value to look for
* @returns the `ChipSelectOption` associated with the provided value, or null if not found * @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; let result = null;
if (tree.value !== null && compareValues(tree.value, value)) { if (tree.value !== null && compareValues(tree.value, value)) {
return tree; 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); 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 * 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 * direct their focus to the first item in the new menu
*/ */
this.menuItems.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { this.menuItems?.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.menu.keyManager.setFirstItemActive(); this.menu()?.keyManager?.setFirstItemActive();
}); });
} }
@@ -227,17 +232,17 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
* the initially rendered options * the initially rendered options
*/ */
protected setMenuWidth() { protected setMenuWidth() {
const chipWidth = this.chipSelectButton.nativeElement.getBoundingClientRect().width; const chipWidth = this.chipSelectButton()?.nativeElement.getBoundingClientRect().width ?? 0;
const firstMenuItemWidth = 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); this.menuWidth = Math.max(chipWidth, firstMenuItemWidth);
} }
/** Control Value Accessor */ /** Control Value Accessor */
private notifyOnChange?: (value: T) => void; private notifyOnChange?: (value: T | null) => void;
private notifyOnTouched?: () => void; private notifyOnTouched?: () => void;
/** Implemented as part of NG_VALUE_ACCESSOR */ /** 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 */ /** Implemented as part of NG_VALUE_ACCESSOR */
registerOnChange(fn: (value: T) => void): void { registerOnChange(fn: (value: T | null) => void): void {
this.notifyOnChange = fn; 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 { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core"; import { Component, Inject } from "@angular/core";
import { FormGroup, ReactiveFormsModule } from "@angular/forms"; import { FormGroup, ReactiveFormsModule } from "@angular/forms";
@@ -47,10 +45,10 @@ export class SimpleConfigurableDialogComponent {
]; ];
} }
protected title: string; protected title?: string;
protected content: string; protected content?: string;
protected acceptButtonText: string; protected acceptButtonText?: string;
protected cancelButtonText: string; protected cancelButtonText?: string;
protected formGroup = new FormGroup({}); protected formGroup = new FormGroup({});
protected showCancelButton = this.simpleDialogOpts.cancelButtonText !== null; protected showCancelButton = this.simpleDialogOpts.cancelButtonText !== null;
@@ -58,7 +56,7 @@ export class SimpleConfigurableDialogComponent {
constructor( constructor(
public dialogRef: DialogRef, public dialogRef: DialogRef,
private i18nService: I18nService, private i18nService: I18nService,
@Inject(DIALOG_DATA) public simpleDialogOpts?: SimpleDialogOptions, @Inject(DIALOG_DATA) public simpleDialogOpts: SimpleDialogOptions,
) { ) {
this.localizeText(); this.localizeText();
} }
@@ -76,24 +74,27 @@ export class SimpleConfigurableDialogComponent {
private localizeText() { private localizeText() {
this.title = this.translate(this.simpleDialogOpts.title); this.title = this.translate(this.simpleDialogOpts.title);
this.content = this.translate(this.simpleDialogOpts.content); 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 (this.showCancelButton) {
// If accept text is overridden, use cancel, otherwise no // If accept text is overridden, use cancel, otherwise no
this.cancelButtonText = this.translate( this.cancelButtonText = this.translate(
this.simpleDialogOpts.cancelButtonText, this.simpleDialogOpts.cancelButtonText ??
this.simpleDialogOpts.acceptButtonText !== undefined ? "cancel" : "no", (this.simpleDialogOpts.acceptButtonText !== undefined
? { key: "cancel" }
: { key: "no" }),
); );
} }
} }
private translate(translation: string | Translation, defaultKey?: string): string { private translate(translation: string | Translation): string {
// Translation interface use implies we must localize. // Object implies we must localize.
if (typeof translation === "object") { if (typeof translation === "object") {
return this.i18nService.t(translation.key, ...(translation.placeholders ?? [])); return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
} }
// Use string that is already translated or use default key post translate return translation;
return translation ?? this.i18nService.t(defaultKey);
} }
} }

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 { Directive, HostBinding, HostListener, input } from "@angular/core";
import { DisclosureComponent } from "./disclosure.component"; import { DisclosureComponent } from "./disclosure.component";
@@ -12,7 +10,7 @@ export class DisclosureTriggerForDirective {
/** /**
* Accepts template reference for a bit-disclosure component instance * 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() { @HostBinding("attr.aria-expanded") get ariaExpanded() {
return this.disclosure().open; 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 { import {
Component, Component,
EventEmitter, EventEmitter,
@@ -40,11 +38,10 @@ let nextId = 0;
template: `<ng-content></ng-content>`, template: `<ng-content></ng-content>`,
}) })
export class DisclosureComponent { export class DisclosureComponent {
private _open: boolean;
/** Emits the visibility of the disclosure content */ /** Emits the visibility of the disclosure content */
@Output() openChange = new EventEmitter<boolean>(); @Output() openChange = new EventEmitter<boolean>();
private _open?: boolean;
/** /**
* Optionally init the disclosure in its opened state * Optionally init the disclosure in its opened state
*/ */
@@ -54,14 +51,13 @@ export class DisclosureComponent {
this._open = isOpen; this._open = isOpen;
this.openChange.emit(isOpen); this.openChange.emit(isOpen);
} }
get open(): boolean {
return !!this._open;
}
@HostBinding("class") get classList() { @HostBinding("class") get classList() {
return this.open ? "" : "tw-hidden"; return this.open ? "" : "tw-hidden";
} }
@HostBinding("id") id = `bit-disclosure-${nextId++}`; @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 { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; 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 { export abstract class BitFormControlAbstraction {
disabled: boolean; abstract disabled: boolean;
required: boolean; abstract required: boolean;
hasError: boolean; abstract hasError: boolean;
error: [string, any]; abstract error: [string, any];
} }

View File

@@ -1,11 +1,11 @@
<label <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" 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> <ng-content></ng-content>
<span <span
class="tw-inline-flex tw-flex-col" 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"> <span bitTypography="body2">
<ng-content select="bit-label"></ng-content> <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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -22,10 +20,10 @@ export class FormControlComponent {
readonly disableMargin = input(false, { transform: booleanAttribute }); readonly disableMargin = input(false, { transform: booleanAttribute });
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction; protected readonly formControl = contentChild.required(BitFormControlAbstraction);
@HostBinding("class") get classes() { @HostBinding("class") get classes() {
return [] return ([] as string[])
.concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"]) .concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
.concat(this.disableMargin() ? [] : ["tw-mb-4"]); .concat(this.disableMargin() ? [] : ["tw-mb-4"]);
} }
@@ -33,15 +31,15 @@ export class FormControlComponent {
constructor(private i18nService: I18nService) {} constructor(private i18nService: I18nService) {}
get required() { get required() {
return this.formControl.required; return this.formControl().required;
} }
get hasError() { get hasError() {
return this.formControl.hasError; return this.formControl().hasError;
} }
get error() { get error() {
return this.formControl.error; return this.formControl().error;
} }
get displayError() { 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 { CommonModule } from "@angular/common";
import { Component, ElementRef, HostBinding, input, Optional } from "@angular/core"; import { Component, ElementRef, HostBinding, input, Optional } from "@angular/core";
@@ -32,7 +30,7 @@ export class BitLabel {
]; ];
@HostBinding("title") get title() { @HostBinding("title") get title() {
return this.elementRef.nativeElement.textContent.trim(); return this.elementRef.nativeElement.textContent?.trim() ?? "";
} }
readonly id = input(`bit-label-${nextId++}`); 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 { FormControl } from "@angular/forms";
import { forbiddenCharacters } from "./forbidden-characters.validator"; 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); 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 { FormControl } from "@angular/forms";
import { trimValidator as validate } from "./trim.validator"; 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); 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 { Component, input } from "@angular/core";
import { AbstractControl, UntypedFormGroup } from "@angular/forms"; import { AbstractControl, UntypedFormGroup } from "@angular/forms";
@@ -21,7 +18,8 @@ export class BitErrorSummary {
readonly formGroup = input<UntypedFormGroup>(); readonly formGroup = input<UntypedFormGroup>();
get errorCount(): number { get errorCount(): number {
return this.getErrorCount(this.formGroup()); const form = this.formGroup();
return form ? this.getErrorCount(form) : 0;
} }
get errorString() { 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 { Component, HostBinding, input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -24,6 +22,10 @@ export class BitErrorComponent {
get displayError() { get displayError() {
const error = this.error(); const error = this.error();
if (!error) {
return "";
}
switch (error[0]) { switch (error[0]) {
case "required": case "required":
return this.i18nService.t("inputRequired"); 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"; import { ModelSignal, Signal } from "@angular/core";
// @ts-strict-ignore
export type InputTypes = export type InputTypes =
| "text" | "text"
| "password" | "password"
@@ -16,14 +13,14 @@ export type InputTypes =
| "time"; | "time";
export abstract class BitFormFieldControl { export abstract class BitFormFieldControl {
ariaDescribedBy: string; abstract ariaDescribedBy?: string;
id: Signal<string>; abstract id: Signal<string>;
labelForId: string; abstract labelForId: string;
required: boolean; abstract required: boolean;
hasError: boolean; abstract hasError: boolean;
error: [string, any]; abstract error: [string, any];
type?: ModelSignal<InputTypes>; abstract type?: ModelSignal<InputTypes | undefined>;
spellcheck?: ModelSignal<boolean | undefined>; abstract spellcheck?: ModelSignal<boolean | undefined>;
readOnly?: boolean; abstract readOnly?: boolean;
focus?: () => void; abstract focus?: () => void;
} }

View File

@@ -29,10 +29,10 @@
> >
<label <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" 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> <ng-container *ngTemplateOutlet="labelContent"></ng-container>
@if (input.required) { @if (input().required) {
<span class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]"> <span class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]">
({{ "required" | i18n }})</span ({{ "required" | i18n }})</span
> >
@@ -78,7 +78,7 @@
<div class="tw-w-full tw-relative"> <div class="tw-w-full tw-relative">
<label <label
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted tw-mb-0 tw-max-w-full" 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> <ng-container *ngTemplateOutlet="labelContent"></ng-container>
</label> </label>
@@ -109,11 +109,11 @@
</div> </div>
} }
@switch (input.hasError) { @switch (input().hasError) {
@case (false) { @case (false) {
<ng-content select="bit-hint"></ng-content> <ng-content select="bit-hint"></ng-content>
} }
@case (true) { @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 { CommonModule } from "@angular/common";
import { import {
AfterContentChecked, AfterContentChecked,
booleanAttribute, booleanAttribute,
Component, Component,
ContentChild,
ElementRef, ElementRef,
HostBinding, HostBinding,
HostListener, HostListener,
ViewChild,
signal, signal,
input, input,
Input, Input,
contentChild,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -30,14 +28,14 @@ import { BitFormFieldControl } from "./form-field-control";
imports: [CommonModule, BitErrorComponent, I18nPipe], imports: [CommonModule, BitErrorComponent, I18nPipe],
}) })
export class BitFormFieldComponent implements AfterContentChecked { export class BitFormFieldComponent implements AfterContentChecked {
@ContentChild(BitFormFieldControl) input: BitFormFieldControl; readonly input = contentChild.required(BitFormFieldControl);
@ContentChild(BitHintComponent) hint: BitHintComponent; readonly hint = contentChild(BitHintComponent);
@ContentChild(BitLabel) label: BitLabel; readonly label = contentChild(BitLabel);
@ViewChild("prefixContainer") prefixContainer: ElementRef<HTMLDivElement>; readonly prefixContainer = viewChild<ElementRef<HTMLDivElement>>("prefixContainer");
@ViewChild("suffixContainer") suffixContainer: ElementRef<HTMLDivElement>; readonly suffixContainer = viewChild<ElementRef<HTMLDivElement>>("suffixContainer");
@ViewChild(BitErrorComponent) error: BitErrorComponent; readonly error = viewChild(BitErrorComponent);
readonly disableMargin = input(false, { transform: booleanAttribute }); readonly disableMargin = input(false, { transform: booleanAttribute });
@@ -54,7 +52,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
const shouldFocusBorderAppear = this.defaultContentIsFocused(); const shouldFocusBorderAppear = this.defaultContentIsFocused();
const groupClasses = [ const groupClasses = [
this.input.hasError this.input().hasError
? "group-hover/bit-form-field:tw-border-danger-700" ? "group-hover/bit-form-field:tw-border-danger-700"
: "group-hover/bit-form-field:tw-border-primary-600", : "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) // 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); const borderClasses = baseInputBorderClasses.concat(groupClasses);
@@ -100,19 +98,21 @@ export class BitFormFieldComponent implements AfterContentChecked {
} }
protected get readOnly(): boolean { protected get readOnly(): boolean {
return this.input.readOnly; return !!this.input().readOnly;
} }
ngAfterContentChecked(): void { ngAfterContentChecked(): void {
if (this.error) { const error = this.error();
this.input.ariaDescribedBy = this.error.id; const hint = this.hint();
} else if (this.hint) { if (error) {
this.input.ariaDescribedBy = this.hint.id; this.input().ariaDescribedBy = error.id;
} else if (hint) {
this.input().ariaDescribedBy = hint.id;
} else { } else {
this.input.ariaDescribedBy = undefined; this.input().ariaDescribedBy = undefined;
} }
this.prefixHasChildren.set(this.prefixContainer?.nativeElement.childElementCount > 0); this.prefixHasChildren.set((this.prefixContainer()?.nativeElement.childElementCount ?? 0) > 0);
this.suffixHasChildren.set(this.suffixContainer?.nativeElement.childElementCount > 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 { TextFieldModule } from "@angular/cdk/text-field";
import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
import { import {
AbstractControl, AbstractControl,
UntypedFormBuilder, UntypedFormBuilder,
@@ -15,6 +12,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { A11yTitleDirective } from "../a11y/a11y-title.directive";
import { AsyncActionsModule } from "../async-actions"; import { AsyncActionsModule } from "../async-actions";
import { BadgeModule } from "../badge"; import { BadgeModule } from "../badge";
import { ButtonModule } from "../button"; import { ButtonModule } from "../button";
@@ -31,41 +29,6 @@ import { I18nMockService } from "../utils/i18n-mock.service";
import { BitFormFieldComponent } from "./form-field.component"; import { BitFormFieldComponent } from "./form-field.component";
import { FormFieldModule } from "./form-field.module"; 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 { export default {
title: "Component Library/Form/Field", title: "Component Library/Form/Field",
component: BitFormFieldComponent, component: BitFormFieldComponent,

View File

@@ -57,17 +57,19 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
} }
ngAfterContentInit(): void { ngAfterContentInit(): void {
if (this.formField.input?.type) { const input = this.formField.input();
this.toggled.set(this.formField.input.type() !== "password"); if (input?.type) {
this.toggled.set(input.type() !== "password");
} }
this.button.icon.set(this.icon); this.button.icon.set(this.icon);
} }
private update() { private update() {
this.button.icon.set(this.icon); this.button.icon.set(this.icon);
if (this.formField.input?.type != null) { const input = this.formField.input();
this.formField.input.type.set(this.toggled() ? "text" : "password"); if (input?.type != null) {
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined); 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; button = buttonEl.componentInstance;
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent)); const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
const formField: BitFormFieldComponent = formFieldEl.componentInstance; const formField: BitFormFieldComponent = formFieldEl.componentInstance;
input = formField.input; input = formField.input();
}); });
describe("initial state", () => { 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 { NgClass } from "@angular/common";
import { Component, computed, ElementRef, HostBinding, input, model } from "@angular/core"; import { Component, computed, ElementRef, HostBinding, input, model } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop"; import { toObservable, toSignal } from "@angular/core/rxjs-interop";
@@ -90,7 +88,7 @@ const sizes: Record<IconButtonSize, string[]> = {
}, },
}) })
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { 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"); 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 { import {
AfterContentChecked, AfterContentChecked,
booleanAttribute, booleanAttribute,
@@ -73,11 +71,13 @@ export class AutofocusDirective implements AfterContentChecked {
private focus() { private focus() {
const el = this.getElement(); const el = this.getElement();
el.focus(); if (el) {
this.focused = el === document.activeElement; el.focus();
this.focused = el === document.activeElement;
}
} }
private getElement() { private getElement(): HTMLElement | undefined {
if (this.focusableElement) { if (this.focusableElement) {
return this.focusableElement.getFocusTarget(); 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 { import {
Directive, Directive,
ElementRef, ElementRef,
@@ -63,7 +61,7 @@ export class BitInputDirective implements BitFormFieldControl {
readonly id = input(`bit-input-${nextId++}`); readonly id = input(`bit-input-${nextId++}`);
@HostBinding("attr.aria-describedby") ariaDescribedBy: string; @HostBinding("attr.aria-describedby") ariaDescribedBy?: string;
@HostBinding("attr.aria-invalid") get ariaInvalid() { @HostBinding("attr.aria-invalid") get ariaInvalid() {
return this.hasError ? true : undefined; return this.hasError ? true : undefined;
@@ -83,7 +81,7 @@ export class BitInputDirective implements BitFormFieldControl {
set required(value: any) { set required(value: any) {
this._required = value != null && value !== false; this._required = value != null && value !== false;
} }
private _required: boolean; private _required?: boolean;
readonly hasPrefix = input(false); readonly hasPrefix = input(false);
readonly hasSuffix = input(false); readonly hasSuffix = input(false);
@@ -101,19 +99,20 @@ export class BitInputDirective implements BitFormFieldControl {
get hasError() { get hasError() {
if (this.showErrorsWhenDisabled()) { if (this.showErrorsWhenDisabled()) {
return ( return !!(
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") && (this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
this.ngControl?.touched && this.ngControl?.touched &&
this.ngControl?.errors != null this.ngControl?.errors != null
); );
} else { } else {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched; return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
} }
} }
get error(): [string, any] { get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0]; const errors = this.ngControl.errors ?? {};
return [key, this.ngControl.errors[key]]; const key = Object.keys(errors)[0];
return [key, errors[key]];
} }
constructor( 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 { NgClass } from "@angular/common";
import { import {
AfterContentChecked, AfterContentChecked,
@@ -8,8 +5,8 @@ import {
Component, Component,
ElementRef, ElementRef,
signal, signal,
ViewChild,
input, input,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { TypographyModule } from "../typography"; import { TypographyModule } from "../typography";
@@ -30,7 +27,7 @@ import { TypographyModule } from "../typography";
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ItemContentComponent implements AfterContentChecked { export class ItemContentComponent implements AfterContentChecked {
@ViewChild("endSlot") endSlot: ElementRef<HTMLDivElement>; readonly endSlot = viewChild<ElementRef<HTMLDivElement>>("endSlot");
protected endSlotHasChildren = signal(false); protected endSlotHasChildren = signal(false);
@@ -42,6 +39,6 @@ export class ItemContentComponent implements AfterContentChecked {
readonly truncate = input(true); readonly truncate = input(true);
ngAfterContentChecked(): void { ngAfterContentChecked(): void {
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0); this.endSlotHasChildren.set((this.endSlot()?.nativeElement.childElementCount ?? 0) > 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 { hasModifierKey } from "@angular/cdk/keycodes";
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay"; import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal"; import { TemplatePortal } from "@angular/cdk/portal";
@@ -31,9 +29,9 @@ export class MenuTriggerForDirective implements OnDestroy {
readonly role = input("button"); 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 = { private defaultMenuConfig: OverlayConfig = {
panelClass: "bit-menu-panel", panelClass: "bit-menu-panel",
hasBackdrop: true, hasBackdrop: true,
@@ -52,8 +50,8 @@ export class MenuTriggerForDirective implements OnDestroy {
.withFlexibleDimensions(false) .withFlexibleDimensions(false)
.withPush(true), .withPush(true),
}; };
private closedEventsSub: Subscription; private closedEventsSub: Subscription | null = null;
private keyDownEventsSub: Subscription; private keyDownEventsSub: Subscription | null = null;
constructor( constructor(
private elementRef: ElementRef<HTMLElement>, private elementRef: ElementRef<HTMLElement>,
@@ -78,28 +76,30 @@ export class MenuTriggerForDirective implements OnDestroy {
this.isOpen = true; this.isOpen = true;
this.overlayRef = this.overlay.create(this.defaultMenuConfig); 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.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => { this.closedEventsSub =
// Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key this.getClosedEvents()?.subscribe((event: KeyboardEvent | undefined) => {
// from doing its normal default action, which would otherwise cause a parent component // Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key
// (like a dialog) or extension window to close // from doing its normal default action, which would otherwise cause a parent component
if (event?.key === "Escape" && !hasModifierKey(event)) { // (like a dialog) or extension window to close
event.preventDefault(); 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) { if (menu.keyManager) {
menu.keyManager.setFirstItemActive(); menu.keyManager.setFirstItemActive();
this.keyDownEventsSub = this.overlayRef this.keyDownEventsSub = this.overlayRef
.keydownEvents() .keydownEvents()
.subscribe((event: KeyboardEvent) => this.menu().keyManager.onKeydown(event)); .subscribe((event: KeyboardEvent) => this.menu().keyManager?.onKeydown(event));
} }
} }
@@ -113,7 +113,10 @@ export class MenuTriggerForDirective implements OnDestroy {
this.menu().closed.emit(); this.menu().closed.emit();
} }
private getClosedEvents(): Observable<any> { private getClosedEvents(): Observable<any> | null {
if (!this.overlayRef) {
return null;
}
const detachments = this.overlayRef.detachments(); const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef.keydownEvents().pipe( const escKey = this.overlayRef.keydownEvents().pipe(
filter((event: KeyboardEvent) => { filter((event: KeyboardEvent) => {

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 { FocusKeyManager, CdkTrapFocus } from "@angular/cdk/a11y";
import { import {
Component, Component,
Output, Output,
TemplateRef, TemplateRef,
ViewChild,
EventEmitter, EventEmitter,
ContentChildren,
QueryList,
AfterContentInit, AfterContentInit,
input, input,
viewChild,
contentChildren,
} from "@angular/core"; } from "@angular/core";
import { MenuItemDirective } from "./menu-item.directive"; import { MenuItemDirective } from "./menu-item.directive";
@@ -22,10 +19,9 @@ import { MenuItemDirective } from "./menu-item.directive";
imports: [CdkTrapFocus], imports: [CdkTrapFocus],
}) })
export class MenuComponent implements AfterContentInit { export class MenuComponent implements AfterContentInit {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>; readonly templateRef = viewChild.required(TemplateRef);
@Output() closed = new EventEmitter<void>(); @Output() closed = new EventEmitter<void>();
@ContentChildren(MenuItemDirective, { descendants: true }) readonly menuItems = contentChildren(MenuItemDirective, { descendants: true });
menuItems: QueryList<MenuItemDirective>;
keyManager?: FocusKeyManager<MenuItemDirective>; keyManager?: FocusKeyManager<MenuItemDirective>;
readonly ariaRole = input<"menu" | "dialog">("menu"); readonly ariaRole = input<"menu" | "dialog">("menu");
@@ -34,9 +30,9 @@ export class MenuComponent implements AfterContentInit {
ngAfterContentInit() { ngAfterContentInit() {
if (this.ariaRole() === "menu") { if (this.ariaRole() === "menu") {
this.keyManager = new FocusKeyManager(this.menuItems) this.keyManager = new FocusKeyManager(this.menuItems())
.withWrap() .withWrap()
.skipPredicate((item) => item.disabled); .skipPredicate((item) => !!item.disabled);
} }
} }
} }

View File

@@ -46,7 +46,7 @@ export const OpenMenu: Story = {
<div class="tw-h-40"> <div class="tw-h-40">
<div class="cdk-overlay-pane bit-menu-panel"> <div class="cdk-overlay-pane bit-menu-panel">
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container> <ng-container *ngTemplateOutlet="myMenu.templateRef()"></ng-container>
</div> </div>
</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 { hasModifierKey } from "@angular/cdk/keycodes";
import { import {
Component, Component,
Input, Input,
OnInit, OnInit,
Output, Output,
ViewChild,
EventEmitter, EventEmitter,
HostBinding, HostBinding,
Optional, Optional,
@@ -14,6 +11,7 @@ import {
input, input,
model, model,
booleanAttribute, booleanAttribute,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { import {
ControlValueAccessor, ControlValueAccessor,
@@ -48,10 +46,10 @@ let nextId = 0;
* This component has been implemented to only support Multi-select list events * This component has been implemented to only support Multi-select list events
*/ */
export class MultiSelectComponent implements OnInit, BitFormFieldControl, ControlValueAccessor { 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) // 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 // Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
readonly removeSelectedItems = input(false); readonly removeSelectedItems = input(false);
readonly placeholder = model<string>(); readonly placeholder = model<string>();
@@ -61,10 +59,10 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
@Input({ transform: booleanAttribute }) disabled?: boolean; @Input({ transform: booleanAttribute }) disabled?: boolean;
// Internal tracking of selected items // Internal tracking of selected items
protected selectedItems: SelectItemView[]; protected selectedItems: SelectItemView[] | null = null;
// Default values for our implementation // Default values for our implementation
loadingText: string; loadingText?: string;
protected searchInputId = `search-input-${nextId++}`; protected searchInputId = `search-input-${nextId++}`;
@@ -95,13 +93,14 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
/** Function for customizing keyboard navigation */ /** Function for customizing keyboard navigation */
/** Needs to be arrow function to retain `this` scope. */ /** Needs to be arrow function to retain `this` scope. */
keyDown = (event: KeyboardEvent) => { 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; return false;
} }
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) { if (select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
this.selectedItems = []; this.selectedItems = [];
this.select.close(); select.close();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
@@ -183,11 +182,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
get ariaDescribedBy() { get ariaDescribedBy() {
return this._ariaDescribedBy; return this._ariaDescribedBy;
} }
set ariaDescribedBy(value: string) { set ariaDescribedBy(value: string | undefined) {
this._ariaDescribedBy = value; 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 */ /**Implemented as part of BitFormFieldControl */
get labelForId() { get labelForId() {
@@ -208,16 +207,17 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
set required(value: any) { set required(value: any) {
this._required = value != null && value !== false; this._required = value != null && value !== false;
} }
private _required: boolean; private _required?: boolean;
/**Implemented as part of BitFormFieldControl */ /**Implemented as part of BitFormFieldControl */
get hasError() { get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched; return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
} }
/**Implemented as part of BitFormFieldControl */ /**Implemented as part of BitFormFieldControl */
get error(): [string, any] { get error(): [string, any] {
const key = Object.keys(this.ngControl?.errors)[0]; const errors = this.ngControl?.errors ?? {};
return [key, this.ngControl?.errors[key]]; 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 { Directive, EventEmitter, Output, input } from "@angular/core";
import { RouterLink, RouterLinkActive } from "@angular/router"; import { RouterLink, RouterLinkActive } from "@angular/router";

View File

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

View File

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

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 { CommonModule } from "@angular/common";
import { Component, input } from "@angular/core"; import { Component, input } from "@angular/core";
import { RouterLinkActive, RouterLink } from "@angular/router"; import { RouterLinkActive, RouterLink } from "@angular/router";

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 { CdkTrapFocus } from "@angular/cdk/a11y";
import { CommonModule } from "@angular/common"; 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"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -21,15 +19,14 @@ export type SideNavVariant = "primary" | "secondary";
export class SideNavComponent { export class SideNavComponent {
readonly variant = input<SideNavVariant>("primary"); readonly variant = input<SideNavVariant>("primary");
@ViewChild("toggleButton", { read: ElementRef, static: true }) private readonly toggleButton = viewChild("toggleButton", { read: ElementRef });
private toggleButton: ElementRef<HTMLButtonElement>;
constructor(protected sideNavService: SideNavService) {} constructor(protected sideNavService: SideNavService) {}
protected handleKeyDown = (event: KeyboardEvent) => { protected handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") { if (event.key === "Escape") {
this.sideNavService.setClose(); this.sideNavService.setClose();
this.toggleButton?.nativeElement.focus(); this.toggleButton()?.nativeElement.focus();
return false; 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 { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal"; import { TemplatePortal } from "@angular/cdk/portal";
import { import {
@@ -27,12 +25,12 @@ import { PopoverComponent } from "./popover.component";
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit { export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
readonly popoverOpen = model(false); readonly popoverOpen = model(false);
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" }); readonly popover = input.required<PopoverComponent>({ alias: "bitPopoverTriggerFor" });
readonly position = input<string>(); readonly position = input<string>();
private overlayRef: OverlayRef; private overlayRef: OverlayRef | null = null;
private closedEventsSub: Subscription; private closedEventsSub: Subscription | null = null;
get positions() { get positions() {
if (!this.position()) { if (!this.position()) {
@@ -82,7 +80,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
this.popoverOpen.set(true); this.popoverOpen.set(true);
this.overlayRef = this.overlay.create(this.defaultPopoverConfig); this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
const templatePortal = new TemplatePortal(this.popover().templateRef, this.viewContainerRef); const templatePortal = new TemplatePortal(this.popover().templateRef(), this.viewContainerRef);
this.overlayRef.attach(templatePortal); this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe(() => { this.closedEventsSub = this.getClosedEvents().subscribe(() => {
@@ -91,6 +89,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
} }
private getClosedEvents(): Observable<any> { private getClosedEvents(): Observable<any> {
if (!this.overlayRef) {
throw new Error("Overlay reference is not available");
}
const detachments = this.overlayRef.detachments(); const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef const escKey = this.overlayRef
.keydownEvents() .keydownEvents()
@@ -102,7 +104,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
} }
private destroyPopover() { private destroyPopover() {
if (this.overlayRef == null || !this.popoverOpen()) { if (!this.overlayRef || !this.popoverOpen()) {
return; return;
} }
@@ -112,7 +114,9 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
private disposeAll() { private disposeAll() {
this.closedEventsSub?.unsubscribe(); this.closedEventsSub?.unsubscribe();
this.closedEventsSub = null;
this.overlayRef?.dispose(); this.overlayRef?.dispose();
this.overlayRef = null;
} }
ngAfterViewInit() { 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 { 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 { IconButtonModule } from "../icon-button/icon-button.module";
import { SharedModule } from "../shared/shared.module"; import { SharedModule } from "../shared/shared.module";
@@ -14,7 +12,7 @@ import { TypographyModule } from "../typography";
exportAs: "popoverComponent", exportAs: "popoverComponent",
}) })
export class PopoverComponent { export class PopoverComponent {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>; readonly templateRef = viewChild.required(TemplateRef);
readonly title = input(""); readonly title = input("");
@Output() closed = new EventEmitter(); @Output() closed = new EventEmitter();
} }

View File

@@ -1,4 +1,4 @@
@if (label) { @if (label()) {
<fieldset> <fieldset>
<legend class="tw-mb-1 tw-block tw-text-sm tw-font-semibold tw-text-main"> <legend class="tw-mb-1 tw-block tw-text-sm tw-font-semibold tw-text-main">
<ng-content select="bit-label"></ng-content> <ng-content select="bit-label"></ng-content>
@@ -10,7 +10,7 @@
</fieldset> </fieldset>
} }
@if (!label) { @if (!label()) {
<ng-container *ngTemplateOutlet="content"></ng-container> <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 { 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 { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
@@ -22,14 +20,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
selected: unknown; selected: unknown;
disabled = false; disabled = false;
// TODO: Skipped for signal migration because: get name() {
// Accessor inputs cannot be migrated as they are too complex. return this.ngControl?.name?.toString();
private _name?: string;
@Input() get name() {
return this._name ?? this.ngControl?.name?.toString();
}
set name(value: string) {
this._name = value;
} }
readonly block = input(false); readonly block = input(false);
@@ -38,7 +30,7 @@ export class RadioGroupComponent implements ControlValueAccessor {
readonly id = input(`bit-radio-group-${nextId++}`); readonly id = input(`bit-radio-group-${nextId++}`);
@HostBinding("class") classList = ["tw-block", "tw-mb-4"]; @HostBinding("class") classList = ["tw-block", "tw-mb-4"];
@ContentChild(BitLabel) protected label: BitLabel; protected readonly label = contentChild(BitLabel);
constructor(@Optional() @Self() private ngControl?: NgControl) { constructor(@Optional() @Self() private ngControl?: NgControl) {
if (ngControl != null) { if (ngControl != null) {
@@ -51,8 +43,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
} }
// ControlValueAccessor // ControlValueAccessor
onChange: (value: unknown) => void; onChange?: (value: unknown) => void;
onTouched: () => void; onTouched?: () => void;
writeValue(value: boolean): void { writeValue(value: boolean): void {
this.selected = value; this.selected = value;
@@ -72,10 +64,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
onInputChange(value: unknown) { onInputChange(value: unknown) {
this.selected = value; this.selected = value;
this.onChange(this.selected); this.onChange?.(this.selected);
} }
onBlur() { 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 { Component, HostBinding, input, Input, Optional, Self } from "@angular/core";
import { NgControl, Validators } from "@angular/forms"; import { NgControl, Validators } from "@angular/forms";
@@ -86,7 +84,7 @@ export class RadioInputComponent implements BitFormControlAbstraction {
set disabled(value: any) { set disabled(value: any) {
this._disabled = value != null && value !== false; this._disabled = value != null && value !== false;
} }
private _disabled: boolean; private _disabled?: boolean;
// TODO: Skipped for signal migration because: // TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex. // Accessor inputs cannot be migrated as they are too complex.
@@ -99,14 +97,15 @@ export class RadioInputComponent implements BitFormControlAbstraction {
set required(value: any) { set required(value: any) {
this._required = value != null && value !== false; this._required = value != null && value !== false;
} }
private _required: boolean; private _required?: boolean;
get hasError() { get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched; return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
} }
get error(): [string, any] { get error(): [string, any] {
const key = Object.keys(this.ngControl.errors)[0]; const errors = this.ngControl?.errors ?? {};
return [key, this.ngControl.errors[key]]; 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 { 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 { import {
ControlValueAccessor, ControlValueAccessor,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@@ -37,13 +35,13 @@ let nextId = 0;
imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass], imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass],
}) })
export class SearchComponent implements ControlValueAccessor, FocusableElement { export class SearchComponent implements ControlValueAccessor, FocusableElement {
private notifyOnChange: (v: string) => void; private notifyOnChange?: (v: string) => void;
private notifyOnTouch: () => void; private notifyOnTouch?: () => void;
@ViewChild("input") private input: ElementRef<HTMLInputElement>; private readonly input = viewChild<ElementRef<HTMLInputElement>>("input");
protected id = `search-id-${nextId++}`; protected id = `search-id-${nextId++}`;
protected searchText: string; protected searchText?: string;
// Use `type="text"` for Safari to improve rendering performance // Use `type="text"` for Safari to improve rendering performance
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const); protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
@@ -57,7 +55,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
readonly autocomplete = input<string>(); readonly autocomplete = input<string>();
getFocusTarget() { getFocusTarget() {
return this.input?.nativeElement; return this.input()?.nativeElement;
} }
onChange(searchText: string) { 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 { Component, booleanAttribute, input } from "@angular/core";
import { MappedOptionComponent } from "./option"; 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 { hasModifierKey } from "@angular/cdk/keycodes";
import { import {
Component, Component,
@@ -10,7 +7,6 @@ import {
Optional, Optional,
QueryList, QueryList,
Self, Self,
ViewChild,
Output, Output,
EventEmitter, EventEmitter,
input, input,
@@ -18,6 +14,7 @@ import {
computed, computed,
model, model,
signal, signal,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { import {
ControlValueAccessor, ControlValueAccessor,
@@ -47,7 +44,7 @@ let nextId = 0;
}, },
}) })
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor { 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` */ /** Optional: Options can be provided using an array input or using `bit-option` */
readonly items = model<Option<T>[] | undefined>(); readonly items = model<Option<T>[] | undefined>();
@@ -55,13 +52,13 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
readonly placeholder = input(this.i18nService.t("selectPlaceholder")); readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
@Output() closed = new EventEmitter(); @Output() closed = new EventEmitter();
protected selectedValue = signal<T>(undefined); protected selectedValue = signal<T | undefined | null>(undefined);
selectedOption: Signal<Option<T>> = computed(() => selectedOption: Signal<Option<T> | null | undefined> = computed(() =>
this.findSelectedOption(this.items(), this.selectedValue()), this.findSelectedOption(this.items(), this.selectedValue()),
); );
protected searchInputId = `bit-select-search-input-${nextId++}`; protected searchInputId = `bit-select-search-input-${nextId++}`;
private notifyOnChange?: (value: T) => void; private notifyOnChange?: (value?: T | null) => void;
private notifyOnTouched?: () => void; private notifyOnTouched?: () => void;
constructor( constructor(
@@ -104,7 +101,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
set disabled(value: any) { set disabled(value: any) {
this._disabled = value != null && value !== false; this._disabled = value != null && value !== false;
} }
private _disabled: boolean; private _disabled?: boolean;
/**Implemented as part of NG_VALUE_ACCESSOR */ /**Implemented as part of NG_VALUE_ACCESSOR */
writeValue(obj: T): void { writeValue(obj: T): void {
@@ -112,7 +109,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
} }
/**Implemented as part of NG_VALUE_ACCESSOR */ /**Implemented as part of NG_VALUE_ACCESSOR */
registerOnChange(fn: (value: T) => void): void { registerOnChange(fn: (value?: T | null) => void): void {
this.notifyOnChange = fn; this.notifyOnChange = fn;
} }
@@ -151,11 +148,11 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
get ariaDescribedBy() { get ariaDescribedBy() {
return this._ariaDescribedBy; return this._ariaDescribedBy;
} }
set ariaDescribedBy(value: string) { set ariaDescribedBy(value: string | undefined) {
this._ariaDescribedBy = value; 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 */ /**Implemented as part of BitFormFieldControl */
get labelForId() { get labelForId() {
@@ -176,20 +173,24 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
set required(value: any) { set required(value: any) {
this._required = value != null && value !== false; this._required = value != null && value !== false;
} }
private _required: boolean; private _required?: boolean;
/**Implemented as part of BitFormFieldControl */ /**Implemented as part of BitFormFieldControl */
get hasError() { get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched; return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
} }
/**Implemented as part of BitFormFieldControl */ /**Implemented as part of BitFormFieldControl */
get error(): [string, any] { get error(): [string, any] {
const key = Object.keys(this.ngControl?.errors)[0]; const errors = this.ngControl?.errors ?? {};
return [key, this.ngControl?.errors[key]]; 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); 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. * Needs to be arrow function to retain `this` scope.
*/ */
protected onKeyDown = (event: KeyboardEvent) => { protected onKeyDown = (event: KeyboardEvent) => {
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) { if (this.select().isOpen && event.key === "Escape" && !hasModifierKey(event)) {
event.stopPropagation(); 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"; import { ModelSignal } from "@angular/core";
// @ts-strict-ignore
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled"; export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
export type ButtonSize = "default" | "small"; export type ButtonSize = "default" | "small";
export abstract class ButtonLikeAbstraction { export abstract class ButtonLikeAbstraction {
loading: ModelSignal<boolean>; abstract loading: ModelSignal<boolean>;
disabled: 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"; import { Observable } from "rxjs";
/** Global config for the Bitwarden Design System */ /** 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. * 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. * Interface for implementing focusable components.
* *
* Used by the `AutofocusDirective`. * Used by the `AutofocusDirective`.
*/ */
export abstract class FocusableElement { 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 { coerceBooleanProperty } from "@angular/cdk/coercion";
import { NgClass } from "@angular/common"; import { NgClass } from "@angular/common";
import { Component, HostBinding, OnInit, input } from "@angular/core"; 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 * 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, { readonly default = input(false, {
transform: (value: SortDirection | boolean | "") => { transform: (value: SortDirection | boolean | "") => {
@@ -63,7 +61,7 @@ export class SortableComponent implements OnInit {
if (!this.isActive) { if (!this.isActive) {
return undefined; return undefined;
} }
return this.sort.direction === "asc" ? "ascending" : "descending"; return this.sort?.direction === "asc" ? "ascending" : "descending";
} }
protected setActive() { 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 { _isNumberValue } from "@angular/cdk/coercion";
import { DataSource } from "@angular/cdk/collections"; import { DataSource } from "@angular/cdk/collections";
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs"; 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> { export class TableDataSource<T> extends DataSource<T> {
private readonly _data: BehaviorSubject<T[]>; private readonly _data: BehaviorSubject<T[]>;
private readonly _sort: BehaviorSubject<Sort>; 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 readonly _renderData = new BehaviorSubject<T[]>([]);
private _renderChangesSubscription: Subscription | null = null; 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 * For example, a 'selectAll()' function would likely want to select the set of filtered data
* shown to the user rather than all the data. * shown to the user rather than all the data.
*/ */
filteredData: T[]; filteredData?: T[];
constructor() { constructor() {
super(); super();
this._data = new BehaviorSubject([]); this._data = new BehaviorSubject([] as T[]);
this._sort = new BehaviorSubject({ direction: "asc" }); this._sort = new BehaviorSubject<Sort>({ direction: "asc" } as Sort);
} }
get data() { get data() {

View File

@@ -13,7 +13,9 @@
</thead> </thead>
<tbody> <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> <ng-container
*ngTemplateOutlet="rowDef().template; context: { $implicit: r }"
></ng-container>
</tr> </tr>
</tbody> </tbody>
</table> </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 { import {
CdkVirtualScrollViewport, CdkVirtualScrollViewport,
CdkFixedSizeVirtualScroll, CdkFixedSizeVirtualScroll,
@@ -9,7 +7,6 @@ import { CommonModule } from "@angular/common";
import { import {
AfterContentChecked, AfterContentChecked,
Component, Component,
ContentChild,
OnDestroy, OnDestroy,
TemplateRef, TemplateRef,
Directive, Directive,
@@ -18,6 +15,7 @@ import {
ElementRef, ElementRef,
TrackByFunction, TrackByFunction,
input, input,
contentChild,
} from "@angular/core"; } from "@angular/core";
import { ScrollLayoutDirective } from "../layout"; import { ScrollLayoutDirective } from "../layout";
@@ -69,7 +67,7 @@ export class TableScrollComponent
/** Optional trackBy function. */ /** Optional trackBy function. */
readonly trackBy = input<TrackByFunction<any> | undefined>(); readonly trackBy = input<TrackByFunction<any> | undefined>();
@ContentChild(BitRowDef) protected rowDef: BitRowDef; protected readonly rowDef = contentChild(BitRowDef);
/** /**
* Height of the thead element (in pixels). * Height of the thead element (in pixels).
@@ -81,7 +79,7 @@ export class TableScrollComponent
/** /**
* Observer for table header, applies padding on resize. * Observer for table header, applies padding on resize.
*/ */
private headerObserver: ResizeObserver; private headerObserver?: ResizeObserver;
constructor( constructor(
private zone: NgZone, private zone: NgZone,

View File

@@ -6,7 +6,7 @@
</thead> </thead>
<tbody> <tbody>
<ng-container <ng-container
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows$ }" *ngTemplateOutlet="templateVariable().template; context: { $implicit: rows$ }"
></ng-container> ></ng-container>
</tbody> </tbody>
</table> </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 { isDataSource } from "@angular/cdk/collections";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { import {
AfterContentChecked, AfterContentChecked,
Component, Component,
ContentChild,
Directive, Directive,
OnDestroy, OnDestroy,
TemplateRef, TemplateRef,
input, input,
contentChild,
} from "@angular/core"; } from "@angular/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
@@ -32,9 +30,9 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
readonly dataSource = input<TableDataSource<any>>(); readonly dataSource = input<TableDataSource<any>>();
readonly layout = input<"auto" | "fixed">("auto"); 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; 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 { FocusableOption } from "@angular/cdk/a11y";
import { Directive, ElementRef, HostBinding, Input, input } from "@angular/core"; import { Directive, ElementRef, HostBinding, Input, input } from "@angular/core";
@@ -15,7 +13,7 @@ export class TabListItemDirective implements FocusableOption {
// TODO: Skipped for signal migration because: // TODO: Skipped for signal migration because:
// This input overrides a field from a superclass, while the superclass field // This input overrides a field from a superclass, while the superclass field
// is not migrated. // is not migrated.
@Input() disabled: boolean; @Input() disabled = false;
@HostBinding("attr.disabled") @HostBinding("attr.disabled")
get disabledAttr() { 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 { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal";
import { Component, effect, HostBinding, input } from "@angular/core"; import { Component, effect, HostBinding, input } from "@angular/core";
@@ -9,7 +7,7 @@ import { Component, effect, HostBinding, input } from "@angular/core";
imports: [CdkPortalOutlet], imports: [CdkPortalOutlet],
}) })
export class TabBodyComponent { export class TabBodyComponent {
private _firstRender: boolean; private _firstRender = false;
readonly content = input<TemplatePortal>(); readonly content = input<TemplatePortal>();
readonly preserveContent = input(false); readonly preserveContent = input(false);

View File

@@ -5,7 +5,7 @@
[attr.aria-label]="label()" [attr.aria-label]="label()"
(keydown)="keyManager.onKeydown($event)" (keydown)="keyManager.onKeydown($event)"
> >
@for (tab of tabs; track tab; let i = $index) { @for (tab of tabs(); track tab; let i = $index) {
<button <button
bitTabListItem bitTabListItem
type="button" type="button"
@@ -30,7 +30,7 @@
</div> </div>
</bit-tab-header> </bit-tab-header>
<div class="tw-px-6 tw-pt-5"> <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 <bit-tab-body
role="tabpanel" role="tabpanel"
[id]="getTabContentId(i)" [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 { FocusKeyManager } from "@angular/cdk/a11y";
import { coerceNumberProperty } from "@angular/cdk/coercion"; import { coerceNumberProperty } from "@angular/cdk/coercion";
import { NgTemplateOutlet } from "@angular/common"; import { NgTemplateOutlet } from "@angular/common";
import { import {
AfterContentChecked, AfterContentChecked,
AfterContentInit,
AfterViewInit, AfterViewInit,
Component, Component,
ContentChildren,
EventEmitter, EventEmitter,
Input, Input,
Output, Output,
QueryList, contentChild,
ViewChildren, contentChildren,
effect,
input, input,
viewChildren,
inject, inject,
DestroyRef, DestroyRef,
} from "@angular/core"; } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { TabHeaderComponent } from "../shared/tab-header.component"; import { TabHeaderComponent } from "../shared/tab-header.component";
import { TabListContainerDirective } from "../shared/tab-list-container.directive"; import { TabListContainerDirective } from "../shared/tab-list-container.directive";
@@ -41,7 +38,7 @@ let nextId = 0;
TabBodyComponent, TabBodyComponent,
], ],
}) })
export class TabGroupComponent implements AfterContentChecked, AfterContentInit, AfterViewInit { export class TabGroupComponent implements AfterContentChecked, AfterViewInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly _groupId: number; private readonly _groupId: number;
@@ -59,8 +56,11 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
*/ */
readonly preserveContent = input(false); readonly preserveContent = input(false);
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>; /** Error if no `TabComponent` is supplied. (`contentChildren`, used to query for all the tabs, doesn't support `required`) */
@ViewChildren(TabListItemDirective) tabLabels: QueryList<TabListItemDirective>; private _tab = contentChild.required(TabComponent);
protected tabs = contentChildren(TabComponent);
readonly tabLabels = viewChildren(TabListItemDirective);
/** The index of the active tab. */ /** The index of the active tab. */
// TODO: Skipped for signal migration because: // TODO: Skipped for signal migration because:
@@ -85,78 +85,18 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
* Focus key manager for keeping tab controls accessible. * Focus key manager for keeping tab controls accessible.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
*/ */
keyManager: FocusKeyManager<TabListItemDirective>; keyManager?: FocusKeyManager<TabListItemDirective>;
constructor() { constructor() {
this._groupId = nextId++; this._groupId = nextId++;
}
protected getTabContentId(id: number): string { effect(() => {
return `bit-tab-content-${this._groupId}-${id}`; const indexToSelect = this._clampTabIndex(this._indexToSelect ?? 0);
}
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);
// If the selected tab didn't explicitly change, keep the previously // If the selected tab didn't explicitly change, keep the previously
// selected tab selected/active // selected tab selected/active
if (indexToSelect === this._selectedIndex) { if (indexToSelect === this._selectedIndex) {
const tabs = this.tabs.toArray(); const tabs = this.tabs();
let selectedTab: TabComponent | undefined; let selectedTab: TabComponent | undefined;
for (let i = 0; i < tabs.length; i++) { 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 { 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 * 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 { TemplatePortal } from "@angular/cdk/portal";
import { import {
Component, Component,
ContentChild, ContentChild,
OnInit, OnInit,
TemplateRef, TemplateRef,
ViewChild,
ViewContainerRef, ViewContainerRef,
input, input,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { TabLabelDirective } from "./tab-label.directive"; import { TabLabelDirective } from "./tab-label.directive";
@@ -34,8 +32,8 @@ export class TabComponent implements OnInit {
*/ */
readonly contentTabIndex = input<number | undefined>(); readonly contentTabIndex = input<number | undefined>();
@ViewChild(TemplateRef, { static: true }) implicitContent: TemplateRef<unknown>; readonly implicitContent = viewChild.required(TemplateRef);
@ContentChild(TabLabelDirective) templateLabel: TabLabelDirective; @ContentChild(TabLabelDirective) templateLabel?: TabLabelDirective;
private _contentPortal: TemplatePortal | null = null; private _contentPortal: TemplatePortal | null = null;
@@ -43,11 +41,11 @@ export class TabComponent implements OnInit {
return this._contentPortal; return this._contentPortal;
} }
isActive: boolean; isActive?: boolean;
constructor(private _viewContainerRef: ViewContainerRef) {} constructor(private _viewContainerRef: ViewContainerRef) {}
ngOnInit(): void { 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 { FocusableOption } from "@angular/cdk/a11y";
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
DestroyRef,
HostListener, HostListener,
Input, Input,
ViewChild,
input,
inject, inject,
DestroyRef, input,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router"; import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router";
@@ -24,9 +22,10 @@ import { TabNavBarComponent } from "./tab-nav-bar.component";
imports: [TabListItemDirective, RouterModule], imports: [TabListItemDirective, RouterModule],
}) })
export class TabLinkComponent implements FocusableOption, AfterViewInit { export class TabLinkComponent implements FocusableOption, AfterViewInit {
private readonly destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
@ViewChild(TabListItemDirective) tabItem: TabListItemDirective;
@ViewChild("rla") routerLinkActive: RouterLinkActive; readonly tabItem = viewChild.required(TabListItemDirective);
readonly routerLinkActive = viewChild.required<RouterLinkActive>("rla");
readonly routerLinkMatchOptions: IsActiveMatchOptions = { readonly routerLinkMatchOptions: IsActiveMatchOptions = {
queryParams: "ignored", queryParams: "ignored",
@@ -43,25 +42,25 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit {
@HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) { @HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) {
if (event.code === "Space") { if (event.code === "Space") {
this.tabItem.click(); this.tabItem().click();
} }
} }
get active() { get active() {
return this.routerLinkActive?.isActive ?? false; return this.routerLinkActive()?.isActive ?? false;
} }
constructor(private _tabNavBar: TabNavBarComponent) {} constructor(private _tabNavBar: TabNavBarComponent) {}
focus(): void { focus(): void {
this.tabItem.focus(); this.tabItem().focus();
} }
ngAfterViewInit() { ngAfterViewInit() {
// The active state of tab links are tracked via the routerLinkActive directive // 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 // We need to watch for changes to tell the parent nav group when the tab is active
this.routerLinkActive.isActiveChange this.routerLinkActive()
.pipe(takeUntilDestroyed(this.destroyRef)) .isActiveChange.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((_) => this._tabNavBar.updateActiveLink()); .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 { FocusKeyManager } from "@angular/cdk/a11y";
import { import { AfterContentInit, Component, forwardRef, input, contentChildren } from "@angular/core";
AfterContentInit,
Component,
ContentChildren,
forwardRef,
QueryList,
input,
} from "@angular/core";
import { TabHeaderComponent } from "../shared/tab-header.component"; import { TabHeaderComponent } from "../shared/tab-header.component";
import { TabListContainerDirective } from "../shared/tab-list-container.directive"; import { TabListContainerDirective } from "../shared/tab-list-container.directive";
@@ -24,17 +15,17 @@ import { TabLinkComponent } from "./tab-link.component";
imports: [TabHeaderComponent, TabListContainerDirective], imports: [TabHeaderComponent, TabListContainerDirective],
}) })
export class TabNavBarComponent implements AfterContentInit { export class TabNavBarComponent implements AfterContentInit {
@ContentChildren(forwardRef(() => TabLinkComponent)) tabLabels: QueryList<TabLinkComponent>; readonly tabLabels = contentChildren(forwardRef(() => TabLinkComponent));
readonly label = input(""); readonly label = input("");
/** /**
* Focus key manager for keeping tab controls accessible. * Focus key manager for keeping tab controls accessible.
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
*/ */
keyManager: FocusKeyManager<TabLinkComponent>; keyManager?: FocusKeyManager<TabLinkComponent>;
ngAfterContentInit(): void { ngAfterContentInit(): void {
this.keyManager = new FocusKeyManager(this.tabLabels) this.keyManager = new FocusKeyManager(this.tabLabels())
.withHorizontalOrientation("ltr") .withHorizontalOrientation("ltr")
.withWrap() .withWrap()
.withHomeAndEnd(); .withHomeAndEnd();
@@ -42,10 +33,10 @@ export class TabNavBarComponent implements AfterContentInit {
updateActiveLink() { updateActiveLink() {
// Keep the keyManager in sync with active tabs // 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++) { for (let i = 0; i < items.length; i++) {
if (items[i].active) { 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"; import { ToastContainerDirective, ToastrService } from "ngx-toastr";
@Component({ @Component({
@@ -7,12 +7,11 @@ import { ToastContainerDirective, ToastrService } from "ngx-toastr";
imports: [ToastContainerDirective], imports: [ToastContainerDirective],
}) })
export class ToastContainerComponent implements OnInit { export class ToastContainerComponent implements OnInit {
@ViewChild(ToastContainerDirective, { static: true }) readonly toastContainer = viewChild(ToastContainerDirective);
toastContainer?: ToastContainerDirective;
constructor(private toastrService: ToastrService) {} constructor(private toastrService: ToastrService) {}
ngOnInit(): void { 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 { Injectable } from "@angular/core";
import { IndividualConfig, ToastrService } from "ngx-toastr"; import { IndividualConfig, ToastrService } from "ngx-toastr";
@@ -36,7 +34,7 @@ export class ToastService {
: calculateToastTimeout(options.message), : 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 { NgClass } from "@angular/common";
import { import {
AfterContentChecked, AfterContentChecked,
@@ -8,8 +6,8 @@ import {
ElementRef, ElementRef,
HostBinding, HostBinding,
signal, signal,
ViewChild,
input, input,
viewChild,
} from "@angular/core"; } from "@angular/core";
import { ToggleGroupComponent } from "./toggle-group.component"; import { ToggleGroupComponent } from "./toggle-group.component";
@@ -24,9 +22,9 @@ let nextId = 0;
export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewInit { export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewInit {
id = nextId++; id = nextId++;
readonly value = input<TValue>(); readonly value = input.required<TValue>();
@ViewChild("labelContent") labelContent: ElementRef<HTMLSpanElement>; readonly labelContent = viewChild<ElementRef<HTMLSpanElement>>("labelContent");
@ViewChild("bitBadgeContainer") bitBadgeContainer: ElementRef<HTMLSpanElement>; readonly bitBadgeContainer = viewChild<ElementRef<HTMLSpanElement>>("bitBadgeContainer");
constructor(private groupComponent: ToggleGroupComponent<TValue>) {} 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"]; @HostBinding("class") classList = ["tw-group/toggle", "tw-flex", "tw-min-w-16"];
protected bitBadgeContainerHasChidlren = signal(false); protected bitBadgeContainerHasChidlren = signal(false);
protected labelTitle = signal<string>(null); protected labelTitle = signal<string | null>(null);
get name() { get name() {
return this.groupComponent.name; return this.groupComponent.name;
@@ -100,12 +98,12 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
ngAfterContentChecked() { ngAfterContentChecked() {
this.bitBadgeContainerHasChidlren.set( this.bitBadgeContainerHasChidlren.set(
this.bitBadgeContainer?.nativeElement.childElementCount > 0, (this.bitBadgeContainer()?.nativeElement.childElementCount ?? 0) > 0,
); );
} }
ngAfterViewInit() { ngAfterViewInit() {
const labelText = this.labelContent?.nativeElement.innerText; const labelText = this.labelContent()?.nativeElement.innerText;
if (labelText) { if (labelText) {
this.labelTitle.set(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"; import { booleanAttribute, Directive, HostBinding, input } from "@angular/core";
type TypographyType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "body1" | "body2" | "helper"; type TypographyType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "body1" | "body2" | "helper";
@@ -32,7 +30,7 @@ const margins: Record<TypographyType, string[]> = {
selector: "[bitTypography]", selector: "[bitTypography]",
}) })
export class TypographyDirective { export class TypographyDirective {
readonly bitTypography = input<TypographyType>(); readonly bitTypography = input.required<TypographyType>();
readonly noMargin = input(false, { transform: booleanAttribute }); 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"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
export class I18nMockService implements I18nService { export class I18nMockService implements Pick<I18nService, "t" | "translate"> {
userSetLocale$: Observable<string | undefined>; constructor(
locale$: Observable<string>; private lookupTable: Record<string, string | ((...args: (string | undefined)[]) => string)>,
supportedTranslationLocales: string[]; ) {}
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
constructor(private lookupTable: Record<string, string | ((...args: string[]) => string)>) {}
t(id: string, p1?: string, p2?: string, p3?: string) { t(id: string, p1?: string, p2?: string, p3?: string) {
let value = this.lookupTable[id]; let value = this.lookupTable[id];

View File

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