diff --git a/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md b/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md new file mode 100644 index 0000000..49fb14f --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-inputemail-bouton-ajout-design.md @@ -0,0 +1,157 @@ +# 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.