From a1040d2a5e926d4e272695744173ef5c6a056182 Mon Sep 17 00:00:00 2001 From: tristan Date: Tue, 12 May 2026 08:45:19 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20Ajout=20du=20composant=20t=C3=A9l?= =?UTF-8?q?=C3=A9phone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .playground/pages/composant/form/client.vue | 183 ++++------- .../pages/composant/input/inputPhone.vue | 141 ++++++++ CHANGELOG.md | 1 + COMPONENTS.md | 42 +++ .../malio/checkbox/Checkbox.test.ts | 14 +- app/components/malio/checkbox/Checkbox.vue | 26 +- app/components/malio/input/Input.test.ts | 2 +- .../malio/input/InputAmount.test.ts | 2 +- app/components/malio/input/InputAmount.vue | 4 +- app/components/malio/input/InputEmail.vue | 4 +- app/components/malio/input/InputPassword.vue | 2 +- app/components/malio/input/InputPhone.test.ts | 308 ++++++++++++++++++ app/components/malio/input/InputPhone.vue | 274 ++++++++++++++++ app/components/malio/input/InputText.vue | 4 +- app/components/malio/input/InputTextArea.vue | 2 +- app/components/malio/input/InputUpload.vue | 2 +- .../malio/radio/RadioButton.test.ts | 10 + app/components/malio/radio/RadioButton.vue | 12 +- app/components/malio/select/Select.vue | 16 +- .../malio/select/SelectCheckbox.vue | 16 +- app/components/malio/time/Time.vue | 6 +- app/story/input/inputPhone.story.vue | 285 ++++++++++++++++ 22 files changed, 1201 insertions(+), 155 deletions(-) create mode 100644 .playground/pages/composant/input/inputPhone.vue create mode 100644 app/components/malio/input/InputPhone.test.ts create mode 100644 app/components/malio/input/InputPhone.vue create mode 100644 app/story/input/inputPhone.story.vue diff --git a/.playground/pages/composant/form/client.vue b/.playground/pages/composant/form/client.vue index 6753669..165818d 100644 --- a/.playground/pages/composant/form/client.vue +++ b/.playground/pages/composant/form/client.vue @@ -1,122 +1,83 @@ diff --git a/.playground/pages/composant/input/inputPhone.vue b/.playground/pages/composant/input/inputPhone.vue new file mode 100644 index 0000000..5775e28 --- /dev/null +++ b/.playground/pages/composant/input/inputPhone.vue @@ -0,0 +1,141 @@ + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index d40c554..87c5309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Liste des évolutions de la librairie Malio layer UI * [#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 +* [#MUI-31] Création d'un composant téléphone ### Changed diff --git a/COMPONENTS.md b/COMPONENTS.md index effeb6b..1fe61c1 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -102,6 +102,48 @@ Champ email (`type="email"` + `inputmode="email"`) avec icône `mdi:email-outlin --- +## MalioInputPhone + +Champ téléphone (`type="tel"` + `inputmode="tel"`) avec icône `mdi:phone-outline` à gauche par défaut et bouton `+` optionnel à droite pour gérer une liste de numéros côté parent. + +| 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 `'tel'` pour suggérer un numéro enregistré) | +| `disabled` | `boolean` | `false` | Désactive le champ et le bouton + | +| `readonly` | `boolean` | `false` | Lecture seule (désactive aussi le bouton +) | +| `required` | `boolean` | `false` | Champ requis | +| `hint` | `string` | `''` | Message d'aide | +| `error` | `string` | `''` | Message d'erreur | +| `success` | `string` | `''` | Message de succès | +| `iconName` | `string` | `'mdi:phone-outline'` | Icône Iconify (chaîne vide pour masquer) | +| `iconPosition` | `'left' \| 'right'` | `'left'` | Position de l'icône | +| `iconSize` | `string \| number` | `24` | Taille icône | +| `iconColor` | `string` | `'text-m-muted'` | Classe couleur icône | +| `mask` | `string \| MaskInputOptions` | `undefined` | Masque maska (aucun par défaut, utile pour mono-pays) | +| `addable` | `boolean` | `false` | Affiche un bouton à droite qui émet l'event `add` | +| `addIconName` | `string` | `'mdi:plus'` | Icône Iconify du bouton d'ajout | +| `addButtonLabel` | `string` | `'Ajouter un numéro'` | aria-label du bouton d'ajout | +| `inputClass` | `string` | `''` | Classes CSS input | +| `labelClass` | `string` | `''` | Classes CSS label | +| `groupClass` | `string` | `''` | Classes CSS conteneur | + +**Events :** +- `update:modelValue(value: string)` +- `add()` — émis au clic du bouton `+` (uniquement si `addable`, non `disabled`, non `readonly`) + +```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 6c6ddb2..0f27fa1 100644 --- a/app/components/malio/checkbox/Checkbox.test.ts +++ b/app/components/malio/checkbox/Checkbox.test.ts @@ -114,7 +114,7 @@ describe('MalioCheckbox', () => { }) expect(wrapper.get('input').attributes('aria-invalid')).toBe('true') - expect(wrapper.get('label').classes()).toContain('text-m-error') + expect(wrapper.get('label').classes()).toContain('text-m-danger') expect(wrapper.get('p').text()).toBe('You must accept') }) @@ -125,7 +125,7 @@ describe('MalioCheckbox', () => { }) expect(wrapper.get('p').text()).toBe('Invalid') - expect(wrapper.get('p').classes()).toContain('text-m-error') + expect(wrapper.get('p').classes()).toContain('text-m-danger') }) it('shows success styles and message when there is no error', () => { @@ -151,4 +151,14 @@ describe('MalioCheckbox', () => { expect(wrapper.get('label').classes()).toContain('text-black') }) + + it('updates label color when toggled without v-model (uncontrolled)', async () => { + const wrapper = mountCheckbox({label: 'Accept terms'}) + + expect(wrapper.get('label').classes()).toContain('text-m-muted') + + await wrapper.get('input').setValue(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 59c8ead..a968a15 100644 --- a/app/components/malio/checkbox/Checkbox.vue +++ b/app/components/malio/checkbox/Checkbox.vue @@ -40,7 +40,7 @@ @@ -205,14 +211,14 @@ const onChange = (event: Event) => { stroke-dashoffset: 0; } -.inp-cbx + .cbx.text-m-error span:first-child { - border-color: rgb(var(--m-error) / 1); +.inp-cbx + .cbx.text-m-danger span:first-child { + border-color: rgb(var(--m-danger) / 1); } -.cbx.text-m-error span:first-child svg { - stroke: rgb(var(--m-error) / 1); +.cbx.text-m-danger span:first-child svg { + stroke: rgb(var(--m-danger) / 1); } -.inp-cbx:checked + .cbx.text-m-error span:first-child { - border-color: rgb(var(--m-error) / 1); +.inp-cbx:checked + .cbx.text-m-danger span:first-child { + border-color: rgb(var(--m-danger) / 1); } .inp-cbx + .cbx.text-m-success span:first-child { diff --git a/app/components/malio/input/Input.test.ts b/app/components/malio/input/Input.test.ts index 0bd0e49..167bb35 100644 --- a/app/components/malio/input/Input.test.ts +++ b/app/components/malio/input/Input.test.ts @@ -279,7 +279,7 @@ describe('MalioInputText', () => { expect(wrapper.get('[data-test="icon"]').classes()).toContain('left-[10px]') expect(wrapper.get('input').classes()).toContain('!pl-11') - expect(wrapper.get('label').classes()).toContain('left-8') + expect(wrapper.get('label').classes()).toContain('left-11') }) it('passes icon size props to icon component', () => { diff --git a/app/components/malio/input/InputAmount.test.ts b/app/components/malio/input/InputAmount.test.ts index 0eeec04..a8c4e2f 100644 --- a/app/components/malio/input/InputAmount.test.ts +++ b/app/components/malio/input/InputAmount.test.ts @@ -158,7 +158,7 @@ describe('MalioInputAmount', () => { expect(wrapper.get('[data-test="icon"]').classes()).toContain('left-[10px]') expect(wrapper.get('input').classes()).toContain('!pl-11') - expect(wrapper.get('label').classes()).toContain('left-8') + expect(wrapper.get('label').classes()).toContain('left-11') }) it('shows primary icon color on focus', async () => { diff --git a/app/components/malio/input/InputAmount.vue b/app/components/malio/input/InputAmount.vue index 58d4a55..210cea9 100644 --- a/app/components/malio/input/InputAmount.vue +++ b/app/components/malio/input/InputAmount.vue @@ -135,7 +135,7 @@ const mergedGroupClass = computed(() => ) const mergedInputClass = computed(() => twMerge( - 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 text-lg rounded-md', + 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md', isFilled.value ? 'border-black' : 'border-m-muted', disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text', hasError.value @@ -216,7 +216,7 @@ const iconInputPaddingClass = computed(() => { const disabled = computed(() => props.disabled) const labelPositionClass = computed(() => { - if (props.iconName && props.iconPosition === 'left') return 'left-8' + if (props.iconName && props.iconPosition === 'left') return 'left-11' return 'left-3' }) diff --git a/app/components/malio/input/InputEmail.vue b/app/components/malio/input/InputEmail.vue index 0ba8257..c0498dd 100644 --- a/app/components/malio/input/InputEmail.vue +++ b/app/components/malio/input/InputEmail.vue @@ -129,7 +129,7 @@ const mergedGroupClass = computed(() => ) const mergedInputClass = computed(() => twMerge( - 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 text-lg rounded-md', + 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md', isFilled.value ? 'border-black' : 'border-m-muted', disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text', hasError.value @@ -185,7 +185,7 @@ const iconInputPaddingClass = computed(() => { const disabled = computed(() => props.disabled) const labelPositionClass = computed(() => { - if (props.iconName && props.iconPosition === 'left') return 'left-8' + if (props.iconName && props.iconPosition === 'left') return 'left-11' return 'left-3' }) diff --git a/app/components/malio/input/InputPassword.vue b/app/components/malio/input/InputPassword.vue index bd629ec..c28d73e 100644 --- a/app/components/malio/input/InputPassword.vue +++ b/app/components/malio/input/InputPassword.vue @@ -137,7 +137,7 @@ const mergedGroupClass = computed(() => ) const mergedInputClass = computed(() => twMerge( - 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 text-lg rounded-md', + 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md', isFilled.value ? 'border-black' : 'border-m-muted', disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text', hasError.value diff --git a/app/components/malio/input/InputPhone.test.ts b/app/components/malio/input/InputPhone.test.ts new file mode 100644 index 0000000..95e334c --- /dev/null +++ b/app/components/malio/input/InputPhone.test.ts @@ -0,0 +1,308 @@ +import {describe, expect, it} from 'vitest' +import {mount} from '@vue/test-utils' +import type {DefineComponent} from 'vue' +import { Icon as IconifyIcon } from '@iconify/vue' +import InputPhone from './InputPhone.vue' + +type InputPhoneProps = { + id?: string + label?: string + name?: string + autocomplete?: string + modelValue?: string | null + inputClass?: string + labelClass?: string + groupClass?: string + required?: boolean + disabled?: boolean + readonly?: boolean + hint?: string + error?: string + success?: string + iconName?: string + iconPosition?: 'left' | 'right' + iconSize?: string | number + iconColor?: string + mask?: string + addable?: boolean + addIconName?: string + addButtonLabel?: string +} + +const InputPhoneForTest = InputPhone as DefineComponent + +const mountComponent = (props: InputPhoneProps = {}) => + mount(InputPhoneForTest, { + props, + global: { + stubs: { + IconifyIcon: { + template: '', + }, + }, + }, + }) + +describe('MalioInputPhone', () => { + it('renders the initial input value', () => { + const wrapper = mountComponent({modelValue: '+33 6 12 34 56 78'}) + + expect(wrapper.get('input').element.value).toBe('+33 6 12 34 56 78') + }) + + it('renders the label text', () => { + const wrapper = mountComponent({label: 'Téléphone'}) + + expect(wrapper.get('label').text()).toBe('Téléphone') + }) + + it('has type tel', () => { + const wrapper = mountComponent() + + expect(wrapper.get('input').attributes('type')).toBe('tel') + }) + + it('has inputmode tel', () => { + const wrapper = mountComponent() + + expect(wrapper.get('input').attributes('inputmode')).toBe('tel') + }) + + it('renders the default phone icon', () => { + const wrapper = mountComponent() + + const iconComponent = wrapper.findComponent(IconifyIcon) + expect(iconComponent.props('icon')).toBe('mdi:phone-outline') + }) + + it('allows overriding the icon', () => { + const wrapper = mountComponent({iconName: 'mdi:cellphone'}) + + const iconComponent = wrapper.findComponent(IconifyIcon) + expect(iconComponent.props('icon')).toBe('mdi:cellphone') + }) + + it('does not render icon when iconName is empty', () => { + const wrapper = mountComponent({iconName: ''}) + + expect(wrapper.find('[data-test="icon"]').exists()).toBe(false) + }) + + it('places icon on the left by default', () => { + const wrapper = mountComponent() + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('left-[10px]') + }) + + it('places icon on the right when iconPosition is right', () => { + const wrapper = mountComponent({iconPosition: 'right'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('right-[10px]') + }) + + it('emits update:modelValue on input change', async () => { + const wrapper = mountComponent({modelValue: ''}) + + await wrapper.get('input').setValue('+33612345678') + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['+33612345678']) + }) + + it('sets disabled styles when true', () => { + const wrapper = mountComponent({disabled: true}) + + expect(wrapper.get('input').attributes('disabled')).toBeDefined() + expect(wrapper.get('input').classes()).toContain('cursor-not-allowed') + }) + + it('sets readonly when true', () => { + const wrapper = mountComponent({readonly: true}) + + expect(wrapper.get('input').attributes('readonly')).toBeDefined() + }) + + it('shows error message and styles', () => { + const wrapper = mountComponent({error: 'Numéro invalide'}) + + expect(wrapper.get('p.text-m-danger').text()).toBe('Numéro invalide') + expect(wrapper.get('input').classes()).toContain('border-m-danger') + expect(wrapper.get('input').attributes('aria-invalid')).toBe('true') + }) + + it('shows error style on icon', () => { + const wrapper = mountComponent({error: 'Error'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-danger') + }) + + it('shows success message and styles', () => { + const wrapper = mountComponent({success: 'Numéro valide'}) + + expect(wrapper.get('p.text-m-success').text()).toBe('Numéro valide') + expect(wrapper.get('input').classes()).toContain('border-m-success') + }) + + it('shows success style on icon', () => { + const wrapper = mountComponent({success: 'Success'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success') + }) + + it('shows default icon color when empty and unfocused', () => { + const wrapper = mountComponent() + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-muted') + }) + + 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: '+33612345678'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') + }) + + it('keeps default icon color when disabled, even if filled', () => { + const wrapper = mountComponent({modelValue: '+33612345678', disabled: true}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-muted') + }) + + it('error overrides focus color on icon', async () => { + const wrapper = mountComponent({error: 'Numéro invalide'}) + + await wrapper.get('input').trigger('focus') + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-danger') + }) + + it('shows hint message', () => { + const wrapper = mountComponent({hint: 'Format international recommandé'}) + + expect(wrapper.get('p.text-m-muted').text()).toBe('Format international recommandé') + }) + + it('links label to input via for/id', () => { + const wrapper = mountComponent({id: 'phone-field', label: 'Téléphone'}) + + expect(wrapper.get('input').attributes('id')).toBe('phone-field') + expect(wrapper.get('label').attributes('for')).toBe('phone-field') + }) + + it('generates an id when missing and reuses it on label', () => { + const wrapper = mountComponent({label: 'Téléphone'}) + + const inputId = wrapper.get('input').attributes('id') + + expect(inputId?.startsWith('malio-input-phone-')).toBe(true) + expect(wrapper.get('label').attributes('for')).toBe(inputId) + }) + + it('aria-invalid is false when no error', () => { + const wrapper = mountComponent() + + expect(wrapper.get('input').attributes('aria-invalid')).toBe('false') + }) + + it('uses autocomplete off by default', () => { + const wrapper = mountComponent() + + expect(wrapper.get('input').attributes('autocomplete')).toBe('off') + }) + + it('allows overriding autocomplete', () => { + const wrapper = mountComponent({autocomplete: 'tel'}) + + expect(wrapper.get('input').attributes('autocomplete')).toBe('tel') + }) + + it('does not render add button by default', () => { + const wrapper = mountComponent() + + expect(wrapper.find('[data-test="add-button"]').exists()).toBe(false) + }) + + it('renders add button when addable is true', () => { + const wrapper = mountComponent({addable: true}) + + expect(wrapper.find('[data-test="add-button"]').exists()).toBe(true) + }) + + it('emits add event when add button is clicked', async () => { + const wrapper = mountComponent({addable: true}) + + await wrapper.get('[data-test="add-button"]').trigger('click') + + expect(wrapper.emitted('add')).toHaveLength(1) + }) + + it('does not emit add when disabled', async () => { + const wrapper = mountComponent({addable: true, disabled: true}) + + await wrapper.get('[data-test="add-button"]').trigger('click') + + expect(wrapper.emitted('add')).toBeUndefined() + }) + + it('does not emit add when readonly', async () => { + const wrapper = mountComponent({addable: true, readonly: true}) + + await wrapper.get('[data-test="add-button"]').trigger('click') + + expect(wrapper.emitted('add')).toBeUndefined() + }) + + it('disables add button when disabled', () => { + const wrapper = mountComponent({addable: true, disabled: true}) + + expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined() + }) + + it('disables add button when readonly', () => { + const wrapper = mountComponent({addable: true, readonly: true}) + + expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined() + }) + + it('renders the default add icon (mdi:plus)', () => { + const wrapper = mountComponent({addable: true}) + + const icons = wrapper.findAllComponents(IconifyIcon) + const addIcon = icons[icons.length - 1] + expect(addIcon.props('icon')).toBe('mdi:plus') + }) + + it('allows overriding the add icon', () => { + const wrapper = mountComponent({addable: true, addIconName: 'mdi:phone-plus'}) + + const icons = wrapper.findAllComponents(IconifyIcon) + const addIcon = icons[icons.length - 1] + expect(addIcon.props('icon')).toBe('mdi:phone-plus') + }) + + it('exposes aria-label on add button', () => { + const wrapper = mountComponent({addable: true, addButtonLabel: 'Ajouter un autre numéro'}) + + expect(wrapper.get('[data-test="add-button"]').attributes('aria-label')).toBe('Ajouter un autre numéro') + }) + + it('adds right padding to input when addable', () => { + const wrapper = mountComponent({addable: true}) + + expect(wrapper.get('input').classes()).toContain('!pr-10') + }) + + it('applies mask via maska directive', async () => { + const wrapper = mountComponent({mask: '+## # ## ## ## ##'}) + + await wrapper.get('input').setValue('33612345678') + + expect(wrapper.emitted('update:modelValue')).toBeDefined() + }) +}) diff --git a/app/components/malio/input/InputPhone.vue b/app/components/malio/input/InputPhone.vue new file mode 100644 index 0000000..9d3b9ee --- /dev/null +++ b/app/components/malio/input/InputPhone.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/app/components/malio/input/InputText.vue b/app/components/malio/input/InputText.vue index 50306f4..80a9ccf 100644 --- a/app/components/malio/input/InputText.vue +++ b/app/components/malio/input/InputText.vue @@ -140,7 +140,7 @@ const mergedGroupClass = computed(() => ) const mergedInputClass = computed(() => twMerge( - 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 text-lg rounded-md', + 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md', isFilled.value ? 'border-black' : 'border-m-muted', disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text', hasError.value @@ -196,7 +196,7 @@ const iconInputPaddingClass = computed(() => { const disabled = computed(() => props.disabled) const labelPositionClass = computed(() => { - if (props.iconName && props.iconPosition === 'left') return 'left-8' + if (props.iconName && props.iconPosition === 'left') return 'left-11' return 'left-3' }) diff --git a/app/components/malio/input/InputTextArea.vue b/app/components/malio/input/InputTextArea.vue index 63d2f59..a7c09e6 100644 --- a/app/components/malio/input/InputTextArea.vue +++ b/app/components/malio/input/InputTextArea.vue @@ -7,7 +7,7 @@ :name="name" :autocomplete="autocomplete" - class="floating-input peer w-full border-2 bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent overflow-auto" + class="floating-input peer w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent overflow-auto" :class="[ isFilled ? 'border-black' : 'border-m-muted', disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text', diff --git a/app/components/malio/input/InputUpload.vue b/app/components/malio/input/InputUpload.vue index b5d01ad..70e873a 100644 --- a/app/components/malio/input/InputUpload.vue +++ b/app/components/malio/input/InputUpload.vue @@ -126,7 +126,7 @@ const mergedGroupClass = computed(() => ) const mergedInputClass = computed(() => twMerge( - 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 text-lg rounded-md', + 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md', isFilled.value ? 'border-black' : 'border-m-muted', disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-pointer', hasError.value diff --git a/app/components/malio/radio/RadioButton.test.ts b/app/components/malio/radio/RadioButton.test.ts index 67a04b6..1a8d714 100644 --- a/app/components/malio/radio/RadioButton.test.ts +++ b/app/components/malio/radio/RadioButton.test.ts @@ -172,4 +172,14 @@ describe('MalioRadioButton', () => { expect(wrapper.get('input').classes()).toContain('checked:border-black') }) + + it('updates label color when toggled without v-model (uncontrolled)', async () => { + const wrapper = mountRadioButton({label: 'Option 1', value: 'a'}) + + expect(wrapper.get('.radio-text').classes()).toContain('text-m-muted') + + await wrapper.get('input').trigger('change') + + expect(wrapper.get('.radio-text').classes()).toContain('text-black') + }) }) diff --git a/app/components/malio/radio/RadioButton.vue b/app/components/malio/radio/RadioButton.vue index dd9b422..92250ea 100644 --- a/app/components/malio/radio/RadioButton.vue +++ b/app/components/malio/radio/RadioButton.vue @@ -44,7 +44,7 @@ diff --git a/app/components/malio/select/Select.vue b/app/components/malio/select/Select.vue index 877c400..e36f5bb 100644 --- a/app/components/malio/select/Select.vue +++ b/app/components/malio/select/Select.vue @@ -7,24 +7,24 @@