feat(ui) : prop reserveMessageSpace (défaut true) sur Select/SelectCheckbox/Checkbox/date/time

Ajoute reserveMessageSpace (défaut true) pour permettre de ne pas réserver
la ligne de message d'aide quand aucun message n'est présent. Comportement
inchangé par défaut. La famille date hérite via $attrs → CalendarField.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:53:22 +02:00
parent cda0f994ca
commit 1b5a4c9920
12 changed files with 152 additions and 6 deletions
@@ -17,6 +17,7 @@ type CheckboxProps = {
hint?: string
error?: string
success?: string
reserveMessageSpace?: boolean
}
const CheckboxForTest = Checkbox as DefineComponent<CheckboxProps>
@@ -171,4 +172,23 @@ describe('MalioCheckbox', () => {
const wrapper = mountCheckbox({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mountCheckbox({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 = mountCheckbox({label: 'Champ', reserveMessageSpace: false})
expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false)
})
it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => {
const wrapper = mountCheckbox({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
@@ -30,6 +30,7 @@
</label>
<p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`"
:class="mergedMessageClass"
>
@@ -60,6 +61,7 @@ const props = withDefaults(
hint?: string
error?: string
success?: string
reserveMessageSpace?: boolean
}>(),
{
id: '',
@@ -75,6 +77,7 @@ const props = withDefaults(
hint: '',
error: '',
success: '',
reserveMessageSpace: true,
},
)
@@ -121,7 +124,8 @@ const mergedLabelClass = computed(() =>
const mergedMessageClass = computed(() =>
twMerge(
'text-xs min-h-[1rem]',
'text-xs',
props.reserveMessageSpace ? 'min-h-[1rem]' : '',
hasError.value
? 'text-m-danger'
: hasSuccess.value
+22
View File
@@ -21,6 +21,7 @@ type DateProps = {
inputClass?: string
labelClass?: string
groupClass?: string
reserveMessageSpace?: boolean
}
const DateForTest = Date_ as DefineComponent<DateProps>
@@ -236,4 +237,25 @@ describe('MalioDate', () => {
expect(input.value).toBe('25/12/2026')
})
})
describe('reserveMessageSpace', () => {
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mountDate({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 = mountDate({label: 'Champ', reserveMessageSpace: false})
expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false)
})
it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => {
const wrapper = mountDate({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]')
})
})
})
@@ -85,10 +85,12 @@
</div>
<p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`"
:class="[
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]' : '',
]"
>
{{ error || success || hint }}
@@ -126,6 +128,7 @@ const props = withDefaults(
inputClass?: string
labelClass?: string
groupClass?: string
reserveMessageSpace?: boolean
}>(),
{
id: '',
@@ -142,6 +145,7 @@ const props = withDefaults(
inputClass: '',
labelClass: '',
groupClass: '',
reserveMessageSpace: true,
},
)
@@ -23,6 +23,7 @@ type SelectProps = {
disabled?: boolean
readonly?: boolean
required?: boolean
reserveMessageSpace?: boolean
}
const SelectForTest = Select as DefineComponent<SelectProps>
@@ -359,4 +360,23 @@ describe('MalioSelect', () => {
expect(trigger.attributes('aria-readonly')).toBeUndefined()
expect(trigger.attributes('disabled')).toBeDefined()
})
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mount(SelectForTest, {props: {modelValue: null, 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 = mount(SelectForTest, {props: {modelValue: null, 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 = mount(SelectForTest, {props: {modelValue: null, 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]')
})
})
+5 -1
View File
@@ -166,6 +166,7 @@
</ul>
</div>
<p
v-if="reserveMessageSpace || hint || error || success"
:id="`${buttonId}-describedby`"
:class="[
hasError
@@ -173,7 +174,8 @@
: 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]' : '',
]"
>
{{ error || success || hint }}
@@ -210,6 +212,7 @@ const props = withDefaults(defineProps<{
groupClass?: string
noOptionsText?: string
required?: boolean
reserveMessageSpace?: boolean
}>(), {
options: () => [],
emptyOptionLabel: '',
@@ -226,6 +229,7 @@ const props = withDefaults(defineProps<{
groupClass: '',
noOptionsText: 'Aucune option disponible',
required: false,
reserveMessageSpace: true,
})
const emit = defineEmits<{
@@ -27,6 +27,7 @@ type SelectCheckboxProps = {
readonly?: boolean
groupClass?: string
required?: boolean
reserveMessageSpace?: boolean
}
const SelectCheckboxForTest = SelectCheckbox as DefineComponent<SelectCheckboxProps>
@@ -346,4 +347,23 @@ describe('MalioSelectCheckbox', () => {
expect(trigger.attributes('aria-readonly')).toBeUndefined()
expect(trigger.attributes('disabled')).toBeDefined()
})
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mount(SelectCheckboxForTest, {props: {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 = mount(SelectCheckboxForTest, {props: {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 = mount(SelectCheckboxForTest, {props: {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]')
})
})
@@ -215,6 +215,7 @@
</ul>
</div>
<p
v-if="reserveMessageSpace || hint || error || success"
:id="`${buttonId}-describedby`"
:class="[
hasError
@@ -222,7 +223,8 @@
: 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]' : '',
]"
>
{{ error || success || hint }}
@@ -263,6 +265,7 @@ const props = withDefaults(defineProps<{
groupClass?: string
noOptionsText?: string
required?: boolean
reserveMessageSpace?: boolean
}>(), {
modelValue: () => [],
options: () => [],
@@ -283,6 +286,7 @@ const props = withDefaults(defineProps<{
groupClass: '',
noOptionsText: 'Aucune option disponible',
required: false,
reserveMessageSpace: true,
})
const emit = defineEmits<{
+20
View File
@@ -17,6 +17,7 @@ type TimeProps = {
hint?: string
error?: string
success?: string
reserveMessageSpace?: boolean
}
const TimeForTest = Time as DefineComponent<TimeProps>
@@ -86,4 +87,23 @@ describe('MalioTime', () => {
const wrapper = mountTime({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mountTime({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 = mountTime({label: 'Champ', reserveMessageSpace: false})
expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false)
})
it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => {
const wrapper = mountTime({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
@@ -58,6 +58,7 @@
</div>
<p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`"
:class="[
hasError
@@ -65,7 +66,8 @@
: 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]' : '',
]"
>
{{ error || success || hint }}
@@ -95,6 +97,7 @@ const props = withDefaults(
hint?: string
error?: string
success?: string
reserveMessageSpace?: boolean
}>(),
{
id: '',
@@ -110,6 +113,7 @@ const props = withDefaults(
hint: '',
error: '',
success: '',
reserveMessageSpace: true,
},
)
@@ -19,6 +19,7 @@ type TimePickerProps = {
inputClass?: string
labelClass?: string
groupClass?: string
reserveMessageSpace?: boolean
}
const TimePickerForTest = TimePicker as DefineComponent<TimePickerProps>
@@ -120,4 +121,23 @@ describe('MalioTimePicker', () => {
expect(label.classes()).toContain('text-black')
expect(icon.classes()).toContain('text-black')
})
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mountPicker({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 = mountPicker({label: 'Champ', reserveMessageSpace: false})
expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false)
})
it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => {
const wrapper = mountPicker({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
@@ -78,10 +78,12 @@
</div>
<p
v-if="reserveMessageSpace || hint || error || success"
:id="`${inputId}-describedby`"
:class="[
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]' : '',
]"
>
{{ error || success || hint }}
@@ -116,6 +118,7 @@ const props = withDefaults(
inputClass?: string
labelClass?: string
groupClass?: string
reserveMessageSpace?: boolean
}>(),
{
id: '',
@@ -134,6 +137,7 @@ const props = withDefaults(
inputClass: '',
labelClass: '',
groupClass: '',
reserveMessageSpace: true,
},
)