mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-15847] libs/components strict migration (#15738)
This PR migrates `libs/components` to use strict TypeScript. - Remove `@ts-strict-ignore` from each file in `libs/components` and resolved any new compilation errors - Converted ViewChild and ContentChild decorators to use the new signal-based queries using the [Angular signal queries migration](https://angular.dev/reference/migrations/signal-queries) - Made view/content children `required` where appropriate, eliminating the need for additional null checking. This helped simplify the strict migration. --- Co-authored-by: Vicki League <vleague@bitwarden.com>
This commit is contained in:
@@ -1,39 +1,24 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
|
||||
import { Directive, effect, ElementRef, input, Renderer2 } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[appA11yTitle]",
|
||||
})
|
||||
export class A11yTitleDirective implements OnInit {
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input() set appA11yTitle(title: string) {
|
||||
this.title = title;
|
||||
this.setAttributes();
|
||||
}
|
||||
|
||||
private title: string;
|
||||
private originalTitle: string | null;
|
||||
private originalAriaLabel: string | null;
|
||||
export class A11yTitleDirective {
|
||||
title = input.required<string>({ alias: "appA11yTitle" });
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.originalTitle = this.el.nativeElement.getAttribute("title");
|
||||
this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
|
||||
this.setAttributes();
|
||||
}
|
||||
|
||||
private setAttributes() {
|
||||
if (this.originalTitle === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
|
||||
}
|
||||
if (this.originalAriaLabel === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
|
||||
}
|
||||
) {
|
||||
const originalTitle = this.el.nativeElement.getAttribute("title");
|
||||
const originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
|
||||
effect(() => {
|
||||
if (originalTitle === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "title", this.title());
|
||||
}
|
||||
if (originalAriaLabel === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, OnInit, inject, DestroyRef } from "@angular/core";
|
||||
import { ChangeDetectorRef, Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
||||
import { filter, switchMap, tap } from "rxjs";
|
||||
import { Subject, filter, of, switchMap, tap } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -53,13 +51,15 @@ export interface AnonLayoutWrapperData {
|
||||
imports: [AnonLayoutComponent, RouterModule],
|
||||
})
|
||||
export class AnonLayoutWrapperComponent implements OnInit {
|
||||
protected pageTitle: string;
|
||||
protected pageSubtitle: string;
|
||||
protected pageIcon: Icon;
|
||||
protected showReadonlyHostname: boolean;
|
||||
protected maxWidth: AnonLayoutMaxWidth;
|
||||
protected hideCardWrapper: boolean;
|
||||
protected hideIcon: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected pageTitle?: string | null;
|
||||
protected pageSubtitle?: string | null;
|
||||
protected pageIcon?: Icon | null;
|
||||
protected showReadonlyHostname?: boolean | null;
|
||||
protected maxWidth?: AnonLayoutMaxWidth | null;
|
||||
protected hideCardWrapper?: boolean | null;
|
||||
protected hideIcon?: boolean | null;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@@ -85,7 +85,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
// reset page data on page changes
|
||||
tap(() => this.resetPageData()),
|
||||
switchMap(() => this.route.firstChild?.data || null),
|
||||
switchMap(() => this.route.firstChild?.data || of(null)),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((firstChildRouteData: Data | null) => {
|
||||
@@ -93,7 +93,7 @@ export class AnonLayoutWrapperComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData: Data | null) {
|
||||
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData?: Data | null) {
|
||||
if (!firstChildRouteData) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
Component,
|
||||
@@ -56,8 +54,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
protected logo = BitwardenLogo;
|
||||
protected year: string;
|
||||
protected clientType: ClientType;
|
||||
protected hostname: string;
|
||||
protected version: string;
|
||||
protected hostname?: string;
|
||||
protected version?: string;
|
||||
|
||||
protected hideYearAndVersion = false;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, HostListener, model, Optional, inject, DestroyRef } from "@angular/core";
|
||||
import { DestroyRef, Directive, HostListener, inject, model, Optional } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, finalize, tap } from "rxjs";
|
||||
|
||||
@@ -38,7 +36,7 @@ export class BitActionDirective {
|
||||
|
||||
disabled = false;
|
||||
|
||||
readonly handler = model<FunctionReturningAwaitable>(undefined, { alias: "bitAction" });
|
||||
readonly handler = model.required<FunctionReturningAwaitable>({ alias: "bitAction" });
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit, Optional, input, inject, DestroyRef } from "@angular/core";
|
||||
import { DestroyRef, Directive, OnInit, Optional, inject, input } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormGroupDirective } from "@angular/forms";
|
||||
import { BehaviorSubject, catchError, filter, of, switchMap } from "rxjs";
|
||||
@@ -22,7 +20,7 @@ export class BitSubmitDirective implements OnInit {
|
||||
private _loading$ = new BehaviorSubject<boolean>(false);
|
||||
private _disabled$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
readonly handler = input<FunctionReturningAwaitable>(undefined, { alias: "bitSubmit" });
|
||||
readonly handler = input.required<FunctionReturningAwaitable>({ alias: "bitSubmit" });
|
||||
|
||||
readonly allowDisabledFormSubmit = input<boolean>(false);
|
||||
|
||||
@@ -63,7 +61,7 @@ export class BitSubmitDirective implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formGroupDirective.statusChanges
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
?.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((c) => {
|
||||
if (this.allowDisabledFormSubmit()) {
|
||||
this._disabled$.next(false);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, Optional, input } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, OnChanges, input } from "@angular/core";
|
||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||
@@ -41,7 +39,7 @@ export class AvatarComponent implements OnChanges {
|
||||
private svgFontSize = 20;
|
||||
private svgFontWeight = 300;
|
||||
private svgSize = 48;
|
||||
src: SafeResourceUrl;
|
||||
src?: SafeResourceUrl;
|
||||
|
||||
constructor(public sanitizer: DomSanitizer) {}
|
||||
|
||||
@@ -56,8 +54,14 @@ export class AvatarComponent implements OnChanges {
|
||||
}
|
||||
|
||||
private generate() {
|
||||
let chars: string = null;
|
||||
const upperCaseText = this.text()?.toUpperCase() ?? "";
|
||||
const color = this.color();
|
||||
const text = this.text();
|
||||
const id = this.id();
|
||||
if (!text && !color && !id) {
|
||||
throw new Error("Must supply `text`, `color`, or `id` input.");
|
||||
}
|
||||
let chars: string | null = null;
|
||||
const upperCaseText = text?.toUpperCase() ?? "";
|
||||
|
||||
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
||||
|
||||
@@ -66,18 +70,17 @@ export class AvatarComponent implements OnChanges {
|
||||
}
|
||||
|
||||
// If the chars contain an emoji, only show it.
|
||||
if (chars.match(Utils.regexpEmojiPresentation)) {
|
||||
chars = chars.match(Utils.regexpEmojiPresentation)[0];
|
||||
const emojiMatch = chars.match(Utils.regexpEmojiPresentation);
|
||||
if (emojiMatch) {
|
||||
chars = emojiMatch[0];
|
||||
}
|
||||
|
||||
let svg: HTMLElement;
|
||||
let hexColor = this.color();
|
||||
|
||||
const id = this.id();
|
||||
if (!Utils.isNullOrWhitespace(this.color())) {
|
||||
let hexColor = color ?? "";
|
||||
if (!Utils.isNullOrWhitespace(hexColor)) {
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else if (!Utils.isNullOrWhitespace(id)) {
|
||||
hexColor = Utils.stringToColor(id.toString());
|
||||
} else if (!Utils.isNullOrWhitespace(id ?? "")) {
|
||||
hexColor = Utils.stringToColor(id!.toString());
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else {
|
||||
hexColor = Utils.stringToColor(upperCaseText);
|
||||
@@ -95,7 +98,7 @@ export class AvatarComponent implements OnChanges {
|
||||
);
|
||||
}
|
||||
|
||||
private getFirstLetters(data: string, count: number): string {
|
||||
private getFirstLetters(data: string, count: number): string | null {
|
||||
const parts = data.split(" ");
|
||||
if (parts.length > 1) {
|
||||
let text = "";
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
|
||||
import { Component, EventEmitter, Output, TemplateRef, input, viewChild } from "@angular/core";
|
||||
import { QueryParamsHandling } from "@angular/router";
|
||||
|
||||
@Component({
|
||||
@@ -20,7 +17,7 @@ export class BreadcrumbComponent {
|
||||
@Output()
|
||||
click = new EventEmitter();
|
||||
|
||||
@ViewChild(TemplateRef, { static: true }) content: TemplateRef<unknown>;
|
||||
readonly content = viewChild(TemplateRef);
|
||||
|
||||
onClick(args: unknown) {
|
||||
this.click.next(args);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[queryParams]="breadcrumb.queryParams()"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
@@ -18,7 +18,7 @@
|
||||
class="tw-my-2 tw-inline-block"
|
||||
(click)="breadcrumb.onClick($event)"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
|
||||
</button>
|
||||
}
|
||||
@if (!last) {
|
||||
@@ -46,11 +46,11 @@
|
||||
[queryParams]="breadcrumb.queryParams()"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
|
||||
</a>
|
||||
} @else {
|
||||
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@
|
||||
[queryParams]="breadcrumb.queryParams()"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
@@ -76,7 +76,7 @@
|
||||
class="tw-my-2 tw-inline-block"
|
||||
(click)="breadcrumb.onClick($event)"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content()"></ng-container>
|
||||
</button>
|
||||
}
|
||||
@if (!last) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<aside
|
||||
class="tw-mb-4 tw-box-border tw-rounded-lg tw-bg-background tw-ps-3 tw-pe-3 tw-py-2 tw-leading-5 tw-text-main"
|
||||
[ngClass]="calloutClass"
|
||||
[ngClass]="calloutClass()"
|
||||
[attr.aria-labelledby]="titleId"
|
||||
>
|
||||
@if (titleComputed(); as title) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, computed, input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -50,11 +48,11 @@ export class CalloutComponent {
|
||||
return title;
|
||||
});
|
||||
|
||||
protected titleId = `bit-callout-title-${nextId++}`;
|
||||
protected readonly titleId = `bit-callout-title-${nextId++}`;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
get calloutClass() {
|
||||
protected readonly calloutClass = computed(() => {
|
||||
switch (this.type()) {
|
||||
case "danger":
|
||||
return "tw-bg-danger-100";
|
||||
@@ -65,5 +63,5 @@ export class CalloutComponent {
|
||||
case "warning":
|
||||
return "tw-bg-warning-100";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, HostBinding, Input, Optional, Self } from "@angular/core";
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
@@ -114,7 +112,7 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
||||
set disabled(value: any) {
|
||||
this._disabled = value != null && value !== false;
|
||||
}
|
||||
private _disabled: boolean;
|
||||
private _disabled?: boolean;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@@ -127,14 +125,15 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
private _required?: boolean;
|
||||
|
||||
get hasError() {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
|
||||
}
|
||||
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl.errors)[0];
|
||||
return [key, this.ngControl.errors[key]];
|
||||
const errors = this.ngControl?.errors ?? {};
|
||||
const key = Object.keys(errors)[0];
|
||||
return [key, errors[key]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
@@ -9,12 +7,12 @@ import {
|
||||
HostListener,
|
||||
Input,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
booleanAttribute,
|
||||
inject,
|
||||
signal,
|
||||
input,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
@@ -50,9 +48,9 @@ export type ChipSelectOption<T> = Option<T> & {
|
||||
],
|
||||
})
|
||||
export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, AfterViewInit {
|
||||
@ViewChild(MenuComponent) menu: MenuComponent;
|
||||
@ViewChildren(MenuItemDirective) menuItems: QueryList<MenuItemDirective>;
|
||||
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>;
|
||||
readonly menu = viewChild(MenuComponent);
|
||||
@ViewChildren(MenuItemDirective) menuItems?: QueryList<MenuItemDirective>;
|
||||
readonly chipSelectButton = viewChild<ElementRef<HTMLButtonElement>>("chipSelectButton");
|
||||
|
||||
/** Text to show when there is no selected option */
|
||||
readonly placeholderText = input.required<string>();
|
||||
@@ -60,7 +58,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
/** Icon to show when there is no selected option or the selected option does not have an icon */
|
||||
readonly placeholderIcon = input<string>();
|
||||
|
||||
private _options: ChipSelectOption<T>[];
|
||||
private _options: ChipSelectOption<T>[] = [];
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@@ -103,13 +101,13 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
/** Tree constructed from `this.options` */
|
||||
private rootTree: ChipSelectOption<T>;
|
||||
private rootTree?: ChipSelectOption<T> | null;
|
||||
|
||||
/** Options that are currently displayed in the menu */
|
||||
protected renderedOptions: ChipSelectOption<T>;
|
||||
protected renderedOptions?: ChipSelectOption<T> | null;
|
||||
|
||||
/** The option that is currently selected by the user */
|
||||
protected selectedOption: ChipSelectOption<T>;
|
||||
protected selectedOption?: ChipSelectOption<T> | null;
|
||||
|
||||
/**
|
||||
* The initial calculated width of the menu when it opens, which is used to
|
||||
@@ -123,7 +121,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
}
|
||||
|
||||
/** The icon to show in the chip button */
|
||||
protected get icon(): string {
|
||||
protected get icon(): string | undefined {
|
||||
return this.selectedOption?.icon || this.placeholderIcon();
|
||||
}
|
||||
|
||||
@@ -133,7 +131,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
*/
|
||||
protected setOrResetRenderedOptions(): void {
|
||||
this.renderedOptions = this.selectedOption
|
||||
? this.selectedOption.children?.length > 0
|
||||
? (this.selectedOption.children?.length ?? 0) > 0
|
||||
? this.selectedOption
|
||||
: this.getParent(this.selectedOption)
|
||||
: this.rootTree;
|
||||
@@ -171,7 +169,14 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
* @param value the option value to look for
|
||||
* @returns the `ChipSelectOption` associated with the provided value, or null if not found
|
||||
*/
|
||||
private findOption(tree: ChipSelectOption<T>, value: T): ChipSelectOption<T> | null {
|
||||
private findOption(
|
||||
tree: ChipSelectOption<T> | null | undefined,
|
||||
value: T,
|
||||
): ChipSelectOption<T> | null {
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = null;
|
||||
if (tree.value !== null && compareValues(tree.value, value)) {
|
||||
return tree;
|
||||
@@ -197,7 +202,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
});
|
||||
}
|
||||
|
||||
protected getParent(option: ChipSelectOption<T>): ChipSelectOption<T> | null {
|
||||
protected getParent(option: ChipSelectOption<T>): ChipSelectOption<T> | null | undefined {
|
||||
return this.childParentMap.get(option);
|
||||
}
|
||||
|
||||
@@ -217,8 +222,8 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
* menuItems will change when the user navigates into or out of a submenu. when that happens, we want to
|
||||
* direct their focus to the first item in the new menu
|
||||
*/
|
||||
this.menuItems.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.menu.keyManager.setFirstItemActive();
|
||||
this.menuItems?.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.menu()?.keyManager?.setFirstItemActive();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -227,17 +232,17 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
* the initially rendered options
|
||||
*/
|
||||
protected setMenuWidth() {
|
||||
const chipWidth = this.chipSelectButton.nativeElement.getBoundingClientRect().width;
|
||||
const chipWidth = this.chipSelectButton()?.nativeElement.getBoundingClientRect().width ?? 0;
|
||||
|
||||
const firstMenuItemWidth =
|
||||
this.menu.menuItems.first.elementRef.nativeElement.getBoundingClientRect().width;
|
||||
this.menu()?.menuItems().at(0)?.elementRef.nativeElement.getBoundingClientRect().width ?? 0;
|
||||
|
||||
this.menuWidth = Math.max(chipWidth, firstMenuItemWidth);
|
||||
}
|
||||
|
||||
/** Control Value Accessor */
|
||||
|
||||
private notifyOnChange?: (value: T) => void;
|
||||
private notifyOnChange?: (value: T | null) => void;
|
||||
private notifyOnTouched?: () => void;
|
||||
|
||||
/** Implemented as part of NG_VALUE_ACCESSOR */
|
||||
@@ -247,7 +252,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
}
|
||||
|
||||
/** Implemented as part of NG_VALUE_ACCESSOR */
|
||||
registerOnChange(fn: (value: T) => void): void {
|
||||
registerOnChange(fn: (value: T | null) => void): void {
|
||||
this.notifyOnChange = fn;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
@@ -47,10 +45,10 @@ export class SimpleConfigurableDialogComponent {
|
||||
];
|
||||
}
|
||||
|
||||
protected title: string;
|
||||
protected content: string;
|
||||
protected acceptButtonText: string;
|
||||
protected cancelButtonText: string;
|
||||
protected title?: string;
|
||||
protected content?: string;
|
||||
protected acceptButtonText?: string;
|
||||
protected cancelButtonText?: string;
|
||||
protected formGroup = new FormGroup({});
|
||||
|
||||
protected showCancelButton = this.simpleDialogOpts.cancelButtonText !== null;
|
||||
@@ -58,7 +56,7 @@ export class SimpleConfigurableDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: DialogRef,
|
||||
private i18nService: I18nService,
|
||||
@Inject(DIALOG_DATA) public simpleDialogOpts?: SimpleDialogOptions,
|
||||
@Inject(DIALOG_DATA) public simpleDialogOpts: SimpleDialogOptions,
|
||||
) {
|
||||
this.localizeText();
|
||||
}
|
||||
@@ -76,24 +74,27 @@ export class SimpleConfigurableDialogComponent {
|
||||
private localizeText() {
|
||||
this.title = this.translate(this.simpleDialogOpts.title);
|
||||
this.content = this.translate(this.simpleDialogOpts.content);
|
||||
this.acceptButtonText = this.translate(this.simpleDialogOpts.acceptButtonText, "yes");
|
||||
this.acceptButtonText = this.translate(
|
||||
this.simpleDialogOpts.acceptButtonText ?? { key: "yes" },
|
||||
);
|
||||
|
||||
if (this.showCancelButton) {
|
||||
// If accept text is overridden, use cancel, otherwise no
|
||||
this.cancelButtonText = this.translate(
|
||||
this.simpleDialogOpts.cancelButtonText,
|
||||
this.simpleDialogOpts.acceptButtonText !== undefined ? "cancel" : "no",
|
||||
this.simpleDialogOpts.cancelButtonText ??
|
||||
(this.simpleDialogOpts.acceptButtonText !== undefined
|
||||
? { key: "cancel" }
|
||||
: { key: "no" }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private translate(translation: string | Translation, defaultKey?: string): string {
|
||||
// Translation interface use implies we must localize.
|
||||
private translate(translation: string | Translation): string {
|
||||
// Object implies we must localize.
|
||||
if (typeof translation === "object") {
|
||||
return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
|
||||
}
|
||||
|
||||
// Use string that is already translated or use default key post translate
|
||||
return translation ?? this.i18nService.t(defaultKey);
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, HostBinding, HostListener, input } from "@angular/core";
|
||||
|
||||
import { DisclosureComponent } from "./disclosure.component";
|
||||
@@ -12,7 +10,7 @@ export class DisclosureTriggerForDirective {
|
||||
/**
|
||||
* Accepts template reference for a bit-disclosure component instance
|
||||
*/
|
||||
readonly disclosure = input<DisclosureComponent>(undefined, { alias: "bitDisclosureTriggerFor" });
|
||||
readonly disclosure = input.required<DisclosureComponent>({ alias: "bitDisclosureTriggerFor" });
|
||||
|
||||
@HostBinding("attr.aria-expanded") get ariaExpanded() {
|
||||
return this.disclosure().open;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -40,11 +38,10 @@ let nextId = 0;
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
export class DisclosureComponent {
|
||||
private _open: boolean;
|
||||
|
||||
/** Emits the visibility of the disclosure content */
|
||||
@Output() openChange = new EventEmitter<boolean>();
|
||||
|
||||
private _open?: boolean;
|
||||
/**
|
||||
* Optionally init the disclosure in its opened state
|
||||
*/
|
||||
@@ -54,14 +51,13 @@ export class DisclosureComponent {
|
||||
this._open = isOpen;
|
||||
this.openChange.emit(isOpen);
|
||||
}
|
||||
get open(): boolean {
|
||||
return !!this._open;
|
||||
}
|
||||
|
||||
@HostBinding("class") get classList() {
|
||||
return this.open ? "" : "tw-hidden";
|
||||
}
|
||||
|
||||
@HostBinding("id") id = `bit-disclosure-${nextId++}`;
|
||||
|
||||
get open(): boolean {
|
||||
return this._open;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
export abstract class BitFormControlAbstraction {
|
||||
disabled: boolean;
|
||||
required: boolean;
|
||||
hasError: boolean;
|
||||
error: [string, any];
|
||||
abstract disabled: boolean;
|
||||
abstract required: boolean;
|
||||
abstract hasError: boolean;
|
||||
abstract error: [string, any];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<label
|
||||
class="tw-transition tw-items-start [&:has(input[type='checkbox'])]:tw-gap-[.25rem] [&:has(input[type='radio'])]:tw-gap-1.5 tw-select-none tw-mb-0 tw-inline-flex tw-rounded has-[:focus-visible]:tw-ring has-[:focus-visible]:tw-ring-primary-600"
|
||||
[ngClass]="[formControl.disabled ? 'tw-cursor-auto' : 'tw-cursor-pointer']"
|
||||
[ngClass]="[formControl().disabled ? 'tw-cursor-auto' : 'tw-cursor-pointer']"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
<span
|
||||
class="tw-inline-flex tw-flex-col"
|
||||
[ngClass]="formControl.disabled ? 'tw-text-muted' : 'tw-text-main'"
|
||||
[ngClass]="formControl().disabled ? 'tw-text-muted' : 'tw-text-main'"
|
||||
>
|
||||
<span bitTypography="body2">
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgClass } from "@angular/common";
|
||||
import { booleanAttribute, Component, ContentChild, HostBinding, input } from "@angular/core";
|
||||
import { booleanAttribute, Component, HostBinding, input, contentChild } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -22,10 +20,10 @@ export class FormControlComponent {
|
||||
|
||||
readonly disableMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction;
|
||||
protected readonly formControl = contentChild.required(BitFormControlAbstraction);
|
||||
|
||||
@HostBinding("class") get classes() {
|
||||
return []
|
||||
return ([] as string[])
|
||||
.concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
|
||||
.concat(this.disableMargin() ? [] : ["tw-mb-4"]);
|
||||
}
|
||||
@@ -33,15 +31,15 @@ export class FormControlComponent {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
get required() {
|
||||
return this.formControl.required;
|
||||
return this.formControl().required;
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
return this.formControl.hasError;
|
||||
return this.formControl().hasError;
|
||||
}
|
||||
|
||||
get error() {
|
||||
return this.formControl.error;
|
||||
return this.formControl().error;
|
||||
}
|
||||
|
||||
get displayError() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, HostBinding, input, Optional } from "@angular/core";
|
||||
|
||||
@@ -32,7 +30,7 @@ export class BitLabel {
|
||||
];
|
||||
|
||||
@HostBinding("title") get title() {
|
||||
return this.elementRef.nativeElement.textContent.trim();
|
||||
return this.elementRef.nativeElement.textContent?.trim() ?? "";
|
||||
}
|
||||
|
||||
readonly id = input(`bit-label-${nextId++}`);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FormControl } from "@angular/forms";
|
||||
|
||||
import { forbiddenCharacters } from "./forbidden-characters.validator";
|
||||
@@ -42,6 +40,6 @@ describe("forbiddenCharacters", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function createControl(input: string) {
|
||||
function createControl(input: string | null) {
|
||||
return new FormControl(input);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FormControl } from "@angular/forms";
|
||||
|
||||
import { trimValidator as validate } from "./trim.validator";
|
||||
@@ -58,6 +56,6 @@ describe("trimValidator", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function createControl(input: string) {
|
||||
function createControl(input: string | null) {
|
||||
return new FormControl(input);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { Component, input } from "@angular/core";
|
||||
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
|
||||
|
||||
@@ -21,7 +18,8 @@ export class BitErrorSummary {
|
||||
readonly formGroup = input<UntypedFormGroup>();
|
||||
|
||||
get errorCount(): number {
|
||||
return this.getErrorCount(this.formGroup());
|
||||
const form = this.formGroup();
|
||||
return form ? this.getErrorCount(form) : 0;
|
||||
}
|
||||
|
||||
get errorString() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, HostBinding, input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -24,6 +22,10 @@ export class BitErrorComponent {
|
||||
|
||||
get displayError() {
|
||||
const error = this.error();
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (error[0]) {
|
||||
case "required":
|
||||
return this.i18nService.t("inputRequired");
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
|
||||
import { ModelSignal, Signal } from "@angular/core";
|
||||
|
||||
// @ts-strict-ignore
|
||||
export type InputTypes =
|
||||
| "text"
|
||||
| "password"
|
||||
@@ -16,14 +13,14 @@ export type InputTypes =
|
||||
| "time";
|
||||
|
||||
export abstract class BitFormFieldControl {
|
||||
ariaDescribedBy: string;
|
||||
id: Signal<string>;
|
||||
labelForId: string;
|
||||
required: boolean;
|
||||
hasError: boolean;
|
||||
error: [string, any];
|
||||
type?: ModelSignal<InputTypes>;
|
||||
spellcheck?: ModelSignal<boolean | undefined>;
|
||||
readOnly?: boolean;
|
||||
focus?: () => void;
|
||||
abstract ariaDescribedBy?: string;
|
||||
abstract id: Signal<string>;
|
||||
abstract labelForId: string;
|
||||
abstract required: boolean;
|
||||
abstract hasError: boolean;
|
||||
abstract error: [string, any];
|
||||
abstract type?: ModelSignal<InputTypes | undefined>;
|
||||
abstract spellcheck?: ModelSignal<boolean | undefined>;
|
||||
abstract readOnly?: boolean;
|
||||
abstract focus?: () => void;
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
>
|
||||
<label
|
||||
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-[0.675rem] tw-mb-0 tw-max-w-full tw-pointer-events-auto"
|
||||
[attr.for]="input.labelForId"
|
||||
[attr.for]="input().labelForId"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
|
||||
@if (input.required) {
|
||||
@if (input().required) {
|
||||
<span class="tw-text-[0.625rem] tw-relative tw-bottom-[-1px]">
|
||||
({{ "required" | i18n }})</span
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="tw-w-full tw-relative">
|
||||
<label
|
||||
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted tw-mb-0 tw-max-w-full"
|
||||
[attr.for]="input.labelForId"
|
||||
[attr.for]="input().labelForId"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
|
||||
</label>
|
||||
@@ -109,11 +109,11 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@switch (input.hasError) {
|
||||
@switch (input().hasError) {
|
||||
@case (false) {
|
||||
<ng-content select="bit-hint"></ng-content>
|
||||
}
|
||||
@case (true) {
|
||||
<bit-error [error]="input.error"></bit-error>
|
||||
<bit-error [error]="input().error"></bit-error>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
booleanAttribute,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
ViewChild,
|
||||
signal,
|
||||
input,
|
||||
Input,
|
||||
contentChild,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -30,14 +28,14 @@ import { BitFormFieldControl } from "./form-field-control";
|
||||
imports: [CommonModule, BitErrorComponent, I18nPipe],
|
||||
})
|
||||
export class BitFormFieldComponent implements AfterContentChecked {
|
||||
@ContentChild(BitFormFieldControl) input: BitFormFieldControl;
|
||||
@ContentChild(BitHintComponent) hint: BitHintComponent;
|
||||
@ContentChild(BitLabel) label: BitLabel;
|
||||
readonly input = contentChild.required(BitFormFieldControl);
|
||||
readonly hint = contentChild(BitHintComponent);
|
||||
readonly label = contentChild(BitLabel);
|
||||
|
||||
@ViewChild("prefixContainer") prefixContainer: ElementRef<HTMLDivElement>;
|
||||
@ViewChild("suffixContainer") suffixContainer: ElementRef<HTMLDivElement>;
|
||||
readonly prefixContainer = viewChild<ElementRef<HTMLDivElement>>("prefixContainer");
|
||||
readonly suffixContainer = viewChild<ElementRef<HTMLDivElement>>("suffixContainer");
|
||||
|
||||
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
||||
readonly error = viewChild(BitErrorComponent);
|
||||
|
||||
readonly disableMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
@@ -54,7 +52,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
const shouldFocusBorderAppear = this.defaultContentIsFocused();
|
||||
|
||||
const groupClasses = [
|
||||
this.input.hasError
|
||||
this.input().hasError
|
||||
? "group-hover/bit-form-field:tw-border-danger-700"
|
||||
: "group-hover/bit-form-field:tw-border-primary-600",
|
||||
// the next 2 selectors override the above hover selectors when the input (or text area) is non-interactive (i.e. readonly, disabled)
|
||||
@@ -68,7 +66,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
: "",
|
||||
];
|
||||
|
||||
const baseInputBorderClasses = inputBorderClasses(this.input.hasError);
|
||||
const baseInputBorderClasses = inputBorderClasses(this.input().hasError);
|
||||
|
||||
const borderClasses = baseInputBorderClasses.concat(groupClasses);
|
||||
|
||||
@@ -100,19 +98,21 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
}
|
||||
|
||||
protected get readOnly(): boolean {
|
||||
return this.input.readOnly;
|
||||
return !!this.input().readOnly;
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
if (this.error) {
|
||||
this.input.ariaDescribedBy = this.error.id;
|
||||
} else if (this.hint) {
|
||||
this.input.ariaDescribedBy = this.hint.id;
|
||||
const error = this.error();
|
||||
const hint = this.hint();
|
||||
if (error) {
|
||||
this.input().ariaDescribedBy = error.id;
|
||||
} else if (hint) {
|
||||
this.input().ariaDescribedBy = hint.id;
|
||||
} else {
|
||||
this.input.ariaDescribedBy = undefined;
|
||||
this.input().ariaDescribedBy = undefined;
|
||||
}
|
||||
|
||||
this.prefixHasChildren.set(this.prefixContainer?.nativeElement.childElementCount > 0);
|
||||
this.suffixHasChildren.set(this.suffixContainer?.nativeElement.childElementCount > 0);
|
||||
this.prefixHasChildren.set((this.prefixContainer()?.nativeElement.childElementCount ?? 0) > 0);
|
||||
this.suffixHasChildren.set((this.suffixContainer()?.nativeElement.childElementCount ?? 0) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { TextFieldModule } from "@angular/cdk/text-field";
|
||||
import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
UntypedFormBuilder,
|
||||
@@ -15,6 +12,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { A11yTitleDirective } from "../a11y/a11y-title.directive";
|
||||
import { AsyncActionsModule } from "../async-actions";
|
||||
import { BadgeModule } from "../badge";
|
||||
import { ButtonModule } from "../button";
|
||||
@@ -31,41 +29,6 @@ import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
import { FormFieldModule } from "./form-field.module";
|
||||
|
||||
// TOOD: This solves a circular dependency between components and angular.
|
||||
@Directive({
|
||||
selector: "[appA11yTitle]",
|
||||
})
|
||||
export class A11yTitleDirective implements OnInit {
|
||||
@Input() set appA11yTitle(title: string) {
|
||||
this.title = title;
|
||||
this.setAttributes();
|
||||
}
|
||||
|
||||
private title: string;
|
||||
private originalTitle: string | null;
|
||||
private originalAriaLabel: string | null;
|
||||
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.originalTitle = this.el.nativeElement.getAttribute("title");
|
||||
this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label");
|
||||
this.setAttributes();
|
||||
}
|
||||
|
||||
private setAttributes() {
|
||||
if (this.originalTitle === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
|
||||
}
|
||||
if (this.originalAriaLabel === null) {
|
||||
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Component Library/Form/Field",
|
||||
component: BitFormFieldComponent,
|
||||
|
||||
@@ -57,17 +57,19 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
||||
}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
if (this.formField.input?.type) {
|
||||
this.toggled.set(this.formField.input.type() !== "password");
|
||||
const input = this.formField.input();
|
||||
if (input?.type) {
|
||||
this.toggled.set(input.type() !== "password");
|
||||
}
|
||||
this.button.icon.set(this.icon);
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.button.icon.set(this.icon);
|
||||
if (this.formField.input?.type != null) {
|
||||
this.formField.input.type.set(this.toggled() ? "text" : "password");
|
||||
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined);
|
||||
const input = this.formField.input();
|
||||
if (input?.type != null) {
|
||||
input.type.set(this.toggled() ? "text" : "password");
|
||||
input?.spellcheck?.set(this.toggled() ? false : undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("PasswordInputToggle", () => {
|
||||
button = buttonEl.componentInstance;
|
||||
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
|
||||
const formField: BitFormFieldComponent = formFieldEl.componentInstance;
|
||||
input = formField.input;
|
||||
input = formField.input();
|
||||
});
|
||||
|
||||
describe("initial state", () => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, computed, ElementRef, HostBinding, input, model } from "@angular/core";
|
||||
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
|
||||
@@ -90,7 +88,7 @@ const sizes: Record<IconButtonSize, string[]> = {
|
||||
},
|
||||
})
|
||||
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
|
||||
readonly icon = model<string>(undefined, { alias: "bitIconButton" });
|
||||
readonly icon = model.required<string>({ alias: "bitIconButton" });
|
||||
|
||||
readonly buttonType = input<IconButtonType>("main");
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
AfterContentChecked,
|
||||
booleanAttribute,
|
||||
@@ -73,11 +71,13 @@ export class AutofocusDirective implements AfterContentChecked {
|
||||
private focus() {
|
||||
const el = this.getElement();
|
||||
|
||||
el.focus();
|
||||
this.focused = el === document.activeElement;
|
||||
if (el) {
|
||||
el.focus();
|
||||
this.focused = el === document.activeElement;
|
||||
}
|
||||
}
|
||||
|
||||
private getElement() {
|
||||
private getElement(): HTMLElement | undefined {
|
||||
if (this.focusableElement) {
|
||||
return this.focusableElement.getFocusTarget();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
@@ -63,7 +61,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
|
||||
readonly id = input(`bit-input-${nextId++}`);
|
||||
|
||||
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
||||
@HostBinding("attr.aria-describedby") ariaDescribedBy?: string;
|
||||
|
||||
@HostBinding("attr.aria-invalid") get ariaInvalid() {
|
||||
return this.hasError ? true : undefined;
|
||||
@@ -83,7 +81,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
private _required?: boolean;
|
||||
|
||||
readonly hasPrefix = input(false);
|
||||
readonly hasSuffix = input(false);
|
||||
@@ -101,19 +99,20 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
|
||||
get hasError() {
|
||||
if (this.showErrorsWhenDisabled()) {
|
||||
return (
|
||||
return !!(
|
||||
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
|
||||
this.ngControl?.touched &&
|
||||
this.ngControl?.errors != null
|
||||
);
|
||||
} else {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
|
||||
}
|
||||
}
|
||||
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl.errors)[0];
|
||||
return [key, this.ngControl.errors[key]];
|
||||
const errors = this.ngControl.errors ?? {};
|
||||
const key = Object.keys(errors)[0];
|
||||
return [key, errors[key]];
|
||||
}
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { NgClass } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
@@ -8,8 +5,8 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
signal,
|
||||
ViewChild,
|
||||
input,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { TypographyModule } from "../typography";
|
||||
@@ -30,7 +27,7 @@ import { TypographyModule } from "../typography";
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ItemContentComponent implements AfterContentChecked {
|
||||
@ViewChild("endSlot") endSlot: ElementRef<HTMLDivElement>;
|
||||
readonly endSlot = viewChild<ElementRef<HTMLDivElement>>("endSlot");
|
||||
|
||||
protected endSlotHasChildren = signal(false);
|
||||
|
||||
@@ -42,6 +39,6 @@ export class ItemContentComponent implements AfterContentChecked {
|
||||
readonly truncate = input(true);
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
|
||||
this.endSlotHasChildren.set((this.endSlot()?.nativeElement.childElementCount ?? 0) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { TemplatePortal } from "@angular/cdk/portal";
|
||||
@@ -31,9 +29,9 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
|
||||
readonly role = input("button");
|
||||
|
||||
readonly menu = input<MenuComponent>(undefined, { alias: "bitMenuTriggerFor" });
|
||||
readonly menu = input.required<MenuComponent>({ alias: "bitMenuTriggerFor" });
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private overlayRef: OverlayRef | null = null;
|
||||
private defaultMenuConfig: OverlayConfig = {
|
||||
panelClass: "bit-menu-panel",
|
||||
hasBackdrop: true,
|
||||
@@ -52,8 +50,8 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
.withFlexibleDimensions(false)
|
||||
.withPush(true),
|
||||
};
|
||||
private closedEventsSub: Subscription;
|
||||
private keyDownEventsSub: Subscription;
|
||||
private closedEventsSub: Subscription | null = null;
|
||||
private keyDownEventsSub: Subscription | null = null;
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef<HTMLElement>,
|
||||
@@ -78,28 +76,30 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
this.isOpen = true;
|
||||
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
|
||||
|
||||
const templatePortal = new TemplatePortal(menu.templateRef, this.viewContainerRef);
|
||||
const templatePortal = new TemplatePortal(menu.templateRef(), this.viewContainerRef);
|
||||
this.overlayRef.attach(templatePortal);
|
||||
|
||||
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
|
||||
// Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key
|
||||
// from doing its normal default action, which would otherwise cause a parent component
|
||||
// (like a dialog) or extension window to close
|
||||
if (event?.key === "Escape" && !hasModifierKey(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.closedEventsSub =
|
||||
this.getClosedEvents()?.subscribe((event: KeyboardEvent | undefined) => {
|
||||
// Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key
|
||||
// from doing its normal default action, which would otherwise cause a parent component
|
||||
// (like a dialog) or extension window to close
|
||||
if (event?.key === "Escape" && !hasModifierKey(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (event?.key && ["Tab", "Escape"].includes(event.key)) {
|
||||
// Required to ensure tab order resumes correctly
|
||||
this.elementRef.nativeElement.focus();
|
||||
}
|
||||
this.destroyMenu();
|
||||
}) ?? null;
|
||||
|
||||
if (["Tab", "Escape"].includes(event?.key)) {
|
||||
// Required to ensure tab order resumes correctly
|
||||
this.elementRef.nativeElement.focus();
|
||||
}
|
||||
this.destroyMenu();
|
||||
});
|
||||
if (menu.keyManager) {
|
||||
menu.keyManager.setFirstItemActive();
|
||||
this.keyDownEventsSub = this.overlayRef
|
||||
.keydownEvents()
|
||||
.subscribe((event: KeyboardEvent) => this.menu().keyManager.onKeydown(event));
|
||||
.subscribe((event: KeyboardEvent) => this.menu().keyManager?.onKeydown(event));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,10 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
this.menu().closed.emit();
|
||||
}
|
||||
|
||||
private getClosedEvents(): Observable<any> {
|
||||
private getClosedEvents(): Observable<any> | null {
|
||||
if (!this.overlayRef) {
|
||||
return null;
|
||||
}
|
||||
const detachments = this.overlayRef.detachments();
|
||||
const escKey = this.overlayRef.keydownEvents().pipe(
|
||||
filter((event: KeyboardEvent) => {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FocusKeyManager, CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import {
|
||||
Component,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
EventEmitter,
|
||||
ContentChildren,
|
||||
QueryList,
|
||||
AfterContentInit,
|
||||
input,
|
||||
viewChild,
|
||||
contentChildren,
|
||||
} from "@angular/core";
|
||||
|
||||
import { MenuItemDirective } from "./menu-item.directive";
|
||||
@@ -22,10 +19,9 @@ import { MenuItemDirective } from "./menu-item.directive";
|
||||
imports: [CdkTrapFocus],
|
||||
})
|
||||
export class MenuComponent implements AfterContentInit {
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
readonly templateRef = viewChild.required(TemplateRef);
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
@ContentChildren(MenuItemDirective, { descendants: true })
|
||||
menuItems: QueryList<MenuItemDirective>;
|
||||
readonly menuItems = contentChildren(MenuItemDirective, { descendants: true });
|
||||
keyManager?: FocusKeyManager<MenuItemDirective>;
|
||||
|
||||
readonly ariaRole = input<"menu" | "dialog">("menu");
|
||||
@@ -34,9 +30,9 @@ export class MenuComponent implements AfterContentInit {
|
||||
|
||||
ngAfterContentInit() {
|
||||
if (this.ariaRole() === "menu") {
|
||||
this.keyManager = new FocusKeyManager(this.menuItems)
|
||||
this.keyManager = new FocusKeyManager(this.menuItems())
|
||||
.withWrap()
|
||||
.skipPredicate((item) => item.disabled);
|
||||
.skipPredicate((item) => !!item.disabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const OpenMenu: Story = {
|
||||
|
||||
<div class="tw-h-40">
|
||||
<div class="cdk-overlay-pane bit-menu-panel">
|
||||
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="myMenu.templateRef()"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Optional,
|
||||
@@ -14,6 +11,7 @@ import {
|
||||
input,
|
||||
model,
|
||||
booleanAttribute,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
@@ -48,10 +46,10 @@ let nextId = 0;
|
||||
* This component has been implemented to only support Multi-select list events
|
||||
*/
|
||||
export class MultiSelectComponent implements OnInit, BitFormFieldControl, ControlValueAccessor {
|
||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||
readonly select = viewChild.required(NgSelectComponent);
|
||||
|
||||
// Parent component should only pass selectable items (complete list - selected items = baseItems)
|
||||
readonly baseItems = model<SelectItemView[]>();
|
||||
readonly baseItems = model.required<SelectItemView[]>();
|
||||
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
|
||||
readonly removeSelectedItems = input(false);
|
||||
readonly placeholder = model<string>();
|
||||
@@ -61,10 +59,10 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
@Input({ transform: booleanAttribute }) disabled?: boolean;
|
||||
|
||||
// Internal tracking of selected items
|
||||
protected selectedItems: SelectItemView[];
|
||||
protected selectedItems: SelectItemView[] | null = null;
|
||||
|
||||
// Default values for our implementation
|
||||
loadingText: string;
|
||||
loadingText?: string;
|
||||
|
||||
protected searchInputId = `search-input-${nextId++}`;
|
||||
|
||||
@@ -95,13 +93,14 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
/** Function for customizing keyboard navigation */
|
||||
/** Needs to be arrow function to retain `this` scope. */
|
||||
keyDown = (event: KeyboardEvent) => {
|
||||
if (!this.select.isOpen && event.key === "Enter" && !hasModifierKey(event)) {
|
||||
const select = this.select();
|
||||
if (!select.isOpen && event.key === "Enter" && !hasModifierKey(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
|
||||
if (select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
|
||||
this.selectedItems = [];
|
||||
this.select.close();
|
||||
select.close();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
@@ -183,11 +182,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
get ariaDescribedBy() {
|
||||
return this._ariaDescribedBy;
|
||||
}
|
||||
set ariaDescribedBy(value: string) {
|
||||
set ariaDescribedBy(value: string | undefined) {
|
||||
this._ariaDescribedBy = value;
|
||||
this.select?.searchInput.nativeElement.setAttribute("aria-describedby", value);
|
||||
this.select()?.searchInput.nativeElement.setAttribute("aria-describedby", value ?? "");
|
||||
}
|
||||
private _ariaDescribedBy: string;
|
||||
private _ariaDescribedBy?: string;
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
get labelForId() {
|
||||
@@ -208,16 +207,17 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
private _required?: boolean;
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
get hasError() {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
|
||||
}
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl?.errors)[0];
|
||||
return [key, this.ngControl?.errors[key]];
|
||||
const errors = this.ngControl?.errors ?? {};
|
||||
const key = Object.keys(errors)[0];
|
||||
return [key, errors[key]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Output, input } from "@angular/core";
|
||||
import { RouterLink, RouterLinkActive } from "@angular/router";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- This a higher order component that composes `NavItemComponent` -->
|
||||
@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
|
||||
@if (!hideIfEmpty() || nestedNavComponents().length > 0) {
|
||||
<bit-nav-item
|
||||
[text]="text()"
|
||||
[icon]="icon()"
|
||||
|
||||
@@ -2,14 +2,13 @@ import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
booleanAttribute,
|
||||
Component,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
SkipSelf,
|
||||
input,
|
||||
model,
|
||||
contentChildren,
|
||||
} from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -30,10 +29,7 @@ import { SideNavService } from "./side-nav.service";
|
||||
imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe],
|
||||
})
|
||||
export class NavGroupComponent extends NavBaseComponent {
|
||||
@ContentChildren(NavBaseComponent, {
|
||||
descendants: true,
|
||||
})
|
||||
nestedNavComponents!: QueryList<NavBaseComponent>;
|
||||
readonly nestedNavComponents = contentChildren(NavBaseComponent, { descendants: true });
|
||||
|
||||
/** When the side nav is open, the parent nav item should not show active styles when open. */
|
||||
protected get parentHideActiveStyles(): boolean {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, input } from "@angular/core";
|
||||
import { RouterLinkActive, RouterLink } from "@angular/router";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, ViewChild, input } from "@angular/core";
|
||||
import { Component, ElementRef, input, viewChild } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -21,15 +19,14 @@ export type SideNavVariant = "primary" | "secondary";
|
||||
export class SideNavComponent {
|
||||
readonly variant = input<SideNavVariant>("primary");
|
||||
|
||||
@ViewChild("toggleButton", { read: ElementRef, static: true })
|
||||
private toggleButton: ElementRef<HTMLButtonElement>;
|
||||
private readonly toggleButton = viewChild("toggleButton", { read: ElementRef });
|
||||
|
||||
constructor(protected sideNavService: SideNavService) {}
|
||||
|
||||
protected handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
this.sideNavService.setClose();
|
||||
this.toggleButton?.nativeElement.focus();
|
||||
this.toggleButton()?.nativeElement.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { TemplatePortal } from "@angular/cdk/portal";
|
||||
import {
|
||||
@@ -27,12 +25,12 @@ import { PopoverComponent } from "./popover.component";
|
||||
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
readonly popoverOpen = model(false);
|
||||
|
||||
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" });
|
||||
readonly popover = input.required<PopoverComponent>({ alias: "bitPopoverTriggerFor" });
|
||||
|
||||
readonly position = input<string>();
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private closedEventsSub: Subscription;
|
||||
private overlayRef: OverlayRef | null = null;
|
||||
private closedEventsSub: Subscription | null = null;
|
||||
|
||||
get positions() {
|
||||
if (!this.position()) {
|
||||
@@ -82,7 +80,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
this.popoverOpen.set(true);
|
||||
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
|
||||
|
||||
const templatePortal = new TemplatePortal(this.popover().templateRef, this.viewContainerRef);
|
||||
const templatePortal = new TemplatePortal(this.popover().templateRef(), this.viewContainerRef);
|
||||
|
||||
this.overlayRef.attach(templatePortal);
|
||||
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
|
||||
@@ -91,6 +89,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private getClosedEvents(): Observable<any> {
|
||||
if (!this.overlayRef) {
|
||||
throw new Error("Overlay reference is not available");
|
||||
}
|
||||
|
||||
const detachments = this.overlayRef.detachments();
|
||||
const escKey = this.overlayRef
|
||||
.keydownEvents()
|
||||
@@ -102,7 +104,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private destroyPopover() {
|
||||
if (this.overlayRef == null || !this.popoverOpen()) {
|
||||
if (!this.overlayRef || !this.popoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,7 +114,9 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
|
||||
private disposeAll() {
|
||||
this.closedEventsSub?.unsubscribe();
|
||||
this.closedEventsSub = null;
|
||||
this.overlayRef?.dispose();
|
||||
this.overlayRef = null;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { A11yModule } from "@angular/cdk/a11y";
|
||||
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
|
||||
import { Component, EventEmitter, Output, TemplateRef, input, viewChild } from "@angular/core";
|
||||
|
||||
import { IconButtonModule } from "../icon-button/icon-button.module";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
@@ -14,7 +12,7 @@ import { TypographyModule } from "../typography";
|
||||
exportAs: "popoverComponent",
|
||||
})
|
||||
export class PopoverComponent {
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
readonly templateRef = viewChild.required(TemplateRef);
|
||||
readonly title = input("");
|
||||
@Output() closed = new EventEmitter();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@if (label) {
|
||||
@if (label()) {
|
||||
<fieldset>
|
||||
<legend class="tw-mb-1 tw-block tw-text-sm tw-font-semibold tw-text-main">
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
@@ -10,7 +10,7 @@
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
@if (!label) {
|
||||
@if (!label()) {
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgTemplateOutlet } from "@angular/common";
|
||||
import { Component, ContentChild, HostBinding, Optional, Input, Self, input } from "@angular/core";
|
||||
import { Component, HostBinding, Optional, Self, input, contentChild } from "@angular/core";
|
||||
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -22,14 +20,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
|
||||
selected: unknown;
|
||||
disabled = false;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
private _name?: string;
|
||||
@Input() get name() {
|
||||
return this._name ?? this.ngControl?.name?.toString();
|
||||
}
|
||||
set name(value: string) {
|
||||
this._name = value;
|
||||
get name() {
|
||||
return this.ngControl?.name?.toString();
|
||||
}
|
||||
|
||||
readonly block = input(false);
|
||||
@@ -38,7 +30,7 @@ export class RadioGroupComponent implements ControlValueAccessor {
|
||||
readonly id = input(`bit-radio-group-${nextId++}`);
|
||||
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];
|
||||
|
||||
@ContentChild(BitLabel) protected label: BitLabel;
|
||||
protected readonly label = contentChild(BitLabel);
|
||||
|
||||
constructor(@Optional() @Self() private ngControl?: NgControl) {
|
||||
if (ngControl != null) {
|
||||
@@ -51,8 +43,8 @@ export class RadioGroupComponent implements ControlValueAccessor {
|
||||
}
|
||||
|
||||
// ControlValueAccessor
|
||||
onChange: (value: unknown) => void;
|
||||
onTouched: () => void;
|
||||
onChange?: (value: unknown) => void;
|
||||
onTouched?: () => void;
|
||||
|
||||
writeValue(value: boolean): void {
|
||||
this.selected = value;
|
||||
@@ -72,10 +64,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
|
||||
|
||||
onInputChange(value: unknown) {
|
||||
this.selected = value;
|
||||
this.onChange(this.selected);
|
||||
this.onChange?.(this.selected);
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
this.onTouched?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, HostBinding, input, Input, Optional, Self } from "@angular/core";
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
@@ -86,7 +84,7 @@ export class RadioInputComponent implements BitFormControlAbstraction {
|
||||
set disabled(value: any) {
|
||||
this._disabled = value != null && value !== false;
|
||||
}
|
||||
private _disabled: boolean;
|
||||
private _disabled?: boolean;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@@ -99,14 +97,15 @@ export class RadioInputComponent implements BitFormControlAbstraction {
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
private _required?: boolean;
|
||||
|
||||
get hasError() {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
|
||||
}
|
||||
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl.errors)[0];
|
||||
return [key, this.ngControl.errors[key]];
|
||||
const errors = this.ngControl?.errors ?? {};
|
||||
const key = Object.keys(errors)[0];
|
||||
return [key, errors[key]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgIf, NgClass } from "@angular/common";
|
||||
import { Component, ElementRef, ViewChild, input, model, signal, computed } from "@angular/core";
|
||||
import { Component, ElementRef, input, model, signal, computed, viewChild } from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@@ -37,13 +35,13 @@ let nextId = 0;
|
||||
imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe, NgIf, NgClass],
|
||||
})
|
||||
export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
||||
private notifyOnChange: (v: string) => void;
|
||||
private notifyOnTouch: () => void;
|
||||
private notifyOnChange?: (v: string) => void;
|
||||
private notifyOnTouch?: () => void;
|
||||
|
||||
@ViewChild("input") private input: ElementRef<HTMLInputElement>;
|
||||
private readonly input = viewChild<ElementRef<HTMLInputElement>>("input");
|
||||
|
||||
protected id = `search-id-${nextId++}`;
|
||||
protected searchText: string;
|
||||
protected searchText?: string;
|
||||
// Use `type="text"` for Safari to improve rendering performance
|
||||
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
|
||||
|
||||
@@ -57,7 +55,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
||||
readonly autocomplete = input<string>();
|
||||
|
||||
getFocusTarget() {
|
||||
return this.input?.nativeElement;
|
||||
return this.input()?.nativeElement;
|
||||
}
|
||||
|
||||
onChange(searchText: string) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, booleanAttribute, input } from "@angular/core";
|
||||
|
||||
import { MappedOptionComponent } from "./option";
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||
import {
|
||||
Component,
|
||||
@@ -10,7 +7,6 @@ import {
|
||||
Optional,
|
||||
QueryList,
|
||||
Self,
|
||||
ViewChild,
|
||||
Output,
|
||||
EventEmitter,
|
||||
input,
|
||||
@@ -18,6 +14,7 @@ import {
|
||||
computed,
|
||||
model,
|
||||
signal,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
@@ -47,7 +44,7 @@ let nextId = 0;
|
||||
},
|
||||
})
|
||||
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
|
||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||
readonly select = viewChild.required(NgSelectComponent);
|
||||
|
||||
/** Optional: Options can be provided using an array input or using `bit-option` */
|
||||
readonly items = model<Option<T>[] | undefined>();
|
||||
@@ -55,13 +52,13 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
|
||||
@Output() closed = new EventEmitter();
|
||||
|
||||
protected selectedValue = signal<T>(undefined);
|
||||
selectedOption: Signal<Option<T>> = computed(() =>
|
||||
protected selectedValue = signal<T | undefined | null>(undefined);
|
||||
selectedOption: Signal<Option<T> | null | undefined> = computed(() =>
|
||||
this.findSelectedOption(this.items(), this.selectedValue()),
|
||||
);
|
||||
protected searchInputId = `bit-select-search-input-${nextId++}`;
|
||||
|
||||
private notifyOnChange?: (value: T) => void;
|
||||
private notifyOnChange?: (value?: T | null) => void;
|
||||
private notifyOnTouched?: () => void;
|
||||
|
||||
constructor(
|
||||
@@ -104,7 +101,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
set disabled(value: any) {
|
||||
this._disabled = value != null && value !== false;
|
||||
}
|
||||
private _disabled: boolean;
|
||||
private _disabled?: boolean;
|
||||
|
||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||
writeValue(obj: T): void {
|
||||
@@ -112,7 +109,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
}
|
||||
|
||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||
registerOnChange(fn: (value: T) => void): void {
|
||||
registerOnChange(fn: (value?: T | null) => void): void {
|
||||
this.notifyOnChange = fn;
|
||||
}
|
||||
|
||||
@@ -151,11 +148,11 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
get ariaDescribedBy() {
|
||||
return this._ariaDescribedBy;
|
||||
}
|
||||
set ariaDescribedBy(value: string) {
|
||||
set ariaDescribedBy(value: string | undefined) {
|
||||
this._ariaDescribedBy = value;
|
||||
this.select?.searchInput.nativeElement.setAttribute("aria-describedby", value);
|
||||
this.select()?.searchInput.nativeElement.setAttribute("aria-describedby", value ?? "");
|
||||
}
|
||||
private _ariaDescribedBy: string;
|
||||
private _ariaDescribedBy?: string;
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
get labelForId() {
|
||||
@@ -176,20 +173,24 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
private _required?: boolean;
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
get hasError() {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
return !!(this.ngControl?.status === "INVALID" && this.ngControl?.touched);
|
||||
}
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl?.errors)[0];
|
||||
return [key, this.ngControl?.errors[key]];
|
||||
const errors = this.ngControl?.errors ?? {};
|
||||
const key = Object.keys(errors)[0];
|
||||
return [key, errors[key]];
|
||||
}
|
||||
|
||||
private findSelectedOption(items: Option<T>[] | undefined, value: T): Option<T> | undefined {
|
||||
private findSelectedOption(
|
||||
items: Option<T>[] | undefined,
|
||||
value: T | null | undefined,
|
||||
): Option<T> | undefined {
|
||||
return items?.find((item) => item.value === value);
|
||||
}
|
||||
|
||||
@@ -207,7 +208,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
* Needs to be arrow function to retain `this` scope.
|
||||
*/
|
||||
protected onKeyDown = (event: KeyboardEvent) => {
|
||||
if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) {
|
||||
if (this.select().isOpen && event.key === "Escape" && !hasModifierKey(event)) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
import { ModelSignal } from "@angular/core";
|
||||
|
||||
// @ts-strict-ignore
|
||||
export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
|
||||
|
||||
export type ButtonSize = "default" | "small";
|
||||
|
||||
export abstract class ButtonLikeAbstraction {
|
||||
loading: ModelSignal<boolean>;
|
||||
disabled: ModelSignal<boolean>;
|
||||
abstract loading: ModelSignal<boolean>;
|
||||
abstract disabled: ModelSignal<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
/** Global config for the Bitwarden Design System */
|
||||
@@ -9,5 +7,5 @@ export abstract class CompactModeService {
|
||||
*
|
||||
* Component authors can also hook into compact mode with the `bit-compact:` Tailwind variant.
|
||||
**/
|
||||
enabled$: Observable<boolean>;
|
||||
abstract enabled$: Observable<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
/**
|
||||
* Interface for implementing focusable components.
|
||||
*
|
||||
* Used by the `AutofocusDirective`.
|
||||
*/
|
||||
export abstract class FocusableElement {
|
||||
getFocusTarget: () => HTMLElement | undefined;
|
||||
abstract getFocusTarget(): HTMLElement | undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, HostBinding, OnInit, input } from "@angular/core";
|
||||
@@ -26,7 +24,7 @@ export class SortableComponent implements OnInit {
|
||||
/**
|
||||
* Mark the column as sortable and specify the key to sort by
|
||||
*/
|
||||
readonly bitSortable = input<string>();
|
||||
readonly bitSortable = input.required<string>();
|
||||
|
||||
readonly default = input(false, {
|
||||
transform: (value: SortDirection | boolean | "") => {
|
||||
@@ -63,7 +61,7 @@ export class SortableComponent implements OnInit {
|
||||
if (!this.isActive) {
|
||||
return undefined;
|
||||
}
|
||||
return this.sort.direction === "asc" ? "ascending" : "descending";
|
||||
return this.sort?.direction === "asc" ? "ascending" : "descending";
|
||||
}
|
||||
|
||||
protected setActive() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { _isNumberValue } from "@angular/cdk/coercion";
|
||||
import { DataSource } from "@angular/cdk/collections";
|
||||
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs";
|
||||
@@ -19,7 +17,7 @@ export type FilterFn<T> = (data: T) => boolean;
|
||||
export class TableDataSource<T> extends DataSource<T> {
|
||||
private readonly _data: BehaviorSubject<T[]>;
|
||||
private readonly _sort: BehaviorSubject<Sort>;
|
||||
private readonly _filter = new BehaviorSubject<string | FilterFn<T>>(null);
|
||||
private readonly _filter = new BehaviorSubject<string | FilterFn<T>>(() => true);
|
||||
private readonly _renderData = new BehaviorSubject<T[]>([]);
|
||||
private _renderChangesSubscription: Subscription | null = null;
|
||||
|
||||
@@ -29,12 +27,12 @@ export class TableDataSource<T> extends DataSource<T> {
|
||||
* For example, a 'selectAll()' function would likely want to select the set of filtered data
|
||||
* shown to the user rather than all the data.
|
||||
*/
|
||||
filteredData: T[];
|
||||
filteredData?: T[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._data = new BehaviorSubject([]);
|
||||
this._sort = new BehaviorSubject({ direction: "asc" });
|
||||
this._data = new BehaviorSubject([] as T[]);
|
||||
this._sort = new BehaviorSubject<Sort>({ direction: "asc" } as Sort);
|
||||
}
|
||||
|
||||
get data() {
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *cdkVirtualFor="let r of rows$; trackBy: trackBy(); templateCacheSize: 0" bitRow>
|
||||
<ng-container *ngTemplateOutlet="rowDef.template; context: { $implicit: r }"></ng-container>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="rowDef().template; context: { $implicit: r }"
|
||||
></ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
CdkVirtualScrollViewport,
|
||||
CdkFixedSizeVirtualScroll,
|
||||
@@ -9,7 +7,6 @@ import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
ContentChild,
|
||||
OnDestroy,
|
||||
TemplateRef,
|
||||
Directive,
|
||||
@@ -18,6 +15,7 @@ import {
|
||||
ElementRef,
|
||||
TrackByFunction,
|
||||
input,
|
||||
contentChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { ScrollLayoutDirective } from "../layout";
|
||||
@@ -69,7 +67,7 @@ export class TableScrollComponent
|
||||
/** Optional trackBy function. */
|
||||
readonly trackBy = input<TrackByFunction<any> | undefined>();
|
||||
|
||||
@ContentChild(BitRowDef) protected rowDef: BitRowDef;
|
||||
protected readonly rowDef = contentChild(BitRowDef);
|
||||
|
||||
/**
|
||||
* Height of the thead element (in pixels).
|
||||
@@ -81,7 +79,7 @@ export class TableScrollComponent
|
||||
/**
|
||||
* Observer for table header, applies padding on resize.
|
||||
*/
|
||||
private headerObserver: ResizeObserver;
|
||||
private headerObserver?: ResizeObserver;
|
||||
|
||||
constructor(
|
||||
private zone: NgZone,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows$ }"
|
||||
*ngTemplateOutlet="templateVariable().template; context: { $implicit: rows$ }"
|
||||
></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { isDataSource } from "@angular/cdk/collections";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
ContentChild,
|
||||
Directive,
|
||||
OnDestroy,
|
||||
TemplateRef,
|
||||
input,
|
||||
contentChild,
|
||||
} from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
@@ -32,9 +30,9 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
|
||||
readonly dataSource = input<TableDataSource<any>>();
|
||||
readonly layout = input<"auto" | "fixed">("auto");
|
||||
|
||||
@ContentChild(TableBodyDirective) templateVariable: TableBodyDirective;
|
||||
readonly templateVariable = contentChild(TableBodyDirective);
|
||||
|
||||
protected rows$: Observable<any[]>;
|
||||
protected rows$?: Observable<any[]>;
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FocusableOption } from "@angular/cdk/a11y";
|
||||
import { Directive, ElementRef, HostBinding, Input, input } from "@angular/core";
|
||||
|
||||
@@ -15,7 +13,7 @@ export class TabListItemDirective implements FocusableOption {
|
||||
// TODO: Skipped for signal migration because:
|
||||
// This input overrides a field from a superclass, while the superclass field
|
||||
// is not migrated.
|
||||
@Input() disabled: boolean;
|
||||
@Input() disabled = false;
|
||||
|
||||
@HostBinding("attr.disabled")
|
||||
get disabledAttr() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal";
|
||||
import { Component, effect, HostBinding, input } from "@angular/core";
|
||||
|
||||
@@ -9,7 +7,7 @@ import { Component, effect, HostBinding, input } from "@angular/core";
|
||||
imports: [CdkPortalOutlet],
|
||||
})
|
||||
export class TabBodyComponent {
|
||||
private _firstRender: boolean;
|
||||
private _firstRender = false;
|
||||
|
||||
readonly content = input<TemplatePortal>();
|
||||
readonly preserveContent = input(false);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[attr.aria-label]="label()"
|
||||
(keydown)="keyManager.onKeydown($event)"
|
||||
>
|
||||
@for (tab of tabs; track tab; let i = $index) {
|
||||
@for (tab of tabs(); track tab; let i = $index) {
|
||||
<button
|
||||
bitTabListItem
|
||||
type="button"
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</bit-tab-header>
|
||||
<div class="tw-px-6 tw-pt-5">
|
||||
@for (tab of tabs; track tab; let i = $index) {
|
||||
@for (tab of tabs(); track tab; let i = $index) {
|
||||
<bit-tab-body
|
||||
role="tabpanel"
|
||||
[id]="getTabContentId(i)"
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FocusKeyManager } from "@angular/cdk/a11y";
|
||||
import { coerceNumberProperty } from "@angular/cdk/coercion";
|
||||
import { NgTemplateOutlet } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
AfterContentInit,
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChildren,
|
||||
contentChild,
|
||||
contentChildren,
|
||||
effect,
|
||||
input,
|
||||
viewChildren,
|
||||
inject,
|
||||
DestroyRef,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
import { TabHeaderComponent } from "../shared/tab-header.component";
|
||||
import { TabListContainerDirective } from "../shared/tab-list-container.directive";
|
||||
@@ -41,7 +38,7 @@ let nextId = 0;
|
||||
TabBodyComponent,
|
||||
],
|
||||
})
|
||||
export class TabGroupComponent implements AfterContentChecked, AfterContentInit, AfterViewInit {
|
||||
export class TabGroupComponent implements AfterContentChecked, AfterViewInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
private readonly _groupId: number;
|
||||
@@ -59,8 +56,11 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
|
||||
*/
|
||||
readonly preserveContent = input(false);
|
||||
|
||||
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
|
||||
@ViewChildren(TabListItemDirective) tabLabels: QueryList<TabListItemDirective>;
|
||||
/** Error if no `TabComponent` is supplied. (`contentChildren`, used to query for all the tabs, doesn't support `required`) */
|
||||
private _tab = contentChild.required(TabComponent);
|
||||
|
||||
protected tabs = contentChildren(TabComponent);
|
||||
readonly tabLabels = viewChildren(TabListItemDirective);
|
||||
|
||||
/** The index of the active tab. */
|
||||
// TODO: Skipped for signal migration because:
|
||||
@@ -85,78 +85,18 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
|
||||
* Focus key manager for keeping tab controls accessible.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
|
||||
*/
|
||||
keyManager: FocusKeyManager<TabListItemDirective>;
|
||||
keyManager?: FocusKeyManager<TabListItemDirective>;
|
||||
|
||||
constructor() {
|
||||
this._groupId = nextId++;
|
||||
}
|
||||
|
||||
protected getTabContentId(id: number): string {
|
||||
return `bit-tab-content-${this._groupId}-${id}`;
|
||||
}
|
||||
|
||||
protected getTabLabelId(id: number): string {
|
||||
return `bit-tab-label-${this._groupId}-${id}`;
|
||||
}
|
||||
|
||||
selectTab(index: number) {
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* After content is checked, the tab group knows what tabs are defined and which index
|
||||
* should be currently selected.
|
||||
*/
|
||||
ngAfterContentChecked(): void {
|
||||
const indexToSelect = (this._indexToSelect = this._clampTabIndex(this._indexToSelect));
|
||||
|
||||
if (this._selectedIndex != indexToSelect) {
|
||||
const isFirstRun = this._selectedIndex == null;
|
||||
|
||||
if (!isFirstRun) {
|
||||
this.selectedTabChange.emit({
|
||||
index: indexToSelect,
|
||||
tab: this.tabs.toArray()[indexToSelect],
|
||||
});
|
||||
}
|
||||
|
||||
// These values need to be updated after change detection as
|
||||
// the checked content may have references to them.
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
Promise.resolve().then(() => {
|
||||
this.tabs.forEach((tab, index) => (tab.isActive = index === indexToSelect));
|
||||
|
||||
if (!isFirstRun) {
|
||||
this.selectedIndexChange.emit(indexToSelect);
|
||||
}
|
||||
});
|
||||
|
||||
// Manually update the _selectedIndex and keyManager active item
|
||||
this._selectedIndex = indexToSelect;
|
||||
if (this.keyManager) {
|
||||
this.keyManager.setActiveItem(indexToSelect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.keyManager = new FocusKeyManager(this.tabLabels)
|
||||
.withHorizontalOrientation("ltr")
|
||||
.withWrap()
|
||||
.withHomeAndEnd();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
// Subscribe to any changes in the number of tabs, in order to be able
|
||||
// to re-render content when new tabs are added or removed.
|
||||
this.tabs.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
const indexToSelect = this._clampTabIndex(this._indexToSelect);
|
||||
effect(() => {
|
||||
const indexToSelect = this._clampTabIndex(this._indexToSelect ?? 0);
|
||||
|
||||
// If the selected tab didn't explicitly change, keep the previously
|
||||
// selected tab selected/active
|
||||
if (indexToSelect === this._selectedIndex) {
|
||||
const tabs = this.tabs.toArray();
|
||||
const tabs = this.tabs();
|
||||
let selectedTab: TabComponent | undefined;
|
||||
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
@@ -183,12 +123,66 @@ export class TabGroupComponent implements AfterContentChecked, AfterContentInit,
|
||||
});
|
||||
}
|
||||
|
||||
protected getTabContentId(id: number): string {
|
||||
return `bit-tab-content-${this._groupId}-${id}`;
|
||||
}
|
||||
|
||||
protected getTabLabelId(id: number): string {
|
||||
return `bit-tab-label-${this._groupId}-${id}`;
|
||||
}
|
||||
|
||||
selectTab(index: number) {
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* After content is checked, the tab group knows what tabs are defined and which index
|
||||
* should be currently selected.
|
||||
*/
|
||||
ngAfterContentChecked(): void {
|
||||
const indexToSelect = (this._indexToSelect = this._clampTabIndex(this._indexToSelect ?? 0));
|
||||
|
||||
if (this._selectedIndex != indexToSelect) {
|
||||
const isFirstRun = this._selectedIndex == null;
|
||||
|
||||
if (!isFirstRun) {
|
||||
this.selectedTabChange.emit({
|
||||
index: indexToSelect,
|
||||
tab: this.tabs()[indexToSelect],
|
||||
});
|
||||
}
|
||||
|
||||
// These values need to be updated after change detection as
|
||||
// the checked content may have references to them.
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
Promise.resolve().then(() => {
|
||||
this.tabs().forEach((tab, index) => (tab.isActive = index === indexToSelect));
|
||||
|
||||
if (!isFirstRun) {
|
||||
this.selectedIndexChange.emit(indexToSelect);
|
||||
}
|
||||
});
|
||||
|
||||
// Manually update the _selectedIndex and keyManager active item
|
||||
this._selectedIndex = indexToSelect;
|
||||
this.keyManager?.setActiveItem(indexToSelect);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.keyManager = new FocusKeyManager(this.tabLabels())
|
||||
.withHorizontalOrientation("ltr")
|
||||
.withWrap()
|
||||
.withHomeAndEnd();
|
||||
}
|
||||
|
||||
private _clampTabIndex(index: number): number {
|
||||
return Math.min(this.tabs.length - 1, Math.max(index || 0, 0));
|
||||
return Math.min(this.tabs().length - 1, Math.max(index || 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
export class BitTabChangeEvent {
|
||||
export interface BitTabChangeEvent {
|
||||
/**
|
||||
* The currently selected tab index
|
||||
*/
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { TemplatePortal } from "@angular/cdk/portal";
|
||||
import {
|
||||
Component,
|
||||
ContentChild,
|
||||
OnInit,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
input,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { TabLabelDirective } from "./tab-label.directive";
|
||||
@@ -34,8 +32,8 @@ export class TabComponent implements OnInit {
|
||||
*/
|
||||
readonly contentTabIndex = input<number | undefined>();
|
||||
|
||||
@ViewChild(TemplateRef, { static: true }) implicitContent: TemplateRef<unknown>;
|
||||
@ContentChild(TabLabelDirective) templateLabel: TabLabelDirective;
|
||||
readonly implicitContent = viewChild.required(TemplateRef);
|
||||
@ContentChild(TabLabelDirective) templateLabel?: TabLabelDirective;
|
||||
|
||||
private _contentPortal: TemplatePortal | null = null;
|
||||
|
||||
@@ -43,11 +41,11 @@ export class TabComponent implements OnInit {
|
||||
return this._contentPortal;
|
||||
}
|
||||
|
||||
isActive: boolean;
|
||||
isActive?: boolean;
|
||||
|
||||
constructor(private _viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._contentPortal = new TemplatePortal(this.implicitContent, this._viewContainerRef);
|
||||
this._contentPortal = new TemplatePortal(this.implicitContent(), this._viewContainerRef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FocusableOption } from "@angular/cdk/a11y";
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
DestroyRef,
|
||||
HostListener,
|
||||
Input,
|
||||
ViewChild,
|
||||
input,
|
||||
inject,
|
||||
DestroyRef,
|
||||
input,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router";
|
||||
@@ -24,9 +22,10 @@ import { TabNavBarComponent } from "./tab-nav-bar.component";
|
||||
imports: [TabListItemDirective, RouterModule],
|
||||
})
|
||||
export class TabLinkComponent implements FocusableOption, AfterViewInit {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
@ViewChild(TabListItemDirective) tabItem: TabListItemDirective;
|
||||
@ViewChild("rla") routerLinkActive: RouterLinkActive;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
readonly tabItem = viewChild.required(TabListItemDirective);
|
||||
readonly routerLinkActive = viewChild.required<RouterLinkActive>("rla");
|
||||
|
||||
readonly routerLinkMatchOptions: IsActiveMatchOptions = {
|
||||
queryParams: "ignored",
|
||||
@@ -43,25 +42,25 @@ export class TabLinkComponent implements FocusableOption, AfterViewInit {
|
||||
|
||||
@HostListener("keydown", ["$event"]) onKeyDown(event: KeyboardEvent) {
|
||||
if (event.code === "Space") {
|
||||
this.tabItem.click();
|
||||
this.tabItem().click();
|
||||
}
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this.routerLinkActive?.isActive ?? false;
|
||||
return this.routerLinkActive()?.isActive ?? false;
|
||||
}
|
||||
|
||||
constructor(private _tabNavBar: TabNavBarComponent) {}
|
||||
|
||||
focus(): void {
|
||||
this.tabItem.focus();
|
||||
this.tabItem().focus();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// The active state of tab links are tracked via the routerLinkActive directive
|
||||
// We need to watch for changes to tell the parent nav group when the tab is active
|
||||
this.routerLinkActive.isActiveChange
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
this.routerLinkActive()
|
||||
.isActiveChange.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((_) => this._tabNavBar.updateActiveLink());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FocusKeyManager } from "@angular/cdk/a11y";
|
||||
import {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ContentChildren,
|
||||
forwardRef,
|
||||
QueryList,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import { AfterContentInit, Component, forwardRef, input, contentChildren } from "@angular/core";
|
||||
|
||||
import { TabHeaderComponent } from "../shared/tab-header.component";
|
||||
import { TabListContainerDirective } from "../shared/tab-list-container.directive";
|
||||
@@ -24,17 +15,17 @@ import { TabLinkComponent } from "./tab-link.component";
|
||||
imports: [TabHeaderComponent, TabListContainerDirective],
|
||||
})
|
||||
export class TabNavBarComponent implements AfterContentInit {
|
||||
@ContentChildren(forwardRef(() => TabLinkComponent)) tabLabels: QueryList<TabLinkComponent>;
|
||||
readonly tabLabels = contentChildren(forwardRef(() => TabLinkComponent));
|
||||
readonly label = input("");
|
||||
|
||||
/**
|
||||
* Focus key manager for keeping tab controls accessible.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tablist_role#keyboard_interactions
|
||||
*/
|
||||
keyManager: FocusKeyManager<TabLinkComponent>;
|
||||
keyManager?: FocusKeyManager<TabLinkComponent>;
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.keyManager = new FocusKeyManager(this.tabLabels)
|
||||
this.keyManager = new FocusKeyManager(this.tabLabels())
|
||||
.withHorizontalOrientation("ltr")
|
||||
.withWrap()
|
||||
.withHomeAndEnd();
|
||||
@@ -42,10 +33,10 @@ export class TabNavBarComponent implements AfterContentInit {
|
||||
|
||||
updateActiveLink() {
|
||||
// Keep the keyManager in sync with active tabs
|
||||
const items = this.tabLabels.toArray();
|
||||
const items = this.tabLabels();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].active) {
|
||||
this.keyManager.updateActiveItem(i);
|
||||
this.keyManager?.updateActiveItem(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { Component, OnInit, viewChild } from "@angular/core";
|
||||
import { ToastContainerDirective, ToastrService } from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
@@ -7,12 +7,11 @@ import { ToastContainerDirective, ToastrService } from "ngx-toastr";
|
||||
imports: [ToastContainerDirective],
|
||||
})
|
||||
export class ToastContainerComponent implements OnInit {
|
||||
@ViewChild(ToastContainerDirective, { static: true })
|
||||
toastContainer?: ToastContainerDirective;
|
||||
readonly toastContainer = viewChild(ToastContainerDirective);
|
||||
|
||||
constructor(private toastrService: ToastrService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.toastrService.overlayContainer = this.toastContainer;
|
||||
this.toastrService.overlayContainer = this.toastContainer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
||||
|
||||
@@ -36,7 +34,7 @@ export class ToastService {
|
||||
: calculateToastTimeout(options.message),
|
||||
};
|
||||
|
||||
this.toastrService.show(null, options.title, toastrConfig);
|
||||
this.toastrService.show(undefined, options.title, toastrConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgClass } from "@angular/common";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
@@ -8,8 +6,8 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
signal,
|
||||
ViewChild,
|
||||
input,
|
||||
viewChild,
|
||||
} from "@angular/core";
|
||||
|
||||
import { ToggleGroupComponent } from "./toggle-group.component";
|
||||
@@ -24,9 +22,9 @@ let nextId = 0;
|
||||
export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewInit {
|
||||
id = nextId++;
|
||||
|
||||
readonly value = input<TValue>();
|
||||
@ViewChild("labelContent") labelContent: ElementRef<HTMLSpanElement>;
|
||||
@ViewChild("bitBadgeContainer") bitBadgeContainer: ElementRef<HTMLSpanElement>;
|
||||
readonly value = input.required<TValue>();
|
||||
readonly labelContent = viewChild<ElementRef<HTMLSpanElement>>("labelContent");
|
||||
readonly bitBadgeContainer = viewChild<ElementRef<HTMLSpanElement>>("bitBadgeContainer");
|
||||
|
||||
constructor(private groupComponent: ToggleGroupComponent<TValue>) {}
|
||||
|
||||
@@ -34,7 +32,7 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
|
||||
@HostBinding("class") classList = ["tw-group/toggle", "tw-flex", "tw-min-w-16"];
|
||||
|
||||
protected bitBadgeContainerHasChidlren = signal(false);
|
||||
protected labelTitle = signal<string>(null);
|
||||
protected labelTitle = signal<string | null>(null);
|
||||
|
||||
get name() {
|
||||
return this.groupComponent.name;
|
||||
@@ -100,12 +98,12 @@ export class ToggleComponent<TValue> implements AfterContentChecked, AfterViewIn
|
||||
|
||||
ngAfterContentChecked() {
|
||||
this.bitBadgeContainerHasChidlren.set(
|
||||
this.bitBadgeContainer?.nativeElement.childElementCount > 0,
|
||||
(this.bitBadgeContainer()?.nativeElement.childElementCount ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
const labelText = this.labelContent?.nativeElement.innerText;
|
||||
const labelText = this.labelContent()?.nativeElement.innerText;
|
||||
if (labelText) {
|
||||
this.labelTitle.set(labelText);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { booleanAttribute, Directive, HostBinding, input } from "@angular/core";
|
||||
|
||||
type TypographyType = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "body1" | "body2" | "helper";
|
||||
@@ -32,7 +30,7 @@ const margins: Record<TypographyType, string[]> = {
|
||||
selector: "[bitTypography]",
|
||||
})
|
||||
export class TypographyDirective {
|
||||
readonly bitTypography = input<TypographyType>();
|
||||
readonly bitTypography = input.required<TypographyType>();
|
||||
|
||||
readonly noMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
export class I18nMockService implements I18nService {
|
||||
userSetLocale$: Observable<string | undefined>;
|
||||
locale$: Observable<string>;
|
||||
supportedTranslationLocales: string[];
|
||||
translationLocale: string;
|
||||
collator: Intl.Collator;
|
||||
localeNames: Map<string, string>;
|
||||
|
||||
constructor(private lookupTable: Record<string, string | ((...args: string[]) => string)>) {}
|
||||
export class I18nMockService implements Pick<I18nService, "t" | "translate"> {
|
||||
constructor(
|
||||
private lookupTable: Record<string, string | ((...args: (string | undefined)[]) => string)>,
|
||||
) {}
|
||||
|
||||
t(id: string, p1?: string, p2?: string, p3?: string) {
|
||||
let value = this.lookupTable[id];
|
||||
|
||||
Reference in New Issue
Block a user