[#MUI-9] Ajout composant upload (#14)
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #14 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #14.
This commit is contained in:
88
.playground/pages/composant/inputUpload.vue
Normal file
88
.playground/pages/composant/inputUpload.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Simple</h2>
|
||||
<MalioInputUpload label="Fichier" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Avec label et v-model</h2>
|
||||
<MalioInputUpload
|
||||
v-model="uploadValue"
|
||||
label="Téléverser un document"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-gray-500">Valeur : {{ uploadValue || '(aucun)' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Avec accept (PDF)</h2>
|
||||
<MalioInputUpload
|
||||
label="Document PDF"
|
||||
accept=".pdf"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Désactivé</h2>
|
||||
<MalioInputUpload
|
||||
model-value="document.pdf"
|
||||
disabled
|
||||
label="Fichier"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Avec hint</h2>
|
||||
<MalioInputUpload
|
||||
label="Fichier"
|
||||
hint="Formats acceptés : PDF, DOC, DOCX"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Erreur</h2>
|
||||
<MalioInputUpload
|
||||
model-value="image.bmp"
|
||||
label="Fichier"
|
||||
error="Format non supporté"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Succès</h2>
|
||||
<MalioInputUpload
|
||||
model-value="rapport.pdf"
|
||||
label="Fichier"
|
||||
success="Fichier valide"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Validation dynamique</h2>
|
||||
<MalioInputUpload
|
||||
v-model="dynamicUpload"
|
||||
label="Document PDF"
|
||||
accept=".pdf"
|
||||
hint="Seuls les fichiers PDF sont acceptés"
|
||||
:error="dynamicError"
|
||||
:success="dynamicSuccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const uploadValue = ref('')
|
||||
const dynamicUpload = ref('')
|
||||
|
||||
const dynamicError = computed(() => {
|
||||
if (!dynamicUpload.value) return ''
|
||||
return dynamicUpload.value.endsWith('.pdf') ? '' : 'Seuls les fichiers PDF sont acceptés'
|
||||
})
|
||||
const dynamicSuccess = computed(() => {
|
||||
if (!dynamicUpload.value) return ''
|
||||
return dynamicUpload.value.endsWith('.pdf') ? 'Fichier PDF valide' : ''
|
||||
})
|
||||
</script>
|
||||
@@ -16,6 +16,7 @@ Liste des évolutions de la librairie Malio layer UI
|
||||
* [#407] Création d'un composant time
|
||||
* Création d'un composant textarea
|
||||
* [#MUI-8] Création d'un composant mot de passe
|
||||
* [#MUI-9] Création d'un composant upload
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
175
app/components/malio/InputUpload.test.ts
Normal file
175
app/components/malio/InputUpload.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import {describe, expect, it} from 'vitest'
|
||||
import {mount} from '@vue/test-utils'
|
||||
import type {DefineComponent} from 'vue'
|
||||
import { Icon as IconifyIcon } from '@iconify/vue'
|
||||
import InputUpload from './InputUpload.vue'
|
||||
|
||||
type InputUploadProps = {
|
||||
id?: string
|
||||
label?: string
|
||||
modelValue?: string | null
|
||||
inputClass?: string
|
||||
labelClass?: string
|
||||
groupClass?: string
|
||||
disabled?: boolean
|
||||
hint?: string
|
||||
error?: string
|
||||
success?: string
|
||||
displayIcon?: boolean
|
||||
accept?: string
|
||||
}
|
||||
|
||||
const InputUploadForTest = InputUpload as DefineComponent<InputUploadProps>
|
||||
|
||||
const mountComponent = (props: InputUploadProps = {}) =>
|
||||
mount(InputUploadForTest, {
|
||||
props,
|
||||
global: {
|
||||
stubs: {
|
||||
IconifyIcon: {
|
||||
template: '<span data-test="icon" v-bind="$attrs" />',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe('MalioInputUpload', () => {
|
||||
it('renders the initial display value', () => {
|
||||
const wrapper = mountComponent({modelValue: 'document.pdf'})
|
||||
|
||||
expect(wrapper.get('input[type="text"]').element.value).toBe('document.pdf')
|
||||
})
|
||||
|
||||
it('renders the label text', () => {
|
||||
const wrapper = mountComponent({label: 'Téléverser un fichier'})
|
||||
|
||||
expect(wrapper.get('label').text()).toBe('Téléverser un fichier')
|
||||
})
|
||||
|
||||
it('has a hidden file input', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('input[type="file"]').exists()).toBe(true)
|
||||
expect(wrapper.find('input[type="file"]').classes()).toContain('hidden')
|
||||
})
|
||||
|
||||
it('text input is readonly', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.get('input[type="text"]').attributes('readonly')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders icon by default', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.find('[data-test="icon"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not render icon when displayIcon is false', () => {
|
||||
const wrapper = mountComponent({displayIcon: false})
|
||||
|
||||
expect(wrapper.find('[data-test="icon"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the correct upload icon', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
const iconComponent = wrapper.findComponent(IconifyIcon)
|
||||
expect(iconComponent.props('icon')).toBe('mdi:cloud-arrow-up-outline')
|
||||
})
|
||||
|
||||
it('emits update:modelValue when a file is selected', async () => {
|
||||
const wrapper = mountComponent({modelValue: ''})
|
||||
const fileInput = wrapper.find('input[type="file"]')
|
||||
const file = new File(['content'], 'test.pdf', {type: 'application/pdf'})
|
||||
|
||||
Object.defineProperty(fileInput.element, 'files', {
|
||||
value: [file],
|
||||
})
|
||||
await fileInput.trigger('change')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['test.pdf'])
|
||||
})
|
||||
|
||||
it('emits file-selected with the File object when a file is selected', async () => {
|
||||
const wrapper = mountComponent({modelValue: ''})
|
||||
const fileInput = wrapper.find('input[type="file"]')
|
||||
const file = new File(['content'], 'test.pdf', {type: 'application/pdf'})
|
||||
|
||||
Object.defineProperty(fileInput.element, 'files', {
|
||||
value: [file],
|
||||
})
|
||||
await fileInput.trigger('change')
|
||||
|
||||
expect(wrapper.emitted('file-selected')?.[0]).toEqual([file])
|
||||
})
|
||||
|
||||
it('sets disabled on both inputs when disabled is true', () => {
|
||||
const wrapper = mountComponent({disabled: true})
|
||||
|
||||
expect(wrapper.get('input[type="text"]').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.get('input[type="file"]').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.get('input[type="text"]').classes()).toContain('cursor-not-allowed')
|
||||
})
|
||||
|
||||
it('shows error message and styles', () => {
|
||||
const wrapper = mountComponent({error: 'Fichier requis'})
|
||||
|
||||
expect(wrapper.get('p.text-m-error').text()).toBe('Fichier requis')
|
||||
expect(wrapper.get('input[type="text"]').classes()).toContain('border-m-error')
|
||||
expect(wrapper.get('input[type="text"]').attributes('aria-invalid')).toBe('true')
|
||||
})
|
||||
|
||||
it('shows error style on icon', () => {
|
||||
const wrapper = mountComponent({error: 'Error'})
|
||||
|
||||
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-error')
|
||||
})
|
||||
|
||||
it('shows success message and styles', () => {
|
||||
const wrapper = mountComponent({success: 'Fichier valide'})
|
||||
|
||||
expect(wrapper.get('p.text-m-success').text()).toBe('Fichier valide')
|
||||
expect(wrapper.get('input[type="text"]').classes()).toContain('border-m-success')
|
||||
})
|
||||
|
||||
it('shows success style on icon', () => {
|
||||
const wrapper = mountComponent({success: 'Success'})
|
||||
|
||||
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success')
|
||||
})
|
||||
|
||||
it('shows hint message', () => {
|
||||
const wrapper = mountComponent({hint: 'PDF uniquement'})
|
||||
|
||||
expect(wrapper.get('p.text-m-muted').text()).toBe('PDF uniquement')
|
||||
})
|
||||
|
||||
it('links label to input via for/id', () => {
|
||||
const wrapper = mountComponent({id: 'upload', label: 'Fichier'})
|
||||
|
||||
expect(wrapper.get('input[type="text"]').attributes('id')).toBe('upload')
|
||||
expect(wrapper.get('label').attributes('for')).toBe('upload')
|
||||
})
|
||||
|
||||
it('generates an id when missing and reuses it on label', () => {
|
||||
const wrapper = mountComponent({label: 'Fichier'})
|
||||
|
||||
const inputId = wrapper.get('input[type="text"]').attributes('id')
|
||||
|
||||
expect(inputId?.startsWith('malio-input-upload-')).toBe(true)
|
||||
expect(wrapper.get('label').attributes('for')).toBe(inputId)
|
||||
})
|
||||
|
||||
it('aria-invalid is false when no error', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.get('input[type="text"]').attributes('aria-invalid')).toBe('false')
|
||||
})
|
||||
|
||||
it('passes accept attribute to file input', () => {
|
||||
const wrapper = mountComponent({accept: '.pdf,.doc'})
|
||||
|
||||
expect(wrapper.get('input[type="file"]').attributes('accept')).toBe('.pdf,.doc')
|
||||
})
|
||||
})
|
||||
209
app/components/malio/InputUpload.vue
Normal file
209
app/components/malio/InputUpload.vue
Normal file
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div
|
||||
:class="mergedGroupClass"
|
||||
>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
class="hidden"
|
||||
:disabled="disabled"
|
||||
@change="onFileChange"
|
||||
>
|
||||
|
||||
<input
|
||||
:id="inputId"
|
||||
:class="mergedInputClass"
|
||||
:disabled="disabled"
|
||||
:value="currentDisplayValue"
|
||||
:readonly="true"
|
||||
:aria-invalid="!!error"
|
||||
:aria-describedby="describedBy"
|
||||
v-bind="attrs"
|
||||
placeholder="_"
|
||||
type="text"
|
||||
@click="openFilePicker"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
>
|
||||
|
||||
<label
|
||||
v-if="label"
|
||||
:for="inputId"
|
||||
:class="mergedLabelClass"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
<IconifyIcon
|
||||
v-if="displayIcon"
|
||||
icon="mdi:cloud-arrow-up-outline"
|
||||
:width="24"
|
||||
:height="24"
|
||||
data-test="icon"
|
||||
:class="[
|
||||
hasError
|
||||
? 'text-m-error'
|
||||
: hasSuccess
|
||||
? 'text-m-success' : 'text-m-muted',
|
||||
'pointer-events-none absolute right-[10px] top-1/2 -translate-y-1/2',
|
||||
]"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
? 'text-m-error'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, ref, useAttrs, useId} from 'vue'
|
||||
import { Icon as IconifyIcon } from '@iconify/vue'
|
||||
import {twMerge} from 'tailwind-merge'
|
||||
|
||||
defineOptions({name: 'MalioInputUpload', inheritAttrs: false})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string
|
||||
label?: string
|
||||
modelValue?: string | null | undefined
|
||||
inputClass?: string
|
||||
labelClass?: string
|
||||
groupClass?: string
|
||||
disabled?: boolean
|
||||
hint?: string
|
||||
error?: string
|
||||
success?: string
|
||||
displayIcon?: boolean
|
||||
accept?: string
|
||||
}>(),
|
||||
{
|
||||
id: '',
|
||||
modelValue: undefined,
|
||||
label: '',
|
||||
inputClass: '',
|
||||
labelClass: '',
|
||||
groupClass: '',
|
||||
disabled: false,
|
||||
hint: '',
|
||||
error: '',
|
||||
success: '',
|
||||
displayIcon: true,
|
||||
accept: '',
|
||||
},
|
||||
)
|
||||
|
||||
const attrs = useAttrs()
|
||||
const generatedId = useId()
|
||||
const localValue = ref('')
|
||||
const isFocused = ref(false)
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const inputId = computed(() => props.id?.toString() || `malio-input-upload-${generatedId}`)
|
||||
const isControlled = computed(() => props.modelValue !== undefined)
|
||||
const currentDisplayValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||
const shouldFloatLabel = computed(() => isFocused.value || currentDisplayValue.value.length > 0)
|
||||
const hasError = computed(() => !!props.error)
|
||||
const hasSuccess = computed(() => !!props.success)
|
||||
const isFilled = computed(() => currentDisplayValue.value.trim().length > 0)
|
||||
const mergedGroupClass = computed(() =>
|
||||
twMerge(
|
||||
'relative mt-4 flex h-12 w-full items-center',
|
||||
props.groupClass,
|
||||
),
|
||||
)
|
||||
const mergedInputClass = computed(() =>
|
||||
twMerge(
|
||||
'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent focus:border-2 text-lg rounded-md',
|
||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
||||
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-pointer',
|
||||
hasError.value
|
||||
? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
|
||||
: hasSuccess.value
|
||||
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
|
||||
: 'focus:border-m-primary',
|
||||
props.displayIcon ? '!pr-10' : '',
|
||||
'focus:pl-[11px]',
|
||||
props.inputClass,
|
||||
),
|
||||
)
|
||||
const mergedLabelClass = computed(() =>
|
||||
twMerge(
|
||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||
'left-3',
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-error'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
const describedBy = computed(() => {
|
||||
const ids: string[] = []
|
||||
if (props.hint && !hasSuccess.value && !hasError.value) ids.push(`${inputId.value}-hint`)
|
||||
if (hasError.value) ids.push(`${inputId.value}-error`)
|
||||
if (hasSuccess.value && !hasError.value) ids.push(`${inputId.value}-success`)
|
||||
return ids.length ? ids.join(' ') : undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void
|
||||
(event: 'file-selected', file: File): void
|
||||
}>()
|
||||
|
||||
const openFilePicker = () => {
|
||||
if (props.disabled) return
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
const onFileChange = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (file) {
|
||||
const fileName = file.name
|
||||
if (!isControlled.value) {
|
||||
localValue.value = fileName
|
||||
}
|
||||
emit('update:modelValue', fileName)
|
||||
emit('file-selected', file)
|
||||
}
|
||||
}
|
||||
|
||||
const disabled = computed(() => props.disabled)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.floating-label {
|
||||
background: white;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.grow-height {
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
||||
}
|
||||
|
||||
.grow-height:focus {
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.grow-height { transition: none; }
|
||||
}
|
||||
</style>
|
||||
236
app/story/inputUpload.story.vue
Normal file
236
app/story/inputUpload.story.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<Story title="Input/Upload">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Simple</h2>
|
||||
<MalioInputUpload
|
||||
v-model="simpleValue"
|
||||
label="Fichier"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Sans icône</h2>
|
||||
<MalioInputUpload
|
||||
v-model="noIconValue"
|
||||
label="Fichier"
|
||||
:display-icon="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Avec hint</h2>
|
||||
<MalioInputUpload
|
||||
v-model="hintValue"
|
||||
label="Fichier"
|
||||
hint="Formats acceptés : PDF, DOC, DOCX"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Désactivé</h2>
|
||||
<MalioInputUpload
|
||||
v-model="disabledValue"
|
||||
label="Fichier"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Readonly (avec fichier)</h2>
|
||||
<MalioInputUpload
|
||||
v-model="readonlyValue"
|
||||
label="Fichier"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Erreur</h2>
|
||||
<MalioInputUpload
|
||||
v-model="errorValue"
|
||||
label="Fichier"
|
||||
error="Format non supporté"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Succès</h2>
|
||||
<MalioInputUpload
|
||||
v-model="successValue"
|
||||
label="Fichier"
|
||||
success="Fichier valide"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Avec accept (PDF)</h2>
|
||||
<MalioInputUpload
|
||||
v-model="acceptValue"
|
||||
label="Document PDF"
|
||||
accept=".pdf"
|
||||
hint="Seuls les fichiers PDF sont acceptés"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<docs lang="md">
|
||||
# MalioInputUpload
|
||||
|
||||
Composant input d'upload de fichier avec label flottant, icône cloud,
|
||||
affichage du nom du fichier sélectionné, états visuels (erreur / succès)
|
||||
et accessibilité.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Props détaillées
|
||||
|
||||
### id
|
||||
|
||||
- Type: string
|
||||
- Description: Identifiant HTML de l'input.
|
||||
- Comportement: Si non fourni, un id unique est généré automatiquement.
|
||||
|
||||
### label
|
||||
|
||||
- Type: string
|
||||
- Description: Texte affiché comme label flottant.
|
||||
- Comportement: Si absent, aucun label n'est rendu.
|
||||
|
||||
### modelValue
|
||||
|
||||
- Type: string | null | undefined
|
||||
- Description: Nom du fichier sélectionné (valeur contrôlée).
|
||||
- Comportement:
|
||||
- Si défini → composant contrôlé (v-model).
|
||||
- Sinon → gestion interne de l'état.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Apparence & Style
|
||||
|
||||
### inputClass
|
||||
|
||||
- Type: string
|
||||
- Description: Classes CSS appliquées à l'input texte.
|
||||
|
||||
### labelClass
|
||||
|
||||
- Type: string
|
||||
- Description: Classes CSS appliquées au label.
|
||||
|
||||
### groupClass
|
||||
|
||||
- Type: string
|
||||
- Description: Classes CSS appliquées au conteneur.
|
||||
|
||||
### displayIcon
|
||||
|
||||
- Type: boolean
|
||||
- Défaut: true
|
||||
- Description: Affiche ou masque l'icône d'upload.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Validation & Contraintes
|
||||
|
||||
### disabled
|
||||
|
||||
- Type: boolean
|
||||
- Description: Désactive complètement le champ.
|
||||
|
||||
### accept
|
||||
|
||||
- Type: string
|
||||
- Description: Types de fichiers acceptés (ex: `.pdf,.doc`).
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## États & Messages
|
||||
|
||||
### hint
|
||||
|
||||
- Type: string
|
||||
- Description: Message d'aide affiché sous le champ.
|
||||
|
||||
### error
|
||||
|
||||
- Type: string
|
||||
- Description: Message d'erreur.
|
||||
- Effet:
|
||||
- Active l'état visuel erreur.
|
||||
- aria-invalid=true
|
||||
- Prioritaire sur success et hint.
|
||||
|
||||
### success
|
||||
|
||||
- Type: string
|
||||
- Description: Message de succès.
|
||||
- Effet:
|
||||
- Actif uniquement si error est absent.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Icône
|
||||
|
||||
- `mdi:cloud-arrow-up-outline` : icône d'upload affichée à droite.
|
||||
|
||||
### Couleur de l'icône
|
||||
|
||||
- `text-m-muted` par défaut.
|
||||
- `text-m-error` si la prop `error` est renseignée.
|
||||
- `text-m-success` si la prop `success` est renseignée.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Comportement
|
||||
|
||||
- Au clic sur l'input texte, le sélecteur de fichier natif s'ouvre.
|
||||
- Le nom du fichier sélectionné est affiché dans l'input.
|
||||
- L'input texte est en readonly — la saisie manuelle n'est pas autorisée.
|
||||
|
||||
## Priorité visuelle
|
||||
|
||||
1. error
|
||||
2. success
|
||||
3. neutre
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Accessibilité
|
||||
|
||||
- aria-invalid est activé si error existe.
|
||||
- aria-describedby référence dynamiquement le message affiché.
|
||||
- Fonctionne avec ou sans v-model.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
## Events
|
||||
|
||||
### update:modelValue
|
||||
|
||||
- Émis quand un fichier est sélectionné (valeur = nom du fichier).
|
||||
- Permet l'utilisation avec v-model.
|
||||
|
||||
### file-selected
|
||||
|
||||
- Émis quand un fichier est sélectionné (valeur = objet File).
|
||||
- Permet d'accéder au fichier pour l'upload.
|
||||
|
||||
</docs>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import MalioInputUpload from '../components/malio/InputUpload.vue'
|
||||
|
||||
const simpleValue = ref('')
|
||||
const noIconValue = ref('')
|
||||
const hintValue = ref('')
|
||||
const disabledValue = ref('document.pdf')
|
||||
const readonlyValue = ref('rapport.pdf')
|
||||
const errorValue = ref('image.bmp')
|
||||
const successValue = ref('rapport.pdf')
|
||||
const acceptValue = ref('')
|
||||
</script>
|
||||
Reference in New Issue
Block a user