// FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { TextFieldModule } from "@angular/cdk/text-field"; import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; import { AbstractControl, UntypedFormBuilder, FormsModule, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators, } from "@angular/forms"; 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"; import { BitFormFieldComponent } from "./form-field.component"; import { FormFieldModule } from "./form-field.module"; // TOOD: This solves a circular dependency between components and angular. @Directive({ selector: "[appA11yTitle]", }) export class A11yTitleDirective implements OnInit { @Input() set appA11yTitle(title: string) { this.title = title; this.setAttributes(); } private title: string; private originalTitle: string | null; private originalAriaLabel: string | null; constructor( private el: ElementRef, private renderer: Renderer2, ) {} ngOnInit() { this.originalTitle = this.el.nativeElement.getAttribute("title"); this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label"); this.setAttributes(); } private setAttributes() { if (this.originalTitle === null) { this.renderer.setAttribute(this.el.nativeElement, "title", this.title); } if (this.originalAriaLabel === null) { this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title); } } } export default { title: "Component Library/Form/Field", component: BitFormFieldComponent, decorators: [ moduleMetadata({ imports: [ FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule, IconButtonModule, AsyncActionsModule, CheckboxModule, RadioButtonModule, SelectModule, LinkModule, CardComponent, SectionComponent, TextFieldModule, BadgeModule, ], declarations: [A11yTitleDirective], providers: [ { provide: I18nService, useFactory: () => { return new I18nMockService({ selectPlaceholder: "-- Select --", required: "required", inputRequired: "Input is required.", inputEmail: "Input is not an email-address.", toggleVisibility: "Toggle visibility", }); }, }, ], }), ], parameters: { design: { type: "figma", url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; const fb = new UntypedFormBuilder(); const formObj = fb.group({ test: [""], required: ["", [Validators.required]], }); const defaultFormObj = fb.group({ name: ["", [Validators.required]], email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]], terms: [false, [Validators.requiredTrue]], updates: ["yes"], file: [""], }); // Custom error message, `message` is shown as the error message function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value); return forbidden ? { forbiddenName: { message: "forbiddenName" } } : null; }; } function submit() { defaultFormObj.markAllAsTouched(); } type Story = StoryObj; export const Default: Story = { render: (args) => ({ props: { formObj: defaultFormObj, submit: submit, ...args, }, template: /*html*/ `
Label Optional Hint
`, }), }; export const LabelWithIcon: Story = { render: (args) => ({ props: { formObj: defaultFormObj, submit: submit, ...args, }, template: /*html*/ `
Label Optional Hint
`, }), }; export const LongLabel: Story = { render: (args) => ({ props: { formObj: defaultFormObj, submit: submit, ...args, }, template: /*html*/ `
Hello I am a very long label with lots of very cool helpful information Optional Hint Hello I am a very long label with lots of very cool helpful information Optional Hint
`, }), }; export const Required: Story = { render: (args) => ({ props: { formObj: formObj, ...args, }, template: /*html*/ ` Label FormControl `, }), }; export const Hint: Story = { render: (args) => ({ props: { formObj: formObj, ...args, }, template: /*html*/ ` FormControl Long hint text `, }), }; export const Disabled: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label `, }), args: {}, }; export const Readonly: Story = { render: (args) => ({ props: args, template: /*html*/ ` Input Input Textarea

Inside card

Input Input Textarea Premium Sans margin & border
`, }), args: {}, }; export const InputGroup: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label $ USD `, }), args: {}, }; export const ButtonInputGroup: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label `, }), args: {}, }; export const DisabledButtonInputGroup: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label `, }), args: {}, }; export const PartiallyDisabledButtonInputGroup: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label `, }), args: {}, }; export const Select: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label `, }), args: {}, }; export const AdvancedSelect: Story = { render: (args) => ({ props: args, template: /*html*/ ` Label `, }), }; export const FileInput: Story = { render: (args) => ({ props: { formObj: defaultFormObj, submit: submit, ...args, }, template: /*html*/ `
File
No file chosen
`, }), }; export const Textarea: Story = { render: (args) => ({ props: args, template: /*html*/ ` Textarea `, }), args: {}, };