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
+10 -12
View File
@@ -16,7 +16,6 @@ type TabListProps = {
modelValue?: string
id?: string
maxVisibleTabs?: number
maxWidth?: number
}
const TabListForTest = TabList as DefineComponent<TabListProps>
@@ -157,7 +156,16 @@ describe('MalioTabList', () => {
const wrapper = mountComponent({tabs: disabledTabs})
const buttons = wrapper.findAll('[role="tab"]')
expect(buttons[1].classes()).toContain('cursor-not-allowed')
expect(buttons[1].classes()).not.toContain('hover:text-m-primary/70')
expect(buttons[1].classes()).not.toContain('hover:text-m-primary')
})
it('hover sur un onglet inactif applique le même style que l\'actif (texte plein + barre)', () => {
const wrapper = mountComponent({tabs})
const inactive = wrapper.findAll('[role="tab"]')[1]
expect(inactive.attributes('aria-selected')).toBe('false')
expect(inactive.classes()).toContain('hover:text-m-primary')
expect(inactive.classes()).toContain('hover:after:bg-m-primary')
expect(inactive.classes()).toContain('hover:after:h-[3px]')
})
it('does not emit update:modelValue when clicking a disabled tab', async () => {
@@ -199,16 +207,6 @@ describe('MalioTabList — fenêtrage maxVisibleTabs', () => {
{key: 't7', label: 'Tab 7'},
]
it('applies the default maxWidth (1100px) on the tabs container when windowed', () => {
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
expect(wrapper.find('[role="tablist"]').attributes('style')).toContain('max-width: 1100px')
})
it('applies a custom maxWidth on the tabs container', () => {
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5, maxWidth: 1200})
expect(wrapper.find('[role="tablist"]').attributes('style')).toContain('max-width: 1200px')
})
it('renders only maxVisibleTabs buttons and disables prev at start', () => {
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
const buttons = wrapper.findAll('[role="tab"]')