import type { Meta, StoryObj } from '@storybook/vue3' import { ref } from 'vue' import { VBtn, VForm } from 'vuetify/components' import MonthPicker from '../MonthPicker.vue' import { fn, userEvent, within } from '@storybook/test' import SyForm from '@/components/Customs/SyForm/SyForm.vue' import type { MonthPickerProps } from '../types' import { default as MonthPickerMeta } from '../MonthPicker.stories' const meta = { title: 'Composants/Formulaires/MonthPicker/Validation', component: MonthPicker, decorators: [ () => ({ template: '
', }), ], parameters: { layout: 'fullscreen', docs: { description: { component: `Exemples de validation pour le composant MonthPicker`, }, controls: { exclude: ['onUpdate:modelValue', 'onUpdate:open'], }, }, controls: { exclude: ['width', 'undefined', 'onUpdate:modelValue', 'onUpdate:open'], }, }, argTypes: MonthPickerMeta.argTypes, args: { 'modelValue': '', 'label': 'Mois de début', 'required': false, 'errorMessages': null, 'warningMessages': null, 'successMessages': null, 'readonly': false, 'disabled': false, 'customRules': [], 'customWarningRules': [], 'customSuccessRules': [], 'isValidateOnBlur': true, 'onUpdate:modelValue': fn(), }, } as Meta export default meta type Story = StoryObj export const WithError: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '13/2025', customRules: [ { type: 'custom', options: { validate: (value: string) => { return /^(0[1-9]|1[0-2])\/\d{4}$/.test(value) }, message: 'Le format doit être MM/YYYY avec un mois entre 01 et 12 (ex: 03/2026).', fieldIdentifier: 'selectedMonth', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByRole('textbox') await userEvent.clear(input) await userEvent.type(input, '13/2025') input.blur() }, } export const WithWarning: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '01/2035', customWarningRules: [ { type: 'custom', options: { validate: (value: string) => { if (!value || value === '') return true // Ne pas afficher de warning si le champ est vide const [month, year] = value.split('/').map(Number) as [number, number] const currentDate = new Date() const currentYear = currentDate.getFullYear() const currentMonth = currentDate.getMonth() + 1 if (year > currentYear + 5 || (year === currentYear + 5 && month > currentMonth)) { return false } return true }, warningMessage: 'La date est plus de 5 ans dans le futur.', fieldIdentifier: 'selectedMonth', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByRole('textbox') await userEvent.clear(input) await userEvent.type(input, '01/2035') input.blur() }, } export const WithSuccess: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '06/2026', showSuccessMessages: true, customSuccessRules: [ { type: 'custom', options: { validate: (value: string) => { if (!value || value === '') return true // Ne pas afficher de success si le champ est vide const [, year] = value.split('/').map(Number) as [number, number] const currentYear = new Date().getFullYear() return year === currentYear }, successMessage: 'La date est dans l\'année en cours.', fieldIdentifier: 'selectedMonth', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByRole('textbox') await userEvent.clear(input) const currentYear = new Date().getFullYear() await userEvent.type(input, `06/${currentYear}`) input.blur() }, } export const WithCustomRules: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: args => ({ components: { MonthPicker }, setup() { const selectedMonth = ref(args.modelValue ?? '') const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || !/^\d{2}\/\d{4}$/.test(value)) { return false } return true }, message: 'Le format doit être MM/YYYY (ex: 03/2026).', fieldIdentifier: 'selectedMonth', }, }, { type: 'custom', options: { validate: (value: string) => { if (!value || value === '') return true // Ne pas afficher d'erreur si le champ est vide const [month] = value.split('/').map(Number) if (!month || month < 1 || month > 12) { return false } return true }, message: 'Le mois doit être compris entre 01 et 12.', fieldIdentifier: 'selectedMonth', }, }, { type: 'custom', options: { validate: (value: string) => { if (!value || value === '') return true // Ne pas afficher d'erreur si le champ est vide const [, year] = value.split('/').map(Number) as [number, number] const currentYear = new Date().getFullYear() if (year < currentYear - 10) { return false } return true }, message: 'La date ne peut pas être antérieure à 10 ans.', fieldIdentifier: 'selectedMonth', }, }, ] return { args, selectedMonth, customRules } }, template: `

Saisissez un mois pour voir les différents types de validation :

Conseils pour tester :

  • Laissez le champ vide pour voir l'erreur de champ requis
  • Saisissez un mois invalide (ex: 13/2026) pour voir l'erreur de format
  • Saisissez une date à plus de 5 ans (ex: 01/2032) pour voir l'avertissement
  • Saisissez une date entre aujourd'hui et dans 5 ans pour voir le message de succès
`, }), } export const DisableErrorHandling: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: ` ### Désactivation de la gestion des erreurs Cette story illustre l'utilisation de la propriété \`disableErrorHandling\` qui permet de désactiver complètement la gestion et l'affichage des erreurs dans un champ, même si des règles de validation sont définies. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: () => ({ components: { MonthPicker }, setup() { const value1 = ref('') const value2 = ref('') const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || !/^(0[1-9]|1[0-2])\/\d{4}$/.test(value)) { return 'Le format doit être MM/YYYY avec un mois valide (ex: 03/2026).' } return true }, fieldIdentifier: 'selectedMonth', }, }, ] return { value1, value2, customRules } }, template: `

Cette démonstration compare un MonthPicker standard et un avec disableErrorHandling=true.

Validation normale

Sans gestion d'erreurs

Instructions :

  1. Cliquez dans un champ puis en dehors pour déclencher la validation
  2. Le champ de gauche affichera une erreur requise, mais pas celui de droite
`, }), } /** * Validation déclenchée à chaque frappe (isValidateOnBlur: false). */ export const ValidateOnInput: Story = { parameters: { docs: { description: { story: ` ### Validation à la saisie Lorsque \`isValidateOnBlur\` vaut \`false\`, la validation se déclenche à chaque modification de la valeur plutôt qu'à la perte de focus. Utile pour un retour immédiat à l'utilisateur. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { MonthPicker }, setup() { const selectedMonth = ref('') return { args, selectedMonth } }, template: `

La validation se déclenche à chaque frappe (isValidateOnBlur="false").

`, }), } /** * Messages de validation injectés directement par le parent (errorMessages, warningMessages, successMessages). */ export const ExternalMessages: Story = { parameters: { docs: { description: { story: ` ### Messages externes Les props \`errorMessages\`, \`warningMessages\` et \`successMessages\` permettent d'injecter des messages depuis le parent sans déclencher de règle de validation. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { MonthPicker, VBtn }, setup() { const selectedMonth = ref('') const errorMessages = ref(null) const warningMessages = ref(null) const successMessages = ref(null) function setError() { errorMessages.value = ['Ce mois est déjà utilisé dans un autre dossier'] warningMessages.value = null successMessages.value = null } function setWarning() { errorMessages.value = null warningMessages.value = ['Ce mois est proche d\'une échéance importante'] successMessages.value = null } function setSuccess() { errorMessages.value = null warningMessages.value = null successMessages.value = ['Mois accepté par le serveur'] } function reset() { errorMessages.value = null warningMessages.value = null successMessages.value = null } return { args, selectedMonth, errorMessages, warningMessages, successMessages, setError, setWarning, setSuccess, reset } }, template: `

Les messages ci-dessous sont injectés par le parent sans déclencher de règle de validation.

Simuler une erreur Simuler un avertissement Simuler un succès Réinitialiser
`, }), } export const SyFormValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { MonthPicker, VBtn, SyForm }, setup() { const selectedMonth = ref('') const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || !/^(0[1-9]|1[0-2])\/\d{4}$/.test(value)) { return false } return true }, message: 'Le format doit être MM/YYYY avec un mois valide (ex: 03/2026).', fieldIdentifier: 'selectedMonth', }, }, ] function handleSubmit(e: { isValid: boolean }) { const isValid = e.isValid alert(isValid ? 'Mois valide !' : 'Veuillez corriger les erreurs.') } return { args, selectedMonth, customRules, handleSubmit } }, template: `
Valider
`, }), } export const VFormValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { MonthPicker, VBtn, VForm }, setup() { const selectedMonth = ref('') const fieldRef = ref | null>(null) const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || !/^(0[1-9]|1[0-2])\/\d{4}$/.test(value)) { return false } return true }, message: 'Le format doit être MM/YYYY avec un mois valide (ex: 03/2026).', fieldIdentifier: 'selectedMonth', }, }, ] async function handleSubmit() { const result = await (fieldRef.value!.validateOnSubmit! as () => Promise)() alert(result ? 'Mois valide !' : 'Veuillez corriger les erreurs.') } return { args, selectedMonth, customRules, handleSubmit, fieldRef } }, template: `
Valider
`, }), } export const SyFormVuetifyValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { MonthPicker, VBtn, SyForm }, setup() { const selectedMonth = ref('') const vuetifyRules = [ (value: string) => !!value || 'Le mois est requis', (value: string) => /^(0[1-9]|1[0-2])\/\d{4}$/.test(value) || 'Le format doit être MM/YYYY (ex: 03/2026)', ] function handleSubmit(e: { isValid: boolean }) { const isValid = e.isValid alert(isValid ? 'Mois valide !' : 'Veuillez corriger les erreurs.') } return { args, selectedMonth, vuetifyRules, handleSubmit } }, template: `
Valider
`, }), } export const VFormVuetifyValidation: Story = { parameters: { docs: { description: { story: ` ### Validation de style Vuetify En passant \`useVuetifyValidation="true"\`, le composant délègue la validation à Vuetify. Les règles sont de simples fonctions qui retournent \`true\` si la valeur est valide, ou un message d'erreur (chaîne de caractères) sinon — exactement comme avec la prop \`rules\` native de Vuetify. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { MonthPicker, VBtn, VForm }, setup() { const selectedMonth = ref('') const fieldRef = ref() const rules = [ (value: string) => !!value || 'Le mois est requis', (value: string) => /^(0[1-9]|1[0-2])\/\d{4}$/.test(value) || 'Le format doit être MM/YYYY (ex: 03/2026)', ] async function handleSubmit(e: Promise<{ valid: boolean }>) { const result = await e alert(result.valid ? 'Mois valide !' : 'Veuillez corriger les erreurs.') } return { args, selectedMonth, fieldRef, rules, handleSubmit } }, template: `

Les règles sont des fonctions Vuetify natives (value) => true | 'message'. Cliquez sur Valider ou quittez le champ pour déclencher la validation.

Valider
`, }), args: { showSuccessMessages: true, }, }