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:
@@ -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 l’espace 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]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 l’espace 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 l’espace 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]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 l’espace 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<{
|
||||
|
||||
@@ -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 l’espace 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]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 l’espace 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]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user