feat(ui) : prop required + aria-required + astérisque sur Select/SelectCheckbox/Upload/RichText

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 11:32:10 +02:00
parent 03fe458248
commit 167cc43870
8 changed files with 80 additions and 4 deletions
@@ -19,6 +19,7 @@ type InputRichTextProps = {
groupClass?: string groupClass?: string
labelClass?: string labelClass?: string
editorClass?: string editorClass?: string
required?: boolean
} }
const InputRichTextForTest = InputRichText as DefineComponent<InputRichTextProps> const InputRichTextForTest = InputRichText as DefineComponent<InputRichTextProps>
@@ -162,4 +163,16 @@ describe('MalioInputRichText', () => {
expect(html).toContain('Mon titre') expect(html).toContain('Mon titre')
expect(html).toContain('Un paragraphe.') expect(html).toContain('Un paragraphe.')
}) })
it('affiche l\'astérisque quand required est vrai', async () => {
const wrapper = await mountComponent({label: 'Champ', required: true})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n\'affiche pas l\'astérisque par défaut', async () => {
const wrapper = await mountComponent({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
}) })
+5 -1
View File
@@ -5,7 +5,7 @@
:for="editorId" :for="editorId"
:class="mergedLabelClass" :class="mergedLabelClass"
> >
{{ label }} {{ label }}<MalioRequiredMark v-if="required" />
</label> </label>
<!-- Mode lecture seule (rendu uniquement) --> <!-- Mode lecture seule (rendu uniquement) -->
@@ -22,6 +22,7 @@
v-else v-else
:id="editorId" :id="editorId"
:class="mergedEditorWrapperClass" :class="mergedEditorWrapperClass"
:aria-required="required || undefined"
@click="focusEditor" @click="focusEditor"
> >
<div <div
@@ -202,6 +203,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue' import { Icon as IconifyIcon } from '@iconify/vue'
import MalioRequiredMark from '../shared/RequiredMark.vue'
import { Editor, EditorContent } from '@tiptap/vue-3' import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
@@ -232,6 +234,7 @@ const props = withDefaults(
groupClass?: string groupClass?: string
labelClass?: string labelClass?: string
editorClass?: string editorClass?: string
required?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -249,6 +252,7 @@ const props = withDefaults(
groupClass: '', groupClass: '',
labelClass: '', labelClass: '',
editorClass: '', editorClass: '',
required: false,
}, },
) )
@@ -17,6 +17,7 @@ type InputUploadProps = {
success?: string success?: string
displayIcon?: boolean displayIcon?: boolean
accept?: string accept?: string
required?: boolean
} }
const InputUploadForTest = InputUpload as DefineComponent<InputUploadProps> const InputUploadForTest = InputUpload as DefineComponent<InputUploadProps>
@@ -186,4 +187,16 @@ describe('MalioInputUpload', () => {
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black') expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
}) })
it('affiche l\'astérisque quand required est vrai', () => {
const wrapper = mountComponent({label: 'Champ', required: true})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n\'affiche pas l\'astérisque par défaut', () => {
const wrapper = mountComponent({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
}) })
+5 -1
View File
@@ -9,6 +9,7 @@
:accept="accept" :accept="accept"
class="hidden" class="hidden"
:disabled="disabled" :disabled="disabled"
:required="required"
@change="onFileChange" @change="onFileChange"
> >
@@ -33,7 +34,7 @@
:for="inputId" :for="inputId"
:class="mergedLabelClass" :class="mergedLabelClass"
> >
{{ label }} {{ label }}<MalioRequiredMark v-if="required" />
</label> </label>
<IconifyIcon <IconifyIcon
@@ -70,6 +71,7 @@
import {computed, ref, useAttrs, useId} from 'vue' import {computed, ref, useAttrs, useId} from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue' import { Icon as IconifyIcon } from '@iconify/vue'
import {twMerge} from 'tailwind-merge' import {twMerge} from 'tailwind-merge'
import MalioRequiredMark from '../shared/RequiredMark.vue'
defineOptions({name: 'MalioInputUpload', inheritAttrs: false}) defineOptions({name: 'MalioInputUpload', inheritAttrs: false})
@@ -87,6 +89,7 @@ const props = withDefaults(
success?: string success?: string
displayIcon?: boolean displayIcon?: boolean
accept?: string accept?: string
required?: boolean
}>(), }>(),
{ {
id: '', id: '',
@@ -101,6 +104,7 @@ const props = withDefaults(
success: '', success: '',
displayIcon: true, displayIcon: true,
accept: '', accept: '',
required: false,
}, },
) )
@@ -21,6 +21,7 @@ type SelectProps = {
textLabel?: string textLabel?: string
rounded?: string rounded?: string
disabled?: boolean disabled?: boolean
required?: boolean
} }
const SelectForTest = Select as DefineComponent<SelectProps> const SelectForTest = Select as DefineComponent<SelectProps>
@@ -260,6 +261,22 @@ describe('MalioSelect', () => {
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success')
}) })
it('affiche l\'astérisque quand required est vrai', () => {
const wrapper = mount(SelectForTest, {
props: {modelValue: null, label: 'Champ', required: true},
})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n\'affiche pas l\'astérisque par défaut', () => {
const wrapper = mount(SelectForTest, {
props: {modelValue: null, label: 'Champ'},
})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => { it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => {
const wrapper = mount(SelectForTest, { const wrapper = mount(SelectForTest, {
props: {modelValue: null, options}, props: {modelValue: null, options},
+5 -1
View File
@@ -38,6 +38,7 @@
:aria-controls="listboxId" :aria-controls="listboxId"
:aria-invalid="hasError" :aria-invalid="hasError"
:aria-describedby="describedBy" :aria-describedby="describedBy"
:aria-required="required || undefined"
:disabled="disabled" :disabled="disabled"
@click="toggle" @click="toggle"
> >
@@ -59,7 +60,7 @@
]" ]"
:style="labelTransformStyle" :style="labelTransformStyle"
> >
{{ label }} {{ label }}<MalioRequiredMark v-if="required" />
</label> </label>
<span <span
@@ -171,6 +172,7 @@
import {computed, onBeforeUnmount, onMounted, ref, useId, nextTick} from 'vue' import {computed, onBeforeUnmount, onMounted, ref, useId, nextTick} from 'vue'
import {Icon as IconifyIcon} from '@iconify/vue' import {Icon as IconifyIcon} from '@iconify/vue'
import {twMerge} from 'tailwind-merge' import {twMerge} from 'tailwind-merge'
import MalioRequiredMark from '../shared/RequiredMark.vue'
defineOptions({name: 'MalioSelect', inheritAttrs: false}) defineOptions({name: 'MalioSelect', inheritAttrs: false})
@@ -193,6 +195,7 @@ const props = withDefaults(defineProps<{
disabled?: boolean disabled?: boolean
groupClass?: string groupClass?: string
noOptionsText?: string noOptionsText?: string
required?: boolean
}>(), { }>(), {
options: () => [], options: () => [],
emptyOptionLabel: '', emptyOptionLabel: '',
@@ -207,6 +210,7 @@ const props = withDefaults(defineProps<{
disabled: false, disabled: false,
groupClass: '', groupClass: '',
noOptionsText: 'Aucune option disponible', noOptionsText: 'Aucune option disponible',
required: false,
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@@ -25,6 +25,7 @@ type SelectCheckboxProps = {
selectAllLabel?: string selectAllLabel?: string
disabled?: boolean disabled?: boolean
groupClass?: string groupClass?: string
required?: boolean
} }
const SelectCheckboxForTest = SelectCheckbox as DefineComponent<SelectCheckboxProps> const SelectCheckboxForTest = SelectCheckbox as DefineComponent<SelectCheckboxProps>
@@ -235,6 +236,22 @@ describe('MalioSelectCheckbox', () => {
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success')
}) })
it('affiche l\'astérisque quand required est vrai', () => {
const wrapper = mount(SelectCheckboxForTest, {
props: {modelValue: [], label: 'Champ', required: true},
})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n\'affiche pas l\'astérisque par défaut', () => {
const wrapper = mount(SelectCheckboxForTest, {
props: {modelValue: [], label: 'Champ'},
})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => { it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => {
const wrapper = mount(SelectCheckboxForTest, { const wrapper = mount(SelectCheckboxForTest, {
props: {modelValue: [], options}, props: {modelValue: [], options},
@@ -38,6 +38,7 @@
:aria-controls="listboxId" :aria-controls="listboxId"
:aria-invalid="hasError" :aria-invalid="hasError"
:aria-describedby="describedBy" :aria-describedby="describedBy"
:aria-required="required || undefined"
:disabled="disabled" :disabled="disabled"
@click="toggle" @click="toggle"
> >
@@ -59,7 +60,7 @@
]" ]"
:style="labelTransformStyle" :style="labelTransformStyle"
> >
{{ label }} {{ label }}<MalioRequiredMark v-if="required" />
</label> </label>
<div <div
@@ -221,6 +222,7 @@ import {computed, onBeforeUnmount, onMounted, ref, useId, nextTick} from 'vue'
import {Icon as IconifyIcon} from '@iconify/vue' import {Icon as IconifyIcon} from '@iconify/vue'
import {twMerge} from 'tailwind-merge' import {twMerge} from 'tailwind-merge'
import Checkbox from '../checkbox/Checkbox.vue' import Checkbox from '../checkbox/Checkbox.vue'
import MalioRequiredMark from '../shared/RequiredMark.vue'
defineOptions({name: 'MalioSelectCheckbox', inheritAttrs: false}) defineOptions({name: 'MalioSelectCheckbox', inheritAttrs: false})
@@ -246,6 +248,7 @@ const props = withDefaults(defineProps<{
disabled?: boolean disabled?: boolean
groupClass?: string groupClass?: string
noOptionsText?: string noOptionsText?: string
required?: boolean
}>(), { }>(), {
options: () => [], options: () => [],
emptyOptionLabel: '', emptyOptionLabel: '',
@@ -263,6 +266,7 @@ const props = withDefaults(defineProps<{
disabled: false, disabled: false,
groupClass: '', groupClass: '',
noOptionsText: 'Aucune option disponible', noOptionsText: 'Aucune option disponible',
required: false,
}) })
const emit = defineEmits<{ const emit = defineEmits<{