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.
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\`.
Instructions :
- Cliquez dans un champ puis en dehors pour déclencher la validation
- Le champ de gauche affichera une erreur requise, mais pas celui de droite
- 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: `
Simuler une erreur
Simuler un avertissement
Simuler un succès
Réinitialiser
`,
},
{
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: `
Valider
`,
},
{
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: `
Valider
`,
},
{
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: `
`,
}),
args: {
required: true,
},
}
export const SyFormVuetifyValidation: Story = {
parameters: {
sourceCode: [
{
name: 'Template',
code: `
Valider
`,
},
{
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: `
`,
}),
}
export const VFormValidation: Story = {
parameters: {
sourceCode: [
{
name: 'Template',
code: `
Valider
`,
},
{
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: '',
},
}