Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gestion des erreurs sur les inputs #249

Merged
merged 4 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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