import type { Meta, StoryObj } from '@storybook/vue3' import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue' import { VIcon } from 'vuetify/components' import { ref, watch } from 'vue' import { mdiAccountBox } from '@mdi/js' import { VBtn } from 'vuetify/components' import { getValidationDocumentation } from '@/composables/unifyValidation/documentationValidationProps' import { fn } from '@storybook/test' const meta = { title: 'Composants/Formulaires/SyTextField', component: SyTextField, decorators: [ () => ({ template: '
', }), ], parameters: { layout: 'fullscreen', docs: { description: { component: `SyTextField`, }, }, }, argTypes: { ...getValidationDocumentation('string'), 'modelValue': { control: 'text' }, 'label': { description: 'Texte affiché comme label du champ', control: 'text', }, 'autocomplete': { description: 'Valeur de l\'attribut autocomplete', control: 'text', }, 'prependIcon': { control: 'select', options: ['info', 'success', 'warning', 'error', 'close'], }, 'appendIcon': { control: 'select', options: ['info', 'success', 'warning', 'error', 'close'], }, 'prependInnerIcon': { control: 'select', options: ['info', 'success', 'warning', 'error', 'close'], }, 'appendInnerIcon': { control: 'select', options: ['info', 'success', 'warning', 'error', 'close'], }, 'variantStyle': { control: 'select', options: ['outlined', 'plain', 'underlined', 'filled', 'solo', 'solo-inverted', 'solo-filled'], }, 'color': { control: 'select', options: ['primary', 'secondary', 'success', 'error', 'warning'], description: 'Couleur du champ', }, 'density': { control: 'select', options: ['default', 'comfortable', 'compact'], description: 'Densité du champ', }, 'isActive': { description: 'Force l\'état actif du champ (label flottant et styles visuels)', control: 'boolean', default: false, }, 'isClearable': { description: 'Affiche un bouton pour effacer le contenu du champ', control: 'boolean', default: false, }, 'prependTooltip': { description: 'Si le texte du prepend tooltip est renseigné alors l\'icône du tooltip s\'affiche', control: 'text', }, 'appendTooltip': { description: 'Si le texte du append tooltip est renseigné alors l\'icône du tooltip s\'affiche', control: 'text', }, 'tooltipLocation': { description: 'Position des tooltips', control: 'select', options: ['top', 'bottom', 'start', 'end'], default: 'top', }, 'displayAsterisk': { description: 'Affiche un astérisque à côté du label', control: 'boolean', default: false, }, 'disableClickButton': { description: 'Désactive le click sur les icônes append et prepend', control: 'boolean', default: true, }, 'baseColor': { description: 'Couleur de base du champ (par défaut hérite de color)', control: 'text', }, 'bgColor': { description: 'Couleur de fond du champ', control: 'color', }, 'centerAffix': { description: 'Centre verticalement les éléments ajoutés avant/après le champ', control: 'boolean', }, 'counter': { description: 'Affiche un compteur de caractères', control: 'boolean', }, 'counterValue': { description: 'Fonction personnalisée pour calculer la valeur du compteur', control: 'object', }, 'direction': { description: 'Direction du champ (horizontal ou vertical)', control: 'select', options: ['horizontal', 'vertical'], }, 'isDirty': { description: 'Indique si le champ a été modifié', control: 'boolean', }, 'isFlat': { description: 'Supprime l\'élévation du champ', control: 'boolean', }, 'isFocused': { description: 'Force l\'état focus du champ', control: 'boolean', }, 'areDetailsHidden': { description: 'Masque la section des détails (messages d\'erreur, compteur)', control: 'boolean', }, 'areSpinButtonsHidden': { description: 'Masque les boutons d\'incrémentation pour les champs numériques', control: 'boolean', }, 'hint': { description: 'Texte d\'aide affiché sous le champ', control: 'text', }, 'helpText': { description: 'Texte d\'aide affiché sous le champ', control: 'text', }, 'maxlength': { description: 'Nombre maximal de caractères autorisés dans le champ', control: { type: 'text' }, }, 'loading': { description: 'Affiche un indicateur de chargement', control: 'boolean', }, 'maxWidth': { description: 'Largeur maximale du champ', control: { type: 'text' }, }, 'minWidth': { description: 'Largeur minimale du champ', control: { type: 'text' }, }, 'name': { description: 'Nom du champ pour les formulaires', control: 'text', }, 'displayPersistentClear': { description: 'Affiche toujours le bouton de réinitialisation', control: 'boolean', default: false, }, 'displayPersistentCounter': { description: 'Affiche toujours le compteur', control: 'boolean', default: false, }, 'displayPersistentHint': { description: 'Affiche toujours le texte d\'aide', control: 'boolean', default: false, }, 'displayPersistentPlaceholder': { description: 'Garde le placeholder visible. Si le champ est vide, le placeholder reste affiché', control: 'boolean', default: false, }, 'placeholder': { description: 'Texte affiché quand le champ est vide', control: 'text', default: 'Placeholder', }, 'prefix': { description: 'Texte affiché avant la valeur: prefix="€" : affichera "€" avant la valeur saisie', control: 'text', }, 'isReversed': { description: 'Inverse l\'ordre des éléments', control: 'boolean', default: false, }, 'role': { description: 'Rôle ARIA du champ', control: 'text', }, 'rounded': { description: 'Arrondit les coins du champ', control: { type: 'text' }, }, 'isOnSingleLine': { description: 'Force l\'affichage sur une seule ligne', control: 'boolean', default: false, }, 'suffix': { description: 'Texte affiché après la valeur', control: 'text', }, 'theme': { description: 'Thème à appliquer au champ', control: 'text', }, 'isTiled': { description: 'Applique un style tuile', control: 'boolean', default: false, }, 'type': { description: 'Type du champ de saisie', control: 'select', options: ['text', 'number', 'password', 'email', 'tel', 'url', 'search'], default: 'text', }, 'width': { description: 'Largeur du champ', control: { type: 'text' }, }, 'validateOnSubmit': { description: 'Valide le champ avec la valeur donnée', type: '(value: string | number | null) => Promise', }, 'append': { description: 'Slot pour ajouter du contenu à droite du champ', control: false, table: { type: { summary: 'VNode' }, category: 'slots', }, }, 'prepend': { description: 'Slot pour ajouter du contenu à gauche du champ', control: false, table: { type: { summary: 'VNode' }, category: 'slots', }, }, 'append-inner': { description: 'Slot pour ajouter du contenu à droite dans le champ', control: false, table: { type: { summary: 'VNode' }, category: 'slots', }, }, 'prepend-inner': { description: 'Slot pour ajouter du contenu à gauche dans le champ', control: false, table: { type: { summary: 'VNode' }, category: 'slots', }, }, 'details': { description: 'Slot pour personnaliser la section des détails (messages d\'erreur, compteur)', control: false, table: { type: { summary: 'VNode' }, category: 'slots', }, }, 'showDivider': { description: 'Affiche une ligne de séparation entre le champ et les icônes prepend-inner et append-inner', control: 'boolean', table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props', }, }, }, args: { 'onUpdate:modelValue': fn(), 'onKeydown': fn(), 'onClear': fn(), 'onPrependIconClick': fn(), 'onAppendIconClick': fn(), 'onFocus': fn(), 'onBlur': fn(), }, } as Meta export default meta type Story = StoryObj export const Default: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { showDivider: false, variantStyle: 'outlined', color: 'primary', isClearable: true, label: 'Label', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const HelpText: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { showDivider: false, variantStyle: 'outlined', color: 'primary', isClearable: true, label: 'Label', modelValue: '', helpText: 'Texte d\'aide à la saisie', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const Required: Story = { args: { ...Default.args, required: true, }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `

Ce champ est obligatoire

`, } }, parameters: { docs: { description: { story: ` ### Champ requis sans astérisque Cette story montre un champ requis sans astérisque. Pour afficher l'astérisque sur un champ requis, il faut activer la prop \`displayAsterisk\`.`, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, } export const RequiredWithAsterisk: Story = { args: { ...Default.args, required: true, displayAsterisk: true, }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, parameters: { docs: { description: { story: ` ### Champ requis avec astérisque Cette story montre un champ requis avec astérisque. L'astérisque ne peut être affiché que sur un champ requis, en activant la prop \`displayAsterisk\`.`, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, } export const SlotPrepend: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { variantStyle: 'outlined', isClearable: true, showDivider: false, label: 'Label', color: 'primary', prependIcon: 'info', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const SlotAppend: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { variantStyle: 'outlined', isClearable: true, showDivider: false, label: 'champs de text', color: 'primary', appendIcon: 'success', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const SlotPrependInner: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { variantStyle: 'outlined', isClearable: true, showDivider: false, label: 'Label', color: 'primary', prependInnerIcon: 'info', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const SlotPrependInnerDivider: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { variantStyle: 'outlined', isClearable: true, showDivider: true, label: 'Label', color: 'primary', prependInnerIcon: 'info', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const SlotAppendInner: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { variantStyle: 'outlined', isClearable: true, showDivider: false, label: 'Label', color: 'primary', appendInnerIcon: 'success', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) return { args, value } }, template: `
`, } }, } export const SlotCustomIcon: Story = { parameters: { sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { variantStyle: 'outlined', isClearable: true, showDivider: false, label: 'Label', color: 'primary', modelValue: '', }, render: (args) => { return { components: { SyTextField, VIcon }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const iconName = ref(mdiAccountBox) return { args, value, iconName } }, template: `
`, } }, } export const ValidationRules: Story = { parameters: { docs: { description: { story: ` ### Validation avec règles standard Cette story montre l'utilisation des règles de validation standard. Le champ : - Est requis - Doit contenir au moins 3 caractères - Affiche un message de succès quand valide `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref('') return { args, value } }, template: ` `, }), } export const ValidationWithWarnings: Story = { parameters: { docs: { description: { story: ` ### Validation avec avertissements Cette story montre l'utilisation combinée des règles standard et d'avertissement. Le champ : - Est requis (règle standard) - Affiche un avertissement si le texte dépasse 10 caractères - Les avertissements sont affichés en orange et n'empêchent pas la validation `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref('') return { args, value } }, template: ` `, }), } export const EmailValidation: Story = { parameters: { docs: { description: { story: ` ### Validation d'email Cette story montre un cas d'usage courant : la validation d'une adresse email. Le champ : - Est requis - Vérifie le format de l'email - Affiche un message de succès quand l'email est valide `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref('') return { args, value } }, template: ` `, }), } export const PatternValidation: Story = { parameters: { docs: { description: { story: ` ### Validation par expression régulière Cette story montre l'utilisation de la règle \`matchPattern\` pour valider un format spécifique. Ici, un code postal français : - Doit contenir exactement 5 chiffres - Utilise une expression régulière pour la validation - Affiche des messages personnalisés `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: args => ({ components: { SyTextField }, setup() { const value = ref('') return { args, value } }, template: ` `, }), } // Persistent value for WithTooltips const withTooltipsValueMain = ref('') export const WithTooltips: Story = { args: { label: 'Champ avec tooltips', prependTooltip: 'Information à gauche du champ', appendTooltip: 'Information à droite du champ', tooltipLocation: 'top', isClearable: true, disableClickButton: true, }, render: args => ({ components: { SyTextField }, setup() { return { args, value: withTooltipsValueMain } }, template: `

Des icônes d'information avec tooltips sont affichées de chaque côté du champ. Survolez-les pour voir les messages d'aide qui apparaissent en haut grâce à la prop tooltipLocation="top".

`, }), parameters: { docs: { description: { story: 'Exemple de champ avec des tooltips d\'information. Les icônes d\'information apparaissent automatiquement lorsque les props prependTooltip et/ou appendTooltip sont renseignées. La position des tooltips peut être contrôlée avec la prop tooltipLocation.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, ], }, } /** * Story avec validation désactivée au blur */ export const ValidateOnBlur: Story = { args: { modelValue: '', label: 'Champ texte', required: true, isValidateOnBlur: true, customRules: [ { type: 'custom', options: { message: 'Le champ doit contenir au moins 3 caractères', validate: (value: string) => value.length >= 3, }, }, ], }, render: args => ({ components: { SyTextField, VBtn }, setup() { const value = ref(args.modelValue) const fieldRef = ref() watch(() => args.modelValue, (newValue) => { if (value.value !== newValue) { value.value = newValue } }) async function handleSubmit() { const isValid = await fieldRef.value?.validateOnSubmit() alert(isValid ? 'Formulaire valide !' : 'Formulaire invalide, veuillez corriger les erreurs.') } return { args, value, fieldRef, handleSubmit } }, template: `

La validation ne se déclenche qu'à la perte de focus ou à la soumission du formulaire.

Valider
`, }), parameters: { docs: { description: { story: 'Exemple de champ avec validation désactivée au blur. La validation ne se déclenche que lors de la soumission du formulaire.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, } export const FormValidation: Story = { render: args => ({ components: { SyTextField }, setup() { const nomField = ref() const prenomField = ref() const ageField = ref() const nomValue = ref('') const prenomValue = ref('') const ageValue = ref('') // Règle minLength pour le prénom const prenomRules = [{ type: 'minLength', options: { length: 3, message: 'Le prénom doit contenir au moins 3 caractères', successMessage: 'Le prénom est valide', fieldIdentifier: 'prénom', }, }] // Règle pattern pour l'âge (uniquement des chiffres) const ageRules = [{ type: 'matchPattern', options: { pattern: /^\d+$/, message: 'L\'âge doit contenir uniquement des chiffres', successMessage: 'L\'âge est valide', fieldIdentifier: 'âge', }, }] const handleSubmit = async () => { const fields = [ { ref: nomField, name: 'Nom' }, { ref: prenomField, name: 'Prénom' }, { ref: ageField, name: 'Âge' }, ] const invalidFields: string[] = [] for (const { ref, name } of fields) { const isValid = await ref.value?.validateOnSubmit() if (!isValid) { invalidFields.push(name) } } if (invalidFields.length > 0) { alert(`Les champs suivants sont invalides: ${invalidFields.join(', ')}`) } else { alert('Formulaire soumis avec succès !') } } return { args, nomField, prenomField, ageField, nomValue, prenomValue, ageValue, prenomRules, ageRules, handleSubmit, } }, template: `

Validation de formulaire

Règles de validation :
  • Nom : Champ requis
  • Prénom : Minimum 3 caractères
  • Âge : Uniquement des chiffres
Soumettre
`, }), parameters: { docs: { description: { story: 'Exemple de champ avec validation désactivée au blur. La validation ne se déclenche que lors de la soumission du formulaire.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, } export const WithPrefixAndSuffix: Story = { args: { modelValue: '42', label: 'Montant', prefix: '€', suffix: 'TTC', }, render: args => ({ components: { SyTextField }, setup() { const value = ref(args.modelValue) return { args, value } }, template: `

Utilisation des props prefix et suffix pour ajouter des unités ou des informations complémentaires directement dans le champ.

`, }), parameters: { docs: { description: { story: 'Exemple d\'utilisation des props prefix et suffix pour ajouter des informations complémentaires directement dans le champ de saisie.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, ], }, } export const DisabledErrorHandling: Story = { args: { label: 'Champ requis', required: true, customRules: [ { type: 'required', options: { message: 'Ce champ est obligatoire.', }, }, ], }, render: (args) => { return { components: { SyTextField }, setup() { const value1 = ref('') const value2 = ref('') return { args, value1, value2 } }, template: `

Validation normale

Sans gestion d'erreurs

Instructions :

  1. Cliquez dans un champ puis en dehors pour déclencher la validation
  2. Observez que le champ de gauche affiche un message d'erreur
  3. Le champ de droite n'affiche aucune erreur malgré les mêmes règles
`, } }, parameters: { docs: { description: { story: 'La prop `disableErrorHandling` permet de désactiver complètement la gestion des erreurs de validation, même si des règles sont définies.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, ], }, } export const WithoutSuccessMessages: Story = { args: { label: 'Email', customRules: [ { type: 'matchPattern', options: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Veuillez entrer une adresse email valide', successMessage: 'Le format de l\'email est correct', fieldIdentifier: 'Email', }, }, ], }, render: (args) => { return { components: { SyTextField }, setup() { const value1 = ref('user@example.com') const value2 = ref('user@example.com') return { args, value1, value2 } }, template: `

Avec messages de succès (défaut)

Sans messages de succès

Les deux champs ont la même valeur et passent la validation :

  • Le champ de gauche affiche le message de succès
  • Le champ de droite n'affiche aucun message

Essayez de modifier les valeurs pour voir le comportement.

`, } }, parameters: { docs: { description: { story: 'La prop `showSuccessMessages` (par défaut: `true`) permet de contrôler l\'affichage des messages de succès lors de la validation.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, ], }, }