1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[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
This commit is contained in:
Bryan Cunningham
2025-07-02 14:08:05 -04:00
committed by GitHub
parent 369c1edaf7
commit 023b057f3e
3 changed files with 116 additions and 26 deletions

View File

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

View File

@@ -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*/ `
<form [formGroup]="formObj" (ngSubmit)="submit()">
<bit-form-field>
<bit-label>Required validation</bit-label>
<input bitInput formControlName="required" />
<bit-hint>This field is required. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>Email validation</bit-label>
<input bitInput type="email" formControlName="email" />
<bit-hint>This field contains a malformed email address. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>Min length validation</bit-label>
<input bitInput formControlName="minLength" />
<bit-hint>Value must be at least 8 characters. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>Max length validation</bit-label>
<input bitInput formControlName="maxLength" />
<bit-hint>Value must be less then 8 characters. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>Min number value validation</bit-label>
<input
bitInput
type="number"
formControlName="minValue"
/>
<bit-hint>Value must be greater than 10. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>Max number value validation</bit-label>
<input
bitInput
type="number"
formControlName="maxValue"
/>
<bit-hint>Value must be less than than 10. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>Forbidden characters validation</bit-label>
<input
bitInput
formControlName="forbiddenChars"
/>
<bit-hint>Value must not contain '#', '!' or '$'. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<bit-form-field>
<bit-label>White space validation</bit-label>
<input bitInput formControlName="whitespace" />
<bit-hint>This input contains only white space. Submit form or blur input to see error</bit-hint>
</bit-form-field>
<button type="submit" bitButton buttonType="primary">Submit</button>
<bit-error-summary [formGroup]="formObj"></bit-error-summary>
</form>
`,
}),
play: async (context) => {
const canvas = context.canvasElement;
const submitButton = getByText(canvas, "Submit");
await userEvent.click(submitButton);
},
};

View File

@@ -142,8 +142,20 @@ If a checkbox group has more than 4 options a
<Canvas of={checkboxStories.Default} />
## Validation messages
These are examples of our default validation error messages:
<Canvas of={formStories.Validations} />
## 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