Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.0 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) ; préservation du curseur best-effort (voir caveat ci-dessous) |
| 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 4 composants qui ne l'ont pas et qui possèdent un label de champ : Select, SelectCheckbox, InputUpload, InputRichText.
Câblage accessibilité (a11y)
L'astérisque est aria-hidden : la sémantique « obligatoire » doit donc être portée par le DOM.
- Élément natif
requireddéjà câblé (asterisque suffit) :InputText,InputEmail,InputPhone,InputPassword,InputTextArea,InputAmount,InputNumber,InputAutocomplete,Checkbox,RadioButton,Time,TimePicker, etCalendarField(date family). - Pas de
requirednatif → ajouter:aria-required="required || undefined"sur l'élément interactif :Select/SelectCheckbox: le<button>déclencheur (combobox).InputRichText: le wrapper éditeur (#editorId, contenteditable via TipTap).InputUpload: possède un<input type="file">natif → on câble:required="required"dessus (natif).
Composants concernés par le rendu de l'astérisque
InputText, InputEmail, InputPhone, InputPassword, InputTextArea, InputAmount, InputNumber, InputAutocomplete, InputUpload, InputRichText, Select, SelectCheckbox, Checkbox, RadioButton, Time, TimePicker, et CalendarField (rendu mutualisé pour Date, DateTime, DateRange, DateWeek).
Exclusion : SiteSelector
MalioSiteSelector est un radiogroup de tuiles (segmented control) : il n'a pas de label de champ (son labelClass style le nom de chaque tuile). Y placer un astérisque n'a pas de sens. Il est exclu du périmètre required/astérisque. À rouvrir si un besoin de « groupe obligatoire » émerge (ce serait alors un libellé de groupe distinct, hors de ce ticket).
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) {
// `<input type="email">` ne supporte PAS l'API de sélection :
// selectionStart vaut null, setSelectionRange lève une exception.
// On garde donc la repositionnement défensif (no-op sur type=email).
const caret = target.selectionStart
target.value = sanitized
if (caret !== null) {
const newCaret = sanitizeEmail(raw.slice(0, caret)).length
try {
target.setSelectionRange(newCaret, newCaret)
} catch {
/* type d'input sans support de sélection — ignore */
}
}
}
if (!isControlled.value) localValue.value = sanitized
emit('update:modelValue', sanitized)
}
Points clés :
- Resynchro DOM :
target.value = sanitizedmême en mode contrôlé, pour que l'affichage colle toujours à la valeur émise. - Caveat curseur : la spec HTML interdit l'API de sélection sur
type="email"(selectionStart=null,setSelectionRangelève). La repositionnement est donc best-effort et inactif sur l'email : sur le cas rare d'une suppression d'espace en milieu de chaîne, le curseur peut aller en fin. Les cas courants (espace en fin, collage) gardent naturellement le curseur en fin. Le code est gardé (caret !== null+try/catch) pour ne jamais lever. - 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).