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:
@@ -72,6 +72,7 @@ export default {
|
|||||||
decorators: [
|
decorators: [
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [
|
imports: [
|
||||||
|
A11yTitleDirective,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
@@ -88,7 +89,6 @@ export default {
|
|||||||
TextFieldModule,
|
TextFieldModule,
|
||||||
BadgeModule,
|
BadgeModule,
|
||||||
],
|
],
|
||||||
declarations: [A11yTitleDirective],
|
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: I18nService,
|
provide: I18nService,
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import {
|
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
AbstractControl,
|
|
||||||
FormBuilder,
|
|
||||||
FormsModule,
|
|
||||||
ReactiveFormsModule,
|
|
||||||
ValidationErrors,
|
|
||||||
ValidatorFn,
|
|
||||||
Validators,
|
|
||||||
} from "@angular/forms";
|
|
||||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||||
|
import { userEvent, getByText } from "@storybook/test";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
@@ -15,6 +8,7 @@ import { ButtonModule } from "../button";
|
|||||||
import { CheckboxModule } from "../checkbox";
|
import { CheckboxModule } from "../checkbox";
|
||||||
import { FormControlModule } from "../form-control";
|
import { FormControlModule } from "../form-control";
|
||||||
import { FormFieldModule } from "../form-field";
|
import { FormFieldModule } from "../form-field";
|
||||||
|
import { trimValidator, forbiddenCharacters } from "../form-field/bit-validators";
|
||||||
import { InputModule } from "../input/input.module";
|
import { InputModule } from "../input/input.module";
|
||||||
import { MultiSelectModule } from "../multi-select";
|
import { MultiSelectModule } from "../multi-select";
|
||||||
import { RadioButtonModule } from "../radio-button";
|
import { RadioButtonModule } from "../radio-button";
|
||||||
@@ -48,13 +42,19 @@ export default {
|
|||||||
required: "required",
|
required: "required",
|
||||||
checkboxRequired: "Option is required",
|
checkboxRequired: "Option is required",
|
||||||
inputRequired: "Input 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}.`,
|
inputMinValue: (min) => `Input value must be at least ${min}.`,
|
||||||
inputMaxValue: (max) => `Input value must not exceed ${max}.`,
|
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 --",
|
multiSelectPlaceholder: "-- Type to Filter --",
|
||||||
multiSelectLoading: "Retrieving options...",
|
multiSelectLoading: "Retrieving options...",
|
||||||
multiSelectNotFound: "No items found",
|
multiSelectNotFound: "No items found",
|
||||||
multiSelectClearAll: "Clear all",
|
multiSelectClearAll: "Clear all",
|
||||||
|
fieldsNeedAttention: "__$1__ field(s) above need your attention.",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -72,7 +72,7 @@ export default {
|
|||||||
const fb = new FormBuilder();
|
const fb = new FormBuilder();
|
||||||
const exampleFormObj = fb.group({
|
const exampleFormObj = fb.group({
|
||||||
name: ["", [Validators.required]],
|
name: ["", [Validators.required]],
|
||||||
email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]],
|
email: ["", [Validators.required, Validators.email, forbiddenCharacters(["#"])]],
|
||||||
country: [undefined as string | undefined, [Validators.required]],
|
country: [undefined as string | undefined, [Validators.required]],
|
||||||
groups: [],
|
groups: [],
|
||||||
terms: [false, [Validators.requiredTrue]],
|
terms: [false, [Validators.requiredTrue]],
|
||||||
@@ -80,14 +80,6 @@ const exampleFormObj = fb.group({
|
|||||||
age: [null, [Validators.min(0), Validators.max(150)]],
|
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;
|
type Story = StoryObj;
|
||||||
|
|
||||||
export const FullExample: Story = {
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -142,8 +142,20 @@ If a checkbox group has more than 4 options a
|
|||||||
|
|
||||||
<Canvas of={checkboxStories.Default} />
|
<Canvas of={checkboxStories.Default} />
|
||||||
|
|
||||||
|
## Validation messages
|
||||||
|
|
||||||
|
These are examples of our default validation error messages:
|
||||||
|
|
||||||
|
<Canvas of={formStories.Validations} />
|
||||||
|
|
||||||
## Accessibility
|
## 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
|
### Required Fields
|
||||||
|
|
||||||
- Use "(required)" in the label of each required form field styled the same as the field's helper
|
- 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.
|
helper text.
|
||||||
- **Example:** "Billing Email is required if owned by a business".
|
- **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
|
### Form Field Errors
|
||||||
|
|
||||||
- When a resting field is filled out, validation is triggered when the user de-focuses the field
|
- When a resting field is filled out, validation is triggered when the user de-focuses the field
|
||||||
|
|||||||
Reference in New Issue
Block a user