Files
malio-layer-ui/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md
T
tristan 9f772a84ed
Release / release (push) Successful in 1m9s
fix: accessibilité des composants (#70)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #70
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-09 15:40:44 +00:00

6.4 KiB

MalioInputEmail — bouton « + » d'ajout (event add)

Date : 2026-06-09 Statut : Validé, prêt pour plan d'implémentation Périmètre : MalioInputEmail uniquement.

Objectif

Ajouter à MalioInputEmail le même bouton « + » que MalioInputPhone : un bouton optionnel à droite du champ qui émet un event add, permettant au consommateur d'ajouter dynamiquement un autre champ email (ou toute autre action).

La logique email existante (ajoutée en MUI-41) est conservée intégralement : sanitizeEmail (suppression des espaces), prop lowercase, gestion du caret dans onInput, type="email" / inputmode="email". Ce travail n'y touche pas.

Décisions validées

Sujet Décision
API Calquée sur MalioInputPhone : props addable / addIconName / addButtonLabel, event add, data-test="add-button".
Collision icône/bouton L'icône email étant à droite par défaut, quand addable est actif l'icône passe automatiquement à gauche et le « + » occupe la droite (disposition éprouvée de Phone).
Libellé par défaut addButtonLabel défaut 'Ajouter une adresse email'.
Garde désactivé Le bouton n'émet pas add si disabled ou readonly (comme Phone).
Approche Recopier le pattern de Phone dans Email (pas d'extraction d'un composant partagé — noté comme cleanup futur possible, hors scope).

Conception détaillée

1. Props ajoutées (interface + défauts)

    addable?: boolean       // défaut: false
    addIconName?: string    // défaut: 'mdi:plus'
    addButtonLabel?: string // défaut: 'Ajouter une adresse email'

2. Event ajouté

L'defineEmits passe de :

const emit = defineEmits<{
  (event: 'update:modelValue', value: string): void
}>()

à :

const emit = defineEmits<{
  (event: 'update:modelValue', value: string): void
  (event: 'add'): void
}>()

3. Position d'icône « effective »

Nouvelle règle, unique source du repositionnement :

const effectiveIconPosition = computed(() =>
  props.addable && props.iconName ? 'left' : props.iconPosition,
)

Les quatre computeds qui dépendent aujourd'hui de props.iconPosition utilisent désormais effectiveIconPosition.value :

  • iconInputPaddingClass
  • iconPositionClass
  • labelPositionClass
  • focusPaddingClass

Conséquence :

  • addable=falseeffectiveIconPosition === props.iconPosition → comportement strictement identique à aujourd'hui.
  • addable=true (avec une icône) → icône à gauche + espace réservé à droite pour le bouton.

4. iconInputPaddingClass aligné sur Phone

Remplacement de l'implémentation actuelle d'Email par la forme éprouvée de Phone (rendu identique dans le cas non-addable car la classe de base pl-3 pr-3 est commune aux deux composants) :

const iconInputPaddingClass = computed(() => {
  const leftIcon = props.iconName && effectiveIconPosition.value === 'left'
  const rightIcon = props.iconName && effectiveIconPosition.value === 'right'
  const parts: string[] = []
  if (leftIcon) parts.push('!pl-11')
  if (rightIcon || props.addable) parts.push('!pr-10')
  return parts.join(' ')
})

5. Bouton dans le template

Inséré après le bloc <IconifyIcon v-if="iconName">, à l'identique de Phone :

      <button
        v-if="addable"
        type="button"
        :disabled="disabled"
        :aria-label="addButtonLabel"
        data-test="add-button"
        :class="mergedAddButtonClass"
        @click="onAdd"
      >
        <IconifyIcon
          :icon="addIconName"
          :width="24"
          :height="24"
          data-test="add-icon"
        />
      </button>

6. mergedAddButtonClass (copie de Phone)

const mergedAddButtonClass = computed(() =>
  twMerge(
    'absolute right-[10px] top-1/2 -translate-y-1/2 cursor-pointer transition-opacity hover:opacity-70',
    iconStateClass.value,
    props.disabled ? 'cursor-not-allowed opacity-40 hover:opacity-40' : '',
  ),
)

(iconStateClass existe déjà dans Email.)

7. Handler onAdd (copie de Phone)

const onAdd = () => {
  if (props.disabled || props.readonly) return
  emit('add')
}

Quatre computeds à modifier — détail

Aujourd'hui dans InputEmail.vue ils référencent props.iconPosition ; ils doivent référencer effectiveIconPosition.value. Les corps restent identiques par ailleurs :

  • iconPositionClass : effectiveIconPosition.value === 'left' ? 'left-[10px]' : 'right-[10px]'
  • labelPositionClass : props.iconName && effectiveIconPosition.value === 'left' ? 'left-11' : 'left-3'
  • focusPaddingClass : props.iconName && effectiveIconPosition.value === 'left' ? 'focus:!pl-11' : 'focus:pl-[11px]'
  • iconInputPaddingClass : remplacé par la version §4.

Tests (InputEmail.test.ts)

Ajouts (mirroring InputPhone.test.ts), sans toucher aux tests existants de sanitisation/lowercase :

  • addable=false (défaut) → pas de [data-test="add-button"].
  • addable=true[data-test="add-button"] présent.
  • Clic sur le bouton → un event add émis (longueur 1).
  • addable + disabled → clic n'émet pas add ; le bouton a l'attribut disabled.
  • addable + readonly → clic n'émet pas add ; le bouton n'a PAS l'attribut natif disabled (la garde onAdd bloque).
  • addable=true avec icône → l'icône email est positionnée à gauche (left-[10px] présent / right-[10px] absent sur l'icône [data-test="icon"]).
  • Non-régression : avec addable=false, l'icône reste à droite (right-[10px]).
  • addButtonLabel personnalisé → aria-label respecté ; défaut → 'Ajouter une adresse email'.

Livrables documentaires

  • COMPONENTS.md : ajouter les lignes addable / addIconName / addButtonLabel et l'event add() dans la section ## MalioInputEmail, plus un exemple <MalioInputEmail ... addable @add="..." />.
  • CHANGELOG.md : entrée sous ### Added.
  • Story app/story/input/inputEmail.story.vue : une carte « Addable » avec @add (calquée sur inputPhone.story.vue).
  • Playground .playground/pages/composant/input/inputEmail.vue : un exemple addable avec un handler qui illustre l'ajout d'un champ.

Hors périmètre

  • Extraction d'un composant/bouton partagé entre Phone et Email (refactor dédié futur).
  • Gestion réelle de la liste de champs email côté composant (c'est au consommateur de réagir à l'event add, comme pour Phone).
  • Toute modification de la logique de sanitisation / lowercase / caret existante.