1
0
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:
Will Martin
2025-08-18 15:36:45 -04:00
committed by GitHub
parent f2d2d0a767
commit 827c4c0301
77 changed files with 450 additions and 612 deletions

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FocusableOption } from "@angular/cdk/a11y";
import { Directive, ElementRef, HostBinding, Input, input } from "@angular/core";
@@ -15,7 +13,7 @@ export class TabListItemDirective implements FocusableOption {
// TODO: Skipped for signal migration because:
// This input overrides a field from a superclass, while the superclass field
// is not migrated.
@Input() disabled: boolean;
@Input() disabled = false;
@HostBinding("attr.disabled")
get disabledAttr() {

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal";
import { Component, effect, HostBinding, input } from "@angular/core";
@@ -9,7 +7,7 @@ import { Component, effect, HostBinding, input } from "@angular/core";
imports: [CdkPortalOutlet],
})
export class TabBodyComponent {
private _firstRender: boolean;
private _firstRender = false;
readonly content = input<TemplatePortal>();
readonly preserveContent = input(false);

View File

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

View File

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

View File

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

View File

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

View File

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