From 6081f0c90ce3d64abbc2a474c3f0774f4053bc4b Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 11:54:13 +0200 Subject: [PATCH] feat(inputs) : sanitisation email (suppression des espaces + option lowercase) Co-Authored-By: Claude Opus 4.8 (1M context) --- app/components/malio/input/InputEmail.test.ts | 23 +++++++++++++ app/components/malio/input/InputEmail.vue | 32 +++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/app/components/malio/input/InputEmail.test.ts b/app/components/malio/input/InputEmail.test.ts index 5a61d5b..4809772 100644 --- a/app/components/malio/input/InputEmail.test.ts +++ b/app/components/malio/input/InputEmail.test.ts @@ -23,6 +23,7 @@ type InputEmailProps = { iconPosition?: 'left' | 'right' iconSize?: string | number iconColor?: string + lowercase?: boolean } const InputEmailForTest = InputEmail as DefineComponent @@ -235,4 +236,26 @@ describe('MalioInputEmail', () => { expect(wrapper.get('input').attributes('autocomplete')).toBe('email') }) + + it('supprime tous les espaces saisis', async () => { + const wrapper = mountComponent() + await wrapper.get('input').setValue(' a b @ c.com ') + const emits = wrapper.emitted('update:modelValue')! + expect(emits[emits.length - 1]).toEqual(['ab@c.com']) + expect(wrapper.get('input').element.value).toBe('ab@c.com') + }) + + it('conserve la casse par défaut', async () => { + const wrapper = mountComponent() + await wrapper.get('input').setValue('User@Example.COM') + const emits = wrapper.emitted('update:modelValue')! + expect(emits[emits.length - 1]).toEqual(['User@Example.COM']) + }) + + it('met en minuscules quand lowercase est vrai', async () => { + const wrapper = mountComponent({lowercase: true}) + await wrapper.get('input').setValue('User@Example.COM') + const emits = wrapper.emitted('update:modelValue')! + expect(emits[emits.length - 1]).toEqual(['user@example.com']) + }) }) diff --git a/app/components/malio/input/InputEmail.vue b/app/components/malio/input/InputEmail.vue index c66c0de..519c802 100644 --- a/app/components/malio/input/InputEmail.vue +++ b/app/components/malio/input/InputEmail.vue @@ -86,6 +86,7 @@ const props = withDefaults( iconPosition?: 'left' | 'right' iconSize?: string | number iconColor?: string + lowercase?: boolean }>(), { id: '', @@ -106,6 +107,7 @@ const props = withDefaults( success: '', iconSize: 24, iconColor: 'text-m-muted', + lowercase: false, }, ) @@ -170,12 +172,36 @@ const emit = defineEmits<{ (event: 'update:modelValue', value: string): void }>() +const sanitizeEmail = (v: string) => { + let out = v.replace(/\s+/g, '') + if (props.lowercase) out = out.toLowerCase() + return out +} + const onInput = (event: Event) => { const target = event.target as HTMLInputElement - if (!isControlled.value) { - localValue.value = target.value + const raw = target.value + const sanitized = sanitizeEmail(raw) + + if (sanitized !== raw) { + // `` ne supporte pas l'API de sélection : + // selectionStart vaut null, setSelectionRange lève. On garde défensivement. + const caret = target.selectionStart + target.value = sanitized + if (caret !== null) { + const newCaret = sanitizeEmail(raw.slice(0, caret)).length + try { + target.setSelectionRange(newCaret, newCaret) + } catch { + /* type d'input sans support de sélection — ignore */ + } + } } - emit('update:modelValue', target.value) + + if (!isControlled.value) { + localValue.value = sanitized + } + emit('update:modelValue', sanitized) } const iconInputPaddingClass = computed(() => {