docs: spec required asterisk + email sanitization (MUI-41)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user