From 4a3aa66f85a801282f190689e72b81dca435eb59 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Wed, 24 Jul 2024 08:55:57 -0400 Subject: [PATCH] [CL-244] readonly fields (#10164) * add readonly styles * update label styles; update stories * code review changes --- .../src/form-control/label.directive.ts | 2 +- .../src/form-field/form-field-control.ts | 1 + .../src/form-field/form-field.component.html | 71 +++++++++++++++++-- .../src/form-field/form-field.component.ts | 29 ++++---- .../src/form-field/form-field.stories.ts | 47 ++++++++++++ libs/components/src/input/input.directive.ts | 6 +- 6 files changed, 135 insertions(+), 21 deletions(-) diff --git a/libs/components/src/form-control/label.directive.ts b/libs/components/src/form-control/label.directive.ts index 326c2105a31..d8b76482eff 100644 --- a/libs/components/src/form-control/label.directive.ts +++ b/libs/components/src/form-control/label.directive.ts @@ -10,7 +10,7 @@ export class BitLabel { constructor(private elementRef: ElementRef) {} @HostBinding("class") @Input() get classList() { - return ["tw-truncate"]; + return ["tw-truncate", "tw-inline-flex", "tw-gap-1", "tw-items-baseline", "tw-flex-row"]; } @HostBinding("title") get title() { diff --git a/libs/components/src/form-field/form-field-control.ts b/libs/components/src/form-field/form-field-control.ts index 4fc0e522397..a80bc2d0595 100644 --- a/libs/components/src/form-field/form-field-control.ts +++ b/libs/components/src/form-field/form-field-control.ts @@ -19,5 +19,6 @@ export abstract class BitFormFieldControl { error: [string, any]; type?: InputTypes; spellcheck?: boolean; + readOnly?: boolean; focus?: () => void; } diff --git a/libs/components/src/form-field/form-field.component.html b/libs/components/src/form-field/form-field.component.html index b2b08475771..1fbc7e2a572 100644 --- a/libs/components/src/form-field/form-field.component.html +++ b/libs/components/src/form-field/form-field.component.html @@ -1,4 +1,21 @@ -
+ + + + + + + + + + + + + + + + + +
@@ -7,23 +24,23 @@ [hidden]="prefixSlot.childElementCount === 0" class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2" > - +
- +
- +
@@ -40,7 +57,7 @@ class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-2.5 tw-mb-0 tw-max-w-full tw-pointer-events-auto" [attr.for]="input.labelForId" > - + ({{ "required" | i18n }})
@@ -51,6 +68,48 @@
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 87deb3cfe2d..b45001f58eb 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -1,4 +1,3 @@ -import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { AfterContentChecked, Component, @@ -7,6 +6,7 @@ import { HostListener, Input, ViewChild, + booleanAttribute, signal, } from "@angular/core"; @@ -28,16 +28,15 @@ export class BitFormFieldComponent implements AfterContentChecked { @ViewChild(BitErrorComponent) error: BitErrorComponent; - private _disableMargin = false; - @Input() set disableMargin(value: boolean | "") { - this._disableMargin = coerceBooleanProperty(value); - } - get disableMargin() { - return this._disableMargin; - } + @Input({ transform: booleanAttribute }) + disableMargin = false; + + /** If `true`, remove the bottom border for `readonly` inputs */ + @Input({ transform: booleanAttribute }) + disableReadOnlyBorder = false; get inputBorderClasses(): string { - const shouldFocusBorderAppear = !this.buttonIsFocused(); + const shouldFocusBorderAppear = this.defaultContentIsFocused(); const groupClasses = [ this.input.hasError @@ -64,20 +63,24 @@ export class BitFormFieldComponent implements AfterContentChecked { } /** - * If the currently focused element is a button, then we don't want to show focus on the + * If the currently focused element is not part of the default content, then we don't want to show focus on the * input field itself. * * This is necessary because the `tw-group/bit-form-field` wraps the input and any prefix/suffix * buttons */ - protected buttonIsFocused = signal(false); + protected defaultContentIsFocused = signal(false); @HostListener("focusin", ["$event.target"]) onFocusIn(target: HTMLElement) { - this.buttonIsFocused.set(target.matches("button")); + this.defaultContentIsFocused.set(target.matches(".default-content *:focus-visible")); } @HostListener("focusout") onFocusOut() { - this.buttonIsFocused.set(false); + this.defaultContentIsFocused.set(false); + } + + protected get readOnly(): boolean { + return this.input.readOnly; } ngAfterContentChecked(): void { diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index f95f2a547a5..3557e3ad903 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -1,3 +1,4 @@ +import { TextFieldModule } from "@angular/cdk/text-field"; import { AbstractControl, UntypedFormBuilder, @@ -12,12 +13,15 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AsyncActionsModule } from "../async-actions"; +import { BadgeModule } from "../badge"; import { ButtonModule } from "../button"; +import { CardComponent } from "../card"; import { CheckboxModule } from "../checkbox"; import { IconButtonModule } from "../icon-button"; import { InputModule } from "../input/input.module"; import { LinkModule } from "../link"; import { RadioButtonModule } from "../radio-button"; +import { SectionComponent } from "../section"; import { SelectModule } from "../select"; import { I18nMockService } from "../utils/i18n-mock.service"; @@ -41,6 +45,10 @@ export default { RadioButtonModule, SelectModule, LinkModule, + CardComponent, + SectionComponent, + TextFieldModule, + BadgeModule, ], providers: [ { @@ -51,6 +59,7 @@ export default { required: "required", inputRequired: "Input is required.", inputEmail: "Input is not an email-address.", + toggleVisibility: "Toggle visibility", }); }, }, @@ -214,10 +223,48 @@ export const Readonly: Story = { + + Input + + + + + Textarea + +
+

Inside card

+ + + + Input + + + + + Input + + + + + + + Textarea Premium + + + + + Sans margin & border + + + + +
`, }), args: {}, diff --git a/libs/components/src/input/input.directive.ts b/libs/components/src/input/input.directive.ts index 57664c98f05..294d1717ded 100644 --- a/libs/components/src/input/input.directive.ts +++ b/libs/components/src/input/input.directive.ts @@ -40,7 +40,7 @@ export class BitInputDirective implements BitFormFieldControl { "tw-bg-background", "tw-border-none", "focus:tw-outline-none", - "[&:is(input,textarea):read-only]:tw-bg-secondary-100", + "[&:is(input,textarea):disabled]:tw-bg-secondary-100", ]; if (this.parentFormField === null) { @@ -118,6 +118,10 @@ export class BitInputDirective implements BitFormFieldControl { }); } + get readOnly(): boolean { + return this.elementRef.nativeElement.readOnly; + } + get standaloneInputClasses() { return [ "tw-px-3",