mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[PM-15847] libs/components strict migration (#15738)
This PR migrates `libs/components` to use strict TypeScript. - Remove `@ts-strict-ignore` from each file in `libs/components` and resolved any new compilation errors - Converted ViewChild and ContentChild decorators to use the new signal-based queries using the [Angular signal queries migration](https://angular.dev/reference/migrations/signal-queries) - Made view/content children `required` where appropriate, eliminating the need for additional null checking. This helped simplify the strict migration. --- Co-authored-by: Vicki League <vleague@bitwarden.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user