Skip to content

Commit

Permalink
Gestion des erreurs sur les inputs (#249)
Browse files Browse the repository at this point in the history
* Ajout d'erreur sur les inputs

* test état d'erreur

* Ajout des erreurs sur le formulaire structure

* Ajout d'erreur pour le formulaire coordinateur

---------

Co-authored-by: ornella <[email protected]>
  • Loading branch information
Alezco and Ornella452 committed Nov 28, 2024
1 parent 38ce471 commit dc2584c
Show file tree
Hide file tree
Showing 21 changed files with 381 additions and 56 deletions.
10 changes: 8 additions & 2 deletions src/components/commun/Datepicker.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';

export default function Datepicker({ children, id, isRequired = true, onChange, min }) {
export default function Datepicker({ children, id, isRequired = true, onChange, min, error }) {
return (
<div className="fr-input-group">
<div className={`fr-input-group${error ? ' fr-input-group--error' : ''}`}>
<label className="fr-label" htmlFor={id}>
{children}
</label>
<input className="fr-input cc-datepicker" id={id} type="date" required={isRequired} onChange={onChange} min={min} name={id} />
{error && (
<p id="text-input-error-desc-error" className="fr-error-text">
{error}
</p>
)}
</div>
);
}
Expand All @@ -19,4 +24,5 @@ Datepicker.propTypes = {
onChange: PropTypes.func,
min: PropTypes.string,
name: PropTypes.string,
error: PropTypes.string
};
30 changes: 26 additions & 4 deletions src/components/commun/Input.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';

export default function Input({ children, id, isRequired = true, autoComplete = 'on', testId = '', type = 'text',
pattern, onChange, list, min, disabled, isLoading, ariaBusy, value, maxlength }) {
export default function Input({
children,
id,
isRequired = true,
autoComplete = 'on',
testId = '',
type = 'text',
pattern,
onChange,
list,
min,
disabled,
isLoading,
ariaBusy,
value,
maxlength,
error,
}) {
return (
<div className="fr-fieldset__element">
<div className="fr-input-group">
<div className={`fr-input-group${error ? ' fr-input-group--error' : ''}`}>
<label className="fr-label" htmlFor={id}>{children}</label>
<input
className="fr-input"
Expand All @@ -29,6 +45,11 @@ export default function Input({ children, id, isRequired = true, autoComplete =
<div className="fr-spinner fr-spinner--sm" aria-label="Chargement..."></div>
</div>
)}
{error && (
<p id="text-input-error-desc-error" className="fr-error-text">
{error}
</p>
)}
</div>
</div>
);
Expand All @@ -49,5 +70,6 @@ Input.propTypes = {
ariaBusy: PropTypes.bool,
value: PropTypes.string,
testId: PropTypes.string,
maxlength: PropTypes.string
maxlength: PropTypes.string,
error: PropTypes.string
};
10 changes: 8 additions & 2 deletions src/components/commun/ZoneDeTexte.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';

export default function ZoneDeTexte({ children, id, isRequired = true, maxlength = "2500" }) {
export default function ZoneDeTexte({ children, id, isRequired = true, maxlength = '2500', error }) {
return (
<div className="fr-input-group">
<div className={`fr-input-group${error ? ' fr-input-group--error' : ''}`}>
<label className="fr-label" htmlFor={id}>
{children}
</label>
<textarea className="fr-input" id={id} name={id} required={isRequired} maxLength={maxlength}></textarea>
{error && (
<p id="text-input-error-desc-error" className="fr-error-text">
{error}
</p>
)}
</div>
);
}
Expand All @@ -17,4 +22,5 @@ ZoneDeTexte.propTypes = {
id: PropTypes.string,
isRequired: PropTypes.bool,
maxlength: PropTypes.string,
error: PropTypes.string,
};
9 changes: 9 additions & 0 deletions src/shared/checkValidity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const checkValidity = (ref, setErrors) => {
const formData = new FormData(ref.current);
const keys = Array.from(formData.keys());
const formElements = keys.map(key => document.getElementById(key)).filter(key => key !== null);
const errors = formElements.map(formElement => ({
[formElement.id]: formElement.validationMessage,
}));
setErrors(Object.assign({}, ...errors));
};
12 changes: 9 additions & 3 deletions src/views/candidature-conseiller/AddressChooser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import React, { useState } from 'react';
import Input from '../../components/commun/Input';
import { useGeoApi } from './useGeoApi';
import { debounce } from './debounce';
import PropTypes from 'prop-types';

export default function AddressChooser() {
export default function AddressChooser({ error }) {
const { searchByName, villes } = useGeoApi();
const [codeCommune, setCodeCommune] = useState('');

return (
<>
<Input
autoComplete= "off"
autoComplete="off"
id="lieuHabitation"
list="resultatsRecherche"
error={error}
onChange={debounce(async event => {
if (event.target.value.length >= 3) {
searchByName(event.target.value);
Expand All @@ -25,7 +27,7 @@ export default function AddressChooser() {
Votre lieu d’habitation <span className="cc-obligatoire">*</span>{' '}
<span className="fr-hint-text">Saississez le nom ou le code postal de votre commune.</span>
</Input>
<Input type="hidden" id="lieuHabitationCodeCommune" value={codeCommune} testId="lieuHabitationCodeCommune-hidden"/>
<Input type="hidden" id="lieuHabitationCodeCommune" value={codeCommune} testId="lieuHabitationCodeCommune-hidden" />
<datalist id="resultatsRecherche">
{villes?.map(({ codesPostaux, nom }, key) => (
<option value={`${codesPostaux[0]} ${nom}`} key={key}>
Expand All @@ -36,3 +38,7 @@ export default function AddressChooser() {
</>
);
}

AddressChooser.propTypes = {
error: PropTypes.string
};
17 changes: 11 additions & 6 deletions src/views/candidature-conseiller/CandidatureConseiller.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import SommaireConseiller from './SommaireConseiller';
import InformationsDeContact from './InformationsDeContact';
import SituationEtExperience from './SituationEtExperience';
Expand All @@ -10,6 +10,7 @@ import Captcha from '../../components/commun/Captcha';
import { useScrollToSection } from '../../hooks/useScrollToSection';
import { useNavigate } from 'react-router-dom';
import { useApiAdmin } from './useApiAdmin';
import { checkValidity } from '../../shared/checkValidity';

import '@gouvfr/dsfr/dist/component/form/form.min.css';
import '@gouvfr/dsfr/dist/component/input/input.min.css';
Expand All @@ -25,8 +26,10 @@ export default function CandidatureConseiller() {
const [dateDisponibilite, setDateDisponibilite] = useState('');
const [isSituationValid, setIsSituationValid] = useState(true);
const [validationError, setValidationError] = useState('');
const [errors, setErrors] = useState({});
const [widgetId, setWidgetId] = useState(null);
const { buildConseillerData, creerCandidatureConseiller } = useApiAdmin();
const ref = useRef(null);

const navigate = useNavigate();
useScrollToSection();
Expand Down Expand Up @@ -89,16 +92,18 @@ export default function CandidatureConseiller() {
<form
aria-label="Candidature conseiller"
onSubmit={validerLaCandidature}
onInput={() => checkValidity(ref, setErrors)}
ref={ref}
>
<InformationsDeContact />
<InformationsDeContact errors={errors} />
<SituationEtExperience isSituationValid={isSituationValid} />
<Disponibilite setDateDisponibilite={setDateDisponibilite} />
<Motivation />
<Disponibilite setDateDisponibilite={setDateDisponibilite} errors={errors} />
<Motivation errors={errors} />
<EnResume dateDisponibilite={dateDisponibilite} />
<div className="fr-mt-2w fr-mb-2w">
<Captcha setWidgetId={setWidgetId} widgetId={widgetId}/>
<Captcha setWidgetId={setWidgetId} widgetId={widgetId} />
</div>
<button className="fr-btn cc-envoyer" type="submit">
<button className="fr-btn cc-envoyer" type="submit" onClick={() => checkValidity(ref, setErrors)}>
Envoyer votre candidature
</button>
</form>
Expand Down
57 changes: 56 additions & 1 deletion src/views/candidature-conseiller/CandidatureConseiller.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ describe('candidature conseiller', () => {
vi.useRealTimers();
});

it('quand je remplis le formulaire et que je saisi mon lieu d’habitation alors la recherche est lancer à partir de 3 caractères', async () => {
it('quand je remplis le formulaire et que je saisis mon lieu d’habitation alors la recherche est lancée à partir de 3 caractères', async () => {
// GIVEN
vi.useFakeTimers();
const searchByNameSpy = vi.fn();
Expand Down Expand Up @@ -753,4 +753,59 @@ describe('candidature conseiller', () => {

vi.useRealTimers();
});

it.each([
{
description: 'un prénom',
selector: 'Prénom *',
message: 'Veuillez renseigner le prénom'
},
{
description: 'un nom',
selector: 'Nom *',
message: 'Veuillez renseigner le nom'
},
{
description: 'un email',
selector: 'Adresse électronique * Format attendu : [email protected]',
message: 'Veuillez renseigner le mail'
},
{
description: 'une adresse',
selector: 'Votre lieu d’habitation * Saississez le nom ou le code postal de votre commune.',
message: 'Veuillez renseigner l’adresse'
},
{
description: 'une date',
selector: 'Choisir une date',
message: 'Veuillez renseigner la date'
},
{
description: 'une motivation',
selector: 'Votre message * Limité à 2500 caractères',
message: 'Veuillez renseigner la motivation'
},
])('quand je valide le formulaire avec $description vide alors j’ai un message d’erreur ', async ({ selector, message }) => {
// GIVEN
vi.stubGlobal('turnstile', {
reset: vi.fn(),
remove: vi.fn(),
render: vi.fn()
});
render(<CandidatureConseiller />);
const champDeFormulaire = screen.getByLabelText(selector);
Object.defineProperty(champDeFormulaire, 'validationMessage', {
value: message,
configurable: true,
});

// WHEN
const envoyer = screen.getByRole('button', { name: 'Envoyer votre candidature' });

fireEvent.click(envoyer);

// THEN
const contenuErreurValidation = await screen.findByText(message, { selector: 'p' });
expect(contenuErreurValidation).toBeInTheDocument();
});
});
5 changes: 3 additions & 2 deletions src/views/candidature-conseiller/Disponibilite.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import BoutonRadio from '../../components/commun/BoutonRadio';
import Datepicker from '../../components/commun/Datepicker';
import PropTypes from 'prop-types';

export default function Disponibilite({ setDateDisponibilite }) {
export default function Disponibilite({ setDateDisponibilite, errors }) {
const dateDuJour = new Date().toISOString().slice(0, 10);

return (
Expand All @@ -16,7 +16,7 @@ export default function Disponibilite({ setDateDisponibilite }) {
<p className="fr-text--sm fr-hint-text">
Accompagnement de personnes vers l’autonomie dans leurs usages de technologies, services et médias numériques.
</p>
<Datepicker id="dateDisponibilite" onChange={event => setDateDisponibilite(event.target.value)} min={dateDuJour}>
<Datepicker id="dateDisponibilite" onChange={event => setDateDisponibilite(event.target.value)} min={dateDuJour} error={errors.dateDisponibilite}>
Choisir une date
</Datepicker>
<hr />
Expand Down Expand Up @@ -57,4 +57,5 @@ export default function Disponibilite({ setDateDisponibilite }) {

Disponibilite.propTypes = {
setDateDisponibilite: PropTypes.func,
errors: PropTypes.object
};
13 changes: 11 additions & 2 deletions src/views/candidature-conseiller/InformationsDeContact.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import React from 'react';
import Input from '../../components/commun/Input';
import AddressChooser from './AddressChooser';
import PropTypes from 'prop-types';

export default function InformationsDeContact() {
export default function InformationsDeContact({ errors }) {
return (
<fieldset className="fr-border cc-section fr-p-3w fr-mb-3w" id="informations-de-contact">
<legend className="fr-h5">Vos informations de contact</legend>
<hr />
<Input
id="prenom"
error={errors.prenom}
>
Prénom <span className="cc-obligatoire">*</span>
</Input>
<Input
id="nom"
error={errors.nom}
>
Nom <span className="cc-obligatoire">*</span>
</Input>
<Input
id="email"
type="email"
pattern=".+@.+\..{2,}"
error={errors.email}
>
Adresse électronique <span className="cc-obligatoire">*</span> <span className="fr-hint-text">Format attendu : [email protected]</span>
</Input>
Expand All @@ -29,10 +33,15 @@ export default function InformationsDeContact() {
type="tel"
pattern="([+][0-9]{11,12})|([0-9]{10})"
isRequired={false}
error={errors.telephone}
>
Téléphone <span className="fr-hint-text">Format attendu : 0122334455 ou +33122334455</span>
</Input>
<AddressChooser />
<AddressChooser error={errors.lieuHabitation} />
</fieldset>
);
}

InformationsDeContact.propTypes = {
errors: PropTypes.object
};
9 changes: 7 additions & 2 deletions src/views/candidature-conseiller/Motivation.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React from 'react';
import ZoneDeTexte from '../../components/commun/ZoneDeTexte';
import PropTypes from 'prop-types';

export default function Motivation() {
export default function Motivation({ errors }) {
return (
<fieldset className="fr-border cc-section fr-p-3w fr-mb-3w" id="votre-motivation">
<legend className="fr-h5" id="titreMotivation">Votre motivation</legend>
<p className="fr-text--sm fr-hint-text">
En quelques lignes, décrivez votre motivation personnelle pour devenir conseiller numérique et{' '}
aider les personnes à devenir autonomes dans l’utilisation des outils numériques.
</p>
<ZoneDeTexte id="motivation">
<ZoneDeTexte id="motivation" error={errors.motivation}>
Votre message <span className="cc-obligatoire">*</span> <span className="fr-hint-text">Limité à 2500 caractères</span>
</ZoneDeTexte>
</fieldset>
);
}

Motivation.propTypes = {
errors: PropTypes.object
};
9 changes: 7 additions & 2 deletions src/views/candidature-coordinateur/BesoinEnCoordinateur.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import BoutonRadio from '../../components/commun/BoutonRadio';
import Datepicker from '../../components/commun/Datepicker';
import PropTypes from 'prop-types';

export default function BesoinEnCoordinateur() {
export default function BesoinEnCoordinateur({ errors }) {
const dateDuJour = new Date().toISOString().slice(0, 10);

return (
Expand All @@ -28,9 +29,13 @@ export default function BesoinEnCoordinateur() {
</BoutonRadio>
<hr />
<p className="fr-mb-3w cc-bold">À partir de quand êtes vous prêt à accueillir votre coordinateur ? <span className="cc-obligatoire">*</span></p>
<Datepicker id="dateDebutMission" min={dateDuJour}>
<Datepicker id="dateDebutMission" min={dateDuJour} error={errors.dateDebutMission}>
Choisir une date
</Datepicker>
</fieldset >
);
}

BesoinEnCoordinateur.propTypes = {
errors: PropTypes.object
};
Loading

0 comments on commit dc2584c

Please sign in to comment.