mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[CL-707] Migrate CL codebase to signals (#15340)
This commit is contained in:
@@ -6,6 +6,8 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core";
|
||||
selector: "[appA11yTitle]",
|
||||
})
|
||||
export class A11yTitleDirective implements OnInit {
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input() set appA11yTitle(title: string) {
|
||||
this.title = title;
|
||||
this.setAttributes();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}"
|
||||
>
|
||||
<a
|
||||
*ngIf="!hideLogo"
|
||||
*ngIf="!hideLogo()"
|
||||
[routerLink]="['/']"
|
||||
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
|
||||
>
|
||||
@@ -14,29 +14,29 @@
|
||||
</a>
|
||||
|
||||
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
|
||||
<div *ngIf="!hideIcon" class="tw-w-24 sm:tw-w-28 md:tw-w-32 tw-mx-auto">
|
||||
<bit-icon [icon]="icon"></bit-icon>
|
||||
<div *ngIf="!hideIcon()" class="tw-w-24 sm:tw-w-28 md:tw-w-32 tw-mx-auto">
|
||||
<bit-icon [icon]="icon()"></bit-icon>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="title">
|
||||
<ng-container *ngIf="title()">
|
||||
<!-- Small screens -->
|
||||
<h1 bitTypography="h3" class="tw-mt-2 sm:tw-hidden">
|
||||
{{ title }}
|
||||
{{ title() }}
|
||||
</h1>
|
||||
<!-- Medium to Larger screens -->
|
||||
<h1 bitTypography="h2" class="tw-mt-2 tw-hidden sm:tw-block">
|
||||
{{ title }}
|
||||
{{ title() }}
|
||||
</h1>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="subtitle" class="tw-text-sm sm:tw-text-base">{{ subtitle }}</div>
|
||||
<div *ngIf="subtitle()" class="tw-text-sm sm:tw-text-base">{{ subtitle() }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tw-grow tw-w-full tw-mx-auto tw-flex tw-flex-col tw-items-center sm:tw-min-w-[28rem]"
|
||||
[ngClass]="maxWidthClass"
|
||||
>
|
||||
@if (hideCardWrapper) {
|
||||
@if (hideCardWrapper()) {
|
||||
<div class="tw-mb-6 sm:tw-mb-10">
|
||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||
</div>
|
||||
@@ -50,11 +50,11 @@
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
</div>
|
||||
|
||||
<footer *ngIf="!hideFooter" class="tw-text-center tw-mt-4 sm:tw-mt-6">
|
||||
<div *ngIf="showReadonlyHostname" bitTypography="body2">
|
||||
<footer *ngIf="!hideFooter()" class="tw-text-center tw-mt-4 sm:tw-mt-6">
|
||||
<div *ngIf="showReadonlyHostname()" bitTypography="body2">
|
||||
{{ "accessing" | i18n }} {{ hostname }}
|
||||
</div>
|
||||
<ng-container *ngIf="!showReadonlyHostname">
|
||||
<ng-container *ngIf="!showReadonlyHostname()">
|
||||
<ng-content select="[slot=environment-selector]"></ng-content>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!hideYearAndVersion">
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, HostBinding, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
HostBinding,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
input,
|
||||
model,
|
||||
} from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -29,21 +37,21 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
return ["tw-h-full"];
|
||||
}
|
||||
|
||||
@Input() title: string;
|
||||
@Input() subtitle: string;
|
||||
@Input() icon: Icon;
|
||||
@Input() showReadonlyHostname: boolean;
|
||||
@Input() hideLogo: boolean = false;
|
||||
@Input() hideFooter: boolean = false;
|
||||
@Input() hideIcon: boolean = false;
|
||||
@Input() hideCardWrapper: boolean = false;
|
||||
readonly title = input<string>();
|
||||
readonly subtitle = input<string>();
|
||||
readonly icon = model<Icon>();
|
||||
readonly showReadonlyHostname = input<boolean>(false);
|
||||
readonly hideLogo = input<boolean>(false);
|
||||
readonly hideFooter = input<boolean>(false);
|
||||
readonly hideIcon = input<boolean>(false);
|
||||
readonly hideCardWrapper = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Max width of the anon layout title, subtitle, and content areas.
|
||||
*
|
||||
* @default 'md'
|
||||
*/
|
||||
@Input() maxWidth: AnonLayoutMaxWidth = "md";
|
||||
readonly maxWidth = model<AnonLayoutMaxWidth>("md");
|
||||
|
||||
protected logo = BitwardenLogo;
|
||||
protected year: string;
|
||||
@@ -54,7 +62,8 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
protected hideYearAndVersion = false;
|
||||
|
||||
get maxWidthClass(): string {
|
||||
switch (this.maxWidth) {
|
||||
const maxWidth = this.maxWidth();
|
||||
switch (maxWidth) {
|
||||
case "md":
|
||||
return "tw-max-w-md";
|
||||
case "lg":
|
||||
@@ -78,19 +87,19 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.maxWidth = this.maxWidth ?? "md";
|
||||
this.maxWidth.set(this.maxWidth() ?? "md");
|
||||
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
|
||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||
|
||||
// If there is no icon input, then use the default icon
|
||||
if (this.icon == null) {
|
||||
this.icon = AnonLayoutBitwardenShield;
|
||||
if (this.icon() == null) {
|
||||
this.icon.set(AnonLayoutBitwardenShield);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.maxWidth) {
|
||||
this.maxWidth = changes.maxWidth.currentValue ?? "md";
|
||||
this.maxWidth.set(changes.maxWidth.currentValue ?? "md");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,17 +19,7 @@ class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
|
||||
getClientType = () => ClientType.Web;
|
||||
}
|
||||
|
||||
type StoryArgs = Pick<
|
||||
AnonLayoutComponent,
|
||||
| "title"
|
||||
| "subtitle"
|
||||
| "showReadonlyHostname"
|
||||
| "hideCardWrapper"
|
||||
| "hideIcon"
|
||||
| "hideLogo"
|
||||
| "hideFooter"
|
||||
| "maxWidth"
|
||||
> & {
|
||||
type StoryArgs = AnonLayoutComponent & {
|
||||
contentLength: "normal" | "long" | "thin";
|
||||
showSecondary: boolean;
|
||||
useDefaultIcon: boolean;
|
||||
@@ -72,15 +62,14 @@ export default {
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
render: (args: StoryArgs) => {
|
||||
render: (args) => {
|
||||
const { useDefaultIcon, icon, ...rest } = args;
|
||||
return {
|
||||
props: {
|
||||
...rest,
|
||||
icon: useDefaultIcon ? null : icon,
|
||||
},
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<auth-anon-layout
|
||||
[title]="title"
|
||||
[subtitle]="subtitle"
|
||||
@@ -160,7 +149,7 @@ export default {
|
||||
contentLength: "normal",
|
||||
showSecondary: false,
|
||||
},
|
||||
} as Meta<StoryArgs>;
|
||||
} satisfies Meta<StoryArgs>;
|
||||
|
||||
type Story = StoryObj<StoryArgs>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, HostListener, Input, OnDestroy, Optional } from "@angular/core";
|
||||
import { Directive, HostListener, model, OnDestroy, Optional } from "@angular/core";
|
||||
import { BehaviorSubject, finalize, Subject, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -38,7 +38,7 @@ export class BitActionDirective implements OnDestroy {
|
||||
|
||||
disabled = false;
|
||||
|
||||
@Input("bitAction") handler: FunctionReturningAwaitable;
|
||||
readonly handler = model<FunctionReturningAwaitable>(undefined, { alias: "bitAction" });
|
||||
|
||||
constructor(
|
||||
private buttonComponent: ButtonLikeAbstraction,
|
||||
@@ -48,12 +48,12 @@ export class BitActionDirective implements OnDestroy {
|
||||
|
||||
@HostListener("click")
|
||||
protected async onClick() {
|
||||
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled()) {
|
||||
if (!this.handler() || this.loading || this.disabled || this.buttonComponent.disabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
functionToObservable(this.handler)
|
||||
functionToObservable(this.handler())
|
||||
.pipe(
|
||||
tap({
|
||||
error: (err: unknown) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, Input, OnDestroy, OnInit, Optional } from "@angular/core";
|
||||
import { Directive, OnDestroy, OnInit, Optional, input } from "@angular/core";
|
||||
import { FormGroupDirective } from "@angular/forms";
|
||||
import { BehaviorSubject, catchError, filter, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
@@ -20,9 +20,9 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
|
||||
private _loading$ = new BehaviorSubject<boolean>(false);
|
||||
private _disabled$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
@Input("bitSubmit") handler: FunctionReturningAwaitable;
|
||||
readonly handler = input<FunctionReturningAwaitable>(undefined, { alias: "bitSubmit" });
|
||||
|
||||
@Input() allowDisabledFormSubmit?: boolean = false;
|
||||
readonly allowDisabledFormSubmit = input<boolean>(false);
|
||||
|
||||
readonly loading$ = this._loading$.asObservable();
|
||||
readonly disabled$ = this._disabled$.asObservable();
|
||||
@@ -38,7 +38,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
|
||||
switchMap(() => {
|
||||
// Calling functionToObservable executes the sync part of the handler
|
||||
// allowing the function to check form validity before it gets disabled.
|
||||
const awaitable = functionToObservable(this.handler);
|
||||
const awaitable = functionToObservable(this.handler());
|
||||
|
||||
// Disable form
|
||||
this.loading = true;
|
||||
@@ -61,7 +61,7 @@ export class BitSubmitDirective implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formGroupDirective.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
|
||||
if (this.allowDisabledFormSubmit) {
|
||||
if (this.allowDisabledFormSubmit()) {
|
||||
this._disabled$.next(false);
|
||||
} else {
|
||||
this._disabled$.next(c === "DISABLED");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, Input, OnDestroy, Optional } from "@angular/core";
|
||||
import { Directive, OnDestroy, Optional, input } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
@@ -29,8 +29,8 @@ import { BitSubmitDirective } from "./bit-submit.directive";
|
||||
export class BitFormButtonDirective implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@Input() type: string;
|
||||
@Input() disabled?: boolean;
|
||||
readonly type = input<string>();
|
||||
readonly disabled = input<boolean>();
|
||||
|
||||
constructor(
|
||||
buttonComponent: ButtonLikeAbstraction,
|
||||
@@ -39,16 +39,17 @@ export class BitFormButtonDirective implements OnDestroy {
|
||||
) {
|
||||
if (submitDirective && buttonComponent) {
|
||||
submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
|
||||
if (this.type === "submit") {
|
||||
if (this.type() === "submit") {
|
||||
buttonComponent.loading.set(loading);
|
||||
} else {
|
||||
buttonComponent.disabled.set(this.disabled || loading);
|
||||
buttonComponent.disabled.set(this.disabled() || loading);
|
||||
}
|
||||
});
|
||||
|
||||
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
||||
if (this.disabled !== false) {
|
||||
buttonComponent.disabled.set(this.disabled || disabled);
|
||||
const disabledValue = this.disabled();
|
||||
if (disabledValue !== false) {
|
||||
buttonComponent.disabled.set(disabledValue || disabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ const template = /*html*/ `
|
||||
template,
|
||||
selector: "app-promise-example",
|
||||
imports: [AsyncActionsModule, ButtonModule, IconButtonModule],
|
||||
standalone: true,
|
||||
})
|
||||
class PromiseExampleComponent {
|
||||
statusEmoji = "🟡";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { Component, OnChanges, input } from "@angular/core";
|
||||
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -25,17 +25,17 @@ const SizeClasses: Record<SizeTypes, string[]> = {
|
||||
@Component({
|
||||
selector: "bit-avatar",
|
||||
template: `@if (src) {
|
||||
<img [src]="src" title="{{ title || text }}" [ngClass]="classList" />
|
||||
<img [src]="src" title="{{ title() || text() }}" [ngClass]="classList" />
|
||||
}`,
|
||||
imports: [NgClass],
|
||||
})
|
||||
export class AvatarComponent implements OnChanges {
|
||||
@Input() border = false;
|
||||
@Input() color?: string;
|
||||
@Input() id?: string;
|
||||
@Input() text?: string;
|
||||
@Input() title: string;
|
||||
@Input() size: SizeTypes = "default";
|
||||
readonly border = input(false);
|
||||
readonly color = input<string>();
|
||||
readonly id = input<string>();
|
||||
readonly text = input<string>();
|
||||
readonly title = input<string>();
|
||||
readonly size = input<SizeTypes>("default");
|
||||
|
||||
private svgCharCount = 2;
|
||||
private svgFontSize = 20;
|
||||
@@ -51,13 +51,13 @@ export class AvatarComponent implements OnChanges {
|
||||
|
||||
get classList() {
|
||||
return ["tw-rounded-full"]
|
||||
.concat(SizeClasses[this.size] ?? [])
|
||||
.concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
|
||||
.concat(SizeClasses[this.size()] ?? [])
|
||||
.concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []);
|
||||
}
|
||||
|
||||
private generate() {
|
||||
let chars: string = null;
|
||||
const upperCaseText = this.text?.toUpperCase() ?? "";
|
||||
const upperCaseText = this.text()?.toUpperCase() ?? "";
|
||||
|
||||
chars = this.getFirstLetters(upperCaseText, this.svgCharCount);
|
||||
|
||||
@@ -71,12 +71,13 @@ export class AvatarComponent implements OnChanges {
|
||||
}
|
||||
|
||||
let svg: HTMLElement;
|
||||
let hexColor = this.color;
|
||||
let hexColor = this.color();
|
||||
|
||||
if (!Utils.isNullOrWhitespace(this.color)) {
|
||||
const id = this.id();
|
||||
if (!Utils.isNullOrWhitespace(this.color())) {
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else if (!Utils.isNullOrWhitespace(this.id)) {
|
||||
hexColor = Utils.stringToColor(this.id.toString());
|
||||
} else if (!Utils.isNullOrWhitespace(id)) {
|
||||
hexColor = Utils.stringToColor(id.toString());
|
||||
svg = this.createSvgElement(this.svgSize, hexColor);
|
||||
} else {
|
||||
hexColor = Utils.stringToColor(upperCaseText);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="tw-inline-flex tw-flex-wrap tw-gap-2">
|
||||
@for (item of filteredItems; track item; let last = $last) {
|
||||
<span bitBadge [variant]="variant" [truncate]="truncate">
|
||||
<span bitBadge [variant]="variant()" [truncate]="truncate()">
|
||||
{{ item }}
|
||||
</span>
|
||||
@if (!last || isFiltered) {
|
||||
@@ -8,8 +8,8 @@
|
||||
}
|
||||
}
|
||||
@if (isFiltered) {
|
||||
<span bitBadge [variant]="variant">
|
||||
{{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }}
|
||||
<span bitBadge [variant]="variant()">
|
||||
{{ "plusNMore" | i18n: (items().length - filteredItems.length).toString() }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { Component, OnChanges, input } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { BadgeModule, BadgeVariant } from "../badge";
|
||||
|
||||
function transformMaxItems(value: number | undefined) {
|
||||
return value == undefined ? undefined : Math.max(1, value);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "bit-badge-list",
|
||||
templateUrl: "badge-list.component.html",
|
||||
imports: [BadgeModule, I18nPipe],
|
||||
})
|
||||
export class BadgeListComponent implements OnChanges {
|
||||
private _maxItems: number;
|
||||
|
||||
protected filteredItems: string[] = [];
|
||||
protected isFiltered = false;
|
||||
|
||||
@Input() variant: BadgeVariant = "primary";
|
||||
@Input() items: string[] = [];
|
||||
@Input() truncate = true;
|
||||
readonly variant = input<BadgeVariant>("primary");
|
||||
readonly items = input<string[]>([]);
|
||||
readonly truncate = input(true);
|
||||
|
||||
@Input()
|
||||
get maxItems(): number | undefined {
|
||||
return this._maxItems;
|
||||
}
|
||||
|
||||
set maxItems(value: number | undefined) {
|
||||
this._maxItems = value == undefined ? undefined : Math.max(1, value);
|
||||
}
|
||||
readonly maxItems = input(undefined, { transform: transformMaxItems });
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.maxItems == undefined || this.items.length <= this.maxItems) {
|
||||
this.filteredItems = this.items;
|
||||
const maxItems = this.maxItems();
|
||||
|
||||
if (maxItems == undefined || this.items().length <= maxItems) {
|
||||
this.filteredItems = this.items();
|
||||
} else {
|
||||
this.filteredItems = this.items.slice(0, this.maxItems - 1);
|
||||
this.filteredItems = this.items().slice(0, maxItems - 1);
|
||||
}
|
||||
this.isFiltered = this.items.length > this.filteredItems.length;
|
||||
this.isFiltered = this.items().length > this.filteredItems.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<span [ngClass]="{ 'tw-truncate tw-block': truncate }">
|
||||
<span [ngClass]="{ 'tw-truncate tw-block': truncate() }">
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
|
||||
import { Component, ElementRef, HostBinding, input } from "@angular/core";
|
||||
|
||||
import { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
@@ -89,33 +89,34 @@ export class BadgeComponent implements FocusableElement {
|
||||
"disabled:hover:!tw-text-muted",
|
||||
"disabled:tw-cursor-not-allowed",
|
||||
]
|
||||
.concat(styles[this.variant])
|
||||
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant], "tw-min-w-10"] : [])
|
||||
.concat(this.truncate ? this.maxWidthClass : []);
|
||||
.concat(styles[this.variant()])
|
||||
.concat(this.hasHoverEffects ? [...hoverStyles[this.variant()], "tw-min-w-10"] : [])
|
||||
.concat(this.truncate() ? this.maxWidthClass() : []);
|
||||
}
|
||||
@HostBinding("attr.title") get titleAttr() {
|
||||
if (this.title !== undefined) {
|
||||
return this.title;
|
||||
const title = this.title();
|
||||
if (title !== undefined) {
|
||||
return title;
|
||||
}
|
||||
return this.truncate ? this?.el?.nativeElement?.textContent?.trim() : null;
|
||||
return this.truncate() ? this?.el?.nativeElement?.textContent?.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional override for the automatic badge title when truncating.
|
||||
*/
|
||||
@Input() title?: string;
|
||||
readonly title = input<string>();
|
||||
|
||||
/**
|
||||
* Variant, sets the background color of the badge.
|
||||
*/
|
||||
@Input() variant: BadgeVariant = "primary";
|
||||
readonly variant = input<BadgeVariant>("primary");
|
||||
|
||||
/**
|
||||
* Truncate long text
|
||||
*/
|
||||
@Input() truncate = true;
|
||||
readonly truncate = input(true);
|
||||
|
||||
@Input() maxWidthClass: `tw-max-w-${string}` = "tw-max-w-40";
|
||||
readonly maxWidthClass = input<`tw-max-w-${string}`>("tw-max-w-40");
|
||||
|
||||
getFocusTarget() {
|
||||
return this.el.nativeElement;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-gap-2 tw-p-2 tw-ps-4 tw-text-main tw-border-transparent tw-bg-clip-padding tw-border-solid tw-border-b tw-border-0"
|
||||
[ngClass]="bannerClass"
|
||||
[attr.role]="useAlertRole ? 'status' : null"
|
||||
[attr.aria-live]="useAlertRole ? 'polite' : null"
|
||||
[attr.role]="useAlertRole() ? 'status' : null"
|
||||
[attr.aria-live]="useAlertRole() ? 'polite' : null"
|
||||
>
|
||||
@if (icon) {
|
||||
@if (icon(); as icon) {
|
||||
<i class="bwi tw-align-middle tw-text-base" [ngClass]="icon" aria-hidden="true"></i>
|
||||
}
|
||||
<!-- Overriding focus-visible color for link buttons for a11y against colored background -->
|
||||
@@ -12,7 +12,7 @@
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
<!-- Overriding hover and focus-visible colors for a11y against colored background -->
|
||||
@if (showClose) {
|
||||
@if (showClose()) {
|
||||
<button
|
||||
class="hover:tw-border-text-main focus-visible:before:tw-ring-text-main"
|
||||
type="button"
|
||||
|
||||
@@ -30,17 +30,17 @@ describe("BannerComponent", () => {
|
||||
});
|
||||
|
||||
it("should create with alert", () => {
|
||||
expect(component.useAlertRole).toBe(true);
|
||||
expect(component.useAlertRole()).toBe(true);
|
||||
const el = fixture.nativeElement.children[0];
|
||||
expect(el.getAttribute("role")).toEqual("status");
|
||||
expect(el.getAttribute("aria-live")).toEqual("polite");
|
||||
});
|
||||
|
||||
it("useAlertRole=false", () => {
|
||||
component.useAlertRole = false;
|
||||
fixture.componentRef.setInput("useAlertRole", false);
|
||||
fixture.autoDetectChanges();
|
||||
|
||||
expect(component.useAlertRole).toBe(false);
|
||||
expect(component.useAlertRole()).toBe(false);
|
||||
const el = fixture.nativeElement.children[0];
|
||||
expect(el.getAttribute("role")).toBeNull();
|
||||
expect(el.getAttribute("aria-live")).toBeNull();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
|
||||
import { Component, OnInit, Output, EventEmitter, input, model } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -31,19 +29,22 @@ const defaultIcon: Record<BannerType, string> = {
|
||||
imports: [CommonModule, IconButtonModule, I18nPipe],
|
||||
})
|
||||
export class BannerComponent implements OnInit {
|
||||
@Input("bannerType") bannerType: BannerType = "info";
|
||||
@Input() icon: string;
|
||||
@Input() useAlertRole = true;
|
||||
@Input() showClose = true;
|
||||
readonly bannerType = input<BannerType>("info");
|
||||
|
||||
readonly icon = model<string>();
|
||||
readonly useAlertRole = input(true);
|
||||
readonly showClose = input(true);
|
||||
|
||||
@Output() onClose = new EventEmitter<void>();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.icon ??= defaultIcon[this.bannerType];
|
||||
if (!this.icon()) {
|
||||
this.icon.set(defaultIcon[this.bannerType()]);
|
||||
}
|
||||
}
|
||||
|
||||
get bannerClass() {
|
||||
switch (this.bannerType) {
|
||||
switch (this.bannerType()) {
|
||||
case "danger":
|
||||
return "tw-bg-danger-100 tw-border-b-danger-700";
|
||||
case "info":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-template>
|
||||
@if (icon) {
|
||||
@if (icon(); as icon) {
|
||||
<i class="bwi {{ icon }} !tw-me-2" aria-hidden="true"></i>
|
||||
}
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core";
|
||||
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
|
||||
import { QueryParamsHandling } from "@angular/router";
|
||||
|
||||
@Component({
|
||||
@@ -9,17 +9,13 @@ import { QueryParamsHandling } from "@angular/router";
|
||||
templateUrl: "./breadcrumb.component.html",
|
||||
})
|
||||
export class BreadcrumbComponent {
|
||||
@Input()
|
||||
icon?: string;
|
||||
readonly icon = input<string>();
|
||||
|
||||
@Input()
|
||||
route?: string | any[] = undefined;
|
||||
readonly route = input<string | any[]>();
|
||||
|
||||
@Input()
|
||||
queryParams?: Record<string, string> = {};
|
||||
readonly queryParams = input<Record<string, string>>({});
|
||||
|
||||
@Input()
|
||||
queryParamsHandling?: QueryParamsHandling;
|
||||
readonly queryParamsHandling = input<QueryParamsHandling>();
|
||||
|
||||
@Output()
|
||||
click = new EventEmitter();
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
@for (breadcrumb of beforeOverflow; track breadcrumb; let last = $last) {
|
||||
@if (breadcrumb.route) {
|
||||
@if (breadcrumb.route(); as route) {
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
class="tw-my-2 tw-inline-block"
|
||||
[routerLink]="breadcrumb.route"
|
||||
[queryParams]="breadcrumb.queryParams"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling"
|
||||
[routerLink]="route"
|
||||
[queryParams]="breadcrumb.queryParams()"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</a>
|
||||
}
|
||||
@if (!breadcrumb.route) {
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
@@ -39,18 +38,17 @@
|
||||
></button>
|
||||
<bit-menu #overflowMenu>
|
||||
@for (breadcrumb of overflow; track breadcrumb) {
|
||||
@if (breadcrumb.route) {
|
||||
@if (breadcrumb.route(); as route) {
|
||||
<a
|
||||
bitMenuItem
|
||||
linkType="primary"
|
||||
[routerLink]="breadcrumb.route"
|
||||
[queryParams]="breadcrumb.queryParams"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling"
|
||||
[routerLink]="route"
|
||||
[queryParams]="breadcrumb.queryParams()"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</a>
|
||||
}
|
||||
@if (!breadcrumb.route) {
|
||||
} @else {
|
||||
<button type="button" bitMenuItem linkType="primary" (click)="breadcrumb.onClick($event)">
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</button>
|
||||
@@ -59,19 +57,18 @@
|
||||
</bit-menu>
|
||||
<i class="bwi bwi-angle-right tw-mx-1.5 tw-text-main"></i>
|
||||
@for (breadcrumb of afterOverflow; track breadcrumb; let last = $last) {
|
||||
@if (breadcrumb.route) {
|
||||
@if (breadcrumb.route(); as route) {
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
class="tw-my-2 tw-inline-block"
|
||||
[routerLink]="breadcrumb.route"
|
||||
[queryParams]="breadcrumb.queryParams"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling"
|
||||
[routerLink]="route"
|
||||
[queryParams]="breadcrumb.queryParams()"
|
||||
[queryParamsHandling]="breadcrumb.queryParamsHandling()"
|
||||
>
|
||||
<ng-container [ngTemplateOutlet]="breadcrumb.content"></ng-container>
|
||||
</a>
|
||||
}
|
||||
@if (!breadcrumb.route) {
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ContentChildren, Input, QueryList } from "@angular/core";
|
||||
import { Component, ContentChildren, QueryList, input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
@@ -19,8 +19,7 @@ import { BreadcrumbComponent } from "./breadcrumb.component";
|
||||
imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule],
|
||||
})
|
||||
export class BreadcrumbsComponent {
|
||||
@Input()
|
||||
show = 3;
|
||||
readonly show = input(3);
|
||||
|
||||
private breadcrumbs: BreadcrumbComponent[] = [];
|
||||
|
||||
@@ -31,14 +30,14 @@ export class BreadcrumbsComponent {
|
||||
|
||||
protected get beforeOverflow() {
|
||||
if (this.hasOverflow) {
|
||||
return this.breadcrumbs.slice(0, this.show - 1);
|
||||
return this.breadcrumbs.slice(0, this.show() - 1);
|
||||
}
|
||||
|
||||
return this.breadcrumbs;
|
||||
}
|
||||
|
||||
protected get overflow() {
|
||||
return this.breadcrumbs.slice(this.show - 1, -1);
|
||||
return this.breadcrumbs.slice(this.show() - 1, -1);
|
||||
}
|
||||
|
||||
protected get afterOverflow() {
|
||||
@@ -46,6 +45,6 @@ export class BreadcrumbsComponent {
|
||||
}
|
||||
|
||||
protected get hasOverflow() {
|
||||
return this.breadcrumbs.length > this.show;
|
||||
return this.breadcrumbs.length > this.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass } from "@angular/common";
|
||||
import {
|
||||
Input,
|
||||
HostBinding,
|
||||
Component,
|
||||
model,
|
||||
@@ -10,6 +8,7 @@ import {
|
||||
ElementRef,
|
||||
inject,
|
||||
Signal,
|
||||
booleanAttribute,
|
||||
} from "@angular/core";
|
||||
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
|
||||
import { debounce, interval } from "rxjs";
|
||||
@@ -79,7 +78,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
"hover:tw-no-underline",
|
||||
"focus:tw-outline-none",
|
||||
]
|
||||
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
|
||||
.concat(this.block() ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
|
||||
.concat(
|
||||
this.showDisabledStyles() || this.disabled()
|
||||
? [
|
||||
@@ -95,7 +94,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
]
|
||||
: [],
|
||||
)
|
||||
.concat(buttonStyles[this.buttonType ?? "secondary"])
|
||||
.concat(buttonStyles[this.buttonType() ?? "secondary"])
|
||||
.concat(buttonSizeStyles[this.size() || "default"]);
|
||||
}
|
||||
|
||||
@@ -116,22 +115,13 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
||||
});
|
||||
|
||||
@Input() buttonType: ButtonType = "secondary";
|
||||
readonly buttonType = input<ButtonType>("secondary");
|
||||
|
||||
size = input<ButtonSize>("default");
|
||||
readonly size = input<ButtonSize>("default");
|
||||
|
||||
private _block = false;
|
||||
readonly block = input(false, { transform: booleanAttribute });
|
||||
|
||||
@Input()
|
||||
get block(): boolean {
|
||||
return this._block;
|
||||
}
|
||||
|
||||
set block(value: boolean | "") {
|
||||
this._block = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
loading = model<boolean>(false);
|
||||
readonly loading = model<boolean>(false);
|
||||
|
||||
/**
|
||||
* Determine whether it is appropriate to display a loading spinner. We only want to show
|
||||
@@ -149,7 +139,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
|
||||
);
|
||||
|
||||
disabled = model<boolean>(false);
|
||||
readonly disabled = model<boolean>(false);
|
||||
private el = inject(ElementRef<HTMLButtonElement>);
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
[ngClass]="calloutClass"
|
||||
[attr.aria-labelledby]="titleId"
|
||||
>
|
||||
@if (title) {
|
||||
@if (titleComputed(); as title) {
|
||||
<header
|
||||
id="{{ titleId }}"
|
||||
class="tw-mb-1 tw-mt-0 tw-text-base tw-font-semibold tw-flex tw-gap-2 tw-items-start"
|
||||
>
|
||||
@if (icon) {
|
||||
@if (iconComputed(); as icon) {
|
||||
<i
|
||||
class="bwi !tw-text-main tw-relative tw-top-[3px]"
|
||||
[ngClass]="[icon]"
|
||||
|
||||
@@ -30,31 +30,31 @@ describe("Callout", () => {
|
||||
|
||||
describe("default state", () => {
|
||||
it("success", () => {
|
||||
component.type = "success";
|
||||
fixture.componentRef.setInput("type", "success");
|
||||
fixture.detectChanges();
|
||||
expect(component.title).toBeUndefined();
|
||||
expect(component.icon).toBe("bwi-check-circle");
|
||||
expect(component.titleComputed()).toBeUndefined();
|
||||
expect(component.iconComputed()).toBe("bwi-check-circle");
|
||||
});
|
||||
|
||||
it("info", () => {
|
||||
component.type = "info";
|
||||
fixture.componentRef.setInput("type", "info");
|
||||
fixture.detectChanges();
|
||||
expect(component.title).toBeUndefined();
|
||||
expect(component.icon).toBe("bwi-info-circle");
|
||||
expect(component.titleComputed()).toBeUndefined();
|
||||
expect(component.iconComputed()).toBe("bwi-info-circle");
|
||||
});
|
||||
|
||||
it("warning", () => {
|
||||
component.type = "warning";
|
||||
fixture.componentRef.setInput("type", "warning");
|
||||
fixture.detectChanges();
|
||||
expect(component.title).toBe("Warning");
|
||||
expect(component.icon).toBe("bwi-exclamation-triangle");
|
||||
expect(component.titleComputed()).toBe("Warning");
|
||||
expect(component.iconComputed()).toBe("bwi-exclamation-triangle");
|
||||
});
|
||||
|
||||
it("danger", () => {
|
||||
component.type = "danger";
|
||||
fixture.componentRef.setInput("type", "danger");
|
||||
fixture.detectChanges();
|
||||
expect(component.title).toBe("Error");
|
||||
expect(component.icon).toBe("bwi-error");
|
||||
expect(component.titleComputed()).toBe("Error");
|
||||
expect(component.iconComputed()).toBe("bwi-error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Component, computed, input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@@ -34,24 +34,28 @@ let nextId = 0;
|
||||
templateUrl: "callout.component.html",
|
||||
imports: [SharedModule, TypographyModule],
|
||||
})
|
||||
export class CalloutComponent implements OnInit {
|
||||
@Input() type: CalloutTypes = "info";
|
||||
@Input() icon: string;
|
||||
@Input() title: string;
|
||||
@Input() useAlertRole = false;
|
||||
export class CalloutComponent {
|
||||
readonly type = input<CalloutTypes>("info");
|
||||
readonly icon = input<string>();
|
||||
readonly title = input<string>();
|
||||
readonly useAlertRole = input(false);
|
||||
readonly iconComputed = computed(() => this.icon() ?? defaultIcon[this.type()]);
|
||||
readonly titleComputed = computed(() => {
|
||||
const title = this.title();
|
||||
const type = this.type();
|
||||
if (title == null && defaultI18n[type] != null) {
|
||||
return this.i18nService.t(defaultI18n[type]);
|
||||
}
|
||||
|
||||
return title;
|
||||
});
|
||||
|
||||
protected titleId = `bit-callout-title-${nextId++}`;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.icon ??= defaultIcon[this.type];
|
||||
if (this.title == null && defaultI18n[this.type] != null) {
|
||||
this.title = this.i18nService.t(defaultI18n[this.type]);
|
||||
}
|
||||
}
|
||||
|
||||
get calloutClass() {
|
||||
switch (this.type) {
|
||||
switch (this.type()) {
|
||||
case "danger":
|
||||
return "tw-bg-danger-100";
|
||||
case "info":
|
||||
|
||||
@@ -68,3 +68,18 @@ export const Danger: Story = {
|
||||
type: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomIcon: Story = {
|
||||
...Info,
|
||||
args: {
|
||||
...Info.args,
|
||||
icon: "bwi-star",
|
||||
},
|
||||
};
|
||||
|
||||
export const NoTitle: Story = {
|
||||
...Info,
|
||||
args: {
|
||||
icon: "bwi-star",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -104,6 +104,8 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
||||
protected indeterminateImage =
|
||||
`url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@HostBinding()
|
||||
@Input()
|
||||
get disabled() {
|
||||
@@ -114,6 +116,8 @@ export class CheckboxComponent implements BitFormControlAbstraction {
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input()
|
||||
get required() {
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ const template = /*html*/ `
|
||||
@Component({
|
||||
selector: "app-example",
|
||||
template,
|
||||
imports: [CheckboxModule, FormFieldModule, ReactiveFormsModule],
|
||||
imports: [FormControlModule, CheckboxModule, FormsModule, FormFieldModule, ReactiveFormsModule],
|
||||
})
|
||||
class ExampleComponent {
|
||||
protected formObj = this.formBuilder.group({
|
||||
|
||||
@@ -69,10 +69,10 @@
|
||||
bitMenuItem
|
||||
(click)="viewOption(parent, $event)"
|
||||
class="tw-text-[length:inherit]"
|
||||
[title]="'backTo' | i18n: parent.label ?? placeholderText"
|
||||
[title]="'backTo' | i18n: parent.label ?? placeholderText()"
|
||||
>
|
||||
<i slot="start" class="bwi bwi-angle-left" aria-hidden="true"></i>
|
||||
{{ "backTo" | i18n: parent.label ?? placeholderText }}
|
||||
{{ "backTo" | i18n: parent.label ?? placeholderText() }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
booleanAttribute,
|
||||
inject,
|
||||
signal,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
@@ -54,12 +55,15 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
@ViewChild("chipSelectButton") chipSelectButton: ElementRef<HTMLButtonElement>;
|
||||
|
||||
/** Text to show when there is no selected option */
|
||||
@Input({ required: true }) placeholderText: string;
|
||||
readonly placeholderText = input.required<string>();
|
||||
|
||||
/** Icon to show when there is no selected option or the selected option does not have an icon */
|
||||
@Input() placeholderIcon: string;
|
||||
readonly placeholderIcon = input<string>();
|
||||
|
||||
private _options: ChipSelectOption<T>[];
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
/** The select options to render */
|
||||
@Input({ required: true })
|
||||
get options(): ChipSelectOption<T>[] {
|
||||
@@ -71,10 +75,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
}
|
||||
|
||||
/** Disables the entire chip */
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Your application code writes to the input. This prevents migration.
|
||||
@Input({ transform: booleanAttribute }) disabled = false;
|
||||
|
||||
/** Chip will stretch to full width of its container */
|
||||
@Input({ transform: booleanAttribute }) fullWidth?: boolean;
|
||||
readonly fullWidth = input<boolean, unknown>(undefined, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* We have `:focus-within` and `:focus-visible` but no `:focus-visible-within`
|
||||
@@ -91,7 +97,7 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
|
||||
@HostBinding("class")
|
||||
get classList() {
|
||||
return ["tw-inline-block", this.fullWidth ? "tw-w-full" : "tw-max-w-52"];
|
||||
return ["tw-inline-block", this.fullWidth() ? "tw-w-full" : "tw-max-w-52"];
|
||||
}
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
@@ -113,12 +119,12 @@ export class ChipSelectComponent<T = unknown> implements ControlValueAccessor, A
|
||||
|
||||
/** The label to show in the chip button */
|
||||
protected get label(): string {
|
||||
return this.selectedOption?.label || this.placeholderText;
|
||||
return this.selectedOption?.label || this.placeholderText();
|
||||
}
|
||||
|
||||
/** The icon to show in the chip button */
|
||||
protected get icon(): string {
|
||||
return this.selectedOption?.icon || this.placeholderIcon;
|
||||
return this.selectedOption?.icon || this.placeholderIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Directive, HostListener, Input, InjectionToken, Inject, Optional } from "@angular/core";
|
||||
import {
|
||||
Directive,
|
||||
HostListener,
|
||||
Input,
|
||||
InjectionToken,
|
||||
Inject,
|
||||
Optional,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -28,13 +36,13 @@ export class CopyClickDirective {
|
||||
@Optional() @Inject(COPY_CLICK_LISTENER) private copyListener?: CopyClickListener,
|
||||
) {}
|
||||
|
||||
@Input("appCopyClick") valueToCopy = "";
|
||||
readonly valueToCopy = input("", { alias: "appCopyClick" });
|
||||
|
||||
/**
|
||||
* When set, the toast displayed will show `<valueLabel> copied`
|
||||
* instead of the default messaging.
|
||||
*/
|
||||
@Input() valueLabel?: string;
|
||||
readonly valueLabel = input<string>();
|
||||
|
||||
/**
|
||||
* When set without a value, a success toast will be shown when the value is copied
|
||||
@@ -49,6 +57,8 @@ export class CopyClickDirective {
|
||||
* <app-component [appCopyClick]="value to copy" showToast="info"/></app-component>
|
||||
* ```
|
||||
*/
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input() set showToast(value: ToastVariant | "") {
|
||||
// When the `showToast` is set without a value, an empty string will be passed
|
||||
if (value === "") {
|
||||
@@ -60,15 +70,17 @@ export class CopyClickDirective {
|
||||
}
|
||||
|
||||
@HostListener("click") onClick() {
|
||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
||||
const valueToCopy = this.valueToCopy();
|
||||
this.platformUtilsService.copyToClipboard(valueToCopy);
|
||||
|
||||
if (this.copyListener) {
|
||||
this.copyListener.onCopy(this.valueToCopy);
|
||||
this.copyListener.onCopy(valueToCopy);
|
||||
}
|
||||
|
||||
if (this._showToast) {
|
||||
const message = this.valueLabel
|
||||
? this.i18nService.t("valueCopied", this.valueLabel)
|
||||
const valueLabel = this.valueLabel();
|
||||
const message = valueLabel
|
||||
? this.i18nService.t("valueCopied", valueLabel)
|
||||
: this.i18nService.t("copySuccessful");
|
||||
|
||||
this.toastService.showToast({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
>
|
||||
@let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
|
||||
@let showHeaderBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().top;
|
||||
<header
|
||||
class="tw-flex tw-justify-between tw-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid"
|
||||
[ngClass]="{
|
||||
@@ -22,10 +22,10 @@
|
||||
noMargin
|
||||
class="tw-text-main tw-mb-0 tw-truncate"
|
||||
>
|
||||
{{ title }}
|
||||
@if (subtitle) {
|
||||
{{ title() }}
|
||||
@if (subtitle(); as subtitleText) {
|
||||
<span class="tw-text-muted tw-font-normal tw-text-sm">
|
||||
{{ subtitle }}
|
||||
{{ subtitleText }}
|
||||
</span>
|
||||
}
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
@@ -46,12 +46,12 @@
|
||||
<div
|
||||
class="tw-relative tw-flex-1 tw-flex tw-flex-col tw-overflow-hidden"
|
||||
[ngClass]="{
|
||||
'tw-min-h-60': loading,
|
||||
'tw-bg-background': background === 'default',
|
||||
'tw-bg-background-alt': background === 'alt',
|
||||
'tw-min-h-60': loading(),
|
||||
'tw-bg-background': background() === 'default',
|
||||
'tw-bg-background-alt': background() === 'alt',
|
||||
}"
|
||||
>
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<div class="tw-absolute tw-flex tw-size-full tw-items-center tw-justify-center">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-lg" [attr.aria-label]="'loading' | i18n"></i>
|
||||
</div>
|
||||
@@ -59,17 +59,17 @@
|
||||
<div
|
||||
cdkScrollable
|
||||
[ngClass]="{
|
||||
'tw-p-4': !disablePadding && !isDrawer,
|
||||
'tw-px-6 tw-py-4': !disablePadding && isDrawer,
|
||||
'tw-overflow-y-auto': !loading,
|
||||
'tw-invisible tw-overflow-y-hidden': loading,
|
||||
'tw-p-4': !disablePadding() && !isDrawer,
|
||||
'tw-px-6 tw-py-4': !disablePadding() && isDrawer,
|
||||
'tw-overflow-y-auto': !loading(),
|
||||
'tw-invisible tw-overflow-y-hidden': loading(),
|
||||
}"
|
||||
>
|
||||
<ng-content select="[bitDialogContent]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@let showFooterBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().bottom;
|
||||
@let showFooterBorder = !isDrawer || background() === "alt" || bodyHasScrolledFrom().bottom;
|
||||
<footer
|
||||
class="tw-flex tw-flex-row tw-items-center tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background"
|
||||
[ngClass]="[isDrawer ? 'tw-px-6 tw-py-4' : 'tw-p-4']"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { CdkScrollable } from "@angular/cdk/scrolling";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, HostBinding, Input, inject, viewChild } from "@angular/core";
|
||||
import { Component, HostBinding, inject, viewChild, input, booleanAttribute } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -40,39 +37,32 @@ export class DialogComponent {
|
||||
protected bodyHasScrolledFrom = hasScrolledFrom(this.scrollableBody);
|
||||
|
||||
/** Background color */
|
||||
@Input()
|
||||
background: "default" | "alt" = "default";
|
||||
readonly background = input<"default" | "alt">("default");
|
||||
|
||||
/**
|
||||
* Dialog size, more complex dialogs should use large, otherwise default is fine.
|
||||
*/
|
||||
@Input() dialogSize: "small" | "default" | "large" = "default";
|
||||
readonly dialogSize = input<"small" | "default" | "large">("default");
|
||||
|
||||
/**
|
||||
* Title to show in the dialog's header
|
||||
*/
|
||||
@Input() title: string;
|
||||
readonly title = input<string>();
|
||||
|
||||
/**
|
||||
* Subtitle to show in the dialog's header
|
||||
*/
|
||||
@Input() subtitle: string;
|
||||
readonly subtitle = input<string>();
|
||||
|
||||
private _disablePadding = false;
|
||||
/**
|
||||
* Disable the built-in padding on the dialog, for use with tabbed dialogs.
|
||||
*/
|
||||
@Input() set disablePadding(value: boolean | "") {
|
||||
this._disablePadding = coerceBooleanProperty(value);
|
||||
}
|
||||
get disablePadding() {
|
||||
return this._disablePadding;
|
||||
}
|
||||
readonly disablePadding = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Mark the dialog as loading which replaces the content with a spinner.
|
||||
*/
|
||||
@Input() loading = false;
|
||||
readonly loading = input(false);
|
||||
|
||||
@HostBinding("class") get classes() {
|
||||
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
|
||||
@@ -94,7 +84,7 @@ export class DialogComponent {
|
||||
}
|
||||
|
||||
get width() {
|
||||
switch (this.dialogSize) {
|
||||
switch (this.dialogSize()) {
|
||||
case "small": {
|
||||
return "md:tw-max-w-sm";
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Directive, HostBinding, HostListener, Input, Optional } from "@angular/core";
|
||||
import { Directive, HostBinding, HostListener, Optional, input } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitDialogClose]",
|
||||
})
|
||||
export class DialogCloseDirective {
|
||||
@Input("bitDialogClose") dialogResult: any;
|
||||
readonly dialogResult = input<any>(undefined, { alias: "bitDialogClose" });
|
||||
|
||||
constructor(@Optional() public dialogRef: DialogRef) {}
|
||||
|
||||
@@ -20,6 +20,6 @@ export class DialogCloseDirective {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(this.dialogResult);
|
||||
this.dialogRef.close(this.dialogResult());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CdkDialogContainer, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
||||
import { Directive, HostBinding, OnInit, Optional, input } from "@angular/core";
|
||||
|
||||
// Increments for each instance of this component
|
||||
let nextId = 0;
|
||||
@@ -10,7 +10,7 @@ let nextId = 0;
|
||||
export class DialogTitleContainerDirective implements OnInit {
|
||||
@HostBinding("id") id = `bit-dialog-title-${nextId++}`;
|
||||
|
||||
@Input() simple = false;
|
||||
readonly simple = input(false);
|
||||
|
||||
constructor(@Optional() private dialogRef: DialogRef<any>) {}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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";
|
||||
|
||||
@@ -12,17 +12,17 @@ export class DisclosureTriggerForDirective {
|
||||
/**
|
||||
* Accepts template reference for a bit-disclosure component instance
|
||||
*/
|
||||
@Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent;
|
||||
readonly disclosure = input<DisclosureComponent>(undefined, { alias: "bitDisclosureTriggerFor" });
|
||||
|
||||
@HostBinding("attr.aria-expanded") get ariaExpanded() {
|
||||
return this.disclosure.open;
|
||||
return this.disclosure().open;
|
||||
}
|
||||
|
||||
@HostBinding("attr.aria-controls") get ariaControls() {
|
||||
return this.disclosure.id;
|
||||
return this.disclosure().id;
|
||||
}
|
||||
|
||||
@HostListener("click") click() {
|
||||
this.disclosure.open = !this.disclosure.open;
|
||||
this.disclosure().open = !this.disclosure().open;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ export class DisclosureComponent {
|
||||
/**
|
||||
* Optionally init the disclosure in its opened state
|
||||
*/
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input({ transform: booleanAttribute }) set open(isOpen: boolean) {
|
||||
this._open = isOpen;
|
||||
this.openChange.emit(isOpen);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, ContentChild, HostBinding, Input } from "@angular/core";
|
||||
import { booleanAttribute, Component, ContentChild, HostBinding, input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -17,30 +16,18 @@ import { BitFormControlAbstraction } from "./form-control.abstraction";
|
||||
imports: [NgClass, TypographyDirective, I18nPipe],
|
||||
})
|
||||
export class FormControlComponent {
|
||||
@Input() label: string;
|
||||
readonly label = input<string>();
|
||||
|
||||
private _inline = false;
|
||||
@Input() get inline() {
|
||||
return this._inline;
|
||||
}
|
||||
set inline(value: boolean | "") {
|
||||
this._inline = coerceBooleanProperty(value);
|
||||
}
|
||||
readonly inline = input(false, { transform: booleanAttribute });
|
||||
|
||||
private _disableMargin = false;
|
||||
@Input() set disableMargin(value: boolean | "") {
|
||||
this._disableMargin = coerceBooleanProperty(value);
|
||||
}
|
||||
get disableMargin() {
|
||||
return this._disableMargin;
|
||||
}
|
||||
readonly disableMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
@ContentChild(BitFormControlAbstraction) protected formControl: BitFormControlAbstraction;
|
||||
|
||||
@HostBinding("class") get classes() {
|
||||
return []
|
||||
.concat(this.inline ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
|
||||
.concat(this.disableMargin ? [] : ["tw-mb-4"]);
|
||||
.concat(this.inline() ? ["tw-inline-block", "tw-me-4"] : ["tw-block"])
|
||||
.concat(this.disableMargin() ? [] : ["tw-mb-4"]);
|
||||
}
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, HostBinding, Input, Optional } from "@angular/core";
|
||||
import { Component, ElementRef, HostBinding, input, Optional } from "@angular/core";
|
||||
|
||||
import { FormControlComponent } from "./form-control.component";
|
||||
|
||||
@@ -12,6 +12,10 @@ let nextId = 0;
|
||||
selector: "bit-label",
|
||||
templateUrl: "label.component.html",
|
||||
imports: [CommonModule],
|
||||
host: {
|
||||
"[class]": "classList",
|
||||
"[id]": "id()",
|
||||
},
|
||||
})
|
||||
export class BitLabel {
|
||||
constructor(
|
||||
@@ -19,15 +23,19 @@ export class BitLabel {
|
||||
@Optional() private parentFormControl: FormControlComponent,
|
||||
) {}
|
||||
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return ["tw-inline-flex", "tw-gap-1", "tw-items-baseline", "tw-flex-row", "tw-min-w-0"];
|
||||
}
|
||||
readonly classList = [
|
||||
"tw-inline-flex",
|
||||
"tw-gap-1",
|
||||
"tw-items-baseline",
|
||||
"tw-flex-row",
|
||||
"tw-min-w-0",
|
||||
];
|
||||
|
||||
@HostBinding("title") get title() {
|
||||
return this.elementRef.nativeElement.textContent.trim();
|
||||
}
|
||||
|
||||
@HostBinding() @Input() id = `bit-label-${nextId++}`;
|
||||
readonly id = input(`bit-label-${nextId++}`);
|
||||
|
||||
get isInsideFormControl() {
|
||||
return !!this.parentFormControl;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 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 { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -18,11 +18,10 @@ import { I18nPipe } from "@bitwarden/ui-common";
|
||||
imports: [I18nPipe],
|
||||
})
|
||||
export class BitErrorSummary {
|
||||
@Input()
|
||||
formGroup: UntypedFormGroup;
|
||||
readonly formGroup = input<UntypedFormGroup>();
|
||||
|
||||
get errorCount(): number {
|
||||
return this.getErrorCount(this.formGroup);
|
||||
return this.getErrorCount(this.formGroup());
|
||||
}
|
||||
|
||||
get errorString() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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";
|
||||
|
||||
@@ -18,37 +18,38 @@ let nextId = 0;
|
||||
export class BitErrorComponent {
|
||||
@HostBinding() id = `bit-error-${nextId++}`;
|
||||
|
||||
@Input() error: [string, any];
|
||||
readonly error = input<[string, any]>();
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
get displayError() {
|
||||
switch (this.error[0]) {
|
||||
const error = this.error();
|
||||
switch (error[0]) {
|
||||
case "required":
|
||||
return this.i18nService.t("inputRequired");
|
||||
case "email":
|
||||
return this.i18nService.t("inputEmail");
|
||||
case "minlength":
|
||||
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
|
||||
return this.i18nService.t("inputMinLength", error[1]?.requiredLength);
|
||||
case "maxlength":
|
||||
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
|
||||
return this.i18nService.t("inputMaxLength", error[1]?.requiredLength);
|
||||
case "min":
|
||||
return this.i18nService.t("inputMinValue", this.error[1]?.min);
|
||||
return this.i18nService.t("inputMinValue", error[1]?.min);
|
||||
case "max":
|
||||
return this.i18nService.t("inputMaxValue", this.error[1]?.max);
|
||||
return this.i18nService.t("inputMaxValue", error[1]?.max);
|
||||
case "forbiddenCharacters":
|
||||
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
|
||||
return this.i18nService.t("inputForbiddenCharacters", error[1]?.characters.join(", "));
|
||||
case "multipleEmails":
|
||||
return this.i18nService.t("multipleInputEmails");
|
||||
case "trim":
|
||||
return this.i18nService.t("inputTrimValidator");
|
||||
default:
|
||||
// Attempt to show a custom error message.
|
||||
if (this.error[1]?.message) {
|
||||
return this.error[1]?.message;
|
||||
if (error[1]?.message) {
|
||||
return error[1]?.message;
|
||||
}
|
||||
|
||||
return this.error;
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
|
||||
import { ModelSignal, Signal } from "@angular/core";
|
||||
|
||||
// @ts-strict-ignore
|
||||
export type InputTypes =
|
||||
| "text"
|
||||
@@ -14,13 +17,13 @@ export type InputTypes =
|
||||
|
||||
export abstract class BitFormFieldControl {
|
||||
ariaDescribedBy: string;
|
||||
id: string;
|
||||
id: Signal<string>;
|
||||
labelForId: string;
|
||||
required: boolean;
|
||||
hasError: boolean;
|
||||
error: [string, any];
|
||||
type?: InputTypes;
|
||||
spellcheck?: boolean;
|
||||
type?: ModelSignal<InputTypes>;
|
||||
spellcheck?: ModelSignal<boolean | undefined>;
|
||||
readOnly?: boolean;
|
||||
focus?: () => void;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
ViewChild,
|
||||
signal,
|
||||
input,
|
||||
Input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -38,10 +39,11 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
|
||||
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
||||
|
||||
@Input({ transform: booleanAttribute })
|
||||
disableMargin = false;
|
||||
readonly disableMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/** If `true`, remove the bottom border for `readonly` inputs */
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Your application code writes to the input. This prevents migration.
|
||||
@Input({ transform: booleanAttribute })
|
||||
disableReadOnlyBorder = false;
|
||||
|
||||
@@ -76,7 +78,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
@HostBinding("class")
|
||||
get classList() {
|
||||
return ["tw-block"]
|
||||
.concat(this.disableMargin ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
|
||||
.concat(this.disableMargin() ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
|
||||
.concat(this.readOnly ? [] : "tw-pt-2");
|
||||
}
|
||||
|
||||
|
||||
@@ -414,13 +414,18 @@ export const Select: Story = {
|
||||
|
||||
export const AdvancedSelect: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
props: {
|
||||
formObj: fb.group({
|
||||
select: "value1",
|
||||
}),
|
||||
...args,
|
||||
},
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>Label</bit-label>
|
||||
<bit-select>
|
||||
<bit-option label="Select"></bit-option>
|
||||
<bit-option label="Other"></bit-option>
|
||||
<bit-select formControlName="select">
|
||||
<bit-option label="Select" value="value1"></bit-option>
|
||||
<bit-option label="Other" value="value2"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Host,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
model,
|
||||
OnChanges,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
@@ -18,12 +18,15 @@ import { BitFormFieldComponent } from "./form-field.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitPasswordInputToggle]",
|
||||
host: {
|
||||
"[attr.aria-pressed]": "toggled()",
|
||||
},
|
||||
})
|
||||
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
|
||||
/**
|
||||
* Whether the input is toggled to show the password.
|
||||
*/
|
||||
@HostBinding("attr.aria-pressed") @Input() toggled = false;
|
||||
readonly toggled = model(false);
|
||||
@Output() toggledChange = new EventEmitter<boolean>();
|
||||
|
||||
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
|
||||
@@ -33,8 +36,8 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
||||
* Click handler to toggle the state of the input type.
|
||||
*/
|
||||
@HostListener("click") onClick() {
|
||||
this.toggled = !this.toggled;
|
||||
this.toggledChange.emit(this.toggled);
|
||||
this.toggled.update((toggled) => !toggled);
|
||||
this.toggledChange.emit(this.toggled());
|
||||
|
||||
this.update();
|
||||
}
|
||||
@@ -46,7 +49,7 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
||||
) {}
|
||||
|
||||
get icon() {
|
||||
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
|
||||
return this.toggled() ? "bwi-eye-slash" : "bwi-eye";
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@@ -55,16 +58,16 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
if (this.formField.input?.type) {
|
||||
this.toggled = this.formField.input.type !== "password";
|
||||
this.toggled.set(this.formField.input.type() !== "password");
|
||||
}
|
||||
this.button.icon = this.icon;
|
||||
this.button.icon.set(this.icon);
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.button.icon = this.icon;
|
||||
this.button.icon.set(this.icon);
|
||||
if (this.formField.input?.type != null) {
|
||||
this.formField.input.type = this.toggled ? "text" : "password";
|
||||
this.formField.input.spellcheck = this.toggled ? false : undefined;
|
||||
this.formField.input.type.set(this.toggled() ? "text" : "password");
|
||||
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,15 +60,15 @@ describe("PasswordInputToggle", () => {
|
||||
|
||||
describe("initial state", () => {
|
||||
it("has correct icon", () => {
|
||||
expect(button.icon).toBe("bwi-eye");
|
||||
expect(button.icon()).toBe("bwi-eye");
|
||||
});
|
||||
|
||||
it("input is type password", () => {
|
||||
expect(input.type).toBe("password");
|
||||
expect(input.type!()).toBe("password");
|
||||
});
|
||||
|
||||
it("spellcheck is disabled", () => {
|
||||
expect(input.spellcheck).toBe(undefined);
|
||||
expect(input.spellcheck!()).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,15 +78,15 @@ describe("PasswordInputToggle", () => {
|
||||
});
|
||||
|
||||
it("has correct icon", () => {
|
||||
expect(button.icon).toBe("bwi-eye-slash");
|
||||
expect(button.icon()).toBe("bwi-eye-slash");
|
||||
});
|
||||
|
||||
it("input is type text", () => {
|
||||
expect(input.type).toBe("text");
|
||||
expect(input.type!()).toBe("text");
|
||||
});
|
||||
|
||||
it("spellcheck is disabled", () => {
|
||||
expect(input.spellcheck).toBe(false);
|
||||
expect(input.spellcheck!()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,15 +97,15 @@ describe("PasswordInputToggle", () => {
|
||||
});
|
||||
|
||||
it("has correct icon", () => {
|
||||
expect(button.icon).toBe("bwi-eye");
|
||||
expect(button.icon()).toBe("bwi-eye");
|
||||
});
|
||||
|
||||
it("input is type password", () => {
|
||||
expect(input.type).toBe("password");
|
||||
expect(input.type!()).toBe("password");
|
||||
});
|
||||
|
||||
it("spellcheck is disabled", () => {
|
||||
expect(input.spellcheck).toBe(undefined);
|
||||
expect(input.spellcheck!()).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
||||
import { Directive, OnInit, Optional } from "@angular/core";
|
||||
|
||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitPrefix]",
|
||||
host: {
|
||||
"[class]": "classList",
|
||||
},
|
||||
})
|
||||
export class BitPrefixDirective implements OnInit {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return ["tw-text-muted"];
|
||||
}
|
||||
readonly classList = ["tw-text-muted"];
|
||||
|
||||
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.iconButtonComponent) {
|
||||
this.iconButtonComponent.size = "small";
|
||||
this.iconButtonComponent.size.set("small");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
||||
import { Directive, OnInit, Optional } from "@angular/core";
|
||||
|
||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitSuffix]",
|
||||
host: {
|
||||
"[class]": "classList",
|
||||
},
|
||||
})
|
||||
export class BitSuffixDirective implements OnInit {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return ["tw-text-muted"];
|
||||
}
|
||||
readonly classList = ["tw-text-muted"];
|
||||
|
||||
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.iconButtonComponent) {
|
||||
this.iconButtonComponent.size = "small";
|
||||
this.iconButtonComponent.size.set("small");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-lg': size === 'default' }"
|
||||
[ngClass]="{ 'bwi-lg': size() === 'default' }"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
inject,
|
||||
Input,
|
||||
input,
|
||||
model,
|
||||
Signal,
|
||||
} from "@angular/core";
|
||||
@@ -177,11 +177,11 @@ const sizes: Record<IconButtonSize, string[]> = {
|
||||
},
|
||||
})
|
||||
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
|
||||
@Input("bitIconButton") icon: string;
|
||||
readonly icon = model<string>(undefined, { alias: "bitIconButton" });
|
||||
|
||||
@Input() buttonType: IconButtonType = "main";
|
||||
readonly buttonType = input<IconButtonType>("main");
|
||||
|
||||
@Input() size: IconButtonSize = "default";
|
||||
readonly size = model<IconButtonSize>("default");
|
||||
|
||||
@HostBinding("class") get classList() {
|
||||
return [
|
||||
@@ -193,13 +193,15 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
||||
"hover:tw-no-underline",
|
||||
"focus:tw-outline-none",
|
||||
]
|
||||
.concat(styles[this.buttonType])
|
||||
.concat(sizes[this.size])
|
||||
.concat(this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType] : []);
|
||||
.concat(styles[this.buttonType()])
|
||||
.concat(sizes[this.size()])
|
||||
.concat(
|
||||
this.showDisabledStyles() || this.disabled() ? disabledStyles[this.buttonType()] : [],
|
||||
);
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return [this.icon, "!tw-m-0"];
|
||||
return [this.icon(), "!tw-m-0"];
|
||||
}
|
||||
|
||||
protected disabledAttr = computed(() => {
|
||||
@@ -219,7 +221,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
||||
return this.showLoadingStyle() || (this.disabledAttr() && this.loading() === false);
|
||||
});
|
||||
|
||||
loading = model(false);
|
||||
readonly loading = model(false);
|
||||
|
||||
/**
|
||||
* Determine whether it is appropriate to display a loading spinner. We only want to show
|
||||
@@ -237,7 +239,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
||||
toObservable(this.loading).pipe(debounce((isLoading) => interval(isLoading ? 75 : 0))),
|
||||
);
|
||||
|
||||
disabled = model<boolean>(false);
|
||||
readonly disabled = model<boolean>(false);
|
||||
|
||||
getFocusTarget() {
|
||||
return this.elementRef.nativeElement;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, effect, input } from "@angular/core";
|
||||
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
|
||||
|
||||
import { Icon, isIcon } from "./icon";
|
||||
@@ -6,8 +6,8 @@ import { Icon, isIcon } from "./icon";
|
||||
@Component({
|
||||
selector: "bit-icon",
|
||||
host: {
|
||||
"[attr.aria-hidden]": "!ariaLabel",
|
||||
"[attr.aria-label]": "ariaLabel",
|
||||
"[attr.aria-hidden]": "!ariaLabel()",
|
||||
"[attr.aria-label]": "ariaLabel()",
|
||||
"[innerHtml]": "innerHtml",
|
||||
},
|
||||
template: ``,
|
||||
@@ -15,16 +15,18 @@ import { Icon, isIcon } from "./icon";
|
||||
export class BitIconComponent {
|
||||
innerHtml: SafeHtml | null = null;
|
||||
|
||||
@Input() set icon(icon: Icon) {
|
||||
if (!isIcon(icon)) {
|
||||
return;
|
||||
}
|
||||
readonly icon = input<Icon>();
|
||||
|
||||
const svg = icon.svg;
|
||||
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
|
||||
readonly ariaLabel = input<string>();
|
||||
|
||||
constructor(private domSanitizer: DomSanitizer) {
|
||||
effect(() => {
|
||||
const icon = this.icon();
|
||||
if (!isIcon(icon)) {
|
||||
return;
|
||||
}
|
||||
const svg = icon.svg;
|
||||
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
|
||||
});
|
||||
}
|
||||
|
||||
@Input() ariaLabel: string | undefined = undefined;
|
||||
|
||||
constructor(private domSanitizer: DomSanitizer) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Icon, svgIcon } from "./icon";
|
||||
import { BitIconComponent } from "./icon.component";
|
||||
|
||||
describe("IconComponent", () => {
|
||||
let component: BitIconComponent;
|
||||
let fixture: ComponentFixture<BitIconComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -13,14 +12,13 @@ describe("IconComponent", () => {
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BitIconComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should have empty innerHtml when input is not an Icon", () => {
|
||||
const fakeIcon = { svg: "harmful user input" } as Icon;
|
||||
|
||||
component.icon = fakeIcon;
|
||||
fixture.componentRef.setInput("icon", fakeIcon);
|
||||
fixture.detectChanges();
|
||||
|
||||
const el = fixture.nativeElement as HTMLElement;
|
||||
@@ -30,7 +28,7 @@ describe("IconComponent", () => {
|
||||
it("should contain icon when input is a safe Icon", () => {
|
||||
const icon = svgIcon`<svg><text x="0" y="15">safe icon</text></svg>`;
|
||||
|
||||
component.icon = icon;
|
||||
fixture.componentRef.setInput("icon", icon);
|
||||
fixture.detectChanges();
|
||||
|
||||
const el = fixture.nativeElement as HTMLElement;
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AfterContentChecked, Directive, ElementRef, Input, NgZone, Optional } from "@angular/core";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
booleanAttribute,
|
||||
Directive,
|
||||
ElementRef,
|
||||
input,
|
||||
NgZone,
|
||||
Optional,
|
||||
} from "@angular/core";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -21,11 +29,7 @@ import { FocusableElement } from "../shared/focusable-element";
|
||||
selector: "[appAutofocus], [bitAutofocus]",
|
||||
})
|
||||
export class AutofocusDirective implements AfterContentChecked {
|
||||
@Input() set appAutofocus(condition: boolean | string) {
|
||||
this.autofocus = condition === "" || condition === true;
|
||||
}
|
||||
|
||||
private autofocus: boolean;
|
||||
readonly appAutofocus = input(undefined, { transform: booleanAttribute });
|
||||
|
||||
// Track if we have already focused the element.
|
||||
private focused = false;
|
||||
@@ -46,7 +50,7 @@ export class AutofocusDirective implements AfterContentChecked {
|
||||
*/
|
||||
ngAfterContentChecked() {
|
||||
// We only want to focus the element on initial render and it's not a mobile browser
|
||||
if (this.focused || !this.autofocus || Utils.isMobileBrowser) {
|
||||
if (this.focused || !this.appAutofocus() || Utils.isMobileBrowser) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
21
libs/components/src/input/autofocus.mdx
Normal file
21
libs/components/src/input/autofocus.mdx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Meta, Canvas, Primary, Controls, Title, Description } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./autofocus.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
```ts
|
||||
import { AutofocusDirective } from "@bitwarden/components";
|
||||
```
|
||||
|
||||
<Title />
|
||||
<Description />
|
||||
|
||||
<Primary />
|
||||
<Controls />
|
||||
|
||||
## Accessibility
|
||||
|
||||
The autofocus directive has accessibility implications, because it will steal focus from wherever it
|
||||
would naturally be placed on page load. Please consider whether or not the user truly needs the
|
||||
element truly needs to be manually focused on their behalf.
|
||||
26
libs/components/src/input/autofocus.stories.ts
Normal file
26
libs/components/src/input/autofocus.stories.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
|
||||
import { FormFieldModule } from "../form-field";
|
||||
|
||||
import { AutofocusDirective } from "./autofocus.directive";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Form/Autofocus Directive",
|
||||
component: AutofocusDirective,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [AutofocusDirective, FormFieldModule],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
export const AutofocusField: StoryObj = {
|
||||
render: (args) => ({
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Email</bit-label>
|
||||
<input bitInput formControlName="email" appAutofocus />
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
NgZone,
|
||||
Optional,
|
||||
Self,
|
||||
input,
|
||||
model,
|
||||
} from "@angular/core";
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
@@ -30,9 +32,15 @@ export function inputBorderClasses(error: boolean) {
|
||||
@Directive({
|
||||
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
||||
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
|
||||
host: {
|
||||
"[class]": "classList()",
|
||||
"[id]": "id()",
|
||||
"[attr.type]": "type()",
|
||||
"[attr.spellcheck]": "spellcheck()",
|
||||
},
|
||||
})
|
||||
export class BitInputDirective implements BitFormFieldControl {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
classList() {
|
||||
const classes = [
|
||||
"tw-block",
|
||||
"tw-w-full",
|
||||
@@ -52,7 +60,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
return classes.filter((s) => s != "");
|
||||
}
|
||||
|
||||
@HostBinding() @Input() id = `bit-input-${nextId++}`;
|
||||
readonly id = input(`bit-input-${nextId++}`);
|
||||
|
||||
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
||||
|
||||
@@ -60,10 +68,12 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
return this.hasError ? true : undefined;
|
||||
}
|
||||
|
||||
@HostBinding("attr.type") @Input() type?: InputTypes;
|
||||
readonly type = model<InputTypes>();
|
||||
|
||||
@HostBinding("attr.spellcheck") @Input() spellcheck?: boolean;
|
||||
readonly spellcheck = model<boolean>();
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@HostBinding()
|
||||
@Input()
|
||||
get required() {
|
||||
@@ -74,13 +84,13 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
}
|
||||
private _required: boolean;
|
||||
|
||||
@Input() hasPrefix = false;
|
||||
@Input() hasSuffix = false;
|
||||
readonly hasPrefix = input(false);
|
||||
readonly hasSuffix = input(false);
|
||||
|
||||
@Input() showErrorsWhenDisabled? = false;
|
||||
readonly showErrorsWhenDisabled = input<boolean>(false);
|
||||
|
||||
get labelForId(): string {
|
||||
return this.id;
|
||||
return this.id();
|
||||
}
|
||||
|
||||
@HostListener("input")
|
||||
@@ -89,7 +99,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
if (this.showErrorsWhenDisabled) {
|
||||
if (this.showErrorsWhenDisabled()) {
|
||||
return (
|
||||
(this.ngControl?.status === "INVALID" || this.ngControl?.status === "DISABLED") &&
|
||||
this.ngControl?.touched &&
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
>
|
||||
<div
|
||||
[ngClass]="{
|
||||
'tw-truncate': truncate,
|
||||
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
|
||||
'tw-truncate': truncate(),
|
||||
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
|
||||
}"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
@@ -22,8 +22,8 @@
|
||||
bitTypography="helper"
|
||||
class="tw-text-muted tw-w-full"
|
||||
[ngClass]="{
|
||||
'tw-truncate': truncate,
|
||||
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
|
||||
'tw-truncate': truncate(),
|
||||
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate(),
|
||||
}"
|
||||
>
|
||||
<ng-content select="[slot=secondary]"></ng-content>
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
signal,
|
||||
ViewChild,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { TypographyModule } from "../typography";
|
||||
@@ -39,7 +39,7 @@ export class ItemContentComponent implements AfterContentChecked {
|
||||
*
|
||||
* Default behavior is truncation.
|
||||
*/
|
||||
@Input() truncate = true;
|
||||
readonly truncate = input(true);
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Input,
|
||||
HostBinding,
|
||||
Directive,
|
||||
inject,
|
||||
ElementRef,
|
||||
input,
|
||||
booleanAttribute,
|
||||
} from "@angular/core";
|
||||
import { HostBinding, Directive, inject, ElementRef, input, booleanAttribute } from "@angular/core";
|
||||
|
||||
import { ariaDisableElement } from "../utils";
|
||||
|
||||
@@ -77,8 +69,7 @@ const commonStyles = [
|
||||
|
||||
@Directive()
|
||||
abstract class LinkDirective {
|
||||
@Input()
|
||||
linkType: LinkType = "primary";
|
||||
readonly linkType = input<LinkType>("primary");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +87,7 @@ export class AnchorLinkDirective extends LinkDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return ["before:-tw-inset-y-[0.125rem]"]
|
||||
.concat(commonStyles)
|
||||
.concat(linkStyles[this.linkType] ?? []);
|
||||
.concat(linkStyles[this.linkType()] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +102,7 @@ export class ButtonLinkDirective extends LinkDirective {
|
||||
@HostBinding("class") get classList() {
|
||||
return ["before:-tw-inset-y-[0.25rem]"]
|
||||
.concat(commonStyles)
|
||||
.concat(linkStyles[this.linkType] ?? []);
|
||||
.concat(linkStyles[this.linkType()] ?? []);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -39,6 +39,9 @@ export class MenuItemDirective implements FocusableOption {
|
||||
return this.disabled || null; // native disabled attr must be null when false
|
||||
}
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// This input overrides a field from a superclass, while the superclass field
|
||||
// is not migrated.
|
||||
@Input({ transform: coerceBooleanProperty }) disabled?: boolean = false;
|
||||
|
||||
constructor(public elementRef: ElementRef<HTMLButtonElement>) {}
|
||||
|
||||
@@ -8,26 +8,30 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
ViewContainerRef,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
import { filter, mergeWith } from "rxjs/operators";
|
||||
|
||||
import { MenuComponent } from "./menu.component";
|
||||
|
||||
@Directive({ selector: "[bitMenuTriggerFor]", exportAs: "menuTrigger", standalone: true })
|
||||
@Directive({
|
||||
selector: "[bitMenuTriggerFor]",
|
||||
exportAs: "menuTrigger",
|
||||
standalone: true,
|
||||
host: { "[attr.role]": "this.role()" },
|
||||
})
|
||||
export class MenuTriggerForDirective implements OnDestroy {
|
||||
@HostBinding("attr.aria-expanded") isOpen = false;
|
||||
@HostBinding("attr.aria-haspopup") get hasPopup(): "menu" | "dialog" {
|
||||
return this.menu?.ariaRole || "menu";
|
||||
return this.menu()?.ariaRole() || "menu";
|
||||
}
|
||||
@HostBinding("attr.role")
|
||||
@Input()
|
||||
role = "button";
|
||||
|
||||
@Input("bitMenuTriggerFor") menu: MenuComponent;
|
||||
readonly role = input("button");
|
||||
|
||||
readonly menu = input<MenuComponent>(undefined, { alias: "bitMenuTriggerFor" });
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private defaultMenuConfig: OverlayConfig = {
|
||||
@@ -66,14 +70,15 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
}
|
||||
|
||||
private openMenu() {
|
||||
if (this.menu == null) {
|
||||
const menu = this.menu();
|
||||
if (menu == null) {
|
||||
throw new Error("Cannot find bit-menu element");
|
||||
}
|
||||
|
||||
this.isOpen = true;
|
||||
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
|
||||
|
||||
const templatePortal = new TemplatePortal(this.menu.templateRef, this.viewContainerRef);
|
||||
const templatePortal = new TemplatePortal(menu.templateRef, this.viewContainerRef);
|
||||
this.overlayRef.attach(templatePortal);
|
||||
|
||||
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
|
||||
@@ -90,11 +95,11 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
}
|
||||
this.destroyMenu();
|
||||
});
|
||||
if (this.menu.keyManager) {
|
||||
this.menu.keyManager.setFirstItemActive();
|
||||
if (menu.keyManager) {
|
||||
menu.keyManager.setFirstItemActive();
|
||||
this.keyDownEventsSub = this.overlayRef
|
||||
.keydownEvents()
|
||||
.subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event));
|
||||
.subscribe((event: KeyboardEvent) => this.menu().keyManager.onKeydown(event));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +110,19 @@ export class MenuTriggerForDirective implements OnDestroy {
|
||||
|
||||
this.isOpen = false;
|
||||
this.disposeAll();
|
||||
this.menu.closed.emit();
|
||||
this.menu().closed.emit();
|
||||
}
|
||||
|
||||
private getClosedEvents(): Observable<any> {
|
||||
const detachments = this.overlayRef.detachments();
|
||||
const escKey = this.overlayRef.keydownEvents().pipe(
|
||||
filter((event: KeyboardEvent) => {
|
||||
const keys = this.menu.ariaRole === "menu" ? ["Escape", "Tab"] : ["Escape"];
|
||||
const keys = this.menu().ariaRole() === "menu" ? ["Escape", "Tab"] : ["Escape"];
|
||||
return keys.includes(event.key);
|
||||
}),
|
||||
);
|
||||
const backdrop = this.overlayRef.backdropClick();
|
||||
const menuClosed = this.menu.closed;
|
||||
const menuClosed = this.menu().closed;
|
||||
|
||||
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div
|
||||
(click)="closed.emit()"
|
||||
class="tw-flex tw-shrink-0 tw-flex-col tw-rounded-lg tw-border tw-border-solid tw-border-secondary-500 tw-bg-background tw-bg-clip-padding tw-py-1 tw-overflow-y-auto"
|
||||
[attr.role]="ariaRole"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.role]="ariaRole()"
|
||||
[attr.aria-label]="ariaLabel()"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="ariaRole === 'dialog'"
|
||||
[cdkTrapFocusAutoCapture]="ariaRole() === 'dialog'"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ContentChildren,
|
||||
QueryList,
|
||||
AfterContentInit,
|
||||
Input,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { MenuItemDirective } from "./menu-item.directive";
|
||||
@@ -28,12 +28,12 @@ export class MenuComponent implements AfterContentInit {
|
||||
menuItems: QueryList<MenuItemDirective>;
|
||||
keyManager?: FocusKeyManager<MenuItemDirective>;
|
||||
|
||||
@Input() ariaRole: "menu" | "dialog" = "menu";
|
||||
readonly ariaRole = input<"menu" | "dialog">("menu");
|
||||
|
||||
@Input() ariaLabel: string;
|
||||
readonly ariaLabel = input<string>();
|
||||
|
||||
ngAfterContentInit() {
|
||||
if (this.ariaRole === "menu") {
|
||||
if (this.ariaRole() === "menu") {
|
||||
this.keyManager = new FocusKeyManager(this.menuItems)
|
||||
.withWrap()
|
||||
.skipPredicate((item) => item.disabled);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<ng-select
|
||||
[items]="baseItems"
|
||||
[items]="baseItems()"
|
||||
[(ngModel)]="selectedItems"
|
||||
(ngModelChange)="onChange($event)"
|
||||
(blur)="onBlur()"
|
||||
bindLabel="listName"
|
||||
groupBy="parentGrouping"
|
||||
[placeholder]="placeholder"
|
||||
[loading]="loading"
|
||||
[placeholder]="placeholder()"
|
||||
[loading]="loading()"
|
||||
[loadingText]="loadingText"
|
||||
notFoundText="{{ 'multiSelectNotFound' | i18n }}"
|
||||
clearAllText="{{ 'multiSelectClearAll' | i18n }}"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||
import {
|
||||
Component,
|
||||
@@ -12,6 +11,9 @@ import {
|
||||
HostBinding,
|
||||
Optional,
|
||||
Self,
|
||||
input,
|
||||
model,
|
||||
booleanAttribute,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
@@ -38,6 +40,9 @@ let nextId = 0;
|
||||
templateUrl: "./multi-select.component.html",
|
||||
providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }],
|
||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, I18nPipe],
|
||||
host: {
|
||||
"[id]": "this.id()",
|
||||
},
|
||||
})
|
||||
/**
|
||||
* This component has been implemented to only support Multi-select list events
|
||||
@@ -46,12 +51,14 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||
|
||||
// Parent component should only pass selectable items (complete list - selected items = baseItems)
|
||||
@Input() baseItems: SelectItemView[];
|
||||
readonly baseItems = model<SelectItemView[]>();
|
||||
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
|
||||
@Input() removeSelectedItems = false;
|
||||
@Input() placeholder: string;
|
||||
@Input() loading = false;
|
||||
@Input({ transform: coerceBooleanProperty }) disabled?: boolean;
|
||||
readonly removeSelectedItems = input(false);
|
||||
readonly placeholder = model<string>();
|
||||
readonly loading = input(false);
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Your application code writes to the input. This prevents migration.
|
||||
@Input({ transform: booleanAttribute }) disabled?: boolean;
|
||||
|
||||
// Internal tracking of selected items
|
||||
protected selectedItems: SelectItemView[];
|
||||
@@ -79,7 +86,9 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
|
||||
ngOnInit(): void {
|
||||
// Default Text Values
|
||||
this.placeholder = this.placeholder ?? this.i18nService.t("multiSelectPlaceholder");
|
||||
this.placeholder.update(
|
||||
(placeholder) => placeholder ?? this.i18nService.t("multiSelectPlaceholder"),
|
||||
);
|
||||
this.loadingText = this.i18nService.t("multiSelectLoading");
|
||||
}
|
||||
|
||||
@@ -119,15 +128,15 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
this.onItemsConfirmed.emit(this.selectedItems);
|
||||
|
||||
// Remove selected items from base list based on input property
|
||||
if (this.removeSelectedItems) {
|
||||
let updatedBaseItems = this.baseItems;
|
||||
if (this.removeSelectedItems()) {
|
||||
let updatedBaseItems = this.baseItems();
|
||||
this.selectedItems.forEach((selectedItem) => {
|
||||
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
|
||||
});
|
||||
|
||||
// Reset Lists
|
||||
this.selectedItems = null;
|
||||
this.baseItems = updatedBaseItems;
|
||||
this.baseItems.set(updatedBaseItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +195,11 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
}
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
|
||||
readonly id = input(`bit-multi-select-${nextId++}`);
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@HostBinding("attr.required")
|
||||
@Input()
|
||||
get required() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Directive, EventEmitter, Output, input } from "@angular/core";
|
||||
import { RouterLink, RouterLinkActive } from "@angular/router";
|
||||
|
||||
/**
|
||||
@@ -11,17 +11,17 @@ export abstract class NavBaseComponent {
|
||||
/**
|
||||
* Text to display in main content
|
||||
*/
|
||||
@Input() text: string;
|
||||
readonly text = input<string>();
|
||||
|
||||
/**
|
||||
* `aria-label` for main content
|
||||
*/
|
||||
@Input() ariaLabel: string;
|
||||
readonly ariaLabel = input<string>();
|
||||
|
||||
/**
|
||||
* Optional icon, e.g. `"bwi-collection-shared"`
|
||||
*/
|
||||
@Input() icon: string;
|
||||
readonly icon = input<string>();
|
||||
|
||||
/**
|
||||
* Optional route to be passed to internal `routerLink`. If not provided, the nav component will render as a button.
|
||||
@@ -34,31 +34,31 @@ export abstract class NavBaseComponent {
|
||||
*
|
||||
* See: {@link https://github.com/angular/angular/issues/24482}
|
||||
*/
|
||||
@Input() route?: RouterLink["routerLink"];
|
||||
readonly route = input<RouterLink["routerLink"]>();
|
||||
|
||||
/**
|
||||
* Passed to internal `routerLink`
|
||||
*
|
||||
* See {@link RouterLink.relativeTo}
|
||||
*/
|
||||
@Input() relativeTo?: RouterLink["relativeTo"];
|
||||
readonly relativeTo = input<RouterLink["relativeTo"]>();
|
||||
|
||||
/**
|
||||
* Passed to internal `routerLink`
|
||||
*
|
||||
* See {@link RouterLinkActive.routerLinkActiveOptions}
|
||||
*/
|
||||
@Input() routerLinkActiveOptions?: RouterLinkActive["routerLinkActiveOptions"] = {
|
||||
readonly routerLinkActiveOptions = input<RouterLinkActive["routerLinkActiveOptions"]>({
|
||||
paths: "subset",
|
||||
queryParams: "ignored",
|
||||
fragment: "ignored",
|
||||
matrixParams: "ignored",
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* If `true`, do not change styles when nav item is active.
|
||||
*/
|
||||
@Input() hideActiveStyles = false;
|
||||
readonly hideActiveStyles = input(false);
|
||||
|
||||
/**
|
||||
* Fires when main content is clicked
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<!-- This a higher order component that composes `NavItemComponent` -->
|
||||
@if (!hideIfEmpty || nestedNavComponents.length > 0) {
|
||||
@if (!hideIfEmpty() || nestedNavComponents.length > 0) {
|
||||
<bit-nav-item
|
||||
[text]="text"
|
||||
[icon]="icon"
|
||||
[route]="route"
|
||||
[relativeTo]="relativeTo"
|
||||
[routerLinkActiveOptions]="routerLinkActiveOptions"
|
||||
[text]="text()"
|
||||
[icon]="icon()"
|
||||
[route]="route()"
|
||||
[relativeTo]="relativeTo()"
|
||||
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
||||
(mainContentClicked)="handleMainContentClicked()"
|
||||
[ariaLabel]="ariaLabel"
|
||||
[ariaLabel]="ariaLabel()"
|
||||
[hideActiveStyles]="parentHideActiveStyles"
|
||||
>
|
||||
<ng-template #button>
|
||||
<button
|
||||
type="button"
|
||||
class="tw-ms-auto"
|
||||
[bitIconButton]="open ? 'bwi-angle-up' : 'bwi-angle-down'"
|
||||
[bitIconButton]="open() ? 'bwi-angle-up' : 'bwi-angle-down'"
|
||||
[buttonType]="'light'"
|
||||
(click)="toggle($event)"
|
||||
size="small"
|
||||
[title]="'toggleCollapse' | i18n"
|
||||
aria-haspopup="true"
|
||||
[attr.aria-expanded]="open.toString()"
|
||||
[attr.aria-expanded]="open().toString()"
|
||||
[attr.aria-controls]="contentId"
|
||||
[attr.aria-label]="['toggleCollapse' | i18n, text].join(' ')"
|
||||
[attr.aria-label]="['toggleCollapse' | i18n, text()].join(' ')"
|
||||
></button>
|
||||
</ng-template>
|
||||
<ng-container slot="end">
|
||||
@@ -32,10 +32,10 @@
|
||||
</bit-nav-item>
|
||||
<!-- [attr.aria-controls] of the above button expects a unique ID on the controlled element -->
|
||||
@if (sideNavService.open$ | async) {
|
||||
@if (open) {
|
||||
@if (open()) {
|
||||
<div
|
||||
[attr.id]="contentId"
|
||||
[attr.aria-label]="[text, 'submenu' | i18n].join(' ')"
|
||||
[attr.aria-label]="[text(), 'submenu' | i18n].join(' ')"
|
||||
role="group"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
Component,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
SkipSelf,
|
||||
input,
|
||||
model,
|
||||
} from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -36,7 +37,7 @@ export class NavGroupComponent extends NavBaseComponent {
|
||||
|
||||
/** When the side nav is open, the parent nav item should not show active styles when open. */
|
||||
protected get parentHideActiveStyles(): boolean {
|
||||
return this.hideActiveStyles || (this.open && this.sideNavService.open);
|
||||
return this.hideActiveStyles() || (this.open() && this.sideNavService.open);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,14 +48,12 @@ export class NavGroupComponent extends NavBaseComponent {
|
||||
/**
|
||||
* Is `true` if the expanded content is visible
|
||||
*/
|
||||
@Input()
|
||||
open = false;
|
||||
readonly open = model(false);
|
||||
|
||||
/**
|
||||
* Automatically hide the nav group if there are no child buttons
|
||||
*/
|
||||
@Input({ transform: booleanAttribute })
|
||||
hideIfEmpty = false;
|
||||
readonly hideIfEmpty = input(false, { transform: booleanAttribute });
|
||||
|
||||
@Output()
|
||||
openChange = new EventEmitter<boolean>();
|
||||
@@ -67,24 +66,24 @@ export class NavGroupComponent extends NavBaseComponent {
|
||||
}
|
||||
|
||||
setOpen(isOpen: boolean) {
|
||||
this.open = isOpen;
|
||||
this.openChange.emit(this.open);
|
||||
this.open.set(isOpen);
|
||||
this.openChange.emit(this.open());
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
this.open && this.parentNavGroup?.setOpen(this.open);
|
||||
this.open() && this.parentNavGroup?.setOpen(this.open());
|
||||
}
|
||||
|
||||
protected toggle(event?: MouseEvent) {
|
||||
event?.stopPropagation();
|
||||
this.setOpen(!this.open);
|
||||
this.setOpen(!this.open());
|
||||
}
|
||||
|
||||
protected handleMainContentClicked() {
|
||||
if (!this.sideNavService.open) {
|
||||
if (!this.route) {
|
||||
if (!this.route()) {
|
||||
this.sideNavService.setOpen();
|
||||
}
|
||||
this.open = true;
|
||||
this.open.set(true);
|
||||
} else {
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="tw-ps-2 tw-pe-2">
|
||||
@let open = sideNavService.open$ | async;
|
||||
@if (open || icon) {
|
||||
@if (open || icon()) {
|
||||
<div
|
||||
class="tw-relative tw-rounded-md tw-h-10"
|
||||
[ngClass]="[
|
||||
@@ -16,17 +16,17 @@
|
||||
<!-- Main content of `NavItem` -->
|
||||
<ng-template #anchorAndButtonContent>
|
||||
<div
|
||||
[title]="text"
|
||||
[title]="text()"
|
||||
class="tw-gap-2 tw-items-center tw-font-bold tw-h-full tw-content-center"
|
||||
[ngClass]="{ 'tw-text-center': !open, 'tw-flex': open }"
|
||||
>
|
||||
<i
|
||||
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-alt2 {{ icon }}"
|
||||
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-alt2 {{ icon() }}"
|
||||
[attr.aria-hidden]="open"
|
||||
[attr.aria-label]="text"
|
||||
[attr.aria-label]="text()"
|
||||
></i>
|
||||
@if (open) {
|
||||
<span class="tw-truncate">{{ text }}</span>
|
||||
<span class="tw-truncate">{{ text() }}</span>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -38,11 +38,11 @@
|
||||
<a
|
||||
class="tw-size-full tw-px-3 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none"
|
||||
data-fvw
|
||||
[routerLink]="route"
|
||||
[relativeTo]="relativeTo"
|
||||
[attr.aria-label]="ariaLabel || text"
|
||||
[routerLink]="route()"
|
||||
[relativeTo]="relativeTo()"
|
||||
[attr.aria-label]="ariaLabel() || text()"
|
||||
routerLinkActive
|
||||
[routerLinkActiveOptions]="routerLinkActiveOptions"
|
||||
[routerLinkActiveOptions]="routerLinkActiveOptions()"
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
(isActiveChange)="setIsActive($event)"
|
||||
(click)="mainContentClicked.emit()"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, HostListener, Input, Optional } from "@angular/core";
|
||||
import { Component, HostListener, Optional, input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, map } from "rxjs";
|
||||
|
||||
@@ -21,7 +21,7 @@ export abstract class NavGroupAbstraction {
|
||||
})
|
||||
export class NavItemComponent extends NavBaseComponent {
|
||||
/** Forces active styles to be shown, regardless of the `routerLinkActiveOptions` */
|
||||
@Input() forceActiveStyles? = false;
|
||||
readonly forceActiveStyles = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Is `true` if `to` matches the current route
|
||||
@@ -34,7 +34,7 @@ export class NavItemComponent extends NavBaseComponent {
|
||||
}
|
||||
}
|
||||
protected get showActiveStyles() {
|
||||
return this.forceActiveStyles || (this._isActive && !this.hideActiveStyles);
|
||||
return this.forceActiveStyles() || (this._isActive && !this.hideActiveStyles());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
class="tw-px-2 tw-pt-5"
|
||||
>
|
||||
<a
|
||||
[routerLink]="route"
|
||||
[routerLink]="route()"
|
||||
class="tw-p-3 tw-block tw-rounded-md tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2 hover:tw-bg-hover-contrast [&_path]:tw-fill-text-alt2"
|
||||
[ngClass]="{ '[&_svg]:tw-w-[1.687rem]': !sideNavService.open }"
|
||||
[attr.aria-label]="label"
|
||||
[title]="label"
|
||||
[attr.aria-label]="label()"
|
||||
[title]="label()"
|
||||
routerLinkActive
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
>
|
||||
<bit-icon [icon]="sideNavService.open ? openIcon : closedIcon"></bit-icon>
|
||||
<bit-icon [icon]="sideNavService.open ? openIcon() : closedIcon()"></bit-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, input } from "@angular/core";
|
||||
import { RouterLinkActive, RouterLink } from "@angular/router";
|
||||
|
||||
import { Icon } from "../icon";
|
||||
@@ -17,18 +18,18 @@ import { SideNavService } from "./side-nav.service";
|
||||
})
|
||||
export class NavLogoComponent {
|
||||
/** Icon that is displayed when the side nav is closed */
|
||||
@Input() closedIcon = BitwardenShield;
|
||||
readonly closedIcon = input(BitwardenShield);
|
||||
|
||||
/** Icon that is displayed when the side nav is open */
|
||||
@Input({ required: true }) openIcon: Icon;
|
||||
readonly openIcon = input.required<Icon>();
|
||||
|
||||
/**
|
||||
* Route to be passed to internal `routerLink`
|
||||
*/
|
||||
@Input({ required: true }) route: string | any[];
|
||||
readonly route = input.required<string | any[]>();
|
||||
|
||||
/** Passed to `attr.aria-label` and `attr.title` */
|
||||
@Input({ required: true }) label: string;
|
||||
readonly label = input.required<string>();
|
||||
|
||||
constructor(protected sideNavService: SideNavService) {}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="tw-fixed md:tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-screen tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
|
||||
[ngClass]="{ 'tw-w-60': data.open }"
|
||||
[ngStyle]="
|
||||
variant === 'secondary' && {
|
||||
variant() === 'secondary' && {
|
||||
'--color-text-alt2': 'var(--color-text-main)',
|
||||
'--color-background-alt3': 'var(--color-secondary-100)',
|
||||
'--color-background-alt4': 'var(--color-secondary-300)',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
||||
import { Component, ElementRef, ViewChild, input } from "@angular/core";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@@ -19,7 +19,7 @@ export type SideNavVariant = "primary" | "secondary";
|
||||
imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe],
|
||||
})
|
||||
export class SideNavComponent {
|
||||
@Input() variant: SideNavVariant = "primary";
|
||||
readonly variant = input<SideNavVariant>("primary");
|
||||
|
||||
@ViewChild("toggleButton", { read: ElementRef, static: true })
|
||||
private toggleButton: ElementRef<HTMLButtonElement>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="tw-mx-auto tw-flex tw-flex-col tw-items-center tw-justify-center tw-pt-6">
|
||||
<div class="tw-max-w-sm tw-flex tw-flex-col tw-items-center">
|
||||
<bit-icon [icon]="icon" aria-hidden="true"></bit-icon>
|
||||
<bit-icon [icon]="icon()" aria-hidden="true"></bit-icon>
|
||||
<h3 class="tw-font-semibold tw-text-center tw-mt-4">
|
||||
<ng-content select="[slot=title]"></ng-content>
|
||||
</h3>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, input } from "@angular/core";
|
||||
|
||||
import { Icons } from "..";
|
||||
import { BitIconComponent } from "../icon/icon.component";
|
||||
@@ -12,5 +12,5 @@ import { BitIconComponent } from "../icon/icon.component";
|
||||
imports: [BitIconComponent],
|
||||
})
|
||||
export class NoItemsComponent {
|
||||
@Input() icon = Icons.Search;
|
||||
readonly icon = input(Icons.Search);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
ViewContainerRef,
|
||||
input,
|
||||
model,
|
||||
} from "@angular/core";
|
||||
import { Observable, Subscription, filter, mergeWith } from "rxjs";
|
||||
|
||||
@@ -20,27 +20,26 @@ import { PopoverComponent } from "./popover.component";
|
||||
@Directive({
|
||||
selector: "[bitPopoverTriggerFor]",
|
||||
exportAs: "popoverTrigger",
|
||||
host: {
|
||||
"[attr.aria-expanded]": "this.popoverOpen()",
|
||||
},
|
||||
})
|
||||
export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
@Input()
|
||||
@HostBinding("attr.aria-expanded")
|
||||
popoverOpen = false;
|
||||
readonly popoverOpen = model(false);
|
||||
|
||||
@Input("bitPopoverTriggerFor")
|
||||
popover: PopoverComponent;
|
||||
readonly popover = input<PopoverComponent>(undefined, { alias: "bitPopoverTriggerFor" });
|
||||
|
||||
@Input("position")
|
||||
position: string;
|
||||
readonly position = input<string>();
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private closedEventsSub: Subscription;
|
||||
|
||||
get positions() {
|
||||
if (!this.position) {
|
||||
if (!this.position()) {
|
||||
return defaultPositions;
|
||||
}
|
||||
|
||||
const preferredPosition = defaultPositions.find((position) => position.id === this.position);
|
||||
const preferredPosition = defaultPositions.find((position) => position.id === this.position());
|
||||
|
||||
if (preferredPosition) {
|
||||
return [preferredPosition, ...defaultPositions];
|
||||
@@ -72,7 +71,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
|
||||
@HostListener("click")
|
||||
togglePopover() {
|
||||
if (this.popoverOpen) {
|
||||
if (this.popoverOpen()) {
|
||||
this.closePopover();
|
||||
} else {
|
||||
this.openPopover();
|
||||
@@ -80,10 +79,10 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private openPopover() {
|
||||
this.popoverOpen = true;
|
||||
this.popoverOpen.set(true);
|
||||
this.overlayRef = this.overlay.create(this.defaultPopoverConfig);
|
||||
|
||||
const templatePortal = new TemplatePortal(this.popover.templateRef, this.viewContainerRef);
|
||||
const templatePortal = new TemplatePortal(this.popover().templateRef, this.viewContainerRef);
|
||||
|
||||
this.overlayRef.attach(templatePortal);
|
||||
this.closedEventsSub = this.getClosedEvents().subscribe(() => {
|
||||
@@ -97,17 +96,17 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
.keydownEvents()
|
||||
.pipe(filter((event: KeyboardEvent) => event.key === "Escape"));
|
||||
const backdrop = this.overlayRef.backdropClick();
|
||||
const popoverClosed = this.popover.closed;
|
||||
const popoverClosed = this.popover().closed;
|
||||
|
||||
return detachments.pipe(mergeWith(escKey, backdrop, popoverClosed));
|
||||
}
|
||||
|
||||
private destroyPopover() {
|
||||
if (this.overlayRef == null || !this.popoverOpen) {
|
||||
if (this.overlayRef == null || !this.popoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.popoverOpen = false;
|
||||
this.popoverOpen.set(false);
|
||||
this.disposeAll();
|
||||
}
|
||||
|
||||
@@ -117,7 +116,7 @@ export class PopoverTriggerForDirective implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (this.popoverOpen) {
|
||||
if (this.popoverOpen()) {
|
||||
this.openPopover();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<div class="tw-mb-1 tw-me-2 tw-flex tw-items-start tw-justify-between tw-gap-4 tw-ps-4">
|
||||
<h2 bitTypography="h5" class="tw-mt-1 tw-font-semibold">
|
||||
{{ title }}
|
||||
{{ title() }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { A11yModule } from "@angular/cdk/a11y";
|
||||
import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core";
|
||||
import { Component, EventEmitter, Output, TemplateRef, ViewChild, input } from "@angular/core";
|
||||
|
||||
import { IconButtonModule } from "../icon-button/icon-button.module";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
@@ -15,6 +15,6 @@ import { TypographyModule } from "../typography";
|
||||
})
|
||||
export class PopoverComponent {
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
@Input() title = "";
|
||||
readonly title = input("");
|
||||
@Output() closed = new EventEmitter();
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
attr.aria-valuenow="{{ barWidth }}"
|
||||
[ngStyle]="{ width: barWidth + '%' }"
|
||||
attr.aria-valuenow="{{ barWidth() }}"
|
||||
[ngStyle]="{ width: barWidth() + '%' }"
|
||||
>
|
||||
@if (displayText) {
|
||||
<div class="tw-flex tw-h-full tw-flex-wrap tw-items-center tw-overflow-hidden">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, input } from "@angular/core";
|
||||
|
||||
type ProgressSizeType = "small" | "default" | "large";
|
||||
type BackgroundType = "danger" | "primary" | "success" | "warning";
|
||||
@@ -26,19 +26,19 @@ const BackgroundClasses: Record<BackgroundType, string[]> = {
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class ProgressComponent {
|
||||
@Input() barWidth = 0;
|
||||
@Input() bgColor: BackgroundType = "primary";
|
||||
@Input() showText = true;
|
||||
@Input() size: ProgressSizeType = "default";
|
||||
@Input() text?: string;
|
||||
readonly barWidth = input(0);
|
||||
readonly bgColor = input<BackgroundType>("primary");
|
||||
readonly showText = input(true);
|
||||
readonly size = input<ProgressSizeType>("default");
|
||||
readonly text = input<string>();
|
||||
|
||||
get displayText() {
|
||||
return this.showText && this.size !== "small";
|
||||
return this.showText() && this.size() !== "small";
|
||||
}
|
||||
|
||||
get outerBarStyles() {
|
||||
return ["tw-overflow-hidden", "tw-rounded", "tw-bg-secondary-100"].concat(
|
||||
SizeClasses[this.size],
|
||||
SizeClasses[this.size()],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ export class ProgressComponent {
|
||||
"tw-text-contrast",
|
||||
"tw-transition-all",
|
||||
]
|
||||
.concat(SizeClasses[this.size])
|
||||
.concat(BackgroundClasses[this.bgColor]);
|
||||
.concat(SizeClasses[this.size()])
|
||||
.concat(BackgroundClasses[this.bgColor()]);
|
||||
}
|
||||
|
||||
get textContent() {
|
||||
return this.text || this.barWidth + "%";
|
||||
return this.text() || this.barWidth() + "%";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
type="radio"
|
||||
bitRadio
|
||||
[id]="inputId"
|
||||
[disabled]="groupDisabled || disabled"
|
||||
[value]="value"
|
||||
[disabled]="groupDisabled || disabled()"
|
||||
[value]="value()"
|
||||
[checked]="selected"
|
||||
(change)="onInputChange()"
|
||||
(blur)="onBlur()"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, DebugElement } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { By } from "@angular/platform-browser";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -10,70 +11,62 @@ import { RadioButtonModule } from "./radio-button.module";
|
||||
import { RadioGroupComponent } from "./radio-group.component";
|
||||
|
||||
describe("RadioButton", () => {
|
||||
let mockGroupComponent: MockedButtonGroupComponent;
|
||||
let fixture: ComponentFixture<TestApp>;
|
||||
let testAppComponent: TestApp;
|
||||
let radioButton: HTMLInputElement;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let radioButtonGroup: RadioGroupComponent;
|
||||
let radioButtons: DebugElement[];
|
||||
|
||||
beforeEach(async () => {
|
||||
mockGroupComponent = new MockedButtonGroupComponent();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TestApp],
|
||||
providers: [
|
||||
{ provide: RadioGroupComponent, useValue: mockGroupComponent },
|
||||
{ provide: I18nService, useValue: new I18nMockService({}) },
|
||||
],
|
||||
imports: [TestComponent],
|
||||
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
|
||||
});
|
||||
|
||||
await TestBed.compileComponents();
|
||||
fixture = TestBed.createComponent(TestApp);
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
testAppComponent = fixture.debugElement.componentInstance;
|
||||
radioButton = fixture.debugElement.query(By.css("input[type=radio]")).nativeElement;
|
||||
radioButtonGroup = fixture.debugElement.query(
|
||||
By.directive(RadioGroupComponent),
|
||||
).componentInstance;
|
||||
radioButtons = fixture.debugElement.queryAll(By.css("input[type=radio]"));
|
||||
});
|
||||
|
||||
it("should emit value when clicking on radio button", () => {
|
||||
testAppComponent.value = "value";
|
||||
const spyFn = jest.spyOn(radioButtonGroup, "onInputChange");
|
||||
|
||||
radioButtons[1].triggerEventHandler("change");
|
||||
fixture.detectChanges();
|
||||
|
||||
radioButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockGroupComponent.onInputChange).toHaveBeenCalledWith("value");
|
||||
expect(spyFn).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("should check radio button when selected matches value", () => {
|
||||
testAppComponent.value = "value";
|
||||
it("should check radio button only when selected matches value", () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
mockGroupComponent.selected = "value";
|
||||
expect(radioButtons[0].nativeElement.checked).toBe(true);
|
||||
expect(radioButtons[1].nativeElement.checked).toBe(false);
|
||||
|
||||
radioButtons[1].triggerEventHandler("change");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(radioButton.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("should not check radio button when selected does not match value", () => {
|
||||
testAppComponent.value = "value";
|
||||
fixture.detectChanges();
|
||||
|
||||
mockGroupComponent.selected = "nonMatchingValue";
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(radioButton.checked).toBe(false);
|
||||
expect(radioButtons[0].nativeElement.checked).toBe(false);
|
||||
expect(radioButtons[1].nativeElement.checked).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
class MockedButtonGroupComponent implements Partial<RadioGroupComponent> {
|
||||
onInputChange = jest.fn();
|
||||
selected: unknown = null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "test-app",
|
||||
template: `<bit-radio-button [value]="value"><bit-label>Element</bit-label></bit-radio-button>`,
|
||||
imports: [RadioButtonModule],
|
||||
selector: "test-component",
|
||||
template: `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-radio-group formControlName="radio">
|
||||
<bit-radio-button [value]="0"><bit-label>Element</bit-label></bit-radio-button>
|
||||
<bit-radio-button [value]="1"><bit-label>Element</bit-label></bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</form>
|
||||
`,
|
||||
imports: [FormsModule, ReactiveFormsModule, RadioButtonModule],
|
||||
})
|
||||
class TestApp {
|
||||
value?: string;
|
||||
class TestComponent {
|
||||
formObj = new FormGroup({
|
||||
radio: new FormControl(0),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
import { Component, HostBinding, input } from "@angular/core";
|
||||
|
||||
import { FormControlModule } from "../form-control/form-control.module";
|
||||
|
||||
@@ -11,20 +11,23 @@ let nextId = 0;
|
||||
selector: "bit-radio-button",
|
||||
templateUrl: "radio-button.component.html",
|
||||
imports: [FormControlModule, RadioInputComponent],
|
||||
host: {
|
||||
"[id]": "this.id()",
|
||||
},
|
||||
})
|
||||
export class RadioButtonComponent {
|
||||
@HostBinding("attr.id") @Input() id = `bit-radio-button-${nextId++}`;
|
||||
readonly id = input(`bit-radio-button-${nextId++}`);
|
||||
@HostBinding("class") get classList() {
|
||||
return [this.block ? "tw-block" : "tw-inline-block", "tw-mb-1", "[&_bit-hint]:tw-mt-0"];
|
||||
}
|
||||
|
||||
@Input() value: unknown;
|
||||
@Input() disabled = false;
|
||||
readonly value = input<unknown>();
|
||||
readonly disabled = input(false);
|
||||
|
||||
constructor(private groupComponent: RadioGroupComponent) {}
|
||||
|
||||
get inputId() {
|
||||
return `${this.id}-input`;
|
||||
return `${this.id()}-input`;
|
||||
}
|
||||
|
||||
get name() {
|
||||
@@ -32,7 +35,7 @@ export class RadioButtonComponent {
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this.groupComponent.selected === this.value;
|
||||
return this.groupComponent.selected === this.value();
|
||||
}
|
||||
|
||||
get groupDisabled() {
|
||||
@@ -40,11 +43,11 @@ export class RadioButtonComponent {
|
||||
}
|
||||
|
||||
get block() {
|
||||
return this.groupComponent.block;
|
||||
return this.groupComponent.block();
|
||||
}
|
||||
|
||||
protected onInputChange() {
|
||||
this.groupComponent.onInputChange(this.value);
|
||||
this.groupComponent.onInputChange(this.value());
|
||||
}
|
||||
|
||||
protected onBlur() {
|
||||
|
||||
@@ -177,15 +177,15 @@ export const Disabled: Story = {
|
||||
<bit-radio-group formControlName="radio" aria-label="Example radio group">
|
||||
<bit-label>Group of radio buttons</bit-label>
|
||||
|
||||
<bit-radio-button id="radio-first" [value]="0" [disabled]="true">
|
||||
<bit-radio-button [value]="0" [disabled]="true">
|
||||
<bit-label>First</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-second" [value]="1" [disabled]="true">
|
||||
<bit-radio-button [value]="1" [disabled]="true">
|
||||
<bit-label>Second</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
<bit-radio-button id="radio-third" [value]="2" [disabled]="true">
|
||||
<bit-radio-button [value]="2" [disabled]="true">
|
||||
<bit-label>Third</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NgTemplateOutlet } from "@angular/common";
|
||||
import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core";
|
||||
import { Component, ContentChild, HostBinding, Optional, Input, Self, input } from "@angular/core";
|
||||
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -14,11 +14,16 @@ let nextId = 0;
|
||||
selector: "bit-radio-group",
|
||||
templateUrl: "radio-group.component.html",
|
||||
imports: [NgTemplateOutlet, I18nPipe],
|
||||
host: {
|
||||
"[id]": "id()",
|
||||
},
|
||||
})
|
||||
export class RadioGroupComponent implements ControlValueAccessor {
|
||||
selected: unknown;
|
||||
disabled = false;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
private _name?: string;
|
||||
@Input() get name() {
|
||||
return this._name ?? this.ngControl?.name?.toString();
|
||||
@@ -27,10 +32,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
|
||||
this._name = value;
|
||||
}
|
||||
|
||||
@Input() block = false;
|
||||
readonly block = input(false);
|
||||
|
||||
@HostBinding("attr.role") role = "radiogroup";
|
||||
@HostBinding("attr.id") @Input() id = `bit-radio-group-${nextId++}`;
|
||||
readonly id = input(`bit-radio-group-${nextId++}`);
|
||||
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];
|
||||
|
||||
@ContentChild(BitLabel) protected label: BitLabel;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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, Input, Optional, Self } from "@angular/core";
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
import { BitFormControlAbstraction } from "../form-control";
|
||||
@@ -11,9 +11,12 @@ let nextId = 0;
|
||||
selector: "input[type=radio][bitRadio]",
|
||||
template: "",
|
||||
providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }],
|
||||
host: {
|
||||
"[id]": "this.id()",
|
||||
},
|
||||
})
|
||||
export class RadioInputComponent implements BitFormControlAbstraction {
|
||||
@HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`;
|
||||
readonly id = input(`bit-radio-input-${nextId++}`);
|
||||
|
||||
@HostBinding("class")
|
||||
protected inputClasses = [
|
||||
@@ -73,6 +76,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
|
||||
|
||||
constructor(@Optional() @Self() private ngControl?: NgControl) {}
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@HostBinding()
|
||||
@Input()
|
||||
get disabled() {
|
||||
@@ -83,6 +88,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input()
|
||||
get required() {
|
||||
return (
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
bitInput
|
||||
[type]="inputType"
|
||||
[id]="id"
|
||||
[placeholder]="placeholder ?? ('search' | i18n)"
|
||||
[placeholder]="placeholder() ?? ('search' | i18n)"
|
||||
class="tw-ps-9"
|
||||
[ngModel]="searchText"
|
||||
(ngModelChange)="onChange($event)"
|
||||
(blur)="onTouch()"
|
||||
[disabled]="disabled"
|
||||
[attr.autocomplete]="autocomplete"
|
||||
[disabled]="disabled()"
|
||||
[attr.autocomplete]="autocomplete()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
||||
import { Component, ElementRef, ViewChild, input, model } from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
NG_VALUE_ACCESSOR,
|
||||
@@ -43,9 +43,9 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
||||
// Use `type="text"` for Safari to improve rendering performance
|
||||
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@Input() placeholder: string;
|
||||
@Input() autocomplete: string;
|
||||
readonly disabled = model<boolean>();
|
||||
readonly placeholder = input<string>();
|
||||
readonly autocomplete = input<string>();
|
||||
|
||||
getFocusTarget() {
|
||||
return this.input?.nativeElement;
|
||||
@@ -76,6 +76,6 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
this.disabled.set(isDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Component, input } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "bit-section",
|
||||
@@ -9,7 +9,7 @@ import { Component, Input } from "@angular/core";
|
||||
<section
|
||||
[ngClass]="{
|
||||
'tw-mb-5 bit-compact:tw-mb-4 [&:not(bit-dialog_*):not(popup-page_*)]:md:tw-mb-12':
|
||||
!disableMargin,
|
||||
!disableMargin(),
|
||||
}"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
@@ -17,5 +17,5 @@ import { Component, Input } from "@angular/core";
|
||||
`,
|
||||
})
|
||||
export class SectionComponent {
|
||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||
readonly disableMargin = input(false, { transform: coerceBooleanProperty });
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, booleanAttribute } from "@angular/core";
|
||||
import { Component, booleanAttribute, input } from "@angular/core";
|
||||
|
||||
import { Option } from "./option";
|
||||
import { MappedOptionComponent } from "./option";
|
||||
|
||||
@Component({
|
||||
selector: "bit-option",
|
||||
template: `<ng-template><ng-content></ng-content></ng-template>`,
|
||||
})
|
||||
export class OptionComponent<T = unknown> implements Option<T> {
|
||||
@Input()
|
||||
icon?: string;
|
||||
export class OptionComponent<T = unknown> implements MappedOptionComponent<T> {
|
||||
readonly icon = input<string>();
|
||||
|
||||
@Input({ required: true })
|
||||
value: T;
|
||||
readonly value = input.required<T>();
|
||||
|
||||
@Input({ required: true })
|
||||
label: string;
|
||||
readonly label = input.required<string>();
|
||||
|
||||
@Input({ transform: booleanAttribute })
|
||||
disabled: boolean;
|
||||
readonly disabled = input(undefined, { transform: booleanAttribute });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { MappedDataToSignal } from "../shared/data-to-signal-type";
|
||||
|
||||
export interface Option<T> {
|
||||
icon?: string;
|
||||
value: T | null;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type MappedOptionComponent<T> = MappedDataToSignal<Option<T>>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<ng-select
|
||||
[(ngModel)]="selectedOption"
|
||||
[ngModel]="selectedOption()"
|
||||
(ngModelChange)="onChange($event)"
|
||||
[disabled]="disabled"
|
||||
[placeholder]="placeholder"
|
||||
[items]="items"
|
||||
[placeholder]="placeholder()"
|
||||
[items]="items()"
|
||||
(blur)="onBlur()"
|
||||
[labelForId]="labelForId"
|
||||
[clearable]="false"
|
||||
|
||||
@@ -37,15 +37,15 @@ describe("Select Component", () => {
|
||||
|
||||
describe("initial state", () => {
|
||||
it("selected option should update when items input changes", () => {
|
||||
expect(select.selectedOption?.value).toBeUndefined();
|
||||
expect(select.selectedOption()?.value).toBeUndefined();
|
||||
|
||||
select.items = [
|
||||
select.items.set([
|
||||
{ label: "Apple", value: "apple" },
|
||||
{ label: "Pear", value: "pear" },
|
||||
{ label: "Banana", value: "banana" },
|
||||
];
|
||||
]);
|
||||
|
||||
expect(select.selectedOption?.value).toBe("apple");
|
||||
expect(select.selectedOption()?.value).toBe("apple");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
ViewChild,
|
||||
Output,
|
||||
EventEmitter,
|
||||
input,
|
||||
Signal,
|
||||
computed,
|
||||
model,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
@@ -37,29 +42,23 @@ let nextId = 0;
|
||||
templateUrl: "select.component.html",
|
||||
providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }],
|
||||
imports: [NgSelectModule, ReactiveFormsModule, FormsModule],
|
||||
host: {
|
||||
"[id]": "id()",
|
||||
},
|
||||
})
|
||||
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
|
||||
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||
|
||||
private _items: Option<T>[] = [];
|
||||
/** Optional: Options can be provided using an array input or using `bit-option` */
|
||||
@Input()
|
||||
get items(): Option<T>[] {
|
||||
return this._items;
|
||||
}
|
||||
set items(next: Option<T>[]) {
|
||||
this._items = next;
|
||||
this._selectedOption = this.findSelectedOption(next, this.selectedValue);
|
||||
}
|
||||
readonly items = model<Option<T>[] | undefined>();
|
||||
|
||||
@Input() placeholder = this.i18nService.t("selectPlaceholder");
|
||||
readonly placeholder = input(this.i18nService.t("selectPlaceholder"));
|
||||
@Output() closed = new EventEmitter();
|
||||
|
||||
protected selectedValue: T;
|
||||
protected _selectedOption: Option<T>;
|
||||
get selectedOption() {
|
||||
return this._selectedOption;
|
||||
}
|
||||
protected selectedValue = signal<T>(undefined);
|
||||
selectedOption: Signal<Option<T>> = computed(() =>
|
||||
this.findSelectedOption(this.items(), this.selectedValue()),
|
||||
);
|
||||
protected searchInputId = `bit-select-search-input-${nextId++}`;
|
||||
|
||||
private notifyOnChange?: (value: T) => void;
|
||||
@@ -79,7 +78,14 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
if (value == null || value.length == 0) {
|
||||
return;
|
||||
}
|
||||
this.items = value.toArray();
|
||||
this.items.set(
|
||||
value.toArray().map((option) => ({
|
||||
icon: option.icon(),
|
||||
value: option.value(),
|
||||
label: option.label(),
|
||||
disabled: option.disabled(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@HostBinding("class") protected classes = ["tw-block", "tw-w-full", "tw-h-full"];
|
||||
@@ -89,6 +95,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
get disabledAttr() {
|
||||
return this.disabled || null;
|
||||
}
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input()
|
||||
get disabled() {
|
||||
return this._disabled ?? this.ngControl?.disabled ?? false;
|
||||
@@ -100,8 +108,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
|
||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||
writeValue(obj: T): void {
|
||||
this.selectedValue = obj;
|
||||
this._selectedOption = this.findSelectedOption(this.items, this.selectedValue);
|
||||
this.selectedValue.set(obj);
|
||||
}
|
||||
|
||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||
@@ -121,6 +128,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
|
||||
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||
protected onChange(option: Option<T> | null) {
|
||||
this.selectedValue.set(option?.value);
|
||||
|
||||
if (!this.notifyOnChange) {
|
||||
return;
|
||||
}
|
||||
@@ -154,9 +163,11 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
}
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
|
||||
readonly id = input(`bit-multi-select-${nextId++}`);
|
||||
|
||||
/**Implemented as part of BitFormFieldControl */
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@HostBinding("attr.required")
|
||||
@Input()
|
||||
get required() {
|
||||
@@ -178,8 +189,8 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
return [key, this.ngControl?.errors[key]];
|
||||
}
|
||||
|
||||
private findSelectedOption(items: Option<T>[], value: T): Option<T> | undefined {
|
||||
return items.find((item) => item.value === value);
|
||||
private findSelectedOption(items: Option<T>[] | undefined, value: T): Option<T> | undefined {
|
||||
return items?.find((item) => item.value === value);
|
||||
}
|
||||
|
||||
/**Emits the closed event. */
|
||||
|
||||
5
libs/components/src/shared/data-to-signal-type.ts
Normal file
5
libs/components/src/shared/data-to-signal-type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Signal } from "@angular/core";
|
||||
|
||||
export type MappedDataToSignal<T> = {
|
||||
[Property in keyof T]: Signal<T[Property]>;
|
||||
};
|
||||
@@ -42,6 +42,8 @@ export class StepperComponent extends CdkStepper {
|
||||
private initialOrientation: StepperOrientation | undefined = undefined;
|
||||
|
||||
// overriding CdkStepper orientation input so we can default to vertical
|
||||
// TODO: Skipped for signal migration because:
|
||||
// Accessor inputs cannot be migrated as they are too complex.
|
||||
@Input()
|
||||
override get orientation() {
|
||||
return this.internalOrientation || "vertical";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Directive, HostBinding, Input } from "@angular/core";
|
||||
import { Directive, HostBinding, input } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "tr[bitRow]",
|
||||
})
|
||||
export class RowDirective {
|
||||
@Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle";
|
||||
readonly alignContent = input<"top" | "middle" | "bottom" | "baseline">("middle");
|
||||
|
||||
get alignmentClass(): string {
|
||||
switch (this.alignContent) {
|
||||
switch (this.alignContent()) {
|
||||
case "top":
|
||||
return "tw-align-top";
|
||||
case "middle":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { NgClass } from "@angular/common";
|
||||
import { Component, HostBinding, Input, OnInit } from "@angular/core";
|
||||
import { Component, HostBinding, OnInit, input } from "@angular/core";
|
||||
|
||||
import type { SortDirection, SortFn } from "./table-data-source";
|
||||
import { TableComponent } from "./table.component";
|
||||
@@ -26,19 +26,17 @@ export class SortableComponent implements OnInit {
|
||||
/**
|
||||
* Mark the column as sortable and specify the key to sort by
|
||||
*/
|
||||
@Input() bitSortable: string;
|
||||
readonly bitSortable = input<string>();
|
||||
|
||||
private _default: SortDirection | boolean = false;
|
||||
/**
|
||||
* Mark the column as the default sort column
|
||||
*/
|
||||
@Input() set default(value: SortDirection | boolean | "") {
|
||||
if (value === "desc" || value === "asc") {
|
||||
this._default = value;
|
||||
} else {
|
||||
this._default = coerceBooleanProperty(value) ? "asc" : false;
|
||||
}
|
||||
}
|
||||
readonly default = input(false, {
|
||||
transform: (value: SortDirection | boolean | "") => {
|
||||
if (value === "desc" || value === "asc") {
|
||||
return value as SortDirection;
|
||||
} else {
|
||||
return coerceBooleanProperty(value) ? ("asc" as SortDirection) : false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom sorting function
|
||||
@@ -51,12 +49,12 @@ export class SortableComponent implements OnInit {
|
||||
* return direction === 'asc' ? result : -result;
|
||||
* }
|
||||
*/
|
||||
@Input() fn: SortFn;
|
||||
readonly fn = input<SortFn>();
|
||||
|
||||
constructor(private table: TableComponent) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this._default && !this.isActive) {
|
||||
if (this.default() && !this.isActive) {
|
||||
this.setActive();
|
||||
}
|
||||
}
|
||||
@@ -69,28 +67,29 @@ export class SortableComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected setActive() {
|
||||
if (this.table.dataSource) {
|
||||
const defaultDirection = this._default === "desc" ? "desc" : "asc";
|
||||
const dataSource = this.table.dataSource();
|
||||
if (dataSource) {
|
||||
const defaultDirection = this.default() === "desc" ? "desc" : "asc";
|
||||
const direction = this.isActive
|
||||
? this.direction === "asc"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: defaultDirection;
|
||||
|
||||
this.table.dataSource.sort = {
|
||||
column: this.bitSortable,
|
||||
dataSource.sort = {
|
||||
column: this.bitSortable(),
|
||||
direction: direction,
|
||||
fn: this.fn,
|
||||
fn: this.fn(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private get sort() {
|
||||
return this.table.dataSource?.sort;
|
||||
return this.table.dataSource()?.sort;
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
return this.sort?.column === this.bitSortable;
|
||||
return this.sort?.column === this.bitSortable();
|
||||
}
|
||||
|
||||
get direction() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<cdk-virtual-scroll-viewport
|
||||
bitScrollLayout
|
||||
[itemSize]="rowSize"
|
||||
[itemSize]="rowSize()"
|
||||
[ngStyle]="{ paddingBottom: headerHeight + 'px' }"
|
||||
>
|
||||
<table [ngClass]="tableClass">
|
||||
@@ -12,7 +12,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
ContentChild,
|
||||
Input,
|
||||
OnDestroy,
|
||||
TemplateRef,
|
||||
Directive,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
AfterViewInit,
|
||||
ElementRef,
|
||||
TrackByFunction,
|
||||
input,
|
||||
} from "@angular/core";
|
||||
|
||||
import { ScrollLayoutDirective } from "../layout";
|
||||
@@ -64,10 +64,10 @@ export class TableScrollComponent
|
||||
implements AfterContentChecked, AfterViewInit, OnDestroy
|
||||
{
|
||||
/** The size of the rows in the list (in pixels). */
|
||||
@Input({ required: true }) rowSize: number;
|
||||
readonly rowSize = input.required<number>();
|
||||
|
||||
/** Optional trackBy function. */
|
||||
@Input() trackBy: TrackByFunction<any> | undefined;
|
||||
readonly trackBy = input<TrackByFunction<any> | undefined>();
|
||||
|
||||
@ContentChild(BitRowDef) protected rowDef: BitRowDef;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user