Files
malio-layer-ui/docs/superpowers/specs/2026-06-03-required-asterisk-email-sanitization-design.md
T
2026-06-03 11:16:07 +02:00

146 lines
7.3 KiB
Markdown

# Design — État « obligatoire » cohérent + normalisation email
- **Date** : 2026-06-03
- **Ticket Malio UI** : MUI-41 (branche `feature/MUI-41-props-required-asterisque-dans-le-label-sur-les-co`)
- **Ticket Starseed lié** : ERP-101 (MAJ Malio UI + branchement `required` + stratégie de validation), découvert pendant ERP-63 (écran « Ajouter un client »)
## Contexte & problème
Pendant ERP-63, deux manques ont bloqué la mise en place de champs obligatoires :
1. Certains composants de formulaire n'exposent pas de prop `required` (`MalioSelect`, `MalioSelectCheckbox`), et **aucun composant n'affiche d'indicateur visuel** de champ obligatoire. Résultat : le bouton « Valider » se bloque sans feedback à l'utilisateur — anti-pattern UX.
2. Tentation erronée de « masquer » l'email à la maska. Un email n'a **pas** de structure fixe : pas de masque. Le bon comportement est une **sanitisation** légère à la saisie + validation déléguée à la couche `error`.
État réel constaté (inventaire) : la **majorité** des composants ont déjà la prop `required` (câblée sur l'attribut HTML natif uniquement, sans astérisque). Seuls **5** ne l'ont pas : `Select`, `SelectCheckbox`, `InputUpload`, `InputRichText`, `SiteSelector`. Aucun composant n'affiche d'astérisque. Il n'existe pas de composant de label partagé : chaque composant rend `{{ label }}` dans son propre `<label>` au style spécifique (floating labels).
## Objectifs
- Prop `required: boolean` cohérente sur **toute la famille formulaire**.
- Quand `required` est vrai → **astérisque rouge dans le label**.
- `MalioInputEmail` : sanitisation à la saisie (suppression de tous les espaces, option `lowercase`), **sans** masque ni validation de format.
- Mettre à jour `COMPONENTS.md` et `CHANGELOG.md`.
## Hors scope
- Validation de format email (reste à la charge de la couche validation via la prop `error`, alimentée serveur ou check client).
- Toute logique de masque sur l'email.
- Refonte des suites de tests existantes.
## Décisions de cadrage (validées avec l'utilisateur)
| Décision | Choix retenu |
|---|---|
| Périmètre `required` + astérisque | **Toute la famille formulaire**, y compris `InputUpload`, `InputRichText`, `SiteSelector` |
| Prop `lowercase` (email) | **Opt-in, défaut `false`** |
| Espaces email | **Supprimer tous les espaces** (début, milieu, fin), avec préservation du curseur |
| Accessibilité astérisque | `aria-hidden="true"` — la sémantique est portée par l'attribut HTML natif `required` |
## Section 1 — Indicateur « obligatoire »
### Composant partagé `MalioRequiredMark`
Nouveau composant `app/components/malio/shared/RequiredMark.vue` (auto-importé `<MalioRequiredMark>`). Source unique de vérité pour couleur/espacement.
Rendu :
```vue
<span aria-hidden="true" class="ml-0.5 select-none text-m-danger">*</span>
```
- `aria-hidden="true"` : évite la double annonce, la sémantique est déjà sur l'attribut natif `required`.
- Couleur via token existant `text-m-danger` (`--m-danger`, rouge `#F2696B`).
- `defineOptions({ name: 'MalioRequiredMark', inheritAttrs: false })`.
### Intégration
Dans chaque composant de la famille, remplacer `{{ label }}` par :
```vue
{{ label }}<MalioRequiredMark v-if="required" />
```
L'astérisque vit **à l'intérieur du `<label>`** → il flotte avec le floating-label et reste dans la pastille blanche.
### Props à ajouter
`required?: boolean` (défaut `false`) sur les 5 composants qui ne l'ont pas : `Select`, `SelectCheckbox`, `InputUpload`, `InputRichText`, `SiteSelector`. Câblage sur l'attribut natif quand le composant a un élément de formulaire sous-jacent qui le supporte.
### Composants concernés par le rendu de l'astérisque
Famille formulaire complète : `InputText`, `InputEmail`, `InputPhone`, `InputPassword`, `InputTextArea`, `InputAmount`, `InputNumber`, `InputAutocomplete`, `InputUpload`, `InputRichText`, `Select`, `SelectCheckbox`, `Checkbox`, `RadioButton`, `Date`, `DateTime`, `DateRange`, `DateWeek`, `Time`, `TimePicker`, `SiteSelector`.
### Alternative écartée
Inliner un `<span>` dans chaque composant : duplication, couleur/espacement à changer à ~20 endroits. Le composant partagé est préféré.
## Section 2 — Sanitisation `MalioInputEmail`
### Nouvelle prop
`lowercase?: boolean` (défaut `false`).
### Fonction de sanitisation (pure, testable)
```ts
const sanitizeEmail = (v: string) => {
let out = v.replace(/\s+/g, '') // supprime TOUT espace
if (props.lowercase) out = out.toLowerCase()
return out
}
```
### `onInput` réécrit
```ts
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement
const raw = target.value
const sanitized = sanitizeEmail(raw)
if (sanitized !== raw) {
const caret = target.selectionStart ?? raw.length
const newCaret = sanitizeEmail(raw.slice(0, caret)).length
target.value = sanitized
target.setSelectionRange(newCaret, newCaret)
}
if (!isControlled.value) localValue.value = sanitized
emit('update:modelValue', sanitized)
}
```
Points clés :
- **Curseur** : position recalculée en sanitisant la portion à gauche du curseur → taper un espace au milieu ne fait pas sauter le curseur. `lowercase` ne change pas la longueur, donc n'affecte pas la position.
- **Resynchro DOM** : `target.value = sanitized` même en mode contrôlé, pour que l'affichage colle toujours à la valeur émise.
- **Collage** couvert (paste déclenche `input`).
- **Inchangé** : `type="email"`, `inputmode="email"`, icône, et **aucune validation de format**.
## Section 3 — Tests, docs & livraison
### Tests (colocalisés `*.test.ts`)
- `RequiredMark.test.ts` — rend `*`, `aria-hidden="true"`, classe `text-m-danger`.
- 1 test ciblé par composant équipé : `required: true` → astérisque présent dans le label ; défaut → absent. S'appuie sur le helper `mountComponent` existant de chaque fichier.
- `InputEmail.test.ts` — espaces (début/milieu/fin) supprimés ; `lowercase=false` préserve la casse ; `lowercase=true` minuscule ; valeur émise sanitisée ; valeur DOM resynchronisée. Le curseur n'est pas testé (peu fiable en jsdom) → on teste la valeur.
⚠️ Suite de tests **flaky** connue (timeouts intermittents). Lancer les tests des fichiers touchés ; en cas de timeout non lié aux changements, relancer / documenter plutôt que conclure à un échec.
### Documentation (manuelle, requise par convention)
- `COMPONENTS.md` : ajouter la ligne `required` aux 5 composants manquants ; ajouter `lowercase` à `MalioInputEmail` ; mentionner en intro famille formulaire que `required` affiche un astérisque rouge.
- `CHANGELOG.md` : entrée(s) `MUI-41` sous `### Added`, format existant (`* [#MUI-41] ...`).
### Playground / Histoire
Ajouter un exemple `required` + un exemple email `lowercase` sur les pages playground concernées si coût faible ; sinon signaler (hors scope strict).
### Découpage de livraison (1 PR, commits Conventional)
1. `feat(ui): MalioRequiredMark + prop required sur Select/SelectCheckbox/Upload/RichText/SiteSelector`
2. `feat(ui): astérisque required dans le label de la famille formulaire`
3. `feat(inputs): sanitisation email (suppression espaces + option lowercase)`
4. `docs: COMPONENTS.md + CHANGELOG`
Branche : `feature/MUI-41-props-required-asterisque-dans-le-label-sur-les-co` (inchangée).