From 1ffe63827df827ab2f6bf595eca6f3fcb978509e Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 11 May 2026 08:54:31 +0000 Subject: [PATCH] =?UTF-8?q?[#MUI-30]=20Cr=C3=A9ation=20d'un=20composant=20?= =?UTF-8?q?email=20(#44)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: https://gitea.malio.fr/MALIO-DEV/malio-layer-ui/pulls/44 Co-authored-by: tristan Co-committed-by: tristan --- .playground/pages/composant/form/client.vue | 7 +- .../pages/composant/input/inputEmail.vue | 106 +++++++ CHANGELOG.md | 1 + COMPONENTS.md | 36 +++ .../malio/checkbox/Checkbox.test.ts | 12 + app/components/malio/checkbox/Checkbox.vue | 9 +- app/components/malio/input/Input.test.ts | 14 + .../malio/input/InputAmount.test.ts | 14 + app/components/malio/input/InputAmount.vue | 17 +- app/components/malio/input/InputEmail.test.ts | 228 +++++++++++++++ app/components/malio/input/InputEmail.vue | 229 +++++++++++++++ .../malio/input/InputPassword.test.ts | 14 + app/components/malio/input/InputPassword.vue | 14 +- app/components/malio/input/InputText.vue | 17 +- .../malio/input/InputUpload.test.ts | 14 + app/components/malio/input/InputUpload.vue | 14 +- .../malio/radio/RadioButton.test.ts | 19 ++ app/components/malio/radio/RadioButton.vue | 5 +- app/story/input/inputEmail.story.vue | 261 ++++++++++++++++++ 19 files changed, 1000 insertions(+), 31 deletions(-) create mode 100644 .playground/pages/composant/input/inputEmail.vue create mode 100644 app/components/malio/input/InputEmail.test.ts create mode 100644 app/components/malio/input/InputEmail.vue create mode 100644 app/story/input/inputEmail.story.vue diff --git a/.playground/pages/composant/form/client.vue b/.playground/pages/composant/form/client.vue index ad29579..6753669 100644 --- a/.playground/pages/composant/form/client.vue +++ b/.playground/pages/composant/form/client.vue @@ -23,7 +23,7 @@ - - import {ref} from "vue"; -import MalioSelect from "../../../../app/components/malio/select/Select.vue"; -import MalioCheckbox from "../../../../app/components/malio/checkbox/Checkbox.vue"; -import MalioButton from "../../../../app/components/malio/button/Button.vue"; const multiselectValue = ref>([]) diff --git a/.playground/pages/composant/input/inputEmail.vue b/.playground/pages/composant/input/inputEmail.vue new file mode 100644 index 0000000..38c6791 --- /dev/null +++ b/.playground/pages/composant/input/inputEmail.vue @@ -0,0 +1,106 @@ + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d28167..d40c554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Liste des évolutions de la librairie Malio layer UI * [#MUI-22] Création d'un composant datatable * [#MUI-27] Création d'un composant sélection de site * Création d'un composant rich text (TipTap) avec sortie markdown / HTML +* [#MUI-30] Création d'un composant email ### Changed diff --git a/COMPONENTS.md b/COMPONENTS.md index 8dae0ea..effeb6b 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -66,6 +66,42 @@ Champ mot de passe avec toggle visibilité. --- +## MalioInputEmail + +Champ email (`type="email"` + `inputmode="email"`) avec icône `mdi:email-outline` à droite par défaut. + +| Prop | Type | Défaut | Description | +|------|------|--------|-------------| +| `id` | `string` | auto | Identifiant HTML | +| `label` | `string` | `''` | Label du champ | +| `modelValue` | `string \| null` | `undefined` | Valeur (v-model) | +| `name` | `string` | `''` | Attribut name | +| `autocomplete` | `string` | `'off'` | Autocomplétion (passer `'email'` pour suggérer l'email utilisateur) | +| `disabled` | `boolean` | `false` | Désactive le champ | +| `readonly` | `boolean` | `false` | Lecture seule | +| `required` | `boolean` | `false` | Champ requis | +| `hint` | `string` | `''` | Message d'aide | +| `error` | `string` | `''` | Message d'erreur | +| `success` | `string` | `''` | Message de succès | +| `iconName` | `string` | `'mdi:email-outline'` | Icône Iconify (chaîne vide pour masquer) | +| `iconPosition` | `'left' \| 'right'` | `'right'` | Position de l'icône | +| `iconSize` | `string \| number` | `24` | Taille icône | +| `iconColor` | `string` | `'text-m-muted'` | Classe couleur icône | +| `inputClass` | `string` | `''` | Classes CSS input | +| `labelClass` | `string` | `''` | Classes CSS label | +| `groupClass` | `string` | `''` | Classes CSS conteneur | + +**Events :** `update:modelValue(value: string)` + +```vue + + + + +``` + +--- + ## MalioInputAmount Champ montant avec icône devise (euro par défaut). diff --git a/app/components/malio/checkbox/Checkbox.test.ts b/app/components/malio/checkbox/Checkbox.test.ts index 6fccf55..6c6ddb2 100644 --- a/app/components/malio/checkbox/Checkbox.test.ts +++ b/app/components/malio/checkbox/Checkbox.test.ts @@ -139,4 +139,16 @@ describe('MalioCheckbox', () => { expect(wrapper.get('p').text()).toBe('Valid') expect(wrapper.get('p').classes()).toContain('text-m-success') }) + + it('uses muted label color when unchecked', () => { + const wrapper = mountCheckbox({label: 'Accept terms', modelValue: false}) + + expect(wrapper.get('label').classes()).toContain('text-m-muted') + }) + + it('uses black label color when checked', () => { + const wrapper = mountCheckbox({label: 'Accept terms', modelValue: true}) + + expect(wrapper.get('label').classes()).toContain('text-black') + }) }) diff --git a/app/components/malio/checkbox/Checkbox.vue b/app/components/malio/checkbox/Checkbox.vue index 4b9a698..59c8ead 100644 --- a/app/components/malio/checkbox/Checkbox.vue +++ b/app/components/malio/checkbox/Checkbox.vue @@ -108,7 +108,8 @@ const mergedInputClass = computed(() => const mergedLabelClass = computed(() => twMerge( - 'cbx text-black text-lg', + 'cbx text-lg', + isChecked.value ? 'text-black' : 'text-m-muted', disabled.value ? 'cursor-not-allowed text-black/60' : '', hasError.value ? 'text-m-error' : '', hasSuccess.value ? 'text-m-success' : '', @@ -161,10 +162,14 @@ const onChange = (event: Event) => { height: 18px; flex: 0 0 18px; transform: scale(1); - border: 2px solid rgb(0, 0, 0); + border: 2px solid rgb(var(--m-muted) / 1); transition: all 0.1s ease; } +.inp-cbx:checked + .cbx span:first-child { + border-color: rgb(0, 0, 0); +} + .cbx span:first-child svg { position: absolute; top: 2px; diff --git a/app/components/malio/input/Input.test.ts b/app/components/malio/input/Input.test.ts index 8accb92..0bd0e49 100644 --- a/app/components/malio/input/Input.test.ts +++ b/app/components/malio/input/Input.test.ts @@ -294,4 +294,18 @@ describe('MalioInputText', () => { expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary') }) + + it('shows primary icon color on focus', async () => { + const wrapper = mountInput({iconName: 'mdi:key-outline'}) + + await wrapper.get('input').trigger('focus') + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary') + }) + + it('shows black icon color when filled and unfocused', () => { + const wrapper = mountInput({iconName: 'mdi:key-outline', modelValue: 'hello'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') + }) }) diff --git a/app/components/malio/input/InputAmount.test.ts b/app/components/malio/input/InputAmount.test.ts index 6a57f54..0eeec04 100644 --- a/app/components/malio/input/InputAmount.test.ts +++ b/app/components/malio/input/InputAmount.test.ts @@ -160,4 +160,18 @@ describe('MalioInputAmount', () => { expect(wrapper.get('input').classes()).toContain('!pl-11') expect(wrapper.get('label').classes()).toContain('left-8') }) + + it('shows primary icon color on focus', async () => { + const wrapper = mountInputAmount() + + await wrapper.get('input').trigger('focus') + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary') + }) + + it('shows black icon color when filled and unfocused', () => { + const wrapper = mountInputAmount({modelValue: '12,50'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') + }) }) diff --git a/app/components/malio/input/InputAmount.vue b/app/components/malio/input/InputAmount.vue index ef58742..58d4a55 100644 --- a/app/components/malio/input/InputAmount.vue +++ b/app/components/malio/input/InputAmount.vue @@ -39,13 +39,7 @@ :width="iconSize" :height="iconSize" data-test="icon" - :class="[ - hasError - ? 'text-m-danger' - : hasSuccess - ? 'text-m-success' : iconColor, - iconPositionClass, - ]" + :class="[iconStateClass, iconPositionClass]" /> @@ -235,6 +229,15 @@ const iconPositionClass = computed(() => { const sideClass = props.iconPosition === 'left' ? 'left-[10px]' : 'right-[10px]' return `pointer-events-none absolute ${sideClass} top-1/2 -translate-y-1/2` }) + +const iconStateClass = computed(() => { + if (hasError.value) return 'text-m-danger' + if (hasSuccess.value) return 'text-m-success' + if (disabled.value) return props.iconColor + if (isFocused.value) return 'text-m-primary' + if (isFilled.value) return 'text-black' + return props.iconColor +}) diff --git a/app/components/malio/input/InputPassword.test.ts b/app/components/malio/input/InputPassword.test.ts index 48eed58..a201bdb 100644 --- a/app/components/malio/input/InputPassword.test.ts +++ b/app/components/malio/input/InputPassword.test.ts @@ -171,4 +171,18 @@ describe('MalioInputPassword', () => { expect(wrapper.get('input').attributes('aria-invalid')).toBe('false') }) + + it('shows primary icon color on focus', async () => { + const wrapper = mountComponent() + + await wrapper.get('input').trigger('focus') + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary') + }) + + it('shows black icon color when filled and unfocused', () => { + const wrapper = mountComponent({modelValue: 'secret'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') + }) }) diff --git a/app/components/malio/input/InputPassword.vue b/app/components/malio/input/InputPassword.vue index a63cdd3..bd629ec 100644 --- a/app/components/malio/input/InputPassword.vue +++ b/app/components/malio/input/InputPassword.vue @@ -39,10 +39,7 @@ :height="24" data-test="icon" :class="[ - hasError - ? 'text-m-danger' - : hasSuccess - ? 'text-m-success' : 'text-m-muted', + iconStateClass, 'cursor-pointer absolute right-[10px] top-1/2 -translate-y-1/2', ]" @click="toggleVisibility" @@ -189,6 +186,15 @@ const onInput = (event: Event) => { } const disabled = computed(() => props.disabled) + +const iconStateClass = computed(() => { + if (hasError.value) return 'text-m-danger' + if (hasSuccess.value) return 'text-m-success' + if (disabled.value) return 'text-m-muted' + if (isFocused.value) return 'text-m-primary' + if (isFilled.value) return 'text-black' + return 'text-m-muted' +})