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