9f9723d01c
Ticket MUI-43 : exposer l'état de validité de MalioDate (saisie invalide avalée silencieusement) + portage de la saisie clavier sur MalioDateTime.
## Contenu
**MalioDate**
- Nouvel event `update:valid(boolean)` : `false` sur saisie malformée ou hors min/max (qui n'émet pas `modelValue`), `true` sinon ; émis dès le montage. La validité ne couvre pas `required` (champ vide = valide).
**MalioDateTime**
- Prop `editable` : saisie clavier `JJ/MM/AAAA HH:MM` (masque maska, validation au blur/Entrée, `invalidMessage`) + même `update:valid`.
- Nouveau parseur `parseDisplayToIsoDateTime`.
**Famille Date editable (Date + DateTime)**
- Gabarit fantôme progressif : le format s'affiche en gris et se remplit au fil de la saisie (overlay ghost mirror, texte de l'input transparent).
- Séparateurs (/, espace, :) posés automatiquement (maska `eager`), espace insécable pour éviter le collage `12/12/1999HH:MM`.
- `CalendarField` : prop `placeholderTemplate` (le masque maska en est dérivé).
**Corrections**
- La croix d'effacement réinitialise la saisie clavier même après une date invalide (le v-model restant null, le champ ne se vidait pas).
- Fix d'un test `Date.test.ts` cassé sur develop (`trigger('keydown.enter')` envoie key='enter' ≠ handler `e.key === 'Enter'`).
## Portée
MalioDate seul pour la validité (les cousins DateRange/DateWeek n'ont pas de saisie clavier donc pas le bug). Sémantique `valid` = malformé only.
## Tests
`app/components/malio/date/` : 187/187, ESLint propre. Vérifié visuellement dans le playground (page Date & heure).
## Doc
COMPONENTS.md + CHANGELOG.md à jour.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: #71
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
143 lines
3.5 KiB
Vue
143 lines
3.5 KiB
Vue
<template>
|
|
<CalendarField
|
|
:id="id"
|
|
:display-value="displayValue"
|
|
:sync-to="modelValue ?? null"
|
|
:name="name"
|
|
:label="label"
|
|
:placeholder="placeholder"
|
|
:required="required"
|
|
:disabled="disabled"
|
|
:readonly="readonly"
|
|
:hint="hint"
|
|
:error="mergedError"
|
|
:success="success"
|
|
:clearable="clearable"
|
|
:editable="editable"
|
|
:input-class="inputClass"
|
|
:label-class="labelClass"
|
|
:group-class="groupClass"
|
|
v-bind="$attrs"
|
|
@clear="onClear"
|
|
@commit="onCommit"
|
|
>
|
|
<template #default="{ currentMonth, currentYear, close }">
|
|
<MonthGrid
|
|
:month="currentMonth"
|
|
:year="currentYear"
|
|
:selected-date="modelValue ?? null"
|
|
:min="min"
|
|
:max="max"
|
|
@select="(iso) => onSelect(iso, close)"
|
|
/>
|
|
</template>
|
|
</CalendarField>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {computed, ref, watch} from 'vue'
|
|
import CalendarField from './internal/CalendarField.vue'
|
|
import MonthGrid from './internal/MonthGrid.vue'
|
|
import {formatIsoToDisplay, isDateInRange, isValidIso, parseDisplayToIso} from './composables/dateFormat'
|
|
|
|
defineOptions({name: 'MalioDate', inheritAttrs: false})
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
id?: string
|
|
name?: string
|
|
label?: string
|
|
modelValue?: string | null
|
|
placeholder?: string
|
|
required?: boolean
|
|
disabled?: boolean
|
|
readonly?: boolean
|
|
hint?: string
|
|
error?: string
|
|
success?: string
|
|
min?: string
|
|
max?: string
|
|
clearable?: boolean
|
|
editable?: boolean
|
|
invalidMessage?: string
|
|
inputClass?: string
|
|
labelClass?: string
|
|
groupClass?: string
|
|
}>(),
|
|
{
|
|
id: '',
|
|
name: '',
|
|
label: '',
|
|
modelValue: undefined,
|
|
placeholder: 'JJ/MM/AAAA',
|
|
required: false,
|
|
disabled: false,
|
|
readonly: false,
|
|
hint: '',
|
|
error: '',
|
|
success: '',
|
|
min: undefined,
|
|
max: undefined,
|
|
clearable: true,
|
|
editable: false,
|
|
invalidMessage: 'Date invalide',
|
|
inputClass: '',
|
|
labelClass: '',
|
|
groupClass: '',
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: string | null): void
|
|
(e: 'update:valid', value: boolean): void
|
|
}>()
|
|
|
|
const displayValue = computed(() => formatIsoToDisplay(props.modelValue ?? null))
|
|
|
|
const internalError = ref('')
|
|
const mergedError = computed(() => props.error || internalError.value)
|
|
|
|
// La validité ne reflète que la saisie : malformée/hors plage → false. Un champ
|
|
// vide est valide (l'obligation `required` reste à la charge du parent).
|
|
const setError = (message: string) => {
|
|
internalError.value = message
|
|
emit('update:valid', message === '')
|
|
}
|
|
|
|
const onCommit = (text: string) => {
|
|
const trimmed = text.trim()
|
|
if (trimmed === '') {
|
|
setError('')
|
|
emit('update:modelValue', null)
|
|
return
|
|
}
|
|
const iso = parseDisplayToIso(trimmed)
|
|
if (iso && isDateInRange(iso, props.min, props.max)) {
|
|
setError('')
|
|
emit('update:modelValue', iso)
|
|
return
|
|
}
|
|
setError(props.invalidMessage)
|
|
}
|
|
|
|
const onClear = () => {
|
|
setError('')
|
|
emit('update:modelValue', null)
|
|
}
|
|
|
|
const onSelect = (iso: string, close: () => void) => {
|
|
setError('')
|
|
emit('update:modelValue', iso)
|
|
close()
|
|
}
|
|
|
|
// immediate : émet aussi la validité au montage, pour que le parent connaisse
|
|
// l'état d'un champ pré-rempli (formulaire d'édition) sans interaction préalable.
|
|
watch(() => props.modelValue, (val) => {
|
|
setError('')
|
|
if (val && !isValidIso(val) && import.meta.dev) {
|
|
console.warn(`[MalioDate] modelValue invalide ignoré : "${val}"`)
|
|
}
|
|
}, {immediate: true})
|
|
</script>
|