mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 07:13:32 +00:00
[PM-17598] Long custom fields (#13668)
* add custom directive to use the angular CDK resize textarea directive * swap to textarea to allow for full content to be shown when view text or hidden custom fields * add text-field styling to web sass file * move angular import to CL scss file * add `textarea` to selector to enforce directive usage only on textareas
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
@import "../../../../libs/angular/src/scss/icons.scss";
|
@import "../../../../libs/angular/src/scss/icons.scss";
|
||||||
@import "../../../../libs/components/src/multi-select/scss/bw.theme";
|
@import "../../../../libs/components/src/multi-select/scss/bw.theme";
|
||||||
@import "@angular/cdk/overlay-prebuilt.css";
|
@import "@angular/cdk/overlay-prebuilt.css";
|
||||||
|
@import "@angular/cdk/text-field-prebuilt.css";
|
||||||
|
|
||||||
//@import "~bootstrap/scss/bootstrap";
|
//@import "~bootstrap/scss/bootstrap";
|
||||||
@import "~bootstrap/scss/_functions";
|
@import "~bootstrap/scss/_functions";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "@angular/cdk/a11y-prebuilt.css";
|
@import "@angular/cdk/a11y-prebuilt.css";
|
||||||
|
@import "@angular/cdk/text-field-prebuilt.css";
|
||||||
@import "./reset.css";
|
@import "./reset.css";
|
||||||
@import "./popover/popover.component.css";
|
@import "./popover/popover.component.css";
|
||||||
@import "./toast/toast.tokens.css";
|
@import "./toast/toast.tokens.css";
|
||||||
|
|||||||
@@ -5,13 +5,19 @@
|
|||||||
<bit-card>
|
<bit-card>
|
||||||
<div
|
<div
|
||||||
class="tw-border-secondary-300 [&_bit-form-field:last-of-type]:tw-mb-0"
|
class="tw-border-secondary-300 [&_bit-form-field:last-of-type]:tw-mb-0"
|
||||||
*ngFor="let field of cipher.fields; let last = last"
|
*ngFor="let field of cipher.fields; let last = last; let i = index"
|
||||||
[ngClass]="{ 'tw-mb-4': !last }"
|
[ngClass]="{ 'tw-mb-4': !last }"
|
||||||
data-testid="custom-field"
|
data-testid="custom-field"
|
||||||
>
|
>
|
||||||
<bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last">
|
<bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last">
|
||||||
<bit-label [appTextDrag]="field.value">{{ field.name }}</bit-label>
|
<bit-label [appTextDrag]="field.value">{{ field.name }}</bit-label>
|
||||||
<input readonly bitInput type="text" [value]="field.value" aria-readonly="true" />
|
<textarea
|
||||||
|
readonly
|
||||||
|
bitInput
|
||||||
|
[value]="field.value"
|
||||||
|
aria-readonly="true"
|
||||||
|
vaultAutosizeReadOnlyTextArea
|
||||||
|
></textarea>
|
||||||
<button
|
<button
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
@@ -32,14 +38,26 @@
|
|||||||
[value]="field.value"
|
[value]="field.value"
|
||||||
aria-readonly="true"
|
aria-readonly="true"
|
||||||
class="tw-font-mono"
|
class="tw-font-mono"
|
||||||
|
*ngIf="!revealedHiddenFields.includes(i)"
|
||||||
/>
|
/>
|
||||||
|
<!-- `type="password"` is only available for inputs, but textarea should show the entire field when visible. -->
|
||||||
|
<textarea
|
||||||
|
*ngIf="revealedHiddenFields.includes(i)"
|
||||||
|
readonly
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
[value]="field.value"
|
||||||
|
aria-readonly="true"
|
||||||
|
class="tw-font-mono"
|
||||||
|
vaultAutosizeReadOnlyTextArea
|
||||||
|
></textarea>
|
||||||
<button
|
<button
|
||||||
bitSuffix
|
bitSuffix
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton
|
bitIconButton
|
||||||
bitPasswordInputToggle
|
bitPasswordInputToggle
|
||||||
*ngIf="canViewPassword"
|
*ngIf="canViewPassword"
|
||||||
(toggledChange)="logHiddenEvent($event)"
|
(toggledChange)="toggleHiddenField($event, i)"
|
||||||
></button>
|
></button>
|
||||||
<button
|
<button
|
||||||
bitIconButton="bwi-clone"
|
bitIconButton="bwi-clone"
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { VaultAutosizeReadOnlyTextArea } from "../../directives/readonly-textarea.directive";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-custom-fields-v2",
|
selector: "app-custom-fields-v2",
|
||||||
templateUrl: "custom-fields-v2.component.html",
|
templateUrl: "custom-fields-v2.component.html",
|
||||||
@@ -38,6 +40,7 @@ import {
|
|||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
|
VaultAutosizeReadOnlyTextArea,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CustomFieldV2Component implements OnInit {
|
export class CustomFieldV2Component implements OnInit {
|
||||||
@@ -45,6 +48,9 @@ export class CustomFieldV2Component implements OnInit {
|
|||||||
fieldType = FieldType;
|
fieldType = FieldType;
|
||||||
fieldOptions: any;
|
fieldOptions: any;
|
||||||
|
|
||||||
|
/** Indexes of hidden fields that are revealed */
|
||||||
|
revealedHiddenFields: number[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
@@ -63,7 +69,13 @@ export class CustomFieldV2Component implements OnInit {
|
|||||||
return this.cipher.viewPassword;
|
return this.cipher.viewPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
async logHiddenEvent(hiddenFieldVisible: boolean) {
|
async toggleHiddenField(hiddenFieldVisible: boolean, index: number) {
|
||||||
|
if (hiddenFieldVisible) {
|
||||||
|
this.revealedHiddenFields.push(index);
|
||||||
|
} else {
|
||||||
|
this.revealedHiddenFields = this.revealedHiddenFields.filter((i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
if (hiddenFieldVisible) {
|
if (hiddenFieldVisible) {
|
||||||
await this.eventCollectionService.collect(
|
await this.eventCollectionService.collect(
|
||||||
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { CdkTextareaAutosize } from "@angular/cdk/text-field";
|
||||||
|
import { EventEmitter, NgZone } from "@angular/core";
|
||||||
|
|
||||||
|
import { VaultAutosizeReadOnlyTextArea } from "./readonly-textarea.directive";
|
||||||
|
|
||||||
|
describe("VaultAutosizeReadOnlyTextArea", () => {
|
||||||
|
let directive: VaultAutosizeReadOnlyTextArea;
|
||||||
|
let onStable: EventEmitter<void>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
onStable = new EventEmitter<void>();
|
||||||
|
|
||||||
|
directive = new VaultAutosizeReadOnlyTextArea(
|
||||||
|
{ enabled: undefined } as unknown as CdkTextareaAutosize,
|
||||||
|
{ onStable } as NgZone,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables CdkTextareaAutosize by default", () => {
|
||||||
|
expect(directive["autosize"].enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enables CdkTextareaAutosize after view init", async () => {
|
||||||
|
expect(directive["autosize"].enabled).toBe(false);
|
||||||
|
|
||||||
|
const viewInitPromise = directive.ngAfterViewInit();
|
||||||
|
|
||||||
|
onStable.emit();
|
||||||
|
|
||||||
|
await viewInitPromise;
|
||||||
|
|
||||||
|
expect(directive["autosize"].enabled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
29
libs/vault/src/directives/readonly-textarea.directive.ts
Normal file
29
libs/vault/src/directives/readonly-textarea.directive.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { CdkTextareaAutosize, TextFieldModule } from "@angular/cdk/text-field";
|
||||||
|
import { AfterViewInit, Directive, Host, NgZone } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
standalone: true,
|
||||||
|
selector: "textarea[vaultAutosizeReadOnlyTextArea]",
|
||||||
|
providers: [TextFieldModule],
|
||||||
|
hostDirectives: [CdkTextareaAutosize],
|
||||||
|
})
|
||||||
|
export class VaultAutosizeReadOnlyTextArea implements AfterViewInit {
|
||||||
|
constructor(
|
||||||
|
@Host() private autosize: CdkTextareaAutosize,
|
||||||
|
private ngZone: NgZone,
|
||||||
|
) {
|
||||||
|
// initially disable autosize
|
||||||
|
this.autosize.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <textarea>s are commonly used within `bit-form-field` components which render
|
||||||
|
// content within templates. This causes the `CdkTextareaAutosize` to error out
|
||||||
|
// when trying to access the `parentNode` of the textarea. To avoid this, wait
|
||||||
|
// for the next change detection cycle to enable the autosize directive.
|
||||||
|
async ngAfterViewInit(): Promise<void> {
|
||||||
|
await firstValueFrom(this.ngZone.onStable);
|
||||||
|
|
||||||
|
this.autosize.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user