feat(ui) : InputPhone readonly suit les règles readonly (plus de look disabled)
Découple readonly de disabled : le champ affiche border-black + curseur default, l'icône suit text-black/text-m-muted selon isFilled, et le bouton "add" conserve son guard onAdd sans porter l'apparence désactivée (opacity-40/cursor-not-allowed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -274,10 +274,32 @@ describe('MalioInputPhone', () => {
|
|||||||
expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined()
|
expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('disables add button when readonly', () => {
|
it('add button is not natively disabled in readonly (onAdd guard blocks the action)', () => {
|
||||||
const wrapper = mountComponent({addable: true, readonly: true})
|
const wrapper = mountComponent({addable: true, readonly: true})
|
||||||
|
|
||||||
expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeDefined()
|
expect(wrapper.get('[data-test="add-button"]').attributes('disabled')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly : border-black appliqué sur l\'input', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Tel', readonly: true})
|
||||||
|
expect(wrapper.get('input').classes()).toContain('border-black')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly : icône en text-m-muted quand vide', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Tel', readonly: true})
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-muted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly : icône en text-black quand rempli', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Tel', readonly: true, modelValue: '+33612345678'})
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('readonly : pas d\'apparence désactivée (pas opacity-40)', () => {
|
||||||
|
const wrapper = mountComponent({label: 'Tel', readonly: true})
|
||||||
|
expect(wrapper.get('input').classes()).not.toContain('opacity-40')
|
||||||
|
// and the input is not natively disabled in readonly:
|
||||||
|
expect(wrapper.get('input').attributes('disabled')).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders the default add icon (mdi:plus)', () => {
|
it('renders the default add icon (mdi:plus)', () => {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="addable"
|
v-if="addable"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="disabled || readonly"
|
:disabled="disabled"
|
||||||
:aria-label="addButtonLabel"
|
:aria-label="addButtonLabel"
|
||||||
data-test="add-button"
|
data-test="add-button"
|
||||||
:class="mergedAddButtonClass"
|
:class="mergedAddButtonClass"
|
||||||
@@ -145,10 +145,15 @@ const isFocused = ref(false)
|
|||||||
const inputId = computed(() => props.id?.toString() || `malio-input-phone-${generatedId}`)
|
const inputId = computed(() => props.id?.toString() || `malio-input-phone-${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',
|
||||||
@@ -157,31 +162,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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -190,7 +203,7 @@ const mergedAddButtonClass = computed(() =>
|
|||||||
twMerge(
|
twMerge(
|
||||||
'absolute right-[10px] top-1/2 -translate-y-1/2 cursor-pointer transition-opacity hover:opacity-70',
|
'absolute right-[10px] top-1/2 -translate-y-1/2 cursor-pointer transition-opacity hover:opacity-70',
|
||||||
iconStateClass.value,
|
iconStateClass.value,
|
||||||
(props.disabled || props.readonly) ? 'cursor-not-allowed opacity-40 hover:opacity-40' : '',
|
props.disabled ? 'cursor-not-allowed opacity-40 hover:opacity-40' : '',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -250,6 +263,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
|
||||||
|
|||||||
Reference in New Issue
Block a user