From cda0f994ca9b55ee6fb18b389d4b67a14d8de264 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 16:43:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui)=20:=20prop=20reserveMessageSpace=20(d?= =?UTF-8?q?=C3=A9faut=20true)=20sur=20la=20famille=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute une prop booléenne reserveMessageSpace (défaut true) aux 10 composants de la famille input. Par défaut, comportement inchangé (ligne message toujours rendue avec min-h-[1rem]). À false, la ligne ne prend aucun espace en l'absence de message, et s'affiche sans min-h quand un message est présent. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/components/malio/input/Input.test.ts | 20 +++++++++++++++++ .../malio/input/InputAmount.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputAmount.vue | 6 ++++- .../malio/input/InputAutocomplete.test.ts | 20 +++++++++++++++++ .../malio/input/InputAutocomplete.vue | 6 ++++- app/components/malio/input/InputEmail.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputEmail.vue | 6 ++++- .../malio/input/InputNumber.test.ts | 22 +++++++++++++++++++ app/components/malio/input/InputNumber.vue | 6 ++++- .../malio/input/InputPassword.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputPassword.vue | 6 ++++- app/components/malio/input/InputPhone.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputPhone.vue | 6 ++++- .../malio/input/InputRichText.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputRichText.vue | 6 ++++- app/components/malio/input/InputText.vue | 6 ++++- .../malio/input/InputTextArea.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputTextArea.vue | 7 +++++- .../malio/input/InputUpload.test.ts | 20 +++++++++++++++++ app/components/malio/input/InputUpload.vue | 6 ++++- 20 files changed, 253 insertions(+), 10 deletions(-) diff --git a/app/components/malio/input/Input.test.ts b/app/components/malio/input/Input.test.ts index 8be144e..d961f20 100644 --- a/app/components/malio/input/Input.test.ts +++ b/app/components/malio/input/Input.test.ts @@ -24,6 +24,7 @@ type InputProps = { iconPosition?: 'left' | 'right' iconSize?: string | number iconColor?: string + reserveMessageSpace?: boolean } const InputForTest = Input as DefineComponent @@ -279,6 +280,25 @@ describe('MalioInputText', () => { expect(p.classes()).toContain('min-h-[1rem]') }) + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountInput({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountInput({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountInput({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) + it('does not render label when label prop is missing', () => { const wrapper = mountInput({labelClass: 'text-red-500'}) diff --git a/app/components/malio/input/InputAmount.test.ts b/app/components/malio/input/InputAmount.test.ts index 8eced10..1518dba 100644 --- a/app/components/malio/input/InputAmount.test.ts +++ b/app/components/malio/input/InputAmount.test.ts @@ -24,6 +24,7 @@ type InputAmountProps = { iconPosition?: 'left' | 'right' iconSize?: string | number iconColor?: string + reserveMessageSpace?: boolean } const InputAmountForTest = InputAmount as DefineComponent @@ -210,4 +211,23 @@ describe('MalioInputAmount', () => { expect(wrapper.get('label').classes()).toContain('text-black') expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountInputAmount({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountInputAmount({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountInputAmount({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputAmount.vue b/app/components/malio/input/InputAmount.vue index dbb7302..1993972 100644 --- a/app/components/malio/input/InputAmount.vue +++ b/app/components/malio/input/InputAmount.vue @@ -44,6 +44,7 @@

{{ hint || error || success }} @@ -89,6 +91,7 @@ const props = withDefaults( iconPosition?: 'left' | 'right' iconSize?: string | number iconColor?: string + reserveMessageSpace?: boolean }>(), { id: '', @@ -111,6 +114,7 @@ const props = withDefaults( success: '', iconSize: 20, iconColor: 'text-m-muted', + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputAutocomplete.test.ts b/app/components/malio/input/InputAutocomplete.test.ts index b5247b4..c3e60db 100644 --- a/app/components/malio/input/InputAutocomplete.test.ts +++ b/app/components/malio/input/InputAutocomplete.test.ts @@ -36,6 +36,7 @@ type InputAutocompleteProps = { noResultsText?: string loadingText?: string minSearchText?: string + reserveMessageSpace?: boolean } const InputAutocompleteForTest = InputAutocomplete as DefineComponent @@ -543,4 +544,23 @@ describe('MalioInputAutocomplete', () => { expect(wrapper.get('[data-test="icon-left"]').classes()).toContain('text-black') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black') }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountComponent({label: 'Champ', options}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountComponent({label: 'Champ', options, reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountComponent({label: 'Champ', options, reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputAutocomplete.vue b/app/components/malio/input/InputAutocomplete.vue index 7c40cf1..5343ac0 100644 --- a/app/components/malio/input/InputAutocomplete.vue +++ b/app/components/malio/input/InputAutocomplete.vue @@ -136,10 +136,12 @@

{{ hint || error || success }} @@ -188,6 +190,7 @@ const props = withDefaults( noResultsText?: string loadingText?: string minSearchText?: string + reserveMessageSpace?: boolean }>(), { id: '', @@ -216,6 +219,7 @@ const props = withDefaults( noResultsText: 'Aucun résultat', loadingText: 'Chargement…', minSearchText: 'Tapez pour rechercher', + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputEmail.test.ts b/app/components/malio/input/InputEmail.test.ts index ee0cfc3..75e7b94 100644 --- a/app/components/malio/input/InputEmail.test.ts +++ b/app/components/malio/input/InputEmail.test.ts @@ -24,6 +24,7 @@ type InputEmailProps = { iconSize?: string | number iconColor?: string lowercase?: boolean + reserveMessageSpace?: boolean } const InputEmailForTest = InputEmail as DefineComponent @@ -295,4 +296,23 @@ describe('MalioInputEmail', () => { expect(wrapper.get('label').classes()).toContain('text-black') expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountComponent({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputEmail.vue b/app/components/malio/input/InputEmail.vue index 622ffad..9b940d2 100644 --- a/app/components/malio/input/InputEmail.vue +++ b/app/components/malio/input/InputEmail.vue @@ -42,6 +42,7 @@

{{ hint || error || success }} @@ -87,6 +89,7 @@ const props = withDefaults( iconSize?: string | number iconColor?: string lowercase?: boolean + reserveMessageSpace?: boolean }>(), { id: '', @@ -108,6 +111,7 @@ const props = withDefaults( iconSize: 24, iconColor: 'text-m-muted', lowercase: false, + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputNumber.test.ts b/app/components/malio/input/InputNumber.test.ts index 5e5186b..c9ae904 100644 --- a/app/components/malio/input/InputNumber.test.ts +++ b/app/components/malio/input/InputNumber.test.ts @@ -10,6 +10,9 @@ type InputNumberProps = { readonly?: boolean min?: number | string max?: number | string + error?: string + hint?: string + reserveMessageSpace?: boolean } const InputNumberForTest = InputNumber as DefineComponent @@ -173,4 +176,23 @@ describe('MalioInputNumber', () => { const wrapper = mountInputNumber({label: 'Champ'}) expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false) }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountInputNumber({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountInputNumber({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountInputNumber({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputNumber.vue b/app/components/malio/input/InputNumber.vue index a42653a..f9e009f 100644 --- a/app/components/malio/input/InputNumber.vue +++ b/app/components/malio/input/InputNumber.vue @@ -51,6 +51,7 @@

{{ hint || error || success }} @@ -91,6 +93,7 @@ const props = withDefaults( hint?: string error?: string success?: string + reserveMessageSpace?: boolean }>(), { id: '', @@ -108,6 +111,7 @@ const props = withDefaults( hint: '', error: '', success: '', + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputPassword.test.ts b/app/components/malio/input/InputPassword.test.ts index 4610ef5..d7bc3c3 100644 --- a/app/components/malio/input/InputPassword.test.ts +++ b/app/components/malio/input/InputPassword.test.ts @@ -22,6 +22,7 @@ type InputPasswordProps = { error?: string success?: string displayIcon?: boolean + reserveMessageSpace?: boolean } const InputPasswordForTest = InputPassword as DefineComponent @@ -227,4 +228,23 @@ describe('MalioInputPassword', () => { await wrapper.get('[data-test="icon"]').trigger('click') expect(wrapper.get('input').attributes('type')).toBe('text') }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountComponent({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputPassword.vue b/app/components/malio/input/InputPassword.vue index 8cb018a..cf2996e 100644 --- a/app/components/malio/input/InputPassword.vue +++ b/app/components/malio/input/InputPassword.vue @@ -47,6 +47,7 @@

{{ hint || error || success }} @@ -90,6 +92,7 @@ const props = withDefaults( error?: string success?: string displayIcon?: boolean + reserveMessageSpace?: boolean }>(), { id: '', @@ -109,6 +112,7 @@ const props = withDefaults( error: '', success: '', displayIcon: true, + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputPhone.test.ts b/app/components/malio/input/InputPhone.test.ts index f6f6a07..670949a 100644 --- a/app/components/malio/input/InputPhone.test.ts +++ b/app/components/malio/input/InputPhone.test.ts @@ -27,6 +27,7 @@ type InputPhoneProps = { addable?: boolean addIconName?: string addButtonLabel?: string + reserveMessageSpace?: boolean } const InputPhoneForTest = InputPhone as DefineComponent @@ -383,4 +384,23 @@ describe('MalioInputPhone', () => { expect(wrapper.emitted('update:modelValue')).toBeDefined() }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountComponent({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputPhone.vue b/app/components/malio/input/InputPhone.vue index 3f68fec..9639d2c 100644 --- a/app/components/malio/input/InputPhone.vue +++ b/app/components/malio/input/InputPhone.vue @@ -60,6 +60,7 @@

{{ hint || error || success }} @@ -110,6 +112,7 @@ const props = withDefaults( addable?: boolean addIconName?: string addButtonLabel?: string + reserveMessageSpace?: boolean }>(), { id: '', @@ -134,6 +137,7 @@ const props = withDefaults( addable: false, addIconName: 'mdi:plus', addButtonLabel: 'Ajouter un numéro', + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputRichText.test.ts b/app/components/malio/input/InputRichText.test.ts index 8b7db86..05a3653 100644 --- a/app/components/malio/input/InputRichText.test.ts +++ b/app/components/malio/input/InputRichText.test.ts @@ -20,6 +20,7 @@ type InputRichTextProps = { labelClass?: string editorClass?: string required?: boolean + reserveMessageSpace?: boolean } const InputRichTextForTest = InputRichText as DefineComponent @@ -187,4 +188,23 @@ describe('MalioInputRichText', () => { expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false) }) + + it('réserve l’espace message par défaut même sans message', async () => { + const wrapper = await mountComponent({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', async () => { + const wrapper = await mountComponent({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', async () => { + const wrapper = await mountComponent({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputRichText.vue b/app/components/malio/input/InputRichText.vue index b8fcc41..8c3a9af 100644 --- a/app/components/malio/input/InputRichText.vue +++ b/app/components/malio/input/InputRichText.vue @@ -185,6 +185,7 @@

{{ error || success || hint }} @@ -235,6 +237,7 @@ const props = withDefaults( labelClass?: string editorClass?: string required?: boolean + reserveMessageSpace?: boolean }>(), { id: '', @@ -253,6 +256,7 @@ const props = withDefaults( labelClass: '', editorClass: '', required: false, + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputText.vue b/app/components/malio/input/InputText.vue index bdd2992..14c40b1 100644 --- a/app/components/malio/input/InputText.vue +++ b/app/components/malio/input/InputText.vue @@ -44,6 +44,7 @@

{{ hint || error || success }} @@ -94,6 +96,7 @@ const props = withDefaults( iconSize?: string | number iconColor?: string mask?: string | MaskInputOptions + reserveMessageSpace?: boolean }>(), { id: '', @@ -117,6 +120,7 @@ const props = withDefaults( iconSize: 24, iconColor: 'text-m-muted', mask: undefined, + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputTextArea.test.ts b/app/components/malio/input/InputTextArea.test.ts index 97e24e4..e1982c7 100644 --- a/app/components/malio/input/InputTextArea.test.ts +++ b/app/components/malio/input/InputTextArea.test.ts @@ -21,6 +21,7 @@ type InputTextAreaProps = { error?: string success?: string rounded?: string + reserveMessageSpace?: boolean } const InputTextAreaForTest = InputTextArea as DefineComponent @@ -213,4 +214,23 @@ describe('MalioInputTextArea', () => { const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', readonly: true, modelValue: 'du texte'}}) expect(wrapper.get('label').classes()).toContain('text-black') }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ'}}) + const msg = wrapper.find('[data-test="message-line"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', reserveMessageSpace: false}}) + expect(wrapper.find('[data-test="message-line"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}}) + const msg = wrapper.find('[data-test="message-line"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputTextArea.vue b/app/components/malio/input/InputTextArea.vue index d680eac..d8a25a5 100644 --- a/app/components/malio/input/InputTextArea.vue +++ b/app/components/malio/input/InputTextArea.vue @@ -63,7 +63,10 @@

(), { @@ -140,6 +144,7 @@ const props = withDefaults( minResizeHeight: 40, maxResizeHeight: 320, groupClass: '', + reserveMessageSpace: true, }, ) diff --git a/app/components/malio/input/InputUpload.test.ts b/app/components/malio/input/InputUpload.test.ts index 91ccd32..326d581 100644 --- a/app/components/malio/input/InputUpload.test.ts +++ b/app/components/malio/input/InputUpload.test.ts @@ -19,6 +19,7 @@ type InputUploadProps = { displayIcon?: boolean accept?: string required?: boolean + reserveMessageSpace?: boolean } const InputUploadForTest = InputUpload as DefineComponent @@ -240,4 +241,23 @@ describe('MalioInputUpload', () => { await wrapper.get('input[type="text"]').trigger('click') expect(clickSpy).not.toHaveBeenCalled() }) + + it('réserve l’espace message par défaut même sans message', () => { + const wrapper = mountComponent({label: 'Champ'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).toContain('min-h-[1rem]') + }) + + it('reserveMessageSpace=false sans message : pas de ligne réservée', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false}) + expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) + }) + + it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { + const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'}) + const msg = wrapper.find('[id$="-describedby"]') + expect(msg.exists()).toBe(true) + expect(msg.classes()).not.toContain('min-h-[1rem]') + }) }) diff --git a/app/components/malio/input/InputUpload.vue b/app/components/malio/input/InputUpload.vue index d23b2b6..2923ae9 100644 --- a/app/components/malio/input/InputUpload.vue +++ b/app/components/malio/input/InputUpload.vue @@ -52,6 +52,7 @@

{{ hint || error || success }} @@ -92,6 +94,7 @@ const props = withDefaults( displayIcon?: boolean accept?: string required?: boolean + reserveMessageSpace?: boolean }>(), { id: '', @@ -108,6 +111,7 @@ const props = withDefaults( displayIcon: true, accept: '', required: false, + reserveMessageSpace: true, }, )