| 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>
This commit was merged in pull request #70.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# MalioInputAmount — séparateurs de milliers (affichage groupé FR)
|
||||
|
||||
**Date :** 2026-06-09
|
||||
**Statut :** Validé, prêt pour plan d'implémentation
|
||||
**Périmètre :** `MalioInputAmount` uniquement.
|
||||
|
||||
## Objectif
|
||||
|
||||
Afficher les montants avec des **séparateurs de milliers** (espaces) et une **virgule décimale**, à la française : `1 234 567,89`. Demande du client ERP. Le formatage est **purement visuel** ; la valeur émise reste une chaîne numérique propre.
|
||||
|
||||
## Décisions validées
|
||||
|
||||
| Sujet | Décision |
|
||||
|-------|----------|
|
||||
| Valeur émise (`modelValue`) | Reste **propre** : point décimal, sans espaces (`'1234567.89'`). Contrat consommateur inchangé. Les séparateurs ne sont qu'à l'affichage. |
|
||||
| Moment du formatage | **Temps réel** (à la frappe), avec gestion du curseur. |
|
||||
| Format affiché | Français : **espace** pour les milliers, **virgule** pour les décimales (`1 234 567,89`). |
|
||||
| Activation | **Par défaut sur tous** les `MalioInputAmount` (pas de prop opt-in). |
|
||||
| `maxLength` | S'applique à la **longueur du `modelValue`** (chaîne propre), **pas** à l'affichage. Le `maxlength` natif (qui compterait les espaces) est retiré ; le plafond est appliqué en JS. |
|
||||
| Extraction | Les fonctions pures vont dans `app/components/malio/input/composables/amountFormat.ts` (testables en isolation). |
|
||||
|
||||
## Conception détaillée
|
||||
|
||||
### 1. Contrat de valeur & flux de données
|
||||
|
||||
- **Modèle (émis)** : sortie inchangée de `normalizeAmount` — point décimal, max 2 décimales, zéros de tête retirés, `''` si vide. Ex. `1234567.89`.
|
||||
- **Affichage** : `formatGroupedAmount(model)` groupe la partie entière par 3 avec des espaces et remplace le point par une virgule. Ex. `1 234 567,89`. Si le modèle finit par `.` (décimale en cours, ex. `12.`), l'affichage finit par `,` (`12,`).
|
||||
- **Binding** : l'input affiche `formatGroupedAmount(currentValue)` au lieu de `currentValue`.
|
||||
- **Parse** : à la frappe, le texte saisi (avec espaces/virgule) repasse par `normalizeAmount` → modèle propre → émis.
|
||||
|
||||
### 2. Temps réel & gestion du curseur
|
||||
|
||||
À chaque `@input` :
|
||||
1. Lire `rawText = target.value` et `caret = target.selectionStart`.
|
||||
2. `model = normalizeAmount(rawText)`.
|
||||
3. **Plafond `maxLength`** (cf. §3) : si dépassement, ignorer le keystroke (restaurer l'affichage précédent, ne pas émettre).
|
||||
4. Sinon : `display = formatGroupedAmount(model)` ; écrire `target.value = display` ; **émettre** `update:modelValue(model)`.
|
||||
5. **Repositionner le curseur** : compter les caractères significatifs (tout sauf les espaces de groupement) à gauche du curseur dans `rawText`, puis placer le curseur après ce même nombre de caractères significatifs dans `display`.
|
||||
|
||||
Helpers curseur (purs) :
|
||||
```ts
|
||||
export const countSignificant = (str: string, upTo: number): number =>
|
||||
str.slice(0, upTo).replace(/ /g, '').length
|
||||
|
||||
export const caretFromSignificant = (display: string, sig: number): number => {
|
||||
let seen = 0
|
||||
for (let i = 0; i < display.length; i++) {
|
||||
if (display[i] !== ' ') seen++
|
||||
if (seen >= sig) return i + 1
|
||||
}
|
||||
return display.length
|
||||
}
|
||||
```
|
||||
|
||||
`<input type="text">` supporte l'API de sélection, donc `setSelectionRange` fonctionne directement (pas de try/catch nécessaire).
|
||||
|
||||
### 3. `maxLength` sur le modèle
|
||||
|
||||
- On **retire** le binding `:maxlength="maxLength"` natif de l'`<input>` (il compterait les espaces de l'affichage).
|
||||
- Dans `onInput`, après `model = normalizeAmount(rawText)` : si `props.maxLength != null` **et** `model.length > Number(props.maxLength)`, on **ignore** le keystroke :
|
||||
- on restaure `target.value = formatGroupedAmount(currentValue)` (modèle précédent),
|
||||
- on replace le curseur à `max(0, caret - 1)` (le caractère refusé n'est pas inséré),
|
||||
- on **n'émet pas**.
|
||||
- `maxLength` borne donc la **longueur de la chaîne `modelValue`** (point décimal inclus). Ce point est documenté explicitement.
|
||||
- `minLength` : laissé tel quel (attribut natif de validation). Connu : il s'évalue sur le texte affiché ; hors périmètre de cette évolution.
|
||||
|
||||
### 4. Helpers extraits — `composables/amountFormat.ts`
|
||||
|
||||
- `normalizeAmount(value: string): string` — **déplacé tel quel** depuis le composant (parse).
|
||||
- `formatGroupedAmount(model: string): string` — nouveau (format groupé FR). Algorithme :
|
||||
- Si `model === ''` → `''`.
|
||||
- Séparer sur `.` → `integerPart`, `decimalPart` (présent ssi le modèle contient `.`).
|
||||
- Grouper `integerPart` par paquets de 3 depuis la droite avec une espace `' '`.
|
||||
- Si le modèle contient `.` → `groupedInteger + ',' + decimalPart` (decimalPart éventuellement vide).
|
||||
- Sinon → `groupedInteger`.
|
||||
- `countSignificant`, `caretFromSignificant` — helpers curseur (purs).
|
||||
|
||||
Le composant importe ces helpers ; la logique DOM (lecture `target.value`, `setSelectionRange`) reste dans `InputAmount.vue`.
|
||||
|
||||
### 5. Table de vérité (format/parse)
|
||||
|
||||
| Saisie utilisateur | `modelValue` émis | Affichage (`formatGroupedAmount`) |
|
||||
|---|---|---|
|
||||
| `1234567` | `1234567` | `1 234 567` |
|
||||
| `1234,56` ou `1234.56` | `1234.56` | `1 234,56` |
|
||||
| `12.` (décimale en cours) | `12.` | `12,` |
|
||||
| `,5` | `0.5` | `0,5` |
|
||||
| `0012345abc` | `12345` | `12 345` |
|
||||
| `1234.567` (3 décimales) | `1234.56` | `1 234,56` |
|
||||
| `` (vide) | `` | `` |
|
||||
| `0` | `0` | `0` |
|
||||
|
||||
## Tests
|
||||
|
||||
**`composables/amountFormat.test.ts`** (nouveau) :
|
||||
- `normalizeAmount` : reprise des cas existants (espaces, virgule→point, zéros de tête, 2 décimales max, vide, décimale en tête).
|
||||
- `formatGroupedAmount` : table §5 (groupement par 3, virgule décimale, `12.`→`12,`, vide→vide, nombres < 1000 inchangés).
|
||||
- `countSignificant` / `caretFromSignificant` : positions de curseur clés (avant/après un espace inséré, en fin de chaîne).
|
||||
|
||||
**`InputAmount.test.ts`** (mis à jour) :
|
||||
- Les assertions `input.element.value` passent de la valeur brute (`1234.56`) à la valeur groupée (`1 234,56`).
|
||||
- Les assertions d'émission `update:modelValue` restent **inchangées** (modèle propre : `'1234.56'`, `'0.5'`, `''`…).
|
||||
- Nouveaux tests : groupement à la frappe d'un grand montant (`1234567` → affichage `1 234 567`, émis `1234567`) ; `maxLength` plafonne le modèle (un keystroke au-delà est ignoré, pas d'émission supplémentaire) ; position du curseur après insertion d'un séparateur.
|
||||
|
||||
## Livrables documentaires
|
||||
|
||||
- `COMPONENTS.md` : note dans la section `## MalioInputAmount` — affichage groupé FR (`1 234 567,89`), `modelValue` reste propre (`'1234567.89'`), `maxLength` borne la longueur du modèle.
|
||||
- `CHANGELOG.md` : entrée sous `### Added` / `### Changed`.
|
||||
- Story `app/story/input/inputAmount.story.vue` : exemple grand montant montrant les séparateurs.
|
||||
- Playground `.playground/pages/composant/input/inputAmount.vue` : exemple grand montant + affichage de la valeur ISO/propre émise.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
- Internationalisation configurable (autres locales / séparateurs paramétrables) — on fige le format FR.
|
||||
- `minLength` sur le modèle (reste natif sur l'affichage).
|
||||
- Passage à `maska` en mode number (approche écartée au profit du `normalizeAmount` existant).
|
||||
- Devises / symboles dynamiques (l'icône € existante est conservée telle quelle).
|
||||
@@ -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.
|
||||
@@ -0,0 +1,118 @@
|
||||
# MalioDate — saisie manuelle au clavier
|
||||
|
||||
**Date :** 2026-06-09
|
||||
**Statut :** Validé, prêt pour plan d'implémentation
|
||||
**Périmètre :** `MalioDate` uniquement (la famille `DateTime`/`DateRange`/`DateWeek` n'est pas concernée pour l'instant).
|
||||
|
||||
## Objectif
|
||||
|
||||
Permettre à l'utilisateur de saisir une date au clavier (`JJ/MM/AAAA`) dans `MalioDate`, en plus de la sélection via le calendrier. Aujourd'hui l'`<input>` de `CalendarField` est codé en dur en `readonly` : seule la sélection au calendrier est possible.
|
||||
|
||||
## Décisions d'UX (validées)
|
||||
|
||||
| Sujet | Décision |
|
||||
|-------|----------|
|
||||
| Ouverture du popover | Le **focus** (ou le clic) ouvre le calendrier, tout en laissant taper en même temps. |
|
||||
| Masque / validation | Masque `maska` `##/##/####` pendant la frappe ; **validation au blur** (pas à chaque touche). |
|
||||
| Activation | **Opt-in** via une prop `editable` (défaut `false`). Aucune régression pour les consommateurs existants. |
|
||||
| Saisie invalide au blur | On **garde le texte tapé** et on affiche un **état d'erreur visuel** (bordure rouge + message). |
|
||||
| Message d'erreur par défaut | « **Date invalide** » (couvre aussi le hors-bornes min/max), surchargeable via prop. |
|
||||
| Touche Entrée | Déclenche le `commit` (parse immédiat) + ferme le popover. |
|
||||
|
||||
## Approche retenue
|
||||
|
||||
**Mode `editable` dans `CalendarField`, parsing dans `MalioDate`.**
|
||||
|
||||
`CalendarField` reste agnostique au format : il expose un mode éditable (input non `readonly`, masque, buffer local) et émet du texte brut. `MalioDate` conserve toute la logique propre à la date (parse, validation `min`/`max`, état d'erreur). Cela évite de coupler `CalendarField` à un format date spécifique et garde le terrain prêt pour une éventuelle extension future à la famille Date.
|
||||
|
||||
Approches écartées :
|
||||
- **`CalendarField` générique avec fonction `parse` injectée** : trop générique pour le périmètre actuel (YAGNI).
|
||||
- **`MalioDate` gère son propre `<input>`** : duplication du rendu / label flottant / styles de `CalendarField`.
|
||||
|
||||
## Conception détaillée
|
||||
|
||||
### 1. `MalioDate` — props ajoutées
|
||||
|
||||
- `editable?: boolean` — défaut `false`. Active la saisie clavier.
|
||||
- `invalidMessage?: string` — défaut `'Date invalide'`. Message affiché en cas de saisie invalide/hors-bornes.
|
||||
|
||||
Quand `editable === false`, le comportement est **strictement identique** à aujourd'hui (lecture seule, sélection calendrier uniquement).
|
||||
|
||||
### 2. `CalendarField` — mode éditable
|
||||
|
||||
Ajout d'une prop `editable?: boolean` (défaut `false`). Quand `true` :
|
||||
|
||||
- L'`<input>` perd l'attribut `readonly` et reçoit `v-maska="'##/##/####'"`.
|
||||
- Un buffer local `draft` (ref) alimente l'input : `:value="editable ? draft : displayValue"`.
|
||||
- `draft` est **resynchronisé** sur `displayValue` via un `watch` → couvre la sélection au calendrier, le clear, et tout changement externe de `modelValue`. Cette resynchro **efface aussi l'état d'erreur** côté `MalioDate` (via le nouveau `displayValue` émis).
|
||||
- À la frappe (`@input`) : met à jour `draft` et émet `input(text)`. **Pas de validation** à ce stade.
|
||||
- Au blur (`@blur`) : émet `commit(text)`.
|
||||
- À la touche Entrée (`@keydown.enter`) : émet `commit(text)` + ferme le popover.
|
||||
- `@focus` ouvre le popover, tout en laissant taper (input non `readonly`).
|
||||
|
||||
Quand `editable === false`, aucun de ces comportements ne s'applique : le chemin de code actuel reste inchangé.
|
||||
|
||||
`disabled` et `readonly` priment toujours sur `editable` (champ non éditable).
|
||||
|
||||
### 3. `MalioDate` — parsing, validation, état d'erreur
|
||||
|
||||
Une ref locale `internalError` est fusionnée avec la prop `error` du consommateur et transmise à `CalendarField` :
|
||||
`:error="error || internalError"` (l'erreur métier du consommateur reste prioritaire).
|
||||
|
||||
Sur réception de `commit(text)` :
|
||||
|
||||
- **Texte vide** → `emit('update:modelValue', null)` ; `internalError = ''`.
|
||||
- **Valide** (`parseDisplayToIso(text)` non `null` **et** `isDateInRange(iso, min, max)`) → `emit('update:modelValue', iso)` ; `internalError = ''`.
|
||||
- **Invalide ou hors-bornes** → on **n'émet pas** de nouveau `modelValue` ; `internalError = props.invalidMessage`. Le texte tapé reste affiché.
|
||||
|
||||
L'état d'erreur s'efface dès qu'une saisie valide ou une sélection calendrier ultérieure produit un nouveau `displayValue`.
|
||||
|
||||
## Flux de données
|
||||
|
||||
```
|
||||
Frappe clavier
|
||||
└─ CalendarField: maj draft + émet input(text) (pas de validation)
|
||||
Blur / Entrée
|
||||
└─ CalendarField: émet commit(text)
|
||||
└─ MalioDate: parseDisplayToIso + isDateInRange
|
||||
├─ valide → emit update:modelValue(iso) ; internalError=''
|
||||
├─ vide → emit update:modelValue(null) ; internalError=''
|
||||
└─ invalide→ internalError = invalidMessage ; (texte conservé)
|
||||
|
||||
Sélection calendrier
|
||||
└─ emit update:modelValue(iso)
|
||||
└─ displayValue change → CalendarField resync draft → erreur effacée
|
||||
```
|
||||
|
||||
## Réutilisation de l'existant
|
||||
|
||||
Les helpers nécessaires existent déjà dans `app/components/malio/date/composables/dateFormat.ts` :
|
||||
- `parseDisplayToIso(display)` → `string | null`
|
||||
- `isValidIso(iso)` → `boolean`
|
||||
- `isDateInRange(iso, min?, max?)` → `boolean`
|
||||
- `formatIsoToDisplay(iso)` → `string`
|
||||
|
||||
`maska` est déjà une dépendance du projet (utilisée par `InputText`/`InputPhone` via `v-maska` + `vMaska` de `maska/vue`).
|
||||
|
||||
## Tests (`Date.test.ts`)
|
||||
|
||||
- Frappe valide + blur → émet l'ISO attendu.
|
||||
- Saisie invalide (`32/13/2026`) au blur → texte conservé, message « Date invalide », `aria-invalid`.
|
||||
- Date valide hors `min`/`max` au blur → état d'erreur.
|
||||
- Saisie vide au blur → émet `null`.
|
||||
- Sélection au calendrier après une saisie invalide → erreur effacée, valeur mise à jour.
|
||||
- Touche Entrée → commit + fermeture popover.
|
||||
- `editable=false` (défaut) → input reste `readonly`, aucun nouveau comportement (non-régression).
|
||||
- `invalidMessage` personnalisé → message affiché respecté.
|
||||
|
||||
## Livrables documentaires
|
||||
|
||||
- Mise à jour de `COMPONENTS.md` (props `editable`, `invalidMessage`).
|
||||
- Entrée dans `CHANGELOG.md`.
|
||||
- Mise à jour de la story Histoire + page playground de `MalioDate` pour exposer la prop `editable`.
|
||||
|
||||
## Hors périmètre
|
||||
|
||||
- Extension de la saisie manuelle à `DateTime`, `DateRange`, `DateWeek`.
|
||||
- Saisie partielle « intelligente » (auto-complétion d'année, etc.).
|
||||
- Validation à la frappe (on reste sur validation au blur).
|
||||
Reference in New Issue
Block a user