import type { Meta, StoryObj } from '@storybook/vue3-vite' import type { DateModelValue } from '@/composables/date/useDateInitializationDayjs' import DatePicker from '@/components/DatePicker/CalendarMode/DatePicker.vue' import SyAlert from '@/components/SyAlert/SyAlert.vue' import { ref, onMounted } from 'vue' import { fn } from 'storybook/test' import type { CalendarModeProps } from '../types' const meta = { title: 'Composants/Formulaires/DatePicker/CombinedMode', component: DatePicker, decorators: [ () => ({ template: '
', }), ], parameters: { layout: 'fullscreen', controls: { exclude: ['modelValue'] }, actions: { argTypesRegex: '^on.*' }, events: { remapEvents: { 'update:modelValue': 'onUpdate:modelValue', 'focus': 'onFocus', 'blur': 'onBlur', 'closed': 'onClosed', 'input': 'onInput', 'date-selected': 'onDate-selected', }, }, docs: { description: { component: '\n## DatePicker en mode combiné (useCombinedMode) - Incompatibilités entre props\n\n### Contrôle d\'affichage des icônes\n- `noIcon: true` masque toutes les icônes, rendant `displayIcon`, `displayAppendIcon` et `displayPrependIcon` sans effet\n- `displayIcon: false` désactive les icônes, rendant `displayAppendIcon` et `displayPrependIcon` sans effet\n- `displayAppendIcon` et `displayPrependIcon` sont mutuellement exclusifs; si les deux sont définis à `true`, `displayAppendIcon` est prioritaire\n\n### Modes de fonctionnement\n- `noCalendar` et `useCombinedMode` sont mutuellement exclusifs\n- `noCalendar: true` annule le mode combiné et rend sans effet : `displayWeekendDays`, `displayHolidayDays`, `showWeekNumber` et `textFieldActivator`\n\n### Validation et états de champ\n- `readonly: true` désactive toutes les validations, y compris `required` et les règles personnalisées\n- `disabled` et `readonly` sont mutuellement exclusifs\n- `disableErrorHandling: true` peut créer une incohérence avec `showSuccessMessages: true`\n\n### Format et saisie\n- `birthDate` et `isBirthDate` sont des alias, utiliser l\'un ou l\'autre mais pas les deux\n- `displayRange: true` nécessite que modelValue soit un tableau de deux dates `[startDate, endDate]`\n- `autoClamp: true` peut court-circuiter certaines validations manuelles\n', }, }, }, argTypes: { 'onUpdate:modelValue': { description: 'Émis lorsque la valeur du champ est mise à jour', table: { category: 'events', type: { summary: '(value: DateValue) => void' }, }, }, 'onClosed': { description: 'Émis lorsque le calendrier est fermé', table: { category: 'events', type: { summary: '() => void' }, }, }, 'onFocus': { description: 'Émis lorsque le champ reçoit le focus', table: { category: 'events', type: { summary: '() => void' }, }, }, 'onBlur': { description: 'Émis lorsque le champ perd le focus', table: { category: 'events', type: { summary: '() => void' }, }, }, 'onInput': { description: 'Émis lors de la saisie dans le champ', table: { category: 'events', type: { summary: '(value: string) => void' }, }, }, 'onDate-selected': { description: 'Émis lorsqu\'une date est sélectionnée via le calendrier ou complétée manuellement', table: { category: 'events', type: { summary: '(value: DateValue) => void' }, }, }, 'validateOnSubmit': { description: 'Valide le champ et retourne true si valide, false sinon', table: { category: 'exposed', type: { summary: '() => boolean' }, }, }, 'isDatePickerVisible': { description: 'Indique si le calendrier est actuellement visible', table: { category: 'exposed', type: { summary: 'Ref' }, }, }, 'selectedDates': { description: 'Dates sélectionnées au format Date', table: { category: 'exposed', type: { summary: 'Ref' }, }, }, 'errorMessages': { description: 'Messages d\'erreur actuels', table: { category: 'exposed', type: { summary: 'Ref' }, }, }, 'handleClickOutside': { description: 'Gestionnaire d\'interactions externes au composant', table: { category: 'exposed', type: { summary: '(event: MouseEvent) => void' }, }, }, 'handleSelectToday': { description: 'Définit la date sur aujourd\'hui', table: { category: 'exposed', type: { summary: '() => void' }, }, }, 'openDatePicker': { description: 'Ouvre le calendrier de sélection de date', table: { category: 'exposed', type: { summary: '() => void' }, }, }, 'toggleDatePicker': { description: 'Bascule l\'affichage du calendrier (affiche/masque)', table: { category: 'exposed', type: { summary: '() => void' }, }, }, 'handleDateSelected': { description: 'Permet de définir une date programmatiquement', table: { category: 'exposed', type: { summary: '(value: DateValue) => void' }, }, }, 'resetViewMode': { description: 'Réinitialise le mode d\'affichage du calendrier', table: { category: 'exposed', type: { summary: '() => void' }, }, }, 'modelValue': { control: 'text', description: 'Valeur du champ (v-model), peut être une chaîne de caractères ou un tableau de deux dates en mode plage', }, 'placeholder': { control: 'text', description: 'Texte indicatif affiché lorsque le champ est vide pour guider l\'utilisateur sur le format attendu', defaultValue: 'Sélectionner une date', }, 'period': { control: 'object', description: 'Définit la période sélectionnable dans le calendrier avec des dates min et max (au format MM/DD/YYYY). Les dates hors de cette période seront désactivées', defaultValue: { min: '', max: '', }, }, 'format': { control: 'select', options: ['DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD'], description: 'Format d\'affichage de la date dans le champ (ex: DD/MM/YYYY pour jour/mois/année)', defaultValue: 'DD/MM/YYYY', }, 'dateFormatReturn': { control: 'select', options: ['', 'DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD'], description: 'Format de la date émise par le v-model. Si vide, utilise le même format que la prop "format"', defaultValue: 'DD/MM/YYYY', }, 'isBirthDate': { control: 'boolean', description: 'Active le mode date de naissance qui ouvre le calendrier sur la vue de sélection d\'année (au lieu de la vue mensuelle), permettant une navigation année → mois → jour', defaultValue: false, }, 'showWeekNumber': { control: 'boolean', description: 'Affiche les numéros de semaine dans la colonne de gauche du calendrier. ⚠️ Sans effet si noCalendar est true.', defaultValue: false, }, 'required': { control: 'boolean', description: 'Définit si le champ est obligatoire et active la validation correspondante', defaultValue: false, }, 'displayRange': { control: 'boolean', description: 'Active la sélection de plage de dates (date début - date fin), le v-model retournera un tableau de deux dates. ⚠️ Nécessite que modelValue soit un tableau de deux dates [startDate, endDate] pour fonctionner correctement.', defaultValue: false, }, 'displayIcon': { control: 'boolean', description: 'Contrôle l\'affichage de l\'icône calendrier, à utiliser en conjonction avec displayPrependIcon ou displayAppendIcon. ⚠️ Sans effet si noIcon est true.', defaultValue: true, }, 'displayAppendIcon': { control: 'boolean', description: 'Affiche l\'icône calendrier à la fin du champ (à droite). ⚠️ Sans effet si displayIcon est false ou si noIcon est true. Prioritaire sur displayPrependIcon si les deux sont true.', defaultValue: false, }, 'displayPrependIcon': { control: 'boolean', description: 'Affiche l\'icône calendrier au début du champ (à gauche). ⚠️ Sans effet si displayIcon est false, si noIcon est true, ou si displayAppendIcon est true.', defaultValue: true, }, 'customRules': { control: 'object', description: 'Règles de validation personnalisées pour la date saisie (DatePickerRule[]), affichant des erreurs si non respectées', defaultValue: [], }, 'customWarningRules': { control: 'object', description: 'Règles d\'avertissement personnalisées (DatePickerRule[]) pour afficher des messages d\'attention sans bloquer la validation', defaultValue: [], }, 'disabled': { control: 'boolean', description: 'Désactive le champ, empêchant toute interaction utilisateur et appliquant un style grisé. ⚠️ Incompatible avec readonly.', defaultValue: false, }, 'noIcon': { control: 'boolean', description: 'Masque toutes les icônes du composant, remplace les props displayIcon, displayAppendIcon et displayPrependIcon. ⚠️ Incompatible avec displayIcon, displayAppendIcon et displayPrependIcon.', defaultValue: false, }, 'noCalendar': { table: { category: 'props', }, control: 'boolean', description: 'Désactive l\'affichage du calendrier, permettant uniquement la saisie manuelle (utile pour les tests automatisés). ⚠️ Incompatible avec useCombinedMode, displayWeekendDays, displayHolidayDays, showWeekNumber et textFieldActivator.', defaultValue: false, }, 'isOutlined': { control: 'boolean', description: 'Affiche le champ avec un contour complet (style outlined de Vuetify) plutôt qu\'un souligné simple', defaultValue: true, }, 'readonly': { control: 'boolean', description: 'Rend le champ en lecture seule, la valeur peut être affichée mais pas modifiée par l\'utilisateur. ⚠️ Désactive toutes les validations (required, customRules, customWarningRules). Incompatible avec disabled.', defaultValue: false, }, 'width': { control: 'text', description: 'Largeur du champ (peut être en px, %, em, rem ou toute unité CSS valide)', defaultValue: '100%', }, 'disableErrorHandling': { control: 'boolean', description: 'Désactive la gestion interne des erreurs, permettant à l\'application parente de gérer les validations. ⚠️ Peut créer une incohérence si showSuccessMessages est true.', defaultValue: false, }, 'showSuccessMessages': { control: 'boolean', description: 'Affiche les messages de succès quand la validation est passée avec succès', defaultValue: false, }, 'bgColor': { control: 'text', description: 'Couleur de fond du champ de saisie (ex: white, transparent, #f5f5f5)', defaultValue: 'white', }, 'textFieldActivator': { control: 'boolean', description: 'Permet d\'ouvrir le calendrier en cliquant n\'importe où sur le champ texte, pas uniquement sur l\'icône. ⚠️ Sans effet si noCalendar est true.', defaultValue: false, }, 'displayTodayButton': { control: 'boolean', description: 'Affiche le bouton "Aujourd\'hui" en bas du calendrier pour sélectionner rapidement la date du jour', defaultValue: true, }, 'displayWeekendDays': { control: 'boolean', description: 'Affiche les jours de week-end avec un style spécifique pour les distinguer dans le calendrier. ⚠️ Sans effet si noCalendar est true.', defaultValue: true, }, 'displayHolidayDays': { control: 'boolean', description: 'Affiche les jours fériés français avec un style spécifique dans le calendrier. ⚠️ Sans effet si noCalendar est true.', defaultValue: true, }, 'autoClamp': { control: 'boolean', description: 'Active la mise en forme automatique lors de la saisie (ajout des séparateurs automatiquement). ⚠️ Peut court-circuiter certaines validations manuelles.', defaultValue: false, }, 'displayAsterisk': { control: 'boolean', description: 'Affiche un astérisque (*) à côté du label pour indiquer visuellement que le champ est obligatoire', defaultValue: false, }, 'label': { control: 'text', description: 'Libellé du champ affiché au-dessus ou dans le champ de saisie', defaultValue: 'Sélectionner une date', }, 'isValidateOnBlur': { control: 'boolean', description: 'Active la validation automatique lorsque le champ perd le focus (onBlur)', defaultValue: true, }, 'birthDate': { control: 'boolean', description: '⚠️ **DEPRECATED** — Utilisez `isBirthDate` à la place.', defaultValue: false, }, 'useCombinedMode': { control: 'boolean', description: 'Active le mode combiné permettant à la fois la sélection via calendrier et la saisie manuelle de date. ⚠️ Incompatible avec noCalendar.', defaultValue: true, }, 'density': { control: 'select', options: ['default', 'comfortable', 'compact'], description: 'Densité du champ, affecte l\'espacement interne et la hauteur (standard Vuetify)', defaultValue: 'default', }, 'title': { control: 'text', }, 'errors': { description: 'Tableau réactif contenant tous les messages d\'erreur. Combine les erreurs injectées via errorMessages et celles générées par la validation. Les doublons sont supprimés et le tableau est limité selon maxErrors. Accessible via template ref du composant.', table: { type: { summary: 'Readonly>' }, category: 'expose', }, }, 'warnings': { description: 'Tableau réactif contenant tous les messages d\'avertissement. Combine les avertissements injectés via warningMessages et ceux générés par customWarningRules. Les doublons sont supprimés et le tableau est limité selon maxErrors. Accessible via template ref du composant.', table: { type: { summary: 'Readonly>' }, category: 'expose', }, }, 'successes': { description: 'Tableau réactif contenant tous les messages de succès. Combine les succès injectés via successMessages et ceux générés par customSuccessRules. Les doublons sont supprimés et le tableau est limité selon maxErrors. Accessible via template ref du composant.', table: { type: { summary: 'Readonly>' }, category: 'expose', }, }, }, } as Meta void 'onFocus'?: () => void 'onBlur'?: () => void 'onClosed'?: () => void 'onInput'?: (value: string) => void 'onDate-selected'?: (value: DateModelValue) => void }> export default meta type Story = StoryObj export const Default: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { 'label': 'Date (JJ/MM/AAAA)', 'placeholder': 'JJ/MM/AAAA', 'format': 'DD/MM/YYYY', 'isBirthDate': false, 'showWeekNumber': false, 'required': false, 'displayRange': false, 'displayIcon': true, 'displayAppendIcon': false, 'displayPrependIcon': true, 'disabled': false, 'noIcon': false, 'noCalendar': false, 'modelValue': '', 'onUpdate:modelValue': fn(), 'onFocus': fn(), 'onBlur': fn(), 'onClosed': fn(), 'onDate-selected': fn(), 'displayTodayButton': true, 'displayWeekendDays': true, 'displayHolidayDays': true, 'useCombinedMode': true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `
`, } }, } export const Required: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { 'label': 'Date (JJ/MM/AAAA)', 'format': 'DD/MM/YYYY', 'isBirthDate': false, 'showWeekNumber': false, 'required': true, 'displayRange': false, 'displayIcon': true, 'displayAppendIcon': false, 'displayPrependIcon': true, 'disabled': false, 'noIcon': false, 'noCalendar': false, 'modelValue': '', 'onUpdate:modelValue': fn(), 'onFocus': fn(), 'onBlur': fn(), 'onClosed': fn(), 'onDate-selected': fn(), 'displayTodayButton': true, 'displayWeekendDays': true, 'displayHolidayDays': true, 'useCombinedMode': true, }, render: (args) => { return { components: { DatePicker }, setup() { const value1 = ref('') const value2 = ref('') return { args, value1, value2 } }, template: `

Sans astérisque & isValidateOnBlur à true (par defaut):

Avec astérisque & isValidateOnBlur à false:

`, } }, } export const DateRange: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { 'label': 'Période (JJ/MM/AAAA - JJ/MM/AAAA)', 'placeholder': 'JJ/MM/AAAA - JJ/MM/AAAA', 'format': 'DD/MM/YYYY', 'dateFormatReturn': '', 'isBirthDate': false, 'showWeekNumber': false, 'required': false, 'displayRange': true, 'displayIcon': true, 'displayAppendIcon': false, 'displayPrependIcon': true, 'disabled': false, 'noIcon': false, 'noCalendar': false, 'modelValue': ['', ''], 'onUpdate:modelValue': fn(), 'onFocus': fn(), 'onBlur': fn(), 'onClosed': fn(), 'onDate-selected': fn(), 'useCombinedMode': true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ['2023-01-15', '2023-01-20'] return { args, value } }, template: `
`, } }, } export const BirthDate: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date de naissance (JJ/MM/AAAA)', placeholder: 'JJ/MM/AAAA', format: 'DD/MM/YYYY', isBirthDate: true, showWeekNumber: false, required: false, displayRange: false, displayIcon: true, displayAppendIcon: false, displayPrependIcon: true, disabled: false, noIcon: false, noCalendar: false, // modelValue est défini dans le setup du render useCombinedMode: true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `
`, } }, } export const WithValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date (JJ/MM/AAAA)', placeholder: 'JJ/MM/AAAA', format: 'DD/MM/YYYY', required: true, useCombinedMode: true, customRules: [ { type: 'custom', options: { validate: (value: unknown) => { if (value && new Date(value as string).getFullYear() === 2024) { return false } return true }, message: 'Les dates en 2024 ne sont pas autorisées', successMessage: 'Les dates hors 2024 sont autorisées', fieldIdentifier: 'date', }, }, ], }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `
`, } }, } export const DifferentFormats: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: () => { return { components: { DatePicker }, setup() { const europeanDate = ref('') const americanDate = ref('') const isoDate = ref('') return { europeanDate, americanDate, isoDate } }, template: `
`, } }, } export const WithDateFormatReturn: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: () => { return { components: { DatePicker }, setup() { const date = ref('') return { date } }, template: `
Valeur du modèle: {{ date }}
`, } }, } export const DisablePickerInteraction: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date (JJ/MM/AAAA)', placeholder: 'JJ/MM/AAAA', format: 'DD/MM/YYYY', displayIcon: true, useCombinedMode: true, noCalendar: true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `

Valeur actuelle: {{ value }}

Le calendrier ne s'ouvrira pas au clic sur l'input ou l'icône.

`, } }, } export const AutoFormattingInput: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date (JJ-MM-AAAA)', placeholder: 'JJ-MM-AAAA', format: 'DD-MM-YYYY', useCombinedMode: true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `
Essayez de saisir des chiffres - les séparateurs seront ajoutés automatiquement
`, } }, } export const CustomDateFormat: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date (AAAA.MM.JJ)', placeholder: 'AAAA.MM.JJ', format: 'YYYY.MM.DD', useCombinedMode: true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `

Valeur actuelle: {{ value }}

Le séparateur "." est automatiquement ajouté pendant la saisie.

`, } }, } export const ReadonlyMode: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date en lecture seule', placeholder: 'Date en lecture seule', format: 'DD/MM/YYYY', useCombinedMode: true, readonly: true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') onMounted(() => { // Initialiser avec une date value.value = '15/06/2023' }) return { args, value } }, template: `

Valeur actuelle: {{ value }}

Le champ est en lecture seule et ne peut pas être modifié.

`, } }, } export const AppendIcon: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date avec icône à la fin', placeholder: 'Date avec icône à la fin', format: 'DD/MM/YYYY', useCombinedMode: true, displayPrependIcon: false, displayAppendIcon: true, }, render: (args) => { return { components: { DatePicker }, setup() { const value = ref('') return { args, value } }, template: `

Valeur actuelle: {{ value }}

L'icône du calendrier est positionnée à la fin du champ.

`, } }, } export const WithCustomPeriod: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { label: 'Date avec icône à la fin', placeholder: 'Date avec icône à la fin', format: 'DD/MM/YYYY', useCombinedMode: true, period: { min: '01/01/1995', max: '12/31/2005', }, customRules: [ { type: 'notBeforeDate', options: { date: '01/01/1995', message: 'La date doit être postérieure ou égale au 01/01/1995', fieldIdentifier: 'date', }, }, { type: 'notAfterDate', options: { date: '31/12/2005', message: 'La date doit être antérieure ou égale au 31/12/2005', fieldIdentifier: 'date', }, }, ], }, render: (args) => { return { components: { DatePicker, SyAlert }, setup() { const value = ref('') return { args, value } }, template: `

Valeur actuelle: {{ value }}

`, } }, } export const AutoClampFeature: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: () => { return { components: { DatePicker }, setup() { const dateSlash = ref('') const dateDash = ref('') const dateDot = ref('') return { dateSlash, dateDash, dateDot } }, template: `

Démonstration de l'auto clamp avec différents formats

Saisissez uniquement des chiffres - les séparateurs seront ajoutés automatiquement selon le format défini

Format JJ/MM/AAAA (séparateur /)

Valeur actuelle: {{ dateSlash || 'aucune date saisie' }}

Format JJ-MM-AAAA (séparateur -)

Valeur actuelle: {{ dateDash || 'aucune date saisie' }}

Format AAAA.MM.JJ (séparateur .)

Valeur actuelle: {{ dateDot || 'aucune date saisie' }}
`, } }, } export const WithFormSubmission: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: () => { return { components: { DatePicker }, setup() { const date = ref('') // Définir le type correct pour la référence datePicker const datePicker = ref | null>(null) const submitted = ref(false) const submitForm = async () => { if (!datePicker.value) return const isValid = await datePicker.value.validateOnSubmit() if (isValid) { submitted.value = true } else { submitted.value = false } } return { date, datePicker, submitted, submitForm } }, template: `
Soumettre
Formulaire soumis avec la date: {{ date }}
`, } }, } export const CustomRules: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: () => { return { components: { DatePicker }, setup() { const customRules = [ { type: 'notBeforeToday', options: { message: 'La date ne peut pas être antérieure à aujourd\'hui', }, }, ] // Valeur du DatePicker const date = ref(null) return { date, customRules } }, template: `
`, } }, } export const CustomWarningRules: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: () => { return { components: { DatePicker }, setup() { const customWarningRules = [ { type: 'custom', options: { validate: (value: string | Date) => { // check typeof value if (typeof value === 'string') { return !value.includes('2025') } else { // check if value is a Date return !value.getFullYear().toString().includes('2025') } }, warningMessage: 'Les dates en 2025 ne sont pas autorisées', successMessage: 'Date hors 2025', fieldIdentifier: 'date', isWarning: true, }, }, ] // Valeur du DatePicker const date = ref('') return { date, customWarningRules } }, template: `
`, } }, }