feat(ui) : prop reserveMessageSpace (défaut true) sur la famille input

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:43:31 +02:00
parent 5f1dc834cd
commit cda0f994ca
20 changed files with 253 additions and 10 deletions
+20
View File
@@ -24,6 +24,7 @@ type InputProps = {
iconPosition?: 'left' | 'right' iconPosition?: 'left' | 'right'
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
reserveMessageSpace?: boolean
} }
const InputForTest = Input as DefineComponent<InputProps> const InputForTest = Input as DefineComponent<InputProps>
@@ -279,6 +280,25 @@ describe('MalioInputText', () => {
expect(p.classes()).toContain('min-h-[1rem]') expect(p.classes()).toContain('min-h-[1rem]')
}) })
it('réserve lespace 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', () => { it('does not render label when label prop is missing', () => {
const wrapper = mountInput({labelClass: 'text-red-500'}) const wrapper = mountInput({labelClass: 'text-red-500'})
@@ -24,6 +24,7 @@ type InputAmountProps = {
iconPosition?: 'left' | 'right' iconPosition?: 'left' | 'right'
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
reserveMessageSpace?: boolean
} }
const InputAmountForTest = InputAmount as DefineComponent<InputAmountProps> const InputAmountForTest = InputAmount as DefineComponent<InputAmountProps>
@@ -210,4 +211,23 @@ describe('MalioInputAmount', () => {
expect(wrapper.get('label').classes()).toContain('text-black') expect(wrapper.get('label').classes()).toContain('text-black')
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -44,6 +44,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -51,7 +52,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -89,6 +91,7 @@ const props = withDefaults(
iconPosition?: 'left' | 'right' iconPosition?: 'left' | 'right'
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -111,6 +114,7 @@ const props = withDefaults(
success: '', success: '',
iconSize: 20, iconSize: 20,
iconColor: 'text-m-muted', iconColor: 'text-m-muted',
reserveMessageSpace: true,
}, },
) )
@@ -36,6 +36,7 @@ type InputAutocompleteProps = {
noResultsText?: string noResultsText?: string
loadingText?: string loadingText?: string
minSearchText?: string minSearchText?: string
reserveMessageSpace?: boolean
} }
const InputAutocompleteForTest = InputAutocomplete as DefineComponent<InputAutocompleteProps> const InputAutocompleteForTest = InputAutocomplete as DefineComponent<InputAutocompleteProps>
@@ -543,4 +544,23 @@ describe('MalioInputAutocomplete', () => {
expect(wrapper.get('[data-test="icon-left"]').classes()).toContain('text-black') expect(wrapper.get('[data-test="icon-left"]').classes()).toContain('text-black')
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black')
}) })
it('réserve lespace 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]')
})
}) })
@@ -136,10 +136,12 @@
</ul> </ul>
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted', hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
'mt-1 ml-[2px] text-xs min-h-[1rem]', 'mt-1 ml-[2px] text-xs',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -188,6 +190,7 @@ const props = withDefaults(
noResultsText?: string noResultsText?: string
loadingText?: string loadingText?: string
minSearchText?: string minSearchText?: string
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -216,6 +219,7 @@ const props = withDefaults(
noResultsText: 'Aucun résultat', noResultsText: 'Aucun résultat',
loadingText: 'Chargement…', loadingText: 'Chargement…',
minSearchText: 'Tapez pour rechercher', minSearchText: 'Tapez pour rechercher',
reserveMessageSpace: true,
}, },
) )
@@ -24,6 +24,7 @@ type InputEmailProps = {
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
lowercase?: boolean lowercase?: boolean
reserveMessageSpace?: boolean
} }
const InputEmailForTest = InputEmail as DefineComponent<InputEmailProps> const InputEmailForTest = InputEmail as DefineComponent<InputEmailProps>
@@ -295,4 +296,23 @@ describe('MalioInputEmail', () => {
expect(wrapper.get('label').classes()).toContain('text-black') expect(wrapper.get('label').classes()).toContain('text-black')
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -42,6 +42,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -49,7 +50,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -87,6 +89,7 @@ const props = withDefaults(
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
lowercase?: boolean lowercase?: boolean
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -108,6 +111,7 @@ const props = withDefaults(
iconSize: 24, iconSize: 24,
iconColor: 'text-m-muted', iconColor: 'text-m-muted',
lowercase: false, lowercase: false,
reserveMessageSpace: true,
}, },
) )
@@ -10,6 +10,9 @@ type InputNumberProps = {
readonly?: boolean readonly?: boolean
min?: number | string min?: number | string
max?: number | string max?: number | string
error?: string
hint?: string
reserveMessageSpace?: boolean
} }
const InputNumberForTest = InputNumber as DefineComponent<InputNumberProps> const InputNumberForTest = InputNumber as DefineComponent<InputNumberProps>
@@ -173,4 +176,23 @@ describe('MalioInputNumber', () => {
const wrapper = mountInputNumber({label: 'Champ'}) const wrapper = mountInputNumber({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false) expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -51,6 +51,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -58,7 +59,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'text-xs ml-[2px] min-h-[1rem]', 'text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -91,6 +93,7 @@ const props = withDefaults(
hint?: string hint?: string
error?: string error?: string
success?: string success?: string
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -108,6 +111,7 @@ const props = withDefaults(
hint: '', hint: '',
error: '', error: '',
success: '', success: '',
reserveMessageSpace: true,
}, },
) )
@@ -22,6 +22,7 @@ type InputPasswordProps = {
error?: string error?: string
success?: string success?: string
displayIcon?: boolean displayIcon?: boolean
reserveMessageSpace?: boolean
} }
const InputPasswordForTest = InputPassword as DefineComponent<InputPasswordProps> const InputPasswordForTest = InputPassword as DefineComponent<InputPasswordProps>
@@ -227,4 +228,23 @@ describe('MalioInputPassword', () => {
await wrapper.get('[data-test="icon"]').trigger('click') await wrapper.get('[data-test="icon"]').trigger('click')
expect(wrapper.get('input').attributes('type')).toBe('text') expect(wrapper.get('input').attributes('type')).toBe('text')
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -47,6 +47,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -54,7 +55,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -90,6 +92,7 @@ const props = withDefaults(
error?: string error?: string
success?: string success?: string
displayIcon?: boolean displayIcon?: boolean
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -109,6 +112,7 @@ const props = withDefaults(
error: '', error: '',
success: '', success: '',
displayIcon: true, displayIcon: true,
reserveMessageSpace: true,
}, },
) )
@@ -27,6 +27,7 @@ type InputPhoneProps = {
addable?: boolean addable?: boolean
addIconName?: string addIconName?: string
addButtonLabel?: string addButtonLabel?: string
reserveMessageSpace?: boolean
} }
const InputPhoneForTest = InputPhone as DefineComponent<InputPhoneProps> const InputPhoneForTest = InputPhone as DefineComponent<InputPhoneProps>
@@ -383,4 +384,23 @@ describe('MalioInputPhone', () => {
expect(wrapper.emitted('update:modelValue')).toBeDefined() expect(wrapper.emitted('update:modelValue')).toBeDefined()
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -60,6 +60,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -67,7 +68,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -110,6 +112,7 @@ const props = withDefaults(
addable?: boolean addable?: boolean
addIconName?: string addIconName?: string
addButtonLabel?: string addButtonLabel?: string
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -134,6 +137,7 @@ const props = withDefaults(
addable: false, addable: false,
addIconName: 'mdi:plus', addIconName: 'mdi:plus',
addButtonLabel: 'Ajouter un numéro', addButtonLabel: 'Ajouter un numéro',
reserveMessageSpace: true,
}, },
) )
@@ -20,6 +20,7 @@ type InputRichTextProps = {
labelClass?: string labelClass?: string
editorClass?: string editorClass?: string
required?: boolean required?: boolean
reserveMessageSpace?: boolean
} }
const InputRichTextForTest = InputRichText as DefineComponent<InputRichTextProps> const InputRichTextForTest = InputRichText as DefineComponent<InputRichTextProps>
@@ -187,4 +188,23 @@ describe('MalioInputRichText', () => {
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false) expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -185,6 +185,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${editorId}-describedby`" :id="`${editorId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -192,7 +193,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ error || success || hint }} {{ error || success || hint }}
@@ -235,6 +237,7 @@ const props = withDefaults(
labelClass?: string labelClass?: string
editorClass?: string editorClass?: string
required?: boolean required?: boolean
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -253,6 +256,7 @@ const props = withDefaults(
labelClass: '', labelClass: '',
editorClass: '', editorClass: '',
required: false, required: false,
reserveMessageSpace: true,
}, },
) )
+5 -1
View File
@@ -44,6 +44,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -51,7 +52,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -94,6 +96,7 @@ const props = withDefaults(
iconSize?: string | number iconSize?: string | number
iconColor?: string iconColor?: string
mask?: string | MaskInputOptions mask?: string | MaskInputOptions
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -117,6 +120,7 @@ const props = withDefaults(
iconSize: 24, iconSize: 24,
iconColor: 'text-m-muted', iconColor: 'text-m-muted',
mask: undefined, mask: undefined,
reserveMessageSpace: true,
}, },
) )
@@ -21,6 +21,7 @@ type InputTextAreaProps = {
error?: string error?: string
success?: string success?: string
rounded?: string rounded?: string
reserveMessageSpace?: boolean
} }
const InputTextAreaForTest = InputTextArea as DefineComponent<InputTextAreaProps> const InputTextAreaForTest = InputTextArea as DefineComponent<InputTextAreaProps>
@@ -213,4 +214,23 @@ describe('MalioInputTextArea', () => {
const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', readonly: true, modelValue: 'du texte'}}) const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', readonly: true, modelValue: 'du texte'}})
expect(wrapper.get('label').classes()).toContain('text-black') expect(wrapper.get('label').classes()).toContain('text-black')
}) })
it('réserve lespace 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]')
})
}) })
+6 -1
View File
@@ -63,7 +63,10 @@
</span> </span>
</div> </div>
<div <div
class="mt-1 flex items-center justify-between gap-2 text-xs min-h-[1rem]" v-if="reserveMessageSpace || hint || error || success"
data-test="message-line"
class="mt-1 flex items-center justify-between gap-2 text-xs"
:class="reserveMessageSpace ? 'min-h-[1rem]' : ''"
> >
<p <p
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
@@ -114,6 +117,7 @@ const props = withDefaults(
success?: string success?: string
rounded?: string rounded?: string
groupClass?: string groupClass?: string
reserveMessageSpace?: boolean
}>(), }>(),
{ {
@@ -140,6 +144,7 @@ const props = withDefaults(
minResizeHeight: 40, minResizeHeight: 40,
maxResizeHeight: 320, maxResizeHeight: 320,
groupClass: '', groupClass: '',
reserveMessageSpace: true,
}, },
) )
@@ -19,6 +19,7 @@ type InputUploadProps = {
displayIcon?: boolean displayIcon?: boolean
accept?: string accept?: string
required?: boolean required?: boolean
reserveMessageSpace?: boolean
} }
const InputUploadForTest = InputUpload as DefineComponent<InputUploadProps> const InputUploadForTest = InputUpload as DefineComponent<InputUploadProps>
@@ -240,4 +241,23 @@ describe('MalioInputUpload', () => {
await wrapper.get('input[type="text"]').trigger('click') await wrapper.get('input[type="text"]').trigger('click')
expect(clickSpy).not.toHaveBeenCalled() expect(clickSpy).not.toHaveBeenCalled()
}) })
it('réserve lespace 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]')
})
}) })
+5 -1
View File
@@ -52,6 +52,7 @@
</div> </div>
<p <p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`" :id="`${inputId}-describedby`"
:class="[ :class="[
hasError hasError
@@ -59,7 +60,8 @@
: hasSuccess : hasSuccess
? 'text-m-success' ? 'text-m-success'
: 'text-m-muted', : 'text-m-muted',
'mt-1 text-xs ml-[2px] min-h-[1rem]', 'mt-1 text-xs ml-[2px]',
reserveMessageSpace ? 'min-h-[1rem]' : '',
]" ]"
> >
{{ hint || error || success }} {{ hint || error || success }}
@@ -92,6 +94,7 @@ const props = withDefaults(
displayIcon?: boolean displayIcon?: boolean
accept?: string accept?: string
required?: boolean required?: boolean
reserveMessageSpace?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -108,6 +111,7 @@ const props = withDefaults(
displayIcon: true, displayIcon: true,
accept: '', accept: '',
required: false, required: false,
reserveMessageSpace: true,
}, },
) )