import type { StoryObj, Meta } from '@storybook/vue3' import PhoneField from './PhoneField.vue' import { ref, watch } from 'vue' import { indicatifs } from './indicatifs' const meta = { title: 'Composants/Formulaires/PhoneField', component: PhoneField, parameters: { layout: 'fullscreen', actions: { handles: ['update:modelValue', 'update:selectedDialCode', 'update:dialCodeModel', 'change'], }, controls: { exclude: ['computedValue', 'phoneMask', 'counter', 'hasError', 'phoneNumber', 'mergedDialCodes'] }, }, argTypes: { 'modelValue': { control: false }, 'dialCodeModel': { control: false }, 'onUpdate:modelValue': { action: 'update:modelValue' }, 'onUpdate:selectedDialCode': { action: 'update:selectedDialCode' }, 'onUpdate:dialCodeModel': { action: 'update:dialCodeModel' }, 'onChange': { action: 'change' }, 'required': { control: 'boolean' }, 'outlined': { control: 'boolean' }, 'outlinedIndicatif': { control: 'boolean' }, 'withCountryCode': { control: 'boolean' }, 'countryCodeRequired': { control: 'boolean' }, 'displayFormat': { control: { type: 'select' }, description: 'Format d\'affichage des items', options: ['code', 'code-abbreviation', 'code-country', 'country', 'abbreviation'], }, 'customIndicatifs': { control: 'object', description: 'Permet d\'ajouter des indicatifs à la liste pre-existante', }, 'useCustomIndicatifsOnly': { control: 'boolean', description: 'Permet d\'utiliser uniquement les indicatifs que vous renseignez dans la props customIndicatifs', }, 'helpText': { control: 'text', description: 'Texte d\'aide affiché sous le champ. Lorsque présent, les messages d\'erreur incluent un exemple concret distinct du texte d\'aide.', }, 'autocompleteCountryCode': { control: 'text', description: 'Valeur de l\'attribut `autocomplete` pour le champ indicatif pays. Utiliser `tel-country-code` (défaut) lorsque le numéro est séparé en deux champs (indicatif + numéro national), conformément à [WHATWG](https://html.spec.whatwg.org/#autofill-field-tel-country-code) et [WCAG 1.3.5](https://www.w3.org/WAI/WCAG21/quickref/#identify-input-purpose).', }, 'autocompletePhone': { control: 'text', description: 'Valeur de l\'attribut `autocomplete` pour le champ numéro de téléphone. Valeurs recommandées selon le scénario :\n\n- `tel-national` (défaut) — numéro sans indicatif, lorsque le composant est en mode deux champs (`withCountryCode`).\n- `tel` — numéro complet avec indicatif intégré, pour un champ unique sans sélecteur de pays.\n- `tel-extension` — poste ou extension téléphonique.', }, 'isValidatedOnBlur': { control: 'boolean' }, 'displayAsterisk': { control: 'boolean' }, 'disableErrorHandling': { control: 'boolean' }, 'disabled': { control: 'boolean' }, 'readonly': { control: 'boolean' }, } as Record, } satisfies Meta export default meta type Story = StoryObj export const Default: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: true, displayFormat: 'code', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, bgColor: 'white', readonly: false, disabled: false, }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `





`, } }, } export const Required: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: true, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: true, displayFormat: 'code', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, bgColor: 'white', readonly: false, disabled: false, }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } /** * Story avec champ requis et astérisque. */ export const RequiredWithAsterisk: Story = { args: { ...Default.args, required: true, displayAsterisk: true, bgColor: 'white', }, parameters: { a11y: { disable: false, }, docs: { description: { story: 'Version du champ téléphone requis avec un astérisque visuel.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const HelpText: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, args: { required: true, helpText: 'Saisissez votre numéro de téléphone français au format 01 23 45 67 89', }, render(args) { return { components: { PhoneField }, setup() { const phoneValue1 = ref('') const phoneValue2 = ref('') const selectedDialCode = ref('') return { args, phoneValue1, phoneValue2, selectedDialCode, } }, template: `

Avec aide à la saisie

Essayez de laisser le champ vide ou de saisir un numéro incorrect pour voir l'exemple dans le message d'erreur (différent de l'aide).

Avec aide à la saisie et indicatif pays

L'exemple dans le message d'erreur s'adapte au format du pays sélectionné.

Valeurs actuelles :

phoneValue1: {{ phoneValue1 }}
phoneValue2: {{ phoneValue2 }}
selectedDialCode: {{ selectedDialCode }}
`, } }, } export const Autocomplete: Story = { parameters: { a11y: { disable: false, }, docs: { description: { story: ` Les attributs \`autocomplete\` permettent aux navigateurs et aux outils d'assistance de remplir automatiquement les champs avec les bonnes informations utilisateur, conformément à [WCAG 1.3.5 — Identifier la finalité de la saisie](https://www.w3.org/WAI/WCAG21/quickref/#identify-input-purpose). | Scénario | Prop à utiliser | Valeur recommandée | Source | |---|---|---|---| | **Code pays** (ex : +33) — champ séparé | \`autocompleteCountryCode\` | \`tel-country-code\` | [WHATWG](https://html.spec.whatwg.org/#autofill-field-tel-country-code) | | **Numéro sans indicatif** (ex : 06 12 34 56 78) — avec \`withCountryCode\` | \`autocompletePhone\` | \`tel-national\` | [WHATWG](https://html.spec.whatwg.org/#autofill-field-tel-national) | | **Numéro complet** (indicatif intégré) — sans \`withCountryCode\` | \`autocompletePhone\` | \`tel\` | [WHATWG](https://html.spec.whatwg.org/#autofill-field-tel) | | **Extension / poste** | \`autocompletePhone\` | \`tel-extension\` | [WHATWG](https://html.spec.whatwg.org/#autofill-field-tel-extension) | `, }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, args: { required: false, withCountryCode: true, autocompleteCountryCode: 'tel-country-code', autocompletePhone: 'tel-national', helpText: 'Utilisez les valeurs autocomplete appropriées pour l\'accessibilité', }, render(args) { return { components: { PhoneField }, setup() { const phoneValue1 = ref('') const selectedDialCode1 = ref('') const phoneValue2 = ref('') const phoneValue3 = ref('') return { args, phoneValue1, selectedDialCode1, phoneValue2, phoneValue3, } }, template: `

Les attributs autocomplete permettent aux navigateurs de remplir automatiquement les champs avec les bonnes informations utilisateur.

Avec indicatif pays (autocomplete="tel-country-code")

Numéro complet (autocomplete="tel")

Extension téléphonique (autocomplete="tel-extension")

Valeurs actuelles :

phoneValue1: {{ phoneValue1 }}
selectedDialCode1: {{ selectedDialCode1 }}
phoneValue2: {{ phoneValue2 }}
phoneValue3: {{ phoneValue3 }}
`, } }, } export const CustomIndicatifs: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'code', customIndicatifs: [ { code: '+33', country: 'France', abbreviation: 'FR', phoneLength: 10, mask: '## ## ## ## ##' }, { code: '+34', country: 'Spain', abbreviation: 'ES', phoneLength: 9, mask: '### ### ###' }, { code: '+99', country: 'Utopia', abbreviation: 'UT', mask: '## ## ## ##', phoneLength: 8 }, { code: '+98', country: 'Paradise', abbreviation: 'PA', mask: '## ## ## ##', phoneLength: 18 }, ], useCustomIndicatifsOnly: true, isValidatedOnBlur: true, bgColor: 'white', readonly: false, disabled: false, }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const NotValidatedOnBlur: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'code', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: false, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const DisplayFormatCode: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'code', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const DisplayFormatCodeAbbreviation: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'code-abbreviation', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const DisplayFormatCodeCountry: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'code-country', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const DisplayFormatCountry: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'country', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const DisplayFormatAbbreviation: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'abbreviation', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `
`, } }, } export const DefaultDialCode: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', dialCodeModel: { code: '+3433', country: 'Exemple', abbreviation: 'EX', phoneLength: 10, mask: '## ## ## ## ##' }, required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: true, displayFormat: 'code-country', customIndicatifs: [ { code: '+3433', country: 'Exemple', abbreviation: 'EX', phoneLength: 10, mask: '## ## ## ## ##' }, { code: '+34', country: 'Espagne', abbreviation: 'ES', phoneLength: 9, mask: '### ### ###' }, { code: '+41', country: 'Suisse', abbreviation: 'CH', phoneLength: 9, mask: '### ### ###' }, ], useCustomIndicatifsOnly: true, isValidatedOnBlur: true, bgColor: 'white', readonly: false, disabled: false, }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `

PhoneField avec indicatif pré-rempli

Cette story montre comment pré-remplir l'indicatif téléphonique avec des indicatifs personnalisés.

`, } }, } export const DefaultDialCodeStandard: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', dialCodeModel: indicatifs.find(ind => ind.country === 'France'), required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: true, displayFormat: 'code-country', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, bgColor: 'white', readonly: false, disabled: false, }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `

PhoneField avec indicatif standard pré-rempli

Cette story montre comment pré-remplir l'indicatif téléphonique avec les indicatifs standards.

Indicatif sélectionné : {{ args.dialCodeModel ? args.dialCodeModel.code + ' ' + args.dialCodeModel.country : 'Aucun' }}
`, } }, } export const DisplayModels: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', dialCodeModel: '', required: false, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: false, displayFormat: 'code-country', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, readonly: false, bgColor: 'white', }, render: args => ({ components: { PhoneField }, setup() { const modelValue = ref(args.modelValue) const selectedDialCode = ref(args.dialCodeModel) // Sync ref -> args (pour afficher les modèles dans la story) watch(modelValue, (val) => { args.modelValue = val }) watch(selectedDialCode, (val) => { args.dialCodeModel = val }) // Sync args -> ref (quand on change les controls Storybook) watch(() => args.modelValue, (val) => { modelValue.value = val }) watch(() => args.dialCodeModel, (val) => { selectedDialCode.value = val }) return { args, modelValue, selectedDialCode, } }, template: `
Indicatif: {{ selectedDialCode }}
Numéro: {{ modelValue }}
`, }), } /** * Story qui montre le comportement du composant lorsque la gestion des erreurs est désactivée. * Aucun message d'erreur ne sera affiché, même si le champ est requis et vide. */ export const DisabledErrorHandling: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: true, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: true, displayFormat: 'code', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, disableErrorHandling: true, readonly: false, disabled: false, bgColor: 'white', }, render: (args) => { return { components: { PhoneField }, setup() { return { args } }, template: `

Gestion des erreurs désactivée

Ce champ est requis mais n'affichera pas d'erreur même s'il est vide.

Comparaison avec gestion des erreurs activée

Ce champ est requis et affichera une erreur s'il est vide.

`, } }, } export const FormValidation: Story = { parameters: { a11y: { disable: false, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` `, }, ], }, args: { modelValue: '', required: true, outlined: true, outlinedIndicatif: true, withCountryCode: true, countryCodeRequired: true, displayFormat: 'code', customIndicatifs: [], useCustomIndicatifsOnly: false, isValidatedOnBlur: true, bgColor: 'white', readonly: false, disabled: false, }, render: (args) => { return { components: { PhoneField }, setup() { const phoneFieldRef = ref(null) const phoneNumber = ref('') const formSubmitted = ref(false) const formIsValid = ref(false) const submitForm = async () => { formSubmitted.value = true // Validation du champ téléphone let isValid = false if (phoneFieldRef.value) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Nécessaire pour accéder à validateOnSubmit isValid = await (phoneFieldRef.value as any).validateOnSubmit() } formIsValid.value = isValid console.log(isValid ? 'Formulaire valide !' : 'Formulaire invalide !') } return { phoneFieldRef, phoneNumber, formSubmitted, formIsValid, submitForm, args } }, template: `
Soumettre le formulaire

Formulaire valide !

Formulaire invalide !

Comment utiliser la validation à la soumission

1. Ajoutez une référence au composant PhoneField avec ref="phoneFieldRef"

2. Désactivez la validation au blur si nécessaire avec :isValidatedOnBlur="false"

3. Dans votre méthode de soumission, appelez phoneFieldRef.value.validateOnSubmit()

4. Cette méthode retourne une Promise qui résout à true si le champ est valide, false sinon

`, } }, }