# 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) ```ts 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 : ```ts const emit = defineEmits<{ (event: 'update:modelValue', value: string): void }>() ``` à : ```ts 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 : ```ts 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=false` → `effectiveIconPosition === 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) : ```ts 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 ``, à l'identique de Phone : ```html ``` ### 6. `mergedAddButtonClass` (copie de Phone) ```ts 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) ```ts 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 ``. - `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.