feat(ui) : état readonly visuel sur les inputs floating-label
Applique le traitement readonly canonique (isReadonly, shouldFloatLabel, mergedInputClass sans grow-height, bordure noire fixe, sans focus:border-m-primary, mergedLabelClass sans peer-focus, iconStateClass sans isFocused) sur les 6 composants InputText, InputEmail, InputAmount, InputAutocomplete, InputPassword et InputTextArea. L'œil de InputPassword reste cliquable en readonly. Tests TDD ajoutés (3 cas par fichier). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -334,4 +334,25 @@ describe('MalioInputText', () => {
|
|||||||
|
|
||||||
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
|
||||||
|
const wrapper = mountInput({label: 'Champ', readonly: true})
|
||||||
|
const field = wrapper.get('input')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
expect(field.classes()).not.toContain('grow-height')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu', () => {
|
||||||
|
const wrapper = mountInput({label: 'Champ', readonly: true})
|
||||||
|
expect(wrapper.get('label').classes()).not.toContain('peer-focus:text-m-primary')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir et icône noire', () => {
|
||||||
|
const wrapper = mountInput({label: 'Champ', readonly: true, modelValue: 'hello', iconName: 'mdi:key-outline'})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -184,4 +184,25 @@ describe('MalioInputAmount', () => {
|
|||||||
const wrapper = mountInputAmount({label: 'Champ'})
|
const wrapper = mountInputAmount({label: 'Champ'})
|
||||||
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 = mountInputAmount({label: 'Champ', readonly: true})
|
||||||
|
const field = wrapper.get('input')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
expect(field.classes()).not.toContain('grow-height')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu', () => {
|
||||||
|
const wrapper = mountInputAmount({label: 'Champ', readonly: true})
|
||||||
|
expect(wrapper.get('label').classes()).not.toContain('peer-focus:text-m-primary')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir et icône noire', () => {
|
||||||
|
const wrapper = mountInputAmount({label: 'Champ', readonly: true, modelValue: '12.50'})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,10 +122,15 @@ const isFocused = ref(false)
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-amount-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-amount-${generatedId}`)
|
||||||
const isControlled = computed(() => props.modelValue !== undefined)
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || currentValue.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(() => currentValue.value.trim().length > 0)
|
const isFilled = computed(() => currentValue.value.trim().length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || currentValue.value.length > 0,
|
||||||
|
)
|
||||||
|
|
||||||
const mergedGroupClass = computed(() =>
|
const mergedGroupClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
@@ -135,31 +140,39 @@ 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',
|
'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',
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
isReadonly.value ? '' : 'grow-height',
|
||||||
|
isReadonly.value
|
||||||
|
? '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' : 'cursor-text',
|
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
|
||||||
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',
|
||||||
|
isReadonly.value ? 'cursor-default' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
iconInputPaddingClass.value,
|
iconInputPaddingClass.value,
|
||||||
focusPaddingClass.value,
|
isReadonly.value ? '' : focusPaddingClass.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const mergedLabelClass = computed(() =>
|
const mergedLabelClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||||
labelPositionClass.value,
|
labelPositionClass.value,
|
||||||
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
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: disabled.value
|
: disabled.value
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 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',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -235,6 +248,7 @@ 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 props.iconColor
|
if (disabled.value) return props.iconColor
|
||||||
|
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 props.iconColor
|
return props.iconColor
|
||||||
|
|||||||
@@ -516,4 +516,25 @@ describe('MalioInputAutocomplete', () => {
|
|||||||
expect(inputClasses).not.toContain('!border-b-0')
|
expect(inputClasses).not.toContain('!border-b-0')
|
||||||
expect(inputClasses).toContain('!border-b-transparent')
|
expect(inputClasses).toContain('!border-b-transparent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
const field = wrapper.get('input')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
expect(field.classes()).not.toContain('grow-height')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
expect(wrapper.get('label').classes()).not.toContain('peer-focus:text-m-primary')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir et icône noire', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true, modelValue: 'fr', options, iconName: 'mdi:magnify', iconPosition: 'left'})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
expect(wrapper.get('[data-test="icon-left"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -249,7 +249,12 @@ const hasSelection = computed(() =>
|
|||||||
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(() => inputValue.value.trim().length > 0 || hasSelection.value)
|
const isFilled = computed(() => inputValue.value.trim().length > 0 || hasSelection.value)
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || inputValue.value.length > 0)
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || inputValue.value.length > 0,
|
||||||
|
)
|
||||||
|
|
||||||
const showMinSearch = computed(() =>
|
const showMinSearch = computed(() =>
|
||||||
props.minSearchLength > 0 && inputValue.value.length < props.minSearchLength,
|
props.minSearchLength > 0 && inputValue.value.length < props.minSearchLength,
|
||||||
@@ -311,8 +316,11 @@ const labelPositionClass = 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',
|
'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',
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
isReadonly.value ? '' : 'grow-height',
|
||||||
|
isReadonly.value
|
||||||
|
? 'border-black'
|
||||||
|
: isFilled.value ? 'border-black' : 'border-m-muted',
|
||||||
props.disabled
|
props.disabled
|
||||||
? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted'
|
? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted'
|
||||||
: 'cursor-text',
|
: 'cursor-text',
|
||||||
@@ -320,7 +328,8 @@ const mergedInputClass = computed(() =>
|
|||||||
? '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',
|
||||||
|
isReadonly.value ? 'cursor-default' : '',
|
||||||
isOpen.value ? '!rounded-b-none !border-b-transparent' : '',
|
isOpen.value ? '!rounded-b-none !border-b-transparent' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
iconInputPaddingClass.value,
|
iconInputPaddingClass.value,
|
||||||
@@ -338,7 +347,9 @@ const mergedLabelClass = computed(() =>
|
|||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: props.disabled
|
: props.disabled
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 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',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -347,6 +358,7 @@ 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 (props.disabled) return props.iconColor
|
if (props.disabled) return props.iconColor
|
||||||
|
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 props.iconColor
|
return props.iconColor
|
||||||
@@ -355,6 +367,7 @@ const iconStateClass = computed(() => {
|
|||||||
const chevronColorClass = computed(() => {
|
const chevronColorClass = 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'
|
||||||
|
|||||||
@@ -274,4 +274,25 @@ describe('MalioInputEmail', () => {
|
|||||||
await input.setValue('ab@c.com ')
|
await input.setValue('ab@c.com ')
|
||||||
expect(input.element.value).toBe('ab@c.com')
|
expect(input.element.value).toBe('ab@c.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
const field = wrapper.get('input')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
expect(field.classes()).not.toContain('grow-height')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
expect(wrapper.get('label').classes()).not.toContain('peer-focus:text-m-primary')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir et icône noire', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true, modelValue: 'user@example.com'})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -119,10 +119,15 @@ const isFocused = ref(false)
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-email-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-email-${generatedId}`)
|
||||||
const isControlled = computed(() => props.modelValue !== undefined)
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || currentValue.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(() => currentValue.value.trim().length > 0)
|
const isFilled = computed(() => currentValue.value.trim().length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || currentValue.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',
|
||||||
@@ -131,31 +136,39 @@ 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',
|
'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',
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
isReadonly.value ? '' : 'grow-height',
|
||||||
|
isReadonly.value
|
||||||
|
? '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' : 'cursor-text',
|
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
|
||||||
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',
|
||||||
|
isReadonly.value ? 'cursor-default' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
iconInputPaddingClass.value,
|
iconInputPaddingClass.value,
|
||||||
focusPaddingClass.value,
|
isReadonly.value ? '' : focusPaddingClass.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const mergedLabelClass = computed(() =>
|
const mergedLabelClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||||
labelPositionClass.value,
|
labelPositionClass.value,
|
||||||
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
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: disabled.value
|
: disabled.value
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 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',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -231,6 +244,7 @@ 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 props.iconColor
|
if (disabled.value) return props.iconColor
|
||||||
|
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 props.iconColor
|
return props.iconColor
|
||||||
|
|||||||
@@ -195,4 +195,31 @@ describe('MalioInputPassword', () => {
|
|||||||
|
|
||||||
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
const field = wrapper.get('input')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
expect(field.classes()).not.toContain('grow-height')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
expect(wrapper.get('label').classes()).not.toContain('peer-focus:text-m-primary')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir et icône noire', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true, modelValue: 'secret'})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly : eye toggle reste cliquable', async () => {
|
||||||
|
const wrapper = mountComponent({label: 'Champ', readonly: true})
|
||||||
|
await wrapper.get('[data-test="icon"]').trigger('click')
|
||||||
|
expect(wrapper.get('input').attributes('type')).toBe('text')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -125,10 +125,15 @@ const toggleVisibility = () => {
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-password-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-password-${generatedId}`)
|
||||||
const isControlled = computed(() => props.modelValue !== undefined)
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || currentValue.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(() => currentValue.value.trim().length > 0)
|
const isFilled = computed(() => currentValue.value.trim().length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || currentValue.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',
|
||||||
@@ -137,16 +142,20 @@ 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',
|
'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',
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
isReadonly.value ? '' : 'grow-height',
|
||||||
|
isReadonly.value
|
||||||
|
? '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' : 'cursor-text',
|
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
|
||||||
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',
|
||||||
|
isReadonly.value ? 'cursor-default' : '',
|
||||||
props.displayIcon ? '!pr-10' : '',
|
props.displayIcon ? '!pr-10' : '',
|
||||||
'focus:pl-[11px]',
|
isReadonly.value ? '' : 'focus:pl-[11px]',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -154,14 +163,18 @@ const mergedLabelClass = computed(() =>
|
|||||||
twMerge(
|
twMerge(
|
||||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||||
'left-3',
|
'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
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: disabled.value
|
: disabled.value
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 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',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -192,6 +205,7 @@ 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'
|
||||||
|
|||||||
@@ -128,10 +128,15 @@ const isFocused = ref(false)
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-text-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-text-${generatedId}`)
|
||||||
const isControlled = computed(() => props.modelValue !== undefined)
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || currentValue.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(() => currentValue.value.trim().length > 0)
|
const isFilled = computed(() => currentValue.value.trim().length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || currentValue.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',
|
||||||
@@ -140,31 +145,39 @@ 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',
|
'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',
|
||||||
isFilled.value ? 'border-black' : 'border-m-muted',
|
isReadonly.value ? '' : 'grow-height',
|
||||||
|
isReadonly.value
|
||||||
|
? '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' : 'cursor-text',
|
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
|
||||||
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',
|
||||||
|
isReadonly.value ? 'cursor-default' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
iconInputPaddingClass.value,
|
iconInputPaddingClass.value,
|
||||||
focusPaddingClass.value,
|
isReadonly.value ? '' : focusPaddingClass.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const mergedLabelClass = computed(() =>
|
const mergedLabelClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||||
labelPositionClass.value,
|
labelPositionClass.value,
|
||||||
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
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: disabled.value
|
: disabled.value
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 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',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -215,6 +228,7 @@ 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 props.iconColor
|
if (disabled.value) return props.iconColor
|
||||||
|
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 props.iconColor
|
return props.iconColor
|
||||||
|
|||||||
@@ -193,4 +193,24 @@ describe('MalioInputTextArea', () => {
|
|||||||
const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ'}})
|
const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ'}})
|
||||||
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 bleu', () => {
|
||||||
|
const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', readonly: true}})
|
||||||
|
const field = wrapper.get('textarea')
|
||||||
|
expect(field.classes()).toContain('border-black')
|
||||||
|
expect(field.classes()).not.toContain('border-m-muted')
|
||||||
|
expect(field.classes()).not.toContain('focus:border-m-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly vide : label gris, pas de bleu focus', () => {
|
||||||
|
const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', readonly: true}})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||||
|
// En readonly, pas de couleur primary sur le label
|
||||||
|
expect(wrapper.get('label').classes()).not.toContain('text-m-primary')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly rempli : label noir', () => {
|
||||||
|
const wrapper = mount(InputTextAreaForTest, {props: {label: 'Champ', readonly: true, modelValue: 'du texte'}})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
class="floating-input peer w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent overflow-auto"
|
class="floating-input peer w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent overflow-auto"
|
||||||
:class="[
|
:class="[
|
||||||
isFilled ? 'border-black' : 'border-m-muted',
|
isReadonly ? 'border-black' : (isFilled ? 'border-black' : 'border-m-muted'),
|
||||||
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
|
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
|
||||||
hasError
|
hasError
|
||||||
? 'border-m-danger focus:border-m-danger'
|
? 'border-m-danger focus:border-m-danger'
|
||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'border-m-success focus:border-m-success'
|
? 'border-m-success focus:border-m-success'
|
||||||
: 'focus:border-m-primary',
|
: isReadonly ? '' : 'focus:border-m-primary',
|
||||||
isFocused ? 'textarea-scrollbar-primary' : '',
|
isReadonly ? '' : (isFocused ? 'textarea-scrollbar-primary' : ''),
|
||||||
textInput,
|
textInput,
|
||||||
showCounterComputed ? 'pb-6' : '',
|
showCounterComputed ? 'pb-6' : '',
|
||||||
rounded,
|
rounded,
|
||||||
@@ -47,7 +47,9 @@
|
|||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: disabled
|
: disabled
|
||||||
? 'text-m-muted'
|
? 'text-m-muted'
|
||||||
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
|
: isReadonly
|
||||||
|
? (isFilled ? 'text-black' : 'text-m-muted')
|
||||||
|
: (isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted'),
|
||||||
textLabel,
|
textLabel,
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -153,9 +155,15 @@ const isFocused = ref(false)
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-textarea-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-textarea-${generatedId}`)
|
||||||
const isControlled = computed(() => props.modelValue !== undefined)
|
const isControlled = computed(() => props.modelValue !== undefined)
|
||||||
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
const currentValue = computed(() => (isControlled.value ? (props.modelValue ?? '') : localValue.value))
|
||||||
const shouldFloatLabel = computed(() => isFocused.value || currentValue.value.length > 0)
|
|
||||||
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(() => currentValue.value.trim().length > 0)
|
||||||
|
const isReadonly = computed(() => props.readonly && !props.disabled)
|
||||||
|
const shouldFloatLabel = computed(() =>
|
||||||
|
isReadonly.value
|
||||||
|
? isFilled.value
|
||||||
|
: isFocused.value || currentValue.value.length > 0,
|
||||||
|
)
|
||||||
const rowsCount = computed(() => Math.max(1, Number(props.size || 3)))
|
const rowsCount = computed(() => Math.max(1, Number(props.size || 3)))
|
||||||
const currentLength = computed(() => (currentValue.value ?? '').length)
|
const currentLength = computed(() => (currentValue.value ?? '').length)
|
||||||
const showCounterComputed = computed(() =>
|
const showCounterComputed = computed(() =>
|
||||||
@@ -169,7 +177,6 @@ const textareaStyle = computed(() => ({
|
|||||||
minHeight: toCssSize(props.minResizeHeight),
|
minHeight: toCssSize(props.minResizeHeight),
|
||||||
maxHeight: toCssSize(props.maxResizeHeight),
|
maxHeight: toCssSize(props.maxResizeHeight),
|
||||||
}))
|
}))
|
||||||
const isFilled = computed(() => currentValue.value.trim().length > 0)
|
|
||||||
const describedBy = computed(() =>
|
const describedBy = computed(() =>
|
||||||
(hasError.value || hasSuccess.value || !!props.hint) ? `${inputId.value}-describedby` : undefined,
|
(hasError.value || hasSuccess.value || !!props.hint) ? `${inputId.value}-describedby` : undefined,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user