import type { Meta, StoryObj } from '@storybook/vue3' import { ref } from 'vue' import { VBtn } from 'vuetify/components' import PasswordField from '../PasswordField.vue' import type { PasswordFieldProps } from '../types' import { fn, userEvent, within } from '@storybook/test' import SyForm from '@/components/Customs/SyForm/SyForm.vue' import { VForm } from 'vuetify/components/VForm' import { getValidationDocumentation } from '@/composables/unifyValidation/documentationValidationProps' type PasswordFieldStoryArgs = PasswordFieldProps & { 'onUpdate:modelValue': (...args: unknown[]) => unknown 'onSubmit': (...args: unknown[]) => unknown } const meta = { title: 'Composants/Formulaires/PasswordField/Validation', component: PasswordField, decorators: [ () => ({ template: '
', }), ], parameters: { layout: 'fullscreen', controls: { exclude: 'on*' }, docs: { description: { component: `PasswordField est un champ de saisie sécurisé pour les mots de passe`, }, }, }, argTypes: { ...getValidationDocumentation('string'), 'modelValue': { control: 'text', description: 'Valeur du champ de mot de passe', }, 'variantStyle': { control: 'select', options: ['outlined', 'underlined'], description: 'Style du champ (contour ou souligné)', }, 'color': { control: 'select', options: ['primary', 'secondary', 'error', 'warning', 'success', 'info'], description: 'Couleur principale du champ', }, 'label': { control: 'text', description: 'Libellé du champ', }, 'displayAsterisk': { control: 'boolean', description: 'Affiche un astérisque à côté du libellé pour indiquer que le champ est obligatoire', }, 'bgColor': { control: 'color', description: 'Couleur de fond du champ', }, 'clearable': { control: 'boolean', description: 'Affiche un bouton pour vider le champ', }, 'autocompleteType': { control: 'select', options: ['current-password', 'new-password'], description: 'Type d\'auto-complétion', default: 'current-password', }, 'helpText': { control: 'text', description: 'Texte d\'aide affiché sous le champ', }, 'hideDetails': { control: 'boolean', description: 'Masque la zone de détails (messages d\'erreur, d\'aide…) sous le champ', }, 'update:modelValue': { action: 'update:modelValue', description: 'Événement émis lors de la mise à jour de la valeur du champ', }, }, args: { 'modelValue': '', 'variantStyle': 'outlined', 'color': 'primary', 'label': 'Mot de passe', 'required': false, 'errorMessages': null, 'warningMessages': null, 'successMessages': null, 'readonly': false, 'disabled': false, 'placeholder': 'Entrez votre mot de passe', 'customRules': [], 'customWarningRules': [], 'customSuccessRules': [], 'showSuccessMessages': false, 'displayAsterisk': false, 'isValidateOnBlur': true, 'bgColor': 'white', 'onUpdate:modelValue': fn(), 'onSubmit': 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: 'Mdp123', customRules: [ { type: 'custom', options: { validate: (value: string) => { if (!value || value.length < 8) { return 'Le mot de passe doit contenir au moins 8 caractères' } return true }, fieldIdentifier: 'password', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByLabelText('Mot de passe') await userEvent.clear(input) await userEvent.type(input, 'Mdp123') input.blur() }, } export const WithWarning: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: 'MotDePasse123', customWarningRules: [ { type: 'custom', options: { validate: (value: string) => { const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value) if (!hasSpecialChar) { return 'Le mot de passe pourrait être plus fort avec des caractères spéciaux' } return true }, fieldIdentifier: 'password', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByLabelText('Mot de passe') await userEvent.clear(input) await userEvent.type(input, 'MotDePasse123') input.blur() }, } export const WithSuccess: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: 'MotDePasse123!@#', showSuccessMessages: true, customSuccessRules: [ { type: 'custom', options: { validate: (value: string) => { const hasUpperCase = /[A-Z]/.test(value) const hasLowerCase = /[a-z]/.test(value) const hasNumber = /[0-9]/.test(value) const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value) const isLongEnough = value.length >= 8 if (hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar && isLongEnough) { return true } return false }, successMessage: 'Mot de passe fort', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByLabelText('Mot de passe') await userEvent.clear(input) await userEvent.type(input, 'MotDePasse123!@#') input.blur() }, } export const WithCustomRules: Story = { parameters: { a11y: { disable: true, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: args => ({ components: { PasswordField }, setup() { const password = ref(args.modelValue ?? '') // Règles personnalisées pour la validation du mot de passe const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || value.length < 8) { return 'Le mot de passe doit contenir au moins 8 caractères' } return true }, fieldIdentifier: 'password', }, }, { type: 'custom', options: { validate: (value: string) => { if (!value || !/[A-Z]/.test(value)) { return 'Le mot de passe doit contenir au moins une lettre majuscule' } return true }, fieldIdentifier: 'password', successMessage: 'Le mot de passe est sécurisé', }, }, { type: 'custom', options: { validate: (value: string) => { if (!value || !/[0-9]/.test(value)) { return 'Le mot de passe doit contenir au moins un chiffre' } return true }, fieldIdentifier: 'password', }, }, ] return { args, password, customRules } }, template: ` `, }), args: { required: true, }, } export const WithSuccessMessages: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: ` ### Messages de succès Cette story illustre l'utilisation de la propriété \`showSuccessMessages\` qui permet de contrôler l'affichage des messages de succès lors de la validation. Par défaut, cette propriété est à \`false\`. Cela peut être utile pour réduire la verbosité de l'interface lorsque les messages de succès ne sont pas nécessaires dans certains contextes. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: () => ({ components: { PasswordField }, setup() { const value1 = ref('P@ssw0rd123') const value2 = ref('P@ssw0rd123') return { value1, value2 } }, template: `

Cette démonstration compare un PasswordField avec showSuccessMessages=true (par défaut) et un avec showSuccessMessages=false.

Avec messages de succès

Sans messages de succès

Observations :

  • Les deux champs ont la même valeur valide
  • Le champ de gauche affiche un message de succès et un indicateur visuel vert
  • Le champ de droite n'affiche pas de message de succès, mais conserve l'indicateur visuel
  • Essayez de modifier les valeurs puis de les rendre à nouveau valides
`, }), } 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. Cela peut être utile dans des cas particuliers où vous souhaitez définir des règles de validation mais gérer leur affichage différemment, ou utiliser la validation uniquement au niveau du formulaire parent. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, ], }, render: () => ({ components: { PasswordField }, setup() { const value1 = ref('') const value2 = ref('') const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || value.length < 8) { return 'Le mot de passe doit contenir au moins 8 caractères' } return true }, fieldIdentifier: 'password', }, }, ] return { value1, value2, customRules } }, template: `

Cette démonstration compare un PasswordField 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
  3. Vous pouvez également essayer de soumettre les deux champs pour voir la différence de comportement
`, }), } /** * 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: { PasswordField }, setup() { const password = ref('') return { args, password } }, 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\` `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { PasswordField }, setup() { const password = ref('') const errorMessages = ref(null) const warningMessages = ref(null) const successMessages = ref(null) function setError() { errorMessages.value = ['Ce mot de passe a déjà été utilisé'] warningMessages.value = null successMessages.value = null } function setWarning() { errorMessages.value = null warningMessages.value = ['Ce mot de passe figure dans une liste de mots de passe courants'] successMessages.value = null } function setSuccess() { errorMessages.value = null warningMessages.value = null successMessages.value = ['Mot de passe accepté par le serveur'] } function reset() { errorMessages.value = null warningMessages.value = null successMessages.value = null } return { args, password, 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 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 (\`VTextField\`, etc.). Cela permet de réutiliser des règles existantes écrites pour Vuetify sans adaptation. `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { VForm, PasswordField, VBtn }, setup() { const password = ref('') const rules = [ (value: string) => !!value || 'Le mot de passe est requis', (value: string) => value.length >= 8 || 'Au moins 8 caractères requis', (value: string) => /[A-Z]/.test(value) || 'Au moins une lettre majuscule requise', (value: string) => /[0-9]/.test(value) || 'Au moins un chiffre requis', (value: string) => /[!@#$%^&*(),.?":{}|<>]/.test(value) || 'Au moins un caractère spécial requis', ] async function handleSubmit(e) { const isValid = (await e).valid alert(isValid ? 'Mot de passe valide !' : 'Veuillez corriger les erreurs.') } return { args, password, 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
`, }), } export const SyFormValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { PasswordField, VBtn, SyForm }, setup() { const password = ref('') const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || value.length < 8) { return 'Le mot de passe doit contenir au moins 8 caractères' } return true }, fieldIdentifier: 'password', }, }, { type: 'custom', options: { validate: (value: string) => { if (!value || !/[A-Z]/.test(value)) { return 'Le mot de passe doit contenir au moins une lettre majuscule' } return true }, fieldIdentifier: 'password', }, }, ] function handleSubmit(e) { // La validation est gérée par SyForm, on peut juste vérifier le résultat const isValid = e.isValid alert(isValid ? 'Mot de passe valide !' : 'Veuillez corriger les erreurs.') } return { args, password, customRules, handleSubmit } }, template: `
Valider
`, }), args: { required: true, }, } export const SyFormVuetifyValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { PasswordField, VBtn, SyForm }, setup() { const password = ref('') const vuetifyRules = [ (value: string) => !!value || 'Le mot de passe est requis', (value: string) => value.length >= 8 || 'Au moins 8 caractères requis', (value: string) => /[A-Z]/.test(value) || 'Au moins une lettre majuscule requise', (value: string) => /[0-9]/.test(value) || 'Au moins un chiffre requis', (value: string) => /[!@#$%^&*(),.?":{}|<>]/.test(value) || 'Au moins un caractère spécial requis', ] function handleSubmit(e) { const isValid = e.isValid alert(isValid ? 'Mot de passe valide !' : 'Veuillez corriger les erreurs.') } return { args, password, vuetifyRules, handleSubmit } }, template: `
Valider
`, }), } export const VFormValidation: Story = { parameters: { sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { PasswordField, VBtn }, setup() { const password = ref('') const fieldRef = ref() const customRules = [ { type: 'custom', options: { validate: (value: string) => { return value.length >= 8 }, message: 'Au moins 8 caractères requis', fieldIdentifier: 'password', }, }, { type: 'custom', options: { validate: (value: string) => { return /[A-Z]/.test(value) }, message: 'Au moins une lettre majuscule requise', fieldIdentifier: 'password', }, }, ] async function handleSubmit() { const isValid = await fieldRef.value?.validateOnSubmit() alert(isValid ? 'Mot de passe valide !' : 'Veuillez corriger les erreurs.') } return { args, password, fieldRef, customRules, handleSubmit } }, template: `

Il est préférable d'utiliser SyForm pour bénéficier de toutes les fonctionnalités de validation, mais voici un exemple d'utilisation avec VForm et la méthode validateOnSubmit() du champ.

Valider
`, }), args: { modelValue: '', }, }