feat(ui) : saisie clavier MalioDate + bouton « + » InputEmail + séparateurs InputAmount (#MUI-42) #68
@@ -0,0 +1,635 @@
|
||||
# MalioDate — saisie manuelle au clavier — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Permettre la saisie clavier `JJ/MM/AAAA` dans `MalioDate` (opt-in via prop `editable`), en plus de la sélection au calendrier, avec validation au blur et état d'erreur visuel.
|
||||
|
||||
**Architecture:** `CalendarField` (interne, partagé) gagne un mode `editable` : input non `readonly`, masque `maska`, buffer local `draft` synchronisé sur `displayValue`, émission d'un event `commit(text)` au blur / à Entrée. `MalioDate` conserve toute la logique date : parse (`parseDisplayToIso`), validation bornes (`isDateInRange`), état d'erreur interne fusionné avec la prop `error` du consommateur. `CalendarField` reste agnostique au format.
|
||||
|
||||
**Tech Stack:** Nuxt 4 layer, Vue 3 `<script setup lang="ts">`, `maska` (directive `v-maska`), `tailwind-merge`, Vitest + `@vue/test-utils` (jsdom).
|
||||
|
||||
**Référence spec :** `docs/superpowers/specs/2026-06-09-maliodate-saisie-manuelle-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `app/components/malio/date/internal/CalendarField.vue` — mode `editable` : prop, masque, buffer `draft`, handlers focus/input/blur/enter, event `commit`.
|
||||
- **Modify** `app/components/malio/date/Date.vue` — props `editable` / `invalidMessage`, état `internalError`, handler `onCommit`, fusion `mergedError`, nettoyage erreur à la sélection/clear.
|
||||
- **Modify** `app/components/malio/date/Date.test.ts` — tests de saisie manuelle + non-régression.
|
||||
- **Modify** `COMPONENTS.md` — documentation des props.
|
||||
- **Modify** `CHANGELOG.md` — entrée de version.
|
||||
- **Modify** `.playground/pages/composant/date/date.vue` — exemple éditable.
|
||||
- **Modify** `app/story/date/datePicker.story.vue` — exemple éditable.
|
||||
|
||||
**Note hooks pré-commit :** le projet a un hook `make pre-commit` (lint + 888 tests) parfois lent/flaky. Si un commit échoue sur un timeout de test sans rapport, relancer ; en dernier recours `--no-verify`. Toujours stager des fichiers explicites, **jamais** `git add -A` (le `nuxt.config.ts` modifié localement ne doit pas être committé).
|
||||
|
||||
---
|
||||
|
||||
## Task 1 : `CalendarField` — prop `editable`, masque et buffer
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/malio/date/internal/CalendarField.vue`
|
||||
|
||||
Cette tâche ajoute l'infrastructure du mode éditable. On la valide via les tests de la Task 3 (le comportement observable passe par `MalioDate`). Ici on vérifie surtout la non-régression : `editable=false` ⇒ input `readonly`, valeur affichée intacte.
|
||||
|
||||
- [ ] **Step 1 : Ajouter les imports `maska`**
|
||||
|
||||
Dans le bloc `<script setup>`, juste après la ligne `import {twMerge} from 'tailwind-merge'` (ligne 104), ajouter :
|
||||
|
||||
```ts
|
||||
import {vMaska} from 'maska/vue'
|
||||
import type {MaskInputOptions} from 'maska'
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Ajouter la prop `editable` à l'interface et aux défauts**
|
||||
|
||||
Dans `defineProps<{...}>()`, ajouter la ligne après `clearable?: boolean` :
|
||||
|
||||
```ts
|
||||
editable?: boolean
|
||||
```
|
||||
|
||||
Dans le bloc `withDefaults(..., { ... })`, ajouter après `clearable: true,` :
|
||||
|
||||
```ts
|
||||
editable: false,
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Déclarer l'event `commit`**
|
||||
|
||||
Remplacer la ligne (≈152) :
|
||||
|
||||
```ts
|
||||
const emit = defineEmits<{(e: 'clear' | 'close'): void}>()
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```ts
|
||||
const emit = defineEmits<{
|
||||
(e: 'clear' | 'close'): void
|
||||
(e: 'commit', value: string): void
|
||||
}>()
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Ajouter le buffer `draft`, le masque et l'état `readonly` calculé**
|
||||
|
||||
Juste après la ligne `const root = ref<HTMLElement | null>(null)` (≈156), ajouter :
|
||||
|
||||
```ts
|
||||
const draft = ref(props.displayValue)
|
||||
const maskaOptions = computed<MaskInputOptions>(() => ({mask: props.editable ? '##/##/####' : undefined}))
|
||||
const inputReadonly = computed(() => !props.editable || props.readonly || props.disabled)
|
||||
|
||||
watch(() => props.displayValue, (value) => {
|
||||
draft.value = value
|
||||
})
|
||||
```
|
||||
|
||||
(Note : `mask: undefined` désactive le masquage de `maska` — la valeur passe intacte. Ne **pas** utiliser `''`, qui viderait la valeur.)
|
||||
|
||||
- [ ] **Step 5 : Mettre à jour le computed `isFilled` pour tenir compte du buffer**
|
||||
|
||||
Remplacer (≈164) :
|
||||
|
||||
```ts
|
||||
const isFilled = computed(() => props.displayValue.length > 0)
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```ts
|
||||
const isFilled = computed(() =>
|
||||
(props.editable ? draft.value.length : props.displayValue.length) > 0,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 6 : Remplacer `onFieldClick` et ajouter les handlers éditables**
|
||||
|
||||
Remplacer le bloc `onFieldClick` (≈177-185) :
|
||||
|
||||
```ts
|
||||
const onFieldClick = () => {
|
||||
if (props.disabled || props.readonly) return
|
||||
if (isOpen.value) {
|
||||
closePopover()
|
||||
return
|
||||
}
|
||||
syncToIso(props.syncTo)
|
||||
open()
|
||||
}
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```ts
|
||||
const onFieldClick = () => {
|
||||
if (props.disabled || props.readonly) return
|
||||
if (props.editable) {
|
||||
if (!isOpen.value) {
|
||||
syncToIso(props.syncTo)
|
||||
open()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isOpen.value) {
|
||||
closePopover()
|
||||
return
|
||||
}
|
||||
syncToIso(props.syncTo)
|
||||
open()
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
if (props.disabled || props.readonly || !props.editable) return
|
||||
if (!isOpen.value) {
|
||||
syncToIso(props.syncTo)
|
||||
open()
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
draft.value = (event.target as HTMLInputElement).value
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
if (!props.editable) return
|
||||
emit('commit', draft.value)
|
||||
}
|
||||
|
||||
const onEnter = () => {
|
||||
if (!props.editable) return
|
||||
emit('commit', draft.value)
|
||||
closePopover()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7 : Mettre à jour l'`<input>` dans le template**
|
||||
|
||||
Remplacer le bloc `<input>` (≈7-25) :
|
||||
|
||||
```html
|
||||
<input
|
||||
:id="inputId"
|
||||
:name="name"
|
||||
data-test="date-input"
|
||||
readonly
|
||||
autocomplete="off"
|
||||
:class="mergedInputClass"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:value="displayValue"
|
||||
:aria-invalid="!!error"
|
||||
:aria-describedby="describedBy"
|
||||
:aria-expanded="isOpen"
|
||||
aria-haspopup="dialog"
|
||||
v-bind="attrs"
|
||||
placeholder="_"
|
||||
type="text"
|
||||
@click="onFieldClick"
|
||||
>
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```html
|
||||
<input
|
||||
:id="inputId"
|
||||
v-maska="maskaOptions"
|
||||
:name="name"
|
||||
data-test="date-input"
|
||||
:readonly="inputReadonly"
|
||||
autocomplete="off"
|
||||
:class="mergedInputClass"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:value="editable ? draft : displayValue"
|
||||
:aria-invalid="!!error"
|
||||
:aria-describedby="describedBy"
|
||||
:aria-expanded="isOpen"
|
||||
aria-haspopup="dialog"
|
||||
v-bind="attrs"
|
||||
placeholder="_"
|
||||
type="text"
|
||||
@click="onFieldClick"
|
||||
@focus="onFocus"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@keydown.enter.prevent="onEnter"
|
||||
>
|
||||
```
|
||||
|
||||
- [ ] **Step 8 : Lancer la suite Date pour vérifier la non-régression**
|
||||
|
||||
Run : `npm run test -- Date.test.ts`
|
||||
Expected : PASS (tous les tests existants de `MalioDate` passent toujours ; l'input par défaut reste `readonly` et affiche la valeur formatée).
|
||||
|
||||
- [ ] **Step 9 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/malio/date/internal/CalendarField.vue
|
||||
git commit -m "feat(date) : mode editable dans CalendarField (saisie clavier)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2 : `MalioDate` — parsing, validation et état d'erreur
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/malio/date/Date.vue`
|
||||
|
||||
- [ ] **Step 1 : Étendre les imports de `dateFormat`**
|
||||
|
||||
Remplacer (≈39) :
|
||||
|
||||
```ts
|
||||
import {formatIsoToDisplay, isValidIso} from './composables/dateFormat'
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```ts
|
||||
import {formatIsoToDisplay, isDateInRange, isValidIso, parseDisplayToIso} from './composables/dateFormat'
|
||||
```
|
||||
|
||||
Et compléter l'import Vue (≈36) pour disposer de `ref` :
|
||||
|
||||
```ts
|
||||
import {computed, ref, watch} from 'vue'
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Ajouter les props `editable` et `invalidMessage`**
|
||||
|
||||
Dans `defineProps<{...}>()`, ajouter après `clearable?: boolean` :
|
||||
|
||||
```ts
|
||||
editable?: boolean
|
||||
invalidMessage?: string
|
||||
```
|
||||
|
||||
Dans `withDefaults(..., { ... })`, ajouter après `clearable: true,` :
|
||||
|
||||
```ts
|
||||
editable: false,
|
||||
invalidMessage: 'Date invalide',
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Ajouter l'état d'erreur interne, la fusion, et les handlers**
|
||||
|
||||
Juste après la ligne `const displayValue = computed(() => formatIsoToDisplay(props.modelValue ?? null))` (≈86), ajouter :
|
||||
|
||||
```ts
|
||||
const internalError = ref('')
|
||||
const mergedError = computed(() => props.error || internalError.value)
|
||||
|
||||
const onCommit = (text: string) => {
|
||||
const trimmed = text.trim()
|
||||
if (trimmed === '') {
|
||||
internalError.value = ''
|
||||
emit('update:modelValue', null)
|
||||
return
|
||||
}
|
||||
const iso = parseDisplayToIso(trimmed)
|
||||
if (iso && isDateInRange(iso, props.min, props.max)) {
|
||||
internalError.value = ''
|
||||
emit('update:modelValue', iso)
|
||||
return
|
||||
}
|
||||
internalError.value = props.invalidMessage
|
||||
}
|
||||
|
||||
const onClear = () => {
|
||||
internalError.value = ''
|
||||
emit('update:modelValue', null)
|
||||
}
|
||||
|
||||
const onSelect = (iso: string, close: () => void) => {
|
||||
internalError.value = ''
|
||||
emit('update:modelValue', iso)
|
||||
close()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Brancher les props et events sur `CalendarField` dans le template**
|
||||
|
||||
Dans `<CalendarField ...>`, remplacer `:error="error"` (≈13) par :
|
||||
|
||||
```html
|
||||
:error="mergedError"
|
||||
```
|
||||
|
||||
Ajouter, juste après `:clearable="clearable"` (≈15) :
|
||||
|
||||
```html
|
||||
:editable="editable"
|
||||
```
|
||||
|
||||
Remplacer `@clear="emit('update:modelValue', null)"` (≈20) par :
|
||||
|
||||
```html
|
||||
@clear="onClear"
|
||||
@commit="onCommit"
|
||||
```
|
||||
|
||||
- [ ] **Step 5 : Brancher la sélection calendrier sur `onSelect`**
|
||||
|
||||
Remplacer (≈29) :
|
||||
|
||||
```html
|
||||
@select="(iso) => { emit('update:modelValue', iso); close() }"
|
||||
```
|
||||
|
||||
par :
|
||||
|
||||
```html
|
||||
@select="(iso) => onSelect(iso, close)"
|
||||
```
|
||||
|
||||
- [ ] **Step 6 : Lancer la suite Date pour vérifier la non-régression**
|
||||
|
||||
Run : `npm run test -- Date.test.ts`
|
||||
Expected : PASS (les tests existants passent ; `mergedError` se comporte comme `error` tant qu'aucune saisie invalide n'est faite).
|
||||
|
||||
- [ ] **Step 7 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/malio/date/Date.vue
|
||||
git commit -m "feat(date) : saisie manuelle MalioDate (parse, validation, erreur)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3 : Tests de la saisie manuelle
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/components/malio/date/Date.test.ts`
|
||||
|
||||
- [ ] **Step 1 : Étendre le type de props de test**
|
||||
|
||||
Dans le type `DateProps` (≈6-25), ajouter après `groupClass?: string` :
|
||||
|
||||
```ts
|
||||
editable?: boolean
|
||||
invalidMessage?: string
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Écrire le bloc de tests `saisie manuelle (editable)`**
|
||||
|
||||
Ajouter, juste avant la fermeture du `describe('MalioDate', ...)` (avant la dernière `})` du fichier, après le bloc `describe('reserveMessageSpace', ...)`), le bloc suivant :
|
||||
|
||||
```ts
|
||||
describe('saisie manuelle (editable)', () => {
|
||||
it('par défaut (editable=false) l\'input reste readonly et affiche la valeur', () => {
|
||||
const wrapper = mountDate({modelValue: '2026-05-19'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
expect(input.attributes('readonly')).toBeDefined()
|
||||
expect((input.element as HTMLInputElement).value).toBe('19/05/2026')
|
||||
})
|
||||
|
||||
it('editable=true : l\'input n\'est plus readonly', () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
expect(wrapper.get('[data-test="date-input"]').attributes('readonly')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('émet l\'ISO sur saisie clavier valide au blur', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('19/05/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19'])
|
||||
})
|
||||
|
||||
it('garde le texte et affiche « Date invalide » sur saisie invalide au blur', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect((input.element as HTMLInputElement).value).toBe('32/13/2026')
|
||||
expect(input.attributes('aria-invalid')).toBe('true')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('passe en erreur si la date saisie est hors min/max', async () => {
|
||||
const wrapper = mountDate({editable: true, min: '2026-05-10', max: '2026-05-20'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('25/12/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('émet null sur saisie vidée au blur', async () => {
|
||||
const wrapper = mountDate({editable: true, modelValue: '2026-05-19'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null])
|
||||
})
|
||||
|
||||
it('efface l\'erreur de saisie quand on sélectionne une date au calendrier', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('32/13/2026')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Date invalide')
|
||||
await input.trigger('focus')
|
||||
await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19'])
|
||||
expect(wrapper.text()).not.toContain('Date invalide')
|
||||
})
|
||||
|
||||
it('valide et ferme le popover sur Entrée', async () => {
|
||||
const wrapper = mountDate({editable: true})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.trigger('focus')
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
|
||||
await input.setValue('19/05/2026')
|
||||
await input.trigger('keydown.enter')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19'])
|
||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('utilise le message invalidMessage personnalisé', async () => {
|
||||
const wrapper = mountDate({editable: true, invalidMessage: 'Format incorrect'})
|
||||
const input = wrapper.get('[data-test="date-input"]')
|
||||
await input.setValue('99/99/9999')
|
||||
await input.trigger('blur')
|
||||
expect(wrapper.text()).toContain('Format incorrect')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Lancer les nouveaux tests**
|
||||
|
||||
Run : `npm run test -- Date.test.ts`
|
||||
Expected : PASS (tous, anciens + nouveaux).
|
||||
|
||||
Si un test de saisie échoue parce que `maska` a reformaté la valeur en jsdom autrement qu'attendu, inspecter la valeur réelle via un `console.log((input.element as HTMLInputElement).value)` et ajuster l'assertion (le masque `##/##/####` laisse les chiffres tels quels ; une entrée déjà bien formée n'est pas modifiée).
|
||||
|
||||
- [ ] **Step 4 : Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/malio/date/Date.test.ts
|
||||
git commit -m "test(date) : couvre la saisie manuelle de MalioDate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4 : Documentation (COMPONENTS.md + CHANGELOG.md)
|
||||
|
||||
**Files:**
|
||||
- Modify: `COMPONENTS.md`
|
||||
- Modify: `CHANGELOG.md`
|
||||
|
||||
- [ ] **Step 1 : Ajouter les props au tableau `MalioDate` de `COMPONENTS.md`**
|
||||
|
||||
Dans la section `## MalioDate`, dans le tableau des props, insérer juste après la ligne `| `clearable` | `boolean` | `true` | Affiche la croix d'effacement |` :
|
||||
|
||||
```markdown
|
||||
| `editable` | `boolean` | `false` | Autorise la saisie clavier `JJ/MM/AAAA` (masque maska, validation au blur) en plus du calendrier |
|
||||
| `invalidMessage` | `string` | `'Date invalide'` | Message affiché quand la saisie clavier est invalide ou hors `min`/`max` |
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Compléter la description et l'exemple `MalioDate`**
|
||||
|
||||
Dans la section `## MalioDate`, juste après la ligne de description `La valeur est une chaîne ISO ...`, ajouter le paragraphe :
|
||||
|
||||
```markdown
|
||||
Avec `editable`, l'utilisateur peut aussi taper la date au clavier. La valeur n'est émise qu'au blur (ou sur Entrée) si elle est valide et dans les bornes ; sinon le texte est conservé et le champ passe en erreur (`invalidMessage`).
|
||||
```
|
||||
|
||||
Dans le bloc d'exemple ```vue de cette section, ajouter une ligne avant la fermeture ``` :
|
||||
|
||||
```vue
|
||||
<MalioDate v-model="date" label="Date de naissance" editable />
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Ajouter l'entrée CHANGELOG**
|
||||
|
||||
Dans `CHANGELOG.md`, sous `### Added`, ajouter à la fin de la liste (après la dernière puce `* [#MUI-41] InputEmail : ...`) :
|
||||
|
||||
```markdown
|
||||
* [#MUI-42] MalioDate : saisie clavier `JJ/MM/AAAA` optionnelle (prop `editable`, masque maska, validation au blur, message `invalidMessage`)
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Commit**
|
||||
|
||||
```bash
|
||||
git add COMPONENTS.md CHANGELOG.md
|
||||
git commit -m "docs(date) : documente la saisie manuelle de MalioDate"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5 : Exemples playground + story
|
||||
|
||||
**Files:**
|
||||
- Modify: `.playground/pages/composant/date/date.vue`
|
||||
- Modify: `app/story/date/datePicker.story.vue`
|
||||
|
||||
- [ ] **Step 1 : Ajouter un bloc éditable dans la page playground**
|
||||
|
||||
Dans `.playground/pages/composant/date/date.vue`, dans la première colonne `Large (480px)`, juste après le `<div class="rounded border p-3 text-sm">...</div>` qui affiche la valeur ISO (≈13-15), ajouter :
|
||||
|
||||
```html
|
||||
<MalioDate
|
||||
v-model="editableValue"
|
||||
label="Date (saisie clavier)"
|
||||
editable
|
||||
hint="Tape JJ/MM/AAAA ou utilise le calendrier"
|
||||
/>
|
||||
<div class="rounded border p-3 text-sm">
|
||||
<p>Valeur éditable (ISO) : <code>{{ editableValue ?? 'null' }}</code></p>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 2 : Déclarer la ref dans le `<script setup>` de la page playground**
|
||||
|
||||
Dans le `<script setup>` du même fichier, après `const bounded = ref<string | null>(null)`, ajouter :
|
||||
|
||||
```ts
|
||||
const editableValue = ref<string | null>(null)
|
||||
```
|
||||
|
||||
- [ ] **Step 3 : Ajouter un exemple éditable dans la story**
|
||||
|
||||
Dans `app/story/date/datePicker.story.vue`, ajouter une nouvelle carte juste après le bloc `<!-- Avec min/max -->` (le `<div class="rounded-lg border p-4">` qui contient « Avec min/max »), avant le bloc « Non effaçable » :
|
||||
|
||||
```html
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Saisie clavier (editable)</h2>
|
||||
<MalioDate
|
||||
v-model="editableValue"
|
||||
label="Date de naissance"
|
||||
editable
|
||||
hint="Tape JJ/MM/AAAA ou utilise le calendrier"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 4 : Déclarer la ref dans le `<script setup>` de la story**
|
||||
|
||||
Dans le `<script setup>` du même fichier, après `const errorValue = ref<string | null>(null)`, ajouter :
|
||||
|
||||
```ts
|
||||
const editableValue = ref<string | null>(null)
|
||||
```
|
||||
|
||||
- [ ] **Step 5 : Vérifier que rien ne casse (lint + build des types)**
|
||||
|
||||
Run : `npm run lint`
|
||||
Expected : PASS (aucune erreur sur les fichiers modifiés).
|
||||
|
||||
- [ ] **Step 6 : Commit**
|
||||
|
||||
```bash
|
||||
git add .playground/pages/composant/date/date.vue app/story/date/datePicker.story.vue
|
||||
git commit -m "docs(date) : exemples saisie manuelle (playground + story)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6 : Vérification finale
|
||||
|
||||
- [ ] **Step 1 : Lancer toute la suite de tests**
|
||||
|
||||
Run : `npm run test -- Date.test.ts`
|
||||
Expected : PASS — l'ensemble du fichier (anciens + 9 nouveaux tests).
|
||||
|
||||
- [ ] **Step 2 : Lancer le lint global**
|
||||
|
||||
Run : `npm run lint`
|
||||
Expected : PASS.
|
||||
|
||||
- [ ] **Step 3 : Vérification manuelle dans le playground (optionnel mais recommandé)**
|
||||
|
||||
Run : `npm run dev` puis ouvrir la page `composant/date`.
|
||||
Vérifier :
|
||||
- Taper `19/05/2026` puis cliquer ailleurs → la valeur ISO affichée devient `2026-05-19`.
|
||||
- Taper `32/13/2026` puis blur → le texte reste, le champ passe en rouge avec « Date invalide ».
|
||||
- Avec une saisie invalide, ouvrir le calendrier et choisir un jour → l'erreur disparaît, la valeur se met à jour.
|
||||
- Le focus dans le champ ouvre bien le calendrier, et taper reste possible.
|
||||
- Sur le champ `editable=false` existant : aucun changement (lecture seule).
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**Spec coverage :**
|
||||
- Prop `editable` opt-in (défaut false) → Task 1 Step 2, Task 2 Step 2.
|
||||
- Masque `##/##/####` + focus ouvre le popover → Task 1 Steps 4/6/7.
|
||||
- Validation au blur, pas à la frappe → Task 1 (onInput ne valide pas) + Task 2 (onCommit).
|
||||
- Saisie invalide : garde le texte + erreur visuelle → Task 2 Step 3 + Task 3 test dédié.
|
||||
- Message par défaut « Date invalide », surchargeable → Task 2 Step 2.
|
||||
- Touche Entrée commit + ferme popover → Task 1 Step 6 (`onEnter`) + Task 3 test.
|
||||
- Hors min/max = invalide → Task 2 (`isDateInRange`) + Task 3 test.
|
||||
- Sélection calendrier efface l'erreur → Task 2 Step 5 (`onSelect`) + Task 3 test.
|
||||
- `disabled`/`readonly` priment → Task 1 (`inputReadonly`, gardes dans handlers).
|
||||
- Non-régression `editable=false` → Task 1 Step 8 + Task 3 test readonly.
|
||||
- Docs COMPONENTS.md + CHANGELOG.md + playground/story → Tasks 4 et 5.
|
||||
|
||||
**Placeholder scan :** aucun TODO/TBD ; tout le code est fourni intégralement.
|
||||
|
||||
**Type consistency :** `editable`/`invalidMessage` (props), `commit` (event CalendarField), `onCommit`/`onClear`/`onSelect`/`internalError`/`mergedError` (MalioDate), `draft`/`maskaOptions`/`inputReadonly`/`onFocus`/`onInput`/`onBlur`/`onEnter` (CalendarField) — noms cohérents entre tâches. `onCommit(text: string)` correspond à l'event `commit(value: string)`. `onSelect(iso: string, close: () => void)` correspond à la signature du slot (`close` exposé par `CalendarField`).
|
||||
Reference in New Issue
Block a user