diff --git a/packages/docs/page-config/ui-elements/form/index.ts b/packages/docs/page-config/ui-elements/form/index.ts index 025256dbac..8939ddde9c 100644 --- a/packages/docs/page-config/ui-elements/form/index.ts +++ b/packages/docs/page-config/ui-elements/form/index.ts @@ -10,11 +10,11 @@ export default definePageConfig({ block.example("Default", { title: "Default usage", - description: "`VaForm` component in pair with `useForm` composable provides a simple way to validate multiple form fields. It gives you `isValid`, `errorMessages` computeds and `validate` method to validate all form fields at once. You can also reset validation with `resetValidation` method or reset whole form and it's values with `reset` method." + description: "`VaForm` component in pair with `useForm` composable provides a simple way to validate multiple form fields. It gives you `isValid`, `errorMessages` computeds and `validate` method to validate all form fields at once. You can also reset validation with `resetValidation` method or reset whole form and its values with `reset` method." }), block.example("HideErrors", { title: "Named fields", - description: "If you don't like when form jumps you can name each form field and access it's error messages with `errorMessagesNamed` with `useForm` composable and hide errors under form fields using `hide-error-messages` props, so you can display them in a custom way." + description: "If you don't like when form jumps you can name each form field and access its error messages with `errorMessagesNamed` with `useForm` composable and hide errors under form fields using `hide-error-messages` props, so you can display them in a custom way." }), block.example("FormData", { title: "FormData", diff --git a/packages/ui/src/components/va-form/VaForm-reset.vue b/packages/ui/src/components/va-form/VaForm-reset.vue deleted file mode 100644 index cba5914c4e..0000000000 --- a/packages/ui/src/components/va-form/VaForm-reset.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/packages/ui/src/components/va-form/VaForm.demo.vue b/packages/ui/src/components/va-form/VaForm.demo.vue deleted file mode 100644 index abe9bed242..0000000000 --- a/packages/ui/src/components/va-form/VaForm.demo.vue +++ /dev/null @@ -1,204 +0,0 @@ - - - diff --git a/packages/ui/src/components/va-form/VaForm.stories.ts b/packages/ui/src/components/va-form/VaForm.stories.ts index 013f6b7f5e..d6491b9a72 100644 --- a/packages/ui/src/components/va-form/VaForm.stories.ts +++ b/packages/ui/src/components/va-form/VaForm.stories.ts @@ -1,12 +1,469 @@ -import { defineComponent } from 'vue' -import VaForm from './VaForm.demo.vue' +import { userEvent } from '../../../.storybook/interaction-utils/userEvent' +import { addText } from '../../../.storybook/interaction-utils/addText' +import { expect } from '@storybook/jest' +import { within } from '@storybook/testing-library' + +import { + VaForm, + VaCheckbox, + VaButton, + VaInput, + VaSelect, + VaDateInput, + VaOptionList, + VaTimeInput, + VaFileUpload, + VaSwitch, + VaCounter, + VaRating, + VaSlider, + VaDatePicker, + VaTimePicker, + VaRadio, +} from '../' export default { title: 'VaForm', component: VaForm, + tags: ['autodocs'], +} + +export const Default = () => ({ + components: { VaForm, VaInput, VaButton }, + data: () => ({ input: '' }), + template: ` + + + + + Submit + + `, +}) + +export const Autofocus = () => ({ + components: { VaForm, VaInput }, + template: ` + + + + `, +}) + +addText( + Autofocus, + 'Autofocus is broken', + 'stale', +) + +Autofocus.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const input = canvas.getByRole('textbox', { name: '' }) as HTMLElement + + await step('Focus first input', async () => { + expect(input).toHaveFocus() + }) +} + +export const Stateful = () => ({ + components: { VaForm, VaCheckbox }, + template: ` + [true] + + + + [false] + + + + `, +}) + +Stateful.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const [firstCheckboxInput, secondCheckboxInput] = canvas.getAllByRole('checkbox', { name: '' }) as HTMLElement[] + const [firstCheckbox, secondCheckbox] = document.querySelectorAll('.va-checkbox__input-container') + + await step('Should be checked', async () => { + await userEvent.click(firstCheckbox) + expect(firstCheckboxInput).toBeChecked() + }) + + await step('Should not be checked', async () => { + await userEvent.click(secondCheckbox) + expect(secondCheckboxInput).not.toBeChecked() + }) } -export const Default = defineComponent({ - components: { VaForm }, - template: '', +export const Immediate = () => ({ + components: { VaForm, VaInput }, + + template: ` + [true] + + + + [false] + + + + `, }) + +Immediate.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const [firstInput, secondInput] = canvas.getAllByRole('textbox', { name: '' }) as HTMLElement[] + + await step('First input displays error message', async () => { + expect(firstInput.getAttribute('aria-invalid')).toEqual('true') + }) + + await step('Second input does not display error message', async () => { + expect(secondInput.getAttribute('aria-invalid')).toEqual('false') + }) +} + +export const HideErrorMessages = () => ({ + components: { VaForm, VaInput }, + template: ` + [true] + + + + [false] + + + + `, +}) + +HideErrorMessages.play = async ({ step }) => { + const [firstDefinedInput, secondDefineInput] = document.querySelectorAll('.va-message-list__message') + + await step('Does not display error message', async () => { + expect(firstDefinedInput).toBeDefined() + }) + + await step('Displays error message', async () => { + expect(secondDefineInput).toBeUndefined() + }) +} + +export const HideErrors = () => ({ + components: { VaForm, VaInput }, + template: ` + [true] + + + + [false] + + + + `, +}) + +HideErrors.play = async ({ step }) => { + const [firstInput, secondInput] = document.querySelectorAll('.va-input-wrapper') + + await step('Does not highlight invalid input', async () => { + expect(firstInput).not.toHaveClass('va-input-wrapper--error') + }) + + await step('Highlights invalid input', async () => { + expect(secondInput).toHaveClass('va-input-wrapper--error') + }) +} + +export const Focus = () => ({ + components: { VaForm, VaInput, VaButton }, + template: ` + + + + + + Focus first input + + `, +}) + +Focus.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const [firstInput, secondInput] = canvas.getAllByRole('textbox', { name: '' }) as HTMLElement[] + const button = canvas.getByRole('button', { name: 'Focus first input' }) as HTMLElement + + await step('Focusses first input', async () => { + await userEvent.click(button) + expect(firstInput).toHaveFocus() + }) +} + +export const FocusInvalid = () => ({ + components: { VaForm, VaInput, VaButton }, + template: ` + + + + + + + Focus invalid + + `, +}) + +FocusInvalid.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const [validInput, invaliInput] = canvas.getAllByRole('textbox', { name: '' }) as HTMLElement[] + const button = canvas.getByRole('button', { name: 'Focus invalid' }) as HTMLElement + + await step('Focuses first invalid input', async () => { + await userEvent.click(button) + expect(invaliInput).toHaveFocus() + }) +} + +export const ValidateAndResetValidation = () => ({ + components: { VaForm, VaInput, VaButton }, + template: ` + + + + + Validate + + + Reset validation + + `, +}) + +ValidateAndResetValidation.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const input = canvas.getByRole('textbox', { name: '' }) as HTMLElement + const validateButton = canvas.getByRole('button', { name: 'Validate' }) as HTMLElement + const resetButton = canvas.getByRole('button', { name: 'Reset validation' }) as HTMLElement + + await step('Validates input with error', async () => { + await userEvent.click(validateButton) + expect(input.getAttribute('aria-invalid')).toEqual('true') + }) + + await step('Reset inputs validation', async () => { + await userEvent.click(resetButton) + expect(input.getAttribute('aria-invalid')).toEqual('false') + }) +} + +export const Reset = () => ({ + components: { VaForm, VaInput, VaButton }, + data: () => ({ data: '' }), + methods: { + fillForm () { + this.input = 'data' + this.$refs.form.validate() + }, + }, + template: ` + + + + + Set inputs and validation + + + Reset inputs and validation + + `, +}) + +Reset.play = async ({ canvasElement, step }) => { + const canvas = within(canvasElement) + const input = canvas.getByRole('textbox', { name: '' }) as HTMLElement + + const setButton = canvas.getByRole('button', { name: 'Set inputs and validation' }) as HTMLElement + const resetButton = canvas.getByRole('button', { name: 'Reset inputs and validation' }) as HTMLElement + + await step('Resets form', async () => { + await userEvent.click(setButton) + await userEvent.click(resetButton) + expect(input).toHaveValue('') + expect(input.getAttribute('aria-invalid')).toEqual('false') + }) +} + +const OPTIONS = ['One', 'Two', 'Three'] + +export const ToDoFormReset = () => ({ + components: { + VaForm, + VaInput, + VaSelect, + VaDateInput, + VaTimeInput, + VaOptionList, + VaButton, + }, + data: () => ({ + input: 'value', + checkbox: true, + date: new Date(0), + time: new Date(0), + options: OPTIONS, + select: OPTIONS[0], + optionsListValue: OPTIONS[0], + validationRules: [false], + }), + template: ` + + + + + + + + + + Reset form + + `, +}) + +addText( + ToDoFormReset, + 'This is old demo resqued to have visual tests, but we want to rewrite it eventually.', + 'stale', +) + +export const ToDoFormInteractions = () => ({ + components: { + VaForm, + VaInput, + VaSelect, + VaDateInput, + VaTimeInput, + VaOptionList, + VaFileUpload, + VaSwitch, + VaCounter, + VaRating, + VaSlider, + VaDatePicker, + VaTimePicker, + VaRadio, + VaButton, + }, + data: () => ({ + input: 'input', + select: OPTIONS[0], + checkbox: false, + date: new Date(0), + time: new Date(0), + switch1: false, + options: OPTIONS, + optionsList: '', + counter: 13, + rating: 4, + slider: 29, + radio: OPTIONS[1], + validationRules: [false], + }), + template: ` + + + + + + + + + + + + + + + + + + Validate + + + Reset validation + + + Reset + + `, +}) + +addText( + ToDoFormInteractions, + 'This is old demo resqued to have visual tests, but we want to rewrite it eventually.', + 'stale', +) diff --git a/packages/ui/src/components/va-form/VaFormNew.demo.vue b/packages/ui/src/components/va-form/VaFormNew.demo.vue deleted file mode 100644 index bf177632c8..0000000000 --- a/packages/ui/src/components/va-form/VaFormNew.demo.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - diff --git a/packages/ui/src/components/va-form/VaFormNew.stories.ts b/packages/ui/src/components/va-form/VaFormNew.stories.ts deleted file mode 100644 index 08483cac05..0000000000 --- a/packages/ui/src/components/va-form/VaFormNew.stories.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineComponent } from 'vue' -import VaFormNew from './VaFormNew.demo.vue' - -export default { - title: 'VaFormNew', - component: VaFormNew, -} - -export const Default = defineComponent({ - components: { VaFormNew }, - template: '', -}) diff --git a/packages/ui/src/components/va-form/VaFormValidationLib.stories.ts b/packages/ui/src/components/va-form/VaFormValidationLib.stories.ts deleted file mode 100644 index 7f4c8c4d0e..0000000000 --- a/packages/ui/src/components/va-form/VaFormValidationLib.stories.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineComponent } from 'vue' -import VaFormValidationLib from './VaFormValidationLib.demo.vue' - -export default { - title: 'VaFormValidationLib', - component: VaFormValidationLib, -} - -export const Default = defineComponent({ - components: { VaFormValidationLib }, - template: '', -}) diff --git a/packages/ui/src/components/va-form/VaFormValidationLib.demo.vue b/packages/ui/src/components/va-form/VaFormVuelidate.vue similarity index 99% rename from packages/ui/src/components/va-form/VaFormValidationLib.demo.vue rename to packages/ui/src/components/va-form/VaFormVuelidate.vue index 8ae9ad70aa..484f3cc8b9 100644 --- a/packages/ui/src/components/va-form/VaFormValidationLib.demo.vue +++ b/packages/ui/src/components/va-form/VaFormVuelidate.vue @@ -45,7 +45,6 @@ import { VaForm } from './index' import { VaInput } from '../va-input' import { reactive } from 'vue' - import { required, minLength, email, between, numeric } from '@vuelidate/validators' import { useVuelidate } from '@vuelidate/core' @@ -54,6 +53,7 @@ const state = reactive({ age: 0, email: '', }) + const rules = { name: { required, @@ -69,5 +69,6 @@ const rules = { between: between(10, 100), }, } + const v$ = useVuelidate(rules, state) diff --git a/packages/ui/src/composables/useValidation.ts b/packages/ui/src/composables/useValidation.ts index b436ffa29f..ccbfc96a9e 100644 --- a/packages/ui/src/composables/useValidation.ts +++ b/packages/ui/src/composables/useValidation.ts @@ -67,7 +67,7 @@ export const useValidation = ({ - 'aria-invalid': !!computedErrorMessages.value.length, + 'aria-invalid': computedError.value, 'aria-errormessage': typeof computedErrorMessages.value === 'string' ? computedErrorMessages.value : computedErrorMessages.value.join(', '),