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

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 :

  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 :

<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 :

{{ 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. 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).