From 023b057f3e6be2f4203491aa88f247ab2b94a532 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Wed, 2 Jul 2025 14:08:05 -0400 Subject: [PATCH] [CL-124] Add validator stories (#15400) * adding validation stories * add one story for all validations * fix form field story import * move validation docs * fix maxValue default value * add play function to submit form --- .../src/form-field/form-field.stories.ts | 2 +- libs/components/src/form/form.stories.ts | 122 +++++++++++++++--- libs/components/src/form/forms.mdx | 18 ++- 3 files changed, 116 insertions(+), 26 deletions(-) diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index 738ac96bf76..0c1fa8e6f6c 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -72,6 +72,7 @@ export default { decorators: [ moduleMetadata({ imports: [ + A11yTitleDirective, FormsModule, ReactiveFormsModule, FormFieldModule, @@ -88,7 +89,6 @@ export default { TextFieldModule, BadgeModule, ], - declarations: [A11yTitleDirective], providers: [ { provide: I18nService, diff --git a/libs/components/src/form/form.stories.ts b/libs/components/src/form/form.stories.ts index 6aef140fe5f..1fc9bbef751 100644 --- a/libs/components/src/form/form.stories.ts +++ b/libs/components/src/form/form.stories.ts @@ -1,13 +1,6 @@ -import { - AbstractControl, - FormBuilder, - FormsModule, - ReactiveFormsModule, - ValidationErrors, - ValidatorFn, - Validators, -} from "@angular/forms"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { userEvent, getByText } from "@storybook/test"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,6 +8,7 @@ import { ButtonModule } from "../button"; import { CheckboxModule } from "../checkbox"; import { FormControlModule } from "../form-control"; import { FormFieldModule } from "../form-field"; +import { trimValidator, forbiddenCharacters } from "../form-field/bit-validators"; import { InputModule } from "../input/input.module"; import { MultiSelectModule } from "../multi-select"; import { RadioButtonModule } from "../radio-button"; @@ -48,13 +42,19 @@ export default { required: "required", checkboxRequired: "Option is required", inputRequired: "Input is required.", - inputEmail: "Input is not an email-address.", + inputEmail: "Input is not an email address.", + inputForbiddenCharacters: (char) => + `The following characters are not allowed: "${char}"`, inputMinValue: (min) => `Input value must be at least ${min}.`, inputMaxValue: (max) => `Input value must not exceed ${max}.`, + inputMinLength: (min) => `Input value must be at least ${min} characters long.`, + inputMaxLength: (max) => `Input value must not exceed ${max} characters in length.`, + inputTrimValidator: `Input must not contain only whitespace.`, multiSelectPlaceholder: "-- Type to Filter --", multiSelectLoading: "Retrieving options...", multiSelectNotFound: "No items found", multiSelectClearAll: "Clear all", + fieldsNeedAttention: "__$1__ field(s) above need your attention.", }); }, }, @@ -72,7 +72,7 @@ export default { const fb = new FormBuilder(); const exampleFormObj = fb.group({ name: ["", [Validators.required]], - email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]], + email: ["", [Validators.required, Validators.email, forbiddenCharacters(["#"])]], country: [undefined as string | undefined, [Validators.required]], groups: [], terms: [false, [Validators.requiredTrue]], @@ -80,14 +80,6 @@ const exampleFormObj = fb.group({ age: [null, [Validators.min(0), Validators.max(150)]], }); -// 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; - }; -} - type Story = StoryObj; export const FullExample: Story = { @@ -177,3 +169,95 @@ export const FullExample: Story = { ], }, }; + +const showValidationsFormObj = fb.group({ + required: ["", [Validators.required]], + whitespace: [" ", trimValidator], + email: ["example?bad-email", [Validators.email]], + minLength: ["Hello", [Validators.minLength(8)]], + maxLength: ["Hello there", [Validators.maxLength(8)]], + minValue: [9, [Validators.min(10)]], + maxValue: [15, [Validators.max(10)]], + forbiddenChars: ["Th!$ value cont#in$ forbidden char$", forbiddenCharacters(["#", "!", "$"])], +}); + +export const Validations: Story = { + render: (args) => ({ + props: { + formObj: showValidationsFormObj, + submit: () => showValidationsFormObj.markAllAsTouched(), + ...args, + }, + template: /*html*/ ` +
+ + Required validation + + This field is required. Submit form or blur input to see error + + + + Email validation + + This field contains a malformed email address. Submit form or blur input to see error + + + + Min length validation + + Value must be at least 8 characters. Submit form or blur input to see error + + + + Max length validation + + Value must be less then 8 characters. Submit form or blur input to see error + + + + Min number value validation + + Value must be greater than 10. Submit form or blur input to see error + + + + Max number value validation + + Value must be less than than 10. Submit form or blur input to see error + + + + Forbidden characters validation + + Value must not contain '#', '!' or '$'. Submit form or blur input to see error + + + + White space validation + + This input contains only white space. Submit form or blur input to see error + + + + +
+ `, + }), + play: async (context) => { + const canvas = context.canvasElement; + const submitButton = getByText(canvas, "Submit"); + + await userEvent.click(submitButton); + }, +}; diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index e3baf200f96..498eb8a3ed2 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -142,8 +142,20 @@ If a checkbox group has more than 4 options a +## Validation messages + +These are examples of our default validation error messages: + + + ## Accessibility +### Icon Buttons in Form Fields + +When adding prefix or suffix icon buttons to a form field, be sure to use the `appA11yTitle` +directive to provide a label for screenreaders. Typically, the label should follow this pattern: +`{Action} {field label}`, i.e. "Copy username". + ### Required Fields - Use "(required)" in the label of each required form field styled the same as the field's helper @@ -152,12 +164,6 @@ If a checkbox group has more than 4 options a helper text. - **Example:** "Billing Email is required if owned by a business". -### Icon Buttons in Form Fields - -When adding prefix or suffix icon buttons to a form field, be sure to use the `appA11yTitle` -directive to provide a label for screenreaders. Typically, the label should follow this pattern: -`{Action} {field label}`, i.e. "Copy username". - ### Form Field Errors - When a resting field is filled out, validation is triggered when the user de-focuses the field