feat(ui) : état readonly visuel sur InputUpload (+ prop readonly)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ type InputUploadProps = {
|
|||||||
labelClass?: string
|
labelClass?: string
|
||||||
groupClass?: string
|
groupClass?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
hint?: string
|
hint?: string
|
||||||
error?: string
|
error?: string
|
||||||
success?: string
|
success?: string
|
||||||
@@ -204,4 +205,34 @@ describe('MalioInputUpload', () => {
|
|||||||
|
|
||||||
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
|
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
const field = wrapper.get('input[type="text"]')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('grow-height')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
const label = wrapper.get('label')
|
||||||
|
expect(label.classes()).not.toContain('peer-focus:text-m-primary')
|
||||||
|
expect(label.classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir + icône noire', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true, modelValue: 'fichier.pdf'})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly empêche l\'ouverture du sélecteur de fichier', async () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
// openFilePicker doit être un no-op : cliquer le champ ne déclenche pas l'input file caché.
|
||||||
|
// Vérifie au minimum que le champ visible reste readonly et qu'aucune erreur n'est levée.
|
||||||
|
await wrapper.get('input[type="text"]').trigger('click')
|
||||||
|
expect(wrapper.get('input[type="text"]').attributes('readonly')).toBeDefined()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const props = withDefaults(
|
|||||||
labelClass?: string
|
labelClass?: string
|
||||||
groupClass?: string
|
groupClass?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
hint?: string
|
hint?: string
|
||||||
error?: string
|
error?: string
|
||||||
success?: string
|
success?: string
|
||||||
@@ -100,6 +101,7 @@ const props = withDefaults(
|
|||||||
labelClass: '',
|
labelClass: '',
|
||||||
groupClass: '',
|
groupClass: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
hint: '',
|
hint: '',
|
||||||
error: '',
|
error: '',
|
||||||
success: '',
|
success: '',
|
||||||
@@ -118,10 +120,16 @@ const fileInputRef = ref<HTMLInputElement | null>(null)
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-upload-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-upload-${generatedId}`)
|
||||||
const isControlled = computed(() => props.modelValue !== undefined)
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
const currentDisplayValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
const currentDisplayValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || currentDisplayValue.value.length > 0)
|
|
||||||
const hasError = computed(() => !!props.error)
|
const hasError = computed(() => !!props.error)
|
||||||
const hasSuccess = computed(() => !!props.success)
|
const hasSuccess = computed(() => !!props.success)
|
||||||
const isFilled = computed(() => currentDisplayValue.value.trim().length > 0)
|
const isFilled = computed(() => currentDisplayValue.value.trim().length > 0)
|
||||||
|
const disabled = computed(() => props.disabled)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || currentDisplayValue.value.length > 0,
|
||||||
|
)
|
||||||
const mergedGroupClass = computed(() =>
|
const mergedGroupClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'relative flex h-12 w-full items-center',
|
'relative flex h-12 w-full items-center',
|
||||||
@@ -130,16 +138,21 @@ const mergedGroupClass = computed(() =>
|
|||||||
)
|
)
|
||||||
const mergedInputClass = computed(() =>
|
const mergedInputClass = computed(() =>
|
||||||
twMerge(
|
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 text-lg rounded-md',
|
isReadonly.value
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
? 'floating-input peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md'
|
||||||
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-pointer',
|
: 'floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md',
|
||||||
|
isReadonly.value
|
||||||
|
? 'border-black cursor-default'
|
||||||
|
: isFilled.value ? 'border-black' : 'border-m-muted',
|
||||||
|
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : '',
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger'
|
? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
|
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
|
||||||
: 'focus:border-m-primary',
|
: isReadonly.value ? '' : 'focus:border-m-primary',
|
||||||
props.displayIcon ? '!pr-10' : '',
|
props.displayIcon ? '!pr-10' : '',
|
||||||
'focus:pl-[11px]',
|
isReadonly.value ? '' : 'focus:pl-[11px]',
|
||||||
|
!isReadonly.value && !disabled.value ? 'cursor-pointer' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -154,6 +167,8 @@ const mergedLabelClass = computed(() =>
|
|||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: disabled.value
|
: disabled.value
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
|
: isReadonly.value
|
||||||
|
? isFilled.value ? 'text-black' : 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
@@ -173,7 +188,7 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const openFilePicker = () => {
|
const openFilePicker = () => {
|
||||||
if (props.disabled) return
|
if (props.disabled || props.readonly) return
|
||||||
fileInputRef.value?.click()
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,12 +205,11 @@ const onFileChange = (event: Event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = computed(() => props.disabled)
|
|
||||||
|
|
||||||
const iconStateClass = computed(() => {
|
const iconStateClass = computed(() => {
|
||||||
if (hasError.value) return 'text-m-danger'
|
if (hasError.value) return 'text-m-danger'
|
||||||
if (hasSuccess.value) return 'text-m-success'
|
if (hasSuccess.value) return 'text-m-success'
|
||||||
if (disabled.value) return 'text-m-muted'
|
if (disabled.value) return 'text-m-muted'
|
||||||
|
if (isReadonly.value) return isFilled.value ? 'text-black' : 'text-m-muted'
|
||||||
if (isFocused.value) return 'text-m-primary'
|
if (isFocused.value) return 'text-m-primary'
|
||||||
if (isFilled.value) return 'text-black'
|
if (isFilled.value) return 'text-black'
|
||||||
return 'text-m-muted'
|
return 'text-m-muted'
|
||||||
|
|||||||
Reference in New Issue
Block a user