docs(email) : spec bouton + d'ajout sur MalioInputEmail
This commit is contained in:
@@ -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 `<IconifyIcon v-if="iconName">`, à l'identique de Phone :
|
||||||
|
```html
|
||||||
|
<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)
|
||||||
|
|
||||||
|
```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 `<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.
|
||||||
Reference in New Issue
Block a user