From 289ff036d210736df4a1a833bb9666a0c61f4d7a Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 3 Jun 2026 15:12:15 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui)=20:=20readonly=20InputUpload=20?= =?UTF-8?q?=E2=80=94=20drop=20peer-focus=20float=20+=20idiome=20grow-heigh?= =?UTF-8?q?t/cursor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/components/malio/input/InputUpload.test.ts | 8 ++++---- app/components/malio/input/InputUpload.vue | 16 +++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/components/malio/input/InputUpload.test.ts b/app/components/malio/input/InputUpload.test.ts index d6239b8..4b9cdc3 100644 --- a/app/components/malio/input/InputUpload.test.ts +++ b/app/components/malio/input/InputUpload.test.ts @@ -1,4 +1,4 @@ -import {describe, expect, it} from 'vitest' +import {describe, expect, it, vi} from 'vitest' import {mount} from '@vue/test-utils' import type {DefineComponent} from 'vue' import { Icon as IconifyIcon } from '@iconify/vue' @@ -230,9 +230,9 @@ describe('MalioInputUpload', () => { 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. + const fileInput = wrapper.get('input[type="file"]').element as HTMLInputElement + const clickSpy = vi.spyOn(fileInput, 'click') await wrapper.get('input[type="text"]').trigger('click') - expect(wrapper.get('input[type="text"]').attributes('readonly')).toBeDefined() + expect(clickSpy).not.toHaveBeenCalled() }) }) diff --git a/app/components/malio/input/InputUpload.vue b/app/components/malio/input/InputUpload.vue index 90b0af8..d23b2b6 100644 --- a/app/components/malio/input/InputUpload.vue +++ b/app/components/malio/input/InputUpload.vue @@ -138,13 +138,12 @@ const mergedGroupClass = computed(() => ) const mergedInputClass = computed(() => twMerge( + '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 cursor-pointer', + isReadonly.value ? '' : 'grow-height', isReadonly.value - ? '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' - : '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' + ? 'border-black' : isFilled.value ? 'border-black' : 'border-m-muted', - disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : '', + disabled.value ? 'text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : '', hasError.value ? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger' : hasSuccess.value @@ -152,7 +151,8 @@ const mergedInputClass = computed(() => : isReadonly.value ? '' : 'focus:border-m-primary', props.displayIcon ? '!pr-10' : '', isReadonly.value ? '' : 'focus:pl-[11px]', - !isReadonly.value && !disabled.value ? 'cursor-pointer' : '', + isReadonly.value ? 'cursor-default' : '', + disabled.value ? 'cursor-not-allowed' : '', props.inputClass, ), ) @@ -160,7 +160,9 @@ 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' : '', + shouldFloatLabel.value + ? `-translate-y-[1.25rem] scale-90${isReadonly.value ? '' : ' peer-focus:-translate-y-[1.55rem]'}` + : '', hasError.value ? 'text-m-danger' : hasSuccess.value