feat(ui) : état readonly visuel sur pickers date/heure
Bordure noire forcée (même vide), suppression du bleu focus/primary, label et icône en text-black si rempli sinon text-m-muted, float piloté par isFilled uniquement en readonly. Bouton clear et astérisque inchangés. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,32 @@ describe('MalioDate', () => {
|
|||||||
await wrapper.get('[data-test="date-input"]').trigger('click')
|
await wrapper.get('[data-test="date-input"]').trigger('click')
|
||||||
expect(wrapper.find('[data-test="popover"]').exists()).toBe(false)
|
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é', () => {
|
describe('accessibilité', () => {
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ const inputId = computed(() => props.id?.toString() || `malio-date-${generatedId
|
|||||||
const hasError = computed(() => !!props.error)
|
const hasError = computed(() => !!props.error)
|
||||||
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
||||||
const isFilled = computed(() => props.displayValue.length > 0)
|
const isFilled = computed(() => props.displayValue.length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
const showClear = computed(() =>
|
const showClear = computed(() =>
|
||||||
props.clearable && isFilled.value && !props.disabled && !props.readonly,
|
props.clearable && isFilled.value && !props.disabled && !props.readonly,
|
||||||
)
|
)
|
||||||
@@ -195,13 +196,15 @@ const mergedGroupClass = computed(() =>
|
|||||||
const mergedInputClass = computed(() =>
|
const mergedInputClass = computed(() =>
|
||||||
twMerge(
|
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',
|
'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' : '',
|
props.disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : '',
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'border-m-danger'
|
? 'border-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'border-m-success'
|
? 'border-m-success'
|
||||||
: 'focus:border-m-primary',
|
: isReadonly.value ? '' : 'focus:border-m-primary',
|
||||||
isOpen.value ? 'border-m-primary !py-[9px] !rounded-b-none' : '',
|
isOpen.value ? 'border-m-primary !py-[9px] !rounded-b-none' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
),
|
),
|
||||||
@@ -210,14 +213,16 @@ const mergedInputClass = computed(() =>
|
|||||||
const mergedLabelClass = computed(() =>
|
const mergedLabelClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'floating-label absolute left-3 top-2 mt-[5px] inline-block origin-left font-medium text-sm transition-transform duration-150',
|
'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
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: isOpen.value
|
: isReadonly.value
|
||||||
? 'text-m-primary'
|
? isFilled.value ? 'text-black' : 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted text-black',
|
: isOpen.value
|
||||||
|
? 'text-m-primary'
|
||||||
|
: 'peer-placeholder-shown:text-m-muted text-black',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -225,6 +230,7 @@ const mergedLabelClass = computed(() =>
|
|||||||
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 (isReadonly.value) return isFilled.value ? 'text-black' : 'text-m-muted'
|
||||||
if (isOpen.value) return 'text-m-primary'
|
if (isOpen.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'
|
||||||
|
|||||||
@@ -83,4 +83,30 @@ describe('MalioTimePicker', () => {
|
|||||||
const wrapper = mountPicker({label: 'Champ'})
|
const wrapper = mountPicker({label: 'Champ'})
|
||||||
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
|
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ const hasError = computed(() => !!props.error)
|
|||||||
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
||||||
const displayValue = computed(() => currentValue.value ?? '')
|
const displayValue = computed(() => currentValue.value ?? '')
|
||||||
const isFilled = computed(() => displayValue.value.length > 0)
|
const isFilled = computed(() => displayValue.value.length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
const wheelsValue = computed(() => currentValue.value || '00:00')
|
const wheelsValue = computed(() => currentValue.value || '00:00')
|
||||||
const showClear = computed(() =>
|
const showClear = computed(() =>
|
||||||
props.clearable && isFilled.value && !props.disabled && !props.readonly,
|
props.clearable && isFilled.value && !props.disabled && !props.readonly,
|
||||||
@@ -192,13 +193,15 @@ const mergedGroupClass = computed(() =>
|
|||||||
const mergedInputClass = computed(() =>
|
const mergedInputClass = computed(() =>
|
||||||
twMerge(
|
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',
|
'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' : '',
|
props.disabled ? 'cursor-not-allowed border-m-muted text-black/60' : '',
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'border-m-danger'
|
? 'border-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'border-m-success'
|
? 'border-m-success'
|
||||||
: 'focus:border-m-primary',
|
: isReadonly.value ? '' : 'focus:border-m-primary',
|
||||||
isOpen.value ? 'border-m-primary !rounded-b-none !py-[9px]' : '',
|
isOpen.value ? 'border-m-primary !rounded-b-none !py-[9px]' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
),
|
),
|
||||||
@@ -207,14 +210,16 @@ const mergedInputClass = computed(() =>
|
|||||||
const mergedLabelClass = computed(() =>
|
const mergedLabelClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'floating-label absolute left-3 top-2 mt-[5px] inline-block origin-left text-sm font-medium transition-transform duration-150',
|
'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
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: isOpen.value
|
: isReadonly.value
|
||||||
? 'text-m-primary'
|
? isFilled.value ? 'text-black' : 'text-m-muted'
|
||||||
: 'text-black peer-placeholder-shown:text-m-muted',
|
: isOpen.value
|
||||||
|
? 'text-m-primary'
|
||||||
|
: 'text-black peer-placeholder-shown:text-m-muted',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -222,6 +227,7 @@ const mergedLabelClass = computed(() =>
|
|||||||
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 (isReadonly.value) return isFilled.value ? 'text-black' : 'text-m-muted'
|
||||||
if (isOpen.value) return 'text-m-primary'
|
if (isOpen.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