diff --git a/app/components/malio/date/Date.test.ts b/app/components/malio/date/Date.test.ts index e4fe4d5..9c30a1b 100644 --- a/app/components/malio/date/Date.test.ts +++ b/app/components/malio/date/Date.test.ts @@ -185,6 +185,32 @@ describe('MalioDate', () => { await wrapper.get('[data-test="date-input"]').trigger('click') expect(wrapper.find('[data-test="popover"]').exists()).toBe(false) }) + + it('readonly vide : bordure noire sans bleu', () => { + const wrapper = mountDate({readonly: true}) + const input = wrapper.get('[data-test="date-input"]') + expect(input.classes()).toContain('border-black') + expect(input.classes()).not.toContain('border-m-muted') + expect(input.classes()).not.toContain('focus:border-m-primary') + }) + + it('readonly vide : label muted sans bleu', () => { + const wrapper = mountDate({readonly: true, label: 'Date'}) + const label = wrapper.get('label') + expect(label.classes()).toContain('text-m-muted') + expect(label.classes()).not.toContain('text-m-primary') + }) + + it('readonly rempli : label et icône en noir, bordure noire', () => { + const wrapper = mountDate({readonly: true, label: 'Date', modelValue: '2026-05-19'}) + const input = wrapper.get('[data-test="date-input"]') + const label = wrapper.get('label') + const icon = wrapper.get('[data-test="calendar-icon"]') + expect(input.classes()).toContain('border-black') + expect(input.classes()).not.toContain('focus:border-m-primary') + expect(label.classes()).toContain('text-black') + expect(icon.classes()).toContain('text-black') + }) }) describe('accessibilité', () => { diff --git a/app/components/malio/date/internal/CalendarField.vue b/app/components/malio/date/internal/CalendarField.vue index b777794..1a1af93 100644 --- a/app/components/malio/date/internal/CalendarField.vue +++ b/app/components/malio/date/internal/CalendarField.vue @@ -158,6 +158,7 @@ const inputId = computed(() => props.id?.toString() || `malio-date-${generatedId const hasError = computed(() => !!props.error) const hasSuccess = computed(() => !!props.success && !hasError.value) const isFilled = computed(() => props.displayValue.length > 0) +const isReadonly = computed(() => props.readonly && !props.disabled) const showClear = computed(() => props.clearable && isFilled.value && !props.disabled && !props.readonly, ) @@ -195,13 +196,15 @@ const mergedGroupClass = computed(() => const mergedInputClass = computed(() => twMerge( 'floating-input peer min-h-[40px] w-full cursor-pointer rounded-md border bg-white py-1 pl-3 pr-10 text-lg outline-none transition-[padding] duration-150 placeholder:text-transparent', - isFilled.value ? 'border-black' : 'border-m-muted', + isReadonly.value + ? 'border-black' + : isFilled.value ? 'border-black' : 'border-m-muted', props.disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : '', hasError.value ? 'border-m-danger' : hasSuccess.value ? 'border-m-success' - : 'focus:border-m-primary', + : isReadonly.value ? '' : 'focus:border-m-primary', isOpen.value ? 'border-m-primary !py-[9px] !rounded-b-none' : '', props.inputClass, ), @@ -210,14 +213,16 @@ const mergedInputClass = computed(() => const mergedLabelClass = computed(() => twMerge( 'floating-label absolute left-3 top-2 mt-[5px] inline-block origin-left font-medium text-sm transition-transform duration-150', - (isFilled.value || isOpen.value) ? '-translate-y-[1.25rem] scale-90' : '', + (isReadonly.value ? isFilled.value : (isFilled.value || isOpen.value)) ? '-translate-y-[1.25rem] scale-90' : '', hasError.value ? 'text-m-danger' : hasSuccess.value ? 'text-m-success' - : isOpen.value - ? 'text-m-primary' - : 'peer-placeholder-shown:text-m-muted text-black', + : isReadonly.value + ? isFilled.value ? 'text-black' : 'text-m-muted' + : isOpen.value + ? 'text-m-primary' + : 'peer-placeholder-shown:text-m-muted text-black', props.labelClass, ), ) @@ -225,6 +230,7 @@ const mergedLabelClass = computed(() => const iconStateClass = computed(() => { if (hasError.value) return 'text-m-danger' if (hasSuccess.value) return 'text-m-success' + if (isReadonly.value) return isFilled.value ? 'text-black' : 'text-m-muted' if (isOpen.value) return 'text-m-primary' if (isFilled.value) return 'text-black' return 'text-m-muted' diff --git a/app/components/malio/time/TimePicker.test.ts b/app/components/malio/time/TimePicker.test.ts index 966c2c9..ec53da6 100644 --- a/app/components/malio/time/TimePicker.test.ts +++ b/app/components/malio/time/TimePicker.test.ts @@ -83,4 +83,30 @@ describe('MalioTimePicker', () => { const wrapper = mountPicker({label: 'Champ'}) expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false) }) + + it('readonly vide : bordure noire sans bleu', () => { + const wrapper = mountPicker({readonly: true}) + const input = wrapper.get('[data-test="time-field"]') + expect(input.classes()).toContain('border-black') + expect(input.classes()).not.toContain('border-m-muted') + expect(input.classes()).not.toContain('focus:border-m-primary') + }) + + it('readonly vide : label muted sans bleu', () => { + const wrapper = mountPicker({readonly: true, label: 'Heure'}) + const label = wrapper.get('label') + expect(label.classes()).toContain('text-m-muted') + expect(label.classes()).not.toContain('text-m-primary') + }) + + it('readonly rempli : label et icône en noir, bordure noire', () => { + const wrapper = mountPicker({readonly: true, label: 'Heure', modelValue: '14:30'}) + const input = wrapper.get('[data-test="time-field"]') + const label = wrapper.get('label') + const icon = wrapper.get('[data-test="clock-icon"]') + expect(input.classes()).toContain('border-black') + expect(input.classes()).not.toContain('focus:border-m-primary') + expect(label.classes()).toContain('text-black') + expect(icon.classes()).toContain('text-black') + }) }) diff --git a/app/components/malio/time/TimePicker.vue b/app/components/malio/time/TimePicker.vue index 3cddeb6..4ab484f 100644 --- a/app/components/malio/time/TimePicker.vue +++ b/app/components/malio/time/TimePicker.vue @@ -153,6 +153,7 @@ const hasError = computed(() => !!props.error) const hasSuccess = computed(() => !!props.success && !hasError.value) const displayValue = computed(() => currentValue.value ?? '') const isFilled = computed(() => displayValue.value.length > 0) +const isReadonly = computed(() => props.readonly && !props.disabled) const wheelsValue = computed(() => currentValue.value || '00:00') const showClear = computed(() => props.clearable && isFilled.value && !props.disabled && !props.readonly, @@ -192,13 +193,15 @@ const mergedGroupClass = computed(() => const mergedInputClass = computed(() => twMerge( 'floating-input peer min-h-[40px] w-full cursor-pointer rounded-md border bg-white py-1 pl-3 pr-10 text-lg outline-none transition-[padding] duration-150 placeholder:text-transparent', - isFilled.value ? 'border-black' : 'border-m-muted', + isReadonly.value + ? 'border-black' + : isFilled.value ? 'border-black' : 'border-m-muted', props.disabled ? 'cursor-not-allowed border-m-muted text-black/60' : '', hasError.value ? 'border-m-danger' : hasSuccess.value ? 'border-m-success' - : 'focus:border-m-primary', + : isReadonly.value ? '' : 'focus:border-m-primary', isOpen.value ? 'border-m-primary !rounded-b-none !py-[9px]' : '', props.inputClass, ), @@ -207,14 +210,16 @@ const mergedInputClass = computed(() => const mergedLabelClass = computed(() => twMerge( 'floating-label absolute left-3 top-2 mt-[5px] inline-block origin-left text-sm font-medium transition-transform duration-150', - (isFilled.value || isOpen.value) ? '-translate-y-[1.25rem] scale-90' : '', + (isReadonly.value ? isFilled.value : (isFilled.value || isOpen.value)) ? '-translate-y-[1.25rem] scale-90' : '', hasError.value ? 'text-m-danger' : hasSuccess.value ? 'text-m-success' - : isOpen.value - ? 'text-m-primary' - : 'text-black peer-placeholder-shown:text-m-muted', + : isReadonly.value + ? isFilled.value ? 'text-black' : 'text-m-muted' + : isOpen.value + ? 'text-m-primary' + : 'text-black peer-placeholder-shown:text-m-muted', props.labelClass, ), ) @@ -222,6 +227,7 @@ const mergedLabelClass = computed(() => const iconStateClass = computed(() => { if (hasError.value) return 'text-m-danger' if (hasSuccess.value) return 'text-m-success' + if (isReadonly.value) return isFilled.value ? 'text-black' : 'text-m-muted' if (isOpen.value) return 'text-m-primary' if (isFilled.value) return 'text-black' return 'text-m-muted'