feat(ui) : required cohérent + astérisque label + sanitisation email (MUI-41) #60

Merged
tristan merged 51 commits from feature/MUI-41-props-required-asterisque-dans-le-label-sur-les-co into develop 2026-06-04 06:42:20 +00:00
20 changed files with 253 additions and 10 deletions
Showing only changes of commit cda0f994ca - Show all commits
+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,
}, },
) )