Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.3 KiB
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 :
- 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. - 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: booleancohérente sur toute la famille formulaire. - Quand
requiredest vrai → astérisque rouge dans le label. MalioInputEmail: sanitisation à la saisie (suppression de tous les espaces, optionlowercase), sans masque ni validation de format.- Mettre à jour
COMPONENTS.mdetCHANGELOG.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 :
<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 natifrequired.- 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 :
{{ 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)
const sanitizeEmail = (v: string) => {
let out = v.replace(/\s+/g, '') // supprime TOUT espace
if (props.lowercase) out = out.toLowerCase()
return out
}
onInput réécrit
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.
lowercasene change pas la longueur, donc n'affecte pas la position. - Resynchro DOM :
target.value = sanitizedmê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", classetext-m-danger.- 1 test ciblé par composant équipé :
required: true→ astérisque présent dans le label ; défaut → absent. S'appuie sur le helpermountComponentexistant de chaque fichier. InputEmail.test.ts— espaces (début/milieu/fin) supprimés ;lowercase=falsepréserve la casse ;lowercase=trueminuscule ; 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 lignerequiredaux 5 composants manquants ; ajouterlowercaseàMalioInputEmail; mentionner en intro famille formulaire querequiredaffiche un astérisque rouge.CHANGELOG.md: entrée(s)MUI-41sous### 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)
feat(ui): MalioRequiredMark + prop required sur Select/SelectCheckbox/Upload/RichText/SiteSelectorfeat(ui): astérisque required dans le label de la famille formulairefeat(inputs): sanitisation email (suppression espaces + option lowercase)docs: COMPONENTS.md + CHANGELOG
Branche : feature/MUI-41-props-required-asterisque-dans-le-label-sur-les-co (inchangée).