diff --git a/f2/src/components/ErrorSummary/__tests__/ErrorSummary.test.js b/f2/src/components/ErrorSummary/__tests__/ErrorSummary.test.js new file mode 100644 index 000000000..e260f45a7 --- /dev/null +++ b/f2/src/components/ErrorSummary/__tests__/ErrorSummary.test.js @@ -0,0 +1,113 @@ +import React from 'react' +import { ThemeProvider } from 'emotion-theming' +import wait from 'waait' +import canada from '../../../theme/canada' +import { render, fireEvent, cleanup } from '@testing-library/react' +import { ErrorSummary } from '../' +import { Form } from 'react-final-form' +import { Field } from '../../Field' +import { I18nProvider } from '@lingui/react' +import { i18n } from '@lingui/core' +import en from '../../../locales/en.json' + +i18n.load('en', { en }) +i18n.activate('en') +const clickOn = element => fireEvent.click(element) + +describe('', () => { + afterEach(cleanup) + + it('does not render if validation passes', () => { + const submitMock = jest.fn() + + const validate = values => { + const errors = {} + //condition for an error to occur: append a lingui id to the list of error + if (!values.foo) { + errors.foo = 'bar' + } + return errors + } + + const { queryByText } = render( + + +
{ + submitMock(values) + }} + render={({ handleSubmit, values, errors }) => ( + + + + + )} + /> +
+
, + ) + expect(queryByText(/bar/)).toBeNull() + }) + + it('displays an error summary that links to a error message', async () => { + const submitMock = jest.fn() + + const validate = values => { + const errors = {} + //condition for an error to occur: append a lingui id to the list of error + if (values.foo === 'baz') { + errors.foo = 'bar' + } + return errors + } + + const { queryAllByText, getByText } = render( + + +
{ + submitMock(values) + }} + render={({ + handleSubmit, + values, + errors, + submitFailed, + hasValidationErrors, + }) => ( + + {submitFailed && hasValidationErrors ? ( + + ) : null} + + + + )} + /> +
+
, + ) + + //Errors should display on submit. At first expect to have no error message + expect(queryAllByText(/bar/)).toHaveLength(0) + await wait(0) // Wait for promises to resolve + + //get the submit button, then click on it. Error summary renders only on submit + const context = document.querySelector('[type="submit"]').textContent + const submitButton = getByText(context) + clickOn(submitButton) + await wait(0) // Wait for promises to resolve + + expect(queryAllByText(/bar/)).toHaveLength(2) + }) +}) diff --git a/f2/src/components/ErrorSummary/index.js b/f2/src/components/ErrorSummary/index.js new file mode 100644 index 000000000..ae0c87e87 --- /dev/null +++ b/f2/src/components/ErrorSummary/index.js @@ -0,0 +1,51 @@ +/** @jsx jsx **/ +import { jsx } from '@emotion/core' +import { useLingui } from '@lingui/react' +import { Trans } from '@lingui/macro' +import { Stack } from '@chakra-ui/core' +import { Text } from '../text' +import { Ol } from '../ordered-list' +import { Li } from '../list-item' +import { A } from '../link' +import { useForm } from 'react-final-form' +import { Alert } from '../Messages' +import { useEffect } from 'react' + +export const ErrorSummary = props => { + const { i18n } = useLingui() + + const { errors } = useForm(props.onSubmit).getState() + + useEffect(() => { + const summary = document + .getElementById('error-summary') + .getBoundingClientRect() + + window.scrollTo(0, summary.y - 16) + }) + + return ( + + + + + + +
    + {Object.keys(errors).map(key => ( +
  1. + + {i18n._(errors[key])} + +
  2. + ))} +
+
+
+ ) +} diff --git a/f2/src/components/Field/index.js b/f2/src/components/Field/index.js index a2258e74e..f34fcab23 100644 --- a/f2/src/components/Field/index.js +++ b/f2/src/components/Field/index.js @@ -1,10 +1,11 @@ /** @jsx jsx */ import { jsx } from '@emotion/core' import PropTypes from 'prop-types' -import { FormErrorMessage, FormControl } from '@chakra-ui/core' +import { FormControl } from '@chakra-ui/core' import { FormHelperText } from '../FormHelperText' import { FormLabel } from '../FormLabel' -import { Field as FieldAdapter } from 'react-final-form' +import { FormErrorMessage } from '../FormErrorMessage' +import { Field as FieldAdapter, useField } from 'react-final-form' import { UniqueID } from '../unique-id' import { Input } from '../input' @@ -16,16 +17,31 @@ export const Field = ({ component, ...props }) => { + const { + meta: { invalid, submitFailed }, + } = useField(name, { + subscription: { + invalid: true, + submitFailed: true, + }, + }) + return ( {id => { return ( - + {label} {helperText && {helperText}} - {errorMessage} + {errorMessage && ( + {errorMessage} + )} { const { - meta: { error, touched }, - } = useField(name, { subscription: { touched: true, error: true } }) + meta: { submitFailed, invalid }, + } = useField(name, { + subscription: { submitFailed: true, invalid: true }, + }) + return ( {id => { return ( - - {label} - - {helperText} - {errorMessage} + + + {label} + + {helperText && {helperText}} + {errorMessage && ( + {errorMessage} + )} + + {/** This component comes with a group attribute. We don't need to use Chakra's or as per the Chakra docs */} {children} diff --git a/f2/src/components/FormErrorMessage/index.js b/f2/src/components/FormErrorMessage/index.js new file mode 100644 index 000000000..3c88ee2cd --- /dev/null +++ b/f2/src/components/FormErrorMessage/index.js @@ -0,0 +1,20 @@ +import React from 'react' +import { Text, useFormControl } from '@chakra-ui/core' + +export const FormErrorMessage = props => { + const formControl = useFormControl(props) + if (!formControl.isInvalid) { + return null + } + return {props.children} +} + +FormErrorMessage.defaultProps = { + fontSize: 'md', + fontWeight: 'bold', + color: 'red.700', + fontFamily: 'body', + lineHeight: 1.25, + mb: 1, + maxW: '600px', +} diff --git a/f2/src/components/FormHelperText/index.js b/f2/src/components/FormHelperText/index.js index 7bb1a8320..4ef7f2a6e 100644 --- a/f2/src/components/FormHelperText/index.js +++ b/f2/src/components/FormHelperText/index.js @@ -1,28 +1,11 @@ import styled from '@emotion/styled' import { FormHelperText as ChakraFormHelperText } from '@chakra-ui/core' -import { variant } from 'styled-system' -export const FormHelperText = styled(ChakraFormHelperText)( - variant({ - variants: { - above: { - mt: -2, - mb: 4, - }, - below: { - mt: 4, - mb: 0, - }, - unstyled: { - m: 0, - }, - }, - }), -) +export const FormHelperText = styled(ChakraFormHelperText)() FormHelperText.defaultProps = { - variant: 'above', as: 'p', + mt: 0, fontSize: 'md', color: 'black', fontFamily: 'body', diff --git a/f2/src/components/FormLabel/index.js b/f2/src/components/FormLabel/index.js index 7a498ff04..53135ac4e 100644 --- a/f2/src/components/FormLabel/index.js +++ b/f2/src/components/FormLabel/index.js @@ -7,7 +7,7 @@ FormLabel.defaultProps = { fontSize: 'xl', fontWeight: 'bold', fontFamily: 'body', - mb: 2, + p: 0, lineHeight: 1, maxW: '600px', } diff --git a/f2/src/components/input/index.js b/f2/src/components/input/index.js index d8eff1fd2..41066c035 100644 --- a/f2/src/components/input/index.js +++ b/f2/src/components/input/index.js @@ -8,6 +8,9 @@ export const Input = props => ( onKeyPress={e => { e.key === 'Enter' && e.preventDefault() }} + _invalid={{ + borderColor: 'red.700', + }} autoComplete="off" {...canada.variants.inputs.inputs} {...props.input} diff --git a/f2/src/forms/PrivacyConsentInfoForm.js b/f2/src/forms/PrivacyConsentInfoForm.js index b75e0b47b..69b825f7f 100644 --- a/f2/src/forms/PrivacyConsentInfoForm.js +++ b/f2/src/forms/PrivacyConsentInfoForm.js @@ -1,25 +1,24 @@ import React from 'react' import PropTypes from 'prop-types' import { Trans } from '@lingui/macro' -import { Form, useField } from 'react-final-form' +import { Form } from 'react-final-form' import { NextAndCancelButtons } from '../components/next-and-cancel-buttons' -import { FormControl, Stack } from '@chakra-ui/core' +import { Stack } from '@chakra-ui/core' import { useStateValue } from '../utils/state' import { A } from '../components/link' import { CheckboxAdapter } from '../components/checkbox' import { FormArrayControl } from '../components/FormArrayControl' import { useLingui } from '@lingui/react' -import { Alert } from '../components/Messages' +import { ErrorSummary } from '../components/ErrorSummary' -const Control = ({ name, ...rest }) => { - const { - meta: { error, touched }, - } = useField(name, { subscription: { touched: true, error: true } }) - return -} +const validate = values => { + const errors = {} + //condition for an error to occur: append a lingui id to the list of error + if (!values.consentOptions || values.consentOptions.length < 1) { + errors.consentOptions = 'privacyConsentInfoForm.warning' + } -const validate = () => { - return {} + return errors } export const PrivacyConsentInfoForm = props => { @@ -30,7 +29,6 @@ export const PrivacyConsentInfoForm = props => { ...data.formData.whetherConsent, } const consentOptions = ['privacyConsentInfoForm.yes'] - let showWarning = false return ( @@ -43,53 +41,51 @@ export const PrivacyConsentInfoForm = props => {
{ - if (values.consentOptions.length === 0) { - showWarning = true - } else { - props.onSubmit(values) - } + props.onSubmit(values) }} validate={validate} - render={({ handleSubmit, values }) => ( + render={({ + handleSubmit, + values, + errors, + submitFailed, + hasValidationErrors, + }) => ( - - - - {consentOptions.map(key => { - return ( - - - - - - - - - - ) - })} - {showWarning ? ( - - - - ) : null} - - - + {submitFailed && hasValidationErrors ? ( + + ) : null} + + } + > + {consentOptions.map(key => { + return ( + + + + + + + + + + ) + })} + } button={} diff --git a/f2/src/forms/WhatWasAffectedForm.js b/f2/src/forms/WhatWasAffectedForm.js index 1dc684260..ab2e8dd05 100644 --- a/f2/src/forms/WhatWasAffectedForm.js +++ b/f2/src/forms/WhatWasAffectedForm.js @@ -8,11 +8,16 @@ import { Stack } from '@chakra-ui/core' import { useStateValue } from '../utils/state' import { CheckboxAdapter } from '../components/checkbox' import { FormArrayControl } from '../components/FormArrayControl' +import { ErrorSummary } from '../components/ErrorSummary' import { Text } from '../components/text' -import { Alert } from '../components/Messages' -const validate = () => { - return {} +const validate = values => { + const errors = {} + //condition for an error to occur: append a lingui id to the list of error + if (!values.affectedOptions || values.affectedOptions.length < 1) { + errors.affectedOptions = 'whatWasAffectedForm.warning' + } + return errors } export const whatWasAffectedPages = [ @@ -40,7 +45,6 @@ export const WhatWasAffectedForm = props => { } const affectedOptions = whatWasAffectedPages.map(page => page.key) - let showWarning = false return ( @@ -61,24 +65,31 @@ export const WhatWasAffectedForm = props => { { - if (values.affectedOptions.length === 0) { - showWarning = true - } else { - props.onSubmit(values) - } + props.onSubmit(values) }} validate={validate} - render={({ handleSubmit, values }) => ( + render={({ + handleSubmit, + values, + errors, + submitFailed, + hasValidationErrors, + }) => ( + {submitFailed && hasValidationErrors ? ( + + ) : null} + } helperText={} + errorMessage={} > {affectedOptions.map(key => { return ( @@ -98,11 +109,6 @@ export const WhatWasAffectedForm = props => { ) })} - {showWarning ? ( - - - - ) : null} } diff --git a/f2/src/locales/en.json b/f2/src/locales/en.json index 853c51d1d..190bf6ac3 100644 --- a/f2/src/locales/en.json +++ b/f2/src/locales/en.json @@ -97,6 +97,7 @@ "contactinfoPage.skipInfo": "You can skip this step to report anonymously.", "contactinfoPage.title": "Enter your contact details", "default.end": ", and ", + "default.hasValidationErrors": "Please fix the errors on this page.", "default.middle": ", ", "default.pair": " and ", "devicePage.account": "What type of account was affected?", diff --git a/f2/src/locales/fr.json b/f2/src/locales/fr.json index c8b1ccb43..376b5b4da 100644 --- a/f2/src/locales/fr.json +++ b/f2/src/locales/fr.json @@ -97,6 +97,7 @@ "contactinfoPage.skipInfo": "Vous pouvez ignorer cette étape si vous souhaitez signaler de façon anonyme.", "contactinfoPage.title": "Vos coordonnées", "default.end": " et ", + "default.hasValidationErrors": "Veuillez corriger les erreurs sur cete page", "default.middle": ", ", "default.pair": " et ", "devicePage.account": "Quel type de compte a été affecté?",