fix(date) : borne la saisie clavier pour empêcher les dates absurdes (99/99/9999) (#79)

## Problème

Sur la famille Date editable, le masque maska n'imposait que la *forme* (`##/##/####`). Une valeur structurellement absurde comme `99/99/9999` était donc **saisissable**, puis rejetée *a posteriori* par la validation. Le métier veut que ce soit **impossible à taper**.

## Solution (masque borné + validation en filet)

- `composables/maskTemplate.ts` — `buildBoundedMask(template)` : borne le **premier chiffre de chaque champ** (jour `0-3`, mois `0-1`, heure `0-2`, minute `0-5`). Distingue le mois des minutes (même lettre `M`) selon la présence d'heures dans le gabarit, pour ne pas brider la saisie des minutes du DateTime.
- `internal/CalendarField.vue` — branche le builder dans `maskaOptions` (remplace le `replace(/[A-Za-z]/g, '#')`).

Les impossibilités plus fines (`31/02`, 29/02 non bissextile, hors `min`/`max`) restent captées par la **validation** (`invalidMessage` + `update:valid=false`).

## Tests

- `maskTemplate.test.ts` (5) — bornes par champ, structure du masque, non-confusion mois/minutes.
- `Date.test.ts` — test `invalidMessage` adapté (`32/13/2026`, typable→invalide) + garde de non-régression : `99/99/9999` ne s'inscrit jamais et n'émet aucune date.
- Suite complète : **1004/1004 verte** (DateTime 36 incluse → saisie d'heure intacte).

Doc : `COMPONENTS.md` (MalioDate) + `CHANGELOG.md` (Fixed) à jour.
Reviewed-on: #79
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #79.
This commit is contained in:
2026-06-19 13:04:11 +00:00
committed by Autin
parent 06c739cdc7
commit be3d88ed45
31 changed files with 939 additions and 125 deletions
@@ -375,6 +375,12 @@ describe('MalioInputAutocomplete', () => {
expect(wrapper.get('input').classes()).toContain('cursor-not-allowed')
})
it('hides the chevron when disabled', () => {
const wrapper = mountComponent({disabled: true})
expect(wrapper.find('[data-test="chevron"]').exists()).toBe(false)
})
it('sets readonly attribute', () => {
const wrapper = mountComponent({readonly: true})
@@ -64,7 +64,7 @@
class="animate-spin text-m-primary"
/>
<IconifyIcon
v-else
v-else-if="!disabled"
icon="mdi:chevron-down"
:width="20"
:height="20"
+2 -10
View File
@@ -339,12 +339,10 @@ describe('MalioInputEmail', () => {
expect(wrapper.emitted('add')).toHaveLength(1)
})
it('does not emit add when disabled', async () => {
it('hides the add button when disabled', () => {
const wrapper = mountComponent({addable: true, disabled: true})
await wrapper.get('[data-test="add-button"]').trigger('click')
expect(wrapper.emitted('add')).toBeUndefined()
expect(wrapper.find('[data-test="add-button"]').exists()).toBe(false)
})
it('does not emit add when readonly', async () => {
@@ -355,12 +353,6 @@ describe('MalioInputEmail', () => {
expect(wrapper.emitted('add')).toBeUndefined()
})
it('disables add button when disabled', () => {
const wrapper = mountComponent({addable: true, disabled: true})
expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined()
})
it('add button is not natively disabled in readonly (onAdd guard blocks the action)', () => {
const wrapper = mountComponent({addable: true, readonly: true})
+1 -2
View File
@@ -41,9 +41,8 @@
/>
<button
v-if="addable"
v-if="addable && !disabled"
type="button"
:disabled="disabled"
:aria-label="addButtonLabel"
data-test="add-button"
:class="mergedAddButtonClass"
+1 -1
View File
@@ -205,7 +205,7 @@ const mergedLabelClass = computed(() =>
'cursor-pointer text-black mr-4 text-[18px]',
hasError.value ? 'text-m-danger' : '',
hasSuccess.value ? 'text-m-success' : '',
props.disabled ? 'cursor-not-allowed text-black/60' : '',
props.disabled ? 'cursor-not-allowed text-m-muted' : '',
props.labelClass,
),
)
@@ -91,6 +91,12 @@ describe('MalioInputPassword', () => {
expect(wrapper.find('[data-test="icon"]').exists()).toBe(false)
})
it('hides the eye icon when disabled', () => {
const wrapper = mountComponent({disabled: true})
expect(wrapper.find('[data-test="icon"]').exists()).toBe(false)
})
it('renders icon by default', () => {
const wrapper = mountComponent()
+1 -1
View File
@@ -33,7 +33,7 @@
</label>
<IconifyIcon
v-if="displayIcon"
v-if="displayIcon && !disabled"
:icon="isPasswordVisible ? 'mdi:eye-outline' : 'mdi:eye-off-outline'"
:width="24"
:height="24"
+2 -10
View File
@@ -253,12 +253,10 @@ describe('MalioInputPhone', () => {
expect(wrapper.emitted('add')).toHaveLength(1)
})
it('does not emit add when disabled', async () => {
it('hides the add button when disabled', () => {
const wrapper = mountComponent({addable: true, disabled: true})
await wrapper.get('[data-test="add-button"]').trigger('click')
expect(wrapper.emitted('add')).toBeUndefined()
expect(wrapper.find('[data-test="add-button"]').exists()).toBe(false)
})
it('does not emit add when readonly', async () => {
@@ -269,12 +267,6 @@ describe('MalioInputPhone', () => {
expect(wrapper.emitted('add')).toBeUndefined()
})
it('disables add button when disabled', () => {
const wrapper = mountComponent({addable: true, disabled: true})
expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined()
})
it('add button is not natively disabled in readonly (onAdd guard blocks the action)', () => {
const wrapper = mountComponent({addable: true, readonly: true})
+1 -2
View File
@@ -42,9 +42,8 @@
/>
<button
v-if="addable"
v-if="addable && !disabled"
type="button"
:disabled="disabled"
:aria-label="addButtonLabel"
data-test="add-button"
:class="mergedAddButtonClass"