# 02 — Formulaire de devis : labels orphelins, CAPTCHA cognitif, erreurs muettes

> **Items** : Form labels — **required** · Accessible form errors — **required** · Accessible authentication — recommended · Mobile-friendly inputs — recommended
> **Verdict** : 🔴 **FAIL** (labels), 🟠 PARTIAL (erreurs, mobile) · **Priorité : P1** — c'est l'unique point de conversion du site
> **Effort estimé** : 1 jour

Le seul formulaire du site est la « Demande de devis gratuit » sur `/contact` (aucun autre formulaire site-wide). Chaque défaut ci-dessous filtre des prospects.

## 2.1 — Labels : 7 labels présents… associés à rien (FAIL, required)

Les `<label>` existent visuellement (« Nom * », « Prénom », « Email * », « Téléphone * », « Votre demande * »…) mais **aucun n'a de `for`, aucun n'enveloppe son champ, et aucun input n'a d'`id`**. Résultat mesuré sur les 7 champs : `labelled: false` partout — un lecteur d'écran annonce chaque champ « edit, blank », sans nom. Les placeholders (« pierre.martin@gmail.com », « Votre réponse ») servent de pseudo-labels, ce qui n'en fait pas.

**Fix** (chaque champ) :

```html
<label for="contact-name">Nom *</label>
<input id="contact-name" name="name" type="text" required autocomplete="family-name">

<label for="contact-email">Email *</label>
<input id="contact-email" name="email" type="email" required autocomplete="email">

<label for="contact-phone">Téléphone *</label>
<input id="contact-phone" name="phone" type="tel" required autocomplete="tel">
```

## 2.2 — CAPTCHA mathématique sans alternative (FAIL)

Le formulaire est gardé par « Vérification anti-spam : Combien font 3 - 1 ? * » (champ `type="number"`, bouton « 🔄 Nouveau »). C'est un **test de fonction cognitive** sans alternative — barrière pour les handicaps cognitifs, et le champ n'a de surcroît aucun nom accessible (§2.1).

L'ironie : le formulaire embarque **déjà deux honeypots** (`_gotcha`, `website`) qui font le travail anti-spam sans rien demander à l'humain.

**Fix** : supprimer le CAPTCHA math ; garder honeypots + rate-limiting côté serveur. Si un challenge est exigé, prendre un mécanisme sans épreuve (widget de type proof-of-work invisible) — jamais un puzzle obligatoire devant un formulaire d'urgence.

## 2.3 — Erreurs : validation native seule, ni associée ni stylée (PARTIAL, required)

Constaté sur soumission à vide : la validation HTML5 native bloque bien l'envoi (`required` présents, pas de `novalidate` ✓ — c'est la base attendue), le focus va au premier champ invalide ✓. Mais : `aria-invalid` jamais posé, aucun `aria-describedby`, aucun élément `role="alert"`/`aria-live` dans la page, **aucun style d'état invalide** (pas même une bordure rouge). Et le message natif s'accroche à un champ… sans nom (§2.1) : « edit, blank — Please fill out this field ».

**Fix** (après §2.1) :

```html
<input id="contact-email" … aria-invalid="true" aria-describedby="email-error">
<p id="email-error" role="alert">Merci de renseigner votre email.</p>
```

Styler l'état avec les sélecteurs CSS natifs plutôt que du toggling de classes JS :

```css
input:user-invalid { border-color: #c0392b; }
.field:has(input:user-invalid) .error { display: block; }
```

## 2.4 — Inputs mobiles : bon socle, zéro autocomplete (PARTIAL)

✓ `type="email"` et `type="tel"` corrects (bons claviers), police 16 px partout (pas de zoom iOS).
✗ Aucun `autocomplete` sur les champs réels (seuls les honeypots en ont un — `off`), aucun `enterkeyhint`.

**Fix** : `autocomplete="name|given-name|family-name|email|tel"` selon le champ (permet le remplissage en un geste — décisif sur un formulaire d'urgence rempli dans le jardin), `enterkeyhint="next"` puis `"send"` sur le dernier champ.

## 2.5 — Soumission JS-only (robustesse)

`action: null`, `method: null` : le formulaire ne fonctionne pas sans JavaScript ou si le bundle échoue. Les champs `_gotcha`/`_subject` suggèrent un backend type Formspree qui se câble normalement par `action` statique.

**Fix** : poser `action="https://<endpoint>" method="POST"` en fallback natif ; le handler JS peut continuer d'intercepter pour l'UX.

## Vérification

- Lecteur d'écran : chaque champ annonce son nom (« Nom, obligatoire, édition ») ; une soumission invalide annonce l'erreur.
- `[...document.querySelectorAll('input,textarea')].every(el => el.labels?.length > 0)` → true (hors hidden/honeypots).
- Le formulaire s'envoie sans résoudre d'énigme ; les honeypots restent invisibles et non focusables (`tabindex="-1"` + `aria-hidden="true"` sur les honeypots).
- JS désactivé : la soumission aboutit via `action`.
