1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-13 06:54:07 +00:00

[CL-244] readonly fields (#10164)

* add readonly styles

* update label styles; update stories

* code review changes
This commit is contained in:
Will Martin
2024-07-24 08:55:57 -04:00
committed by GitHub
parent b863488406
commit 4a3aa66f85
6 changed files with 135 additions and 21 deletions

View File

@@ -10,7 +10,7 @@ export class BitLabel {
constructor(private elementRef: ElementRef<HTMLInputElement>) {}
@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() {

View File

@@ -19,5 +19,6 @@ export abstract class BitFormFieldControl {
error: [string, any];
type?: InputTypes;
spellcheck?: boolean;
readOnly?: boolean;
focus?: () => void;
}

View File

@@ -1,4 +1,21 @@
<div class="tw-w-full tw-relative tw-group/bit-form-field">
<!-- We need to use templates since the content slots are repeated between the readonly and read-write views. -->
<ng-template #defaultContent>
<ng-content></ng-content>
</ng-template>
<ng-template #labelContent>
<ng-content select="bit-label"></ng-content>
</ng-template>
<ng-template #prefixContent>
<ng-content select="[bitPrefix]"></ng-content>
</ng-template>
<ng-template #suffixContent>
<ng-content select="[bitSuffix]"></ng-content>
</ng-template>
<div *ngIf="!readOnly; else readOnlyView" class="tw-w-full tw-relative tw-group/bit-form-field">
<div
class="tw-gap-1 tw-bg-background tw-rounded-lg tw-flex tw-min-h-11 [&:not(:has(button:enabled)):has(input:read-only)]:tw-bg-secondary-100 [&:not(:has(button:enabled)):has(textarea:read-only)]:tw-bg-secondary-100"
>
@@ -7,23 +24,23 @@
[hidden]="prefixSlot.childElementCount === 0"
class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2"
>
<ng-content select="[bitPrefix]"></ng-content>
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
</div>
<div
class="tw-w-full tw-relative tw-py-2 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100"
class="default-content tw-w-full tw-relative tw-py-2 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100"
[ngClass]="[
prefixSlot.childElementCount === 0 ? 'tw-rounded-l-lg tw-pl-3' : '',
suffixSlot.childElementCount === 0 ? 'tw-rounded-r-lg tw-pr-3' : ''
]"
>
<ng-content></ng-content>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
<div
#suffixSlot
[hidden]="suffixSlot.childElementCount === 0"
class="tw-flex tw-items-center tw-gap-1 tw-pr-3 tw-py-2"
>
<ng-content select="[bitSuffix]"></ng-content>
<ng-container *ngTemplateOutlet="suffixContent"></ng-container>
</div>
</div>
<div class="tw-absolute tw-w-full tw-h-full tw-top-0 tw-pointer-events-none">
@@ -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"
>
<ng-content select="bit-label"></ng-content>
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
<span *ngIf="input.required" class="tw-text-[0.625rem]"> ({{ "required" | i18n }})</span>
</label>
</div>
@@ -51,6 +68,48 @@
</div>
</div>
</div>
<ng-template #readOnlyView>
<div class="tw-w-full tw-relative">
<label
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted tw-mb-0 tw-max-w-full"
[ngClass]="
defaultContentIsFocused() ? 'tw-text-primary-600 tw-font-semibold' : 'tw-text-muted'
"
[attr.for]="input.labelForId"
>
<ng-container *ngTemplateOutlet="labelContent"></ng-container>
</label>
<div
class="tw-gap-1 tw-flex tw-min-h-11 tw-border-0 tw-border-solid"
[ngClass]="{
'tw-border-secondary-300/50 tw-border-b':
!disableReadOnlyBorder && !defaultContentIsFocused(),
'tw-border-b-2 tw-border-transparent': disableReadOnlyBorder && !defaultContentIsFocused(),
'tw-border-b-2 tw-border-primary-500': defaultContentIsFocused()
}"
>
<div
#prefixSlot
[hidden]="prefixSlot.childElementCount === 0"
class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2"
>
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
</div>
<div class="default-content tw-w-full tw-pb-0 tw-relative [&>*]:tw-p-0">
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
</div>
<div
#suffixSlot
[hidden]="suffixSlot.childElementCount === 0"
class="tw-flex tw-items-center tw-gap-1 tw-pr-3 tw-py-2"
>
<ng-container *ngTemplateOutlet="suffixContent"></ng-container>
</div>
</div>
</div>
</ng-template>
<ng-container [ngSwitch]="input.hasError">
<ng-content select="bit-hint" *ngSwitchCase="false"></ng-content>
<bit-error [error]="input.error" *ngSwitchCase="true"></bit-error>

View File

@@ -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 {

View File

@@ -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 bitInput value="Foobar" readonly />
</bit-form-field>
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput type="password" value="Foobar" [readonly]="true" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" aria-label="Clone"></button>
</bit-form-field>
<bit-form-field>
<bit-label>Textarea</bit-label>
<textarea bitInput rows="4" readonly>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</textarea>
</bit-form-field>
<div class="tw-p-4 tw-mt-10 tw-border-2 tw-border-solid tw-border-black tw-bg-background-alt">
<h2 bitTypography="h2">Inside card</h2>
<bit-section>
<bit-card>
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput value="Foobar" readonly />
</bit-form-field>
<bit-form-field>
<bit-label>Input</bit-label>
<input bitInput type="password" value="Foobar" readonly />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitSuffix bitIconButton="bwi-clone" aria-label="Clone"></button>
</bit-form-field>
<bit-form-field>
<bit-label>Textarea <span bitBadge variant="success">Premium</span></bit-label>
<textarea bitInput rows="3" readonly class="tw-resize-none">Row1
Row2
Row3</textarea>
</bit-form-field>
<bit-form-field disableMargin disableReadOnlyBorder>
<bit-label>Sans margin & border</bit-label>
<input bitInput value="Foobar" readonly />
</bit-form-field>
</bit-card>
</bit-section>
</div>
`,
}),
args: {},

View File

@@ -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",