| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié --------- Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-authored-by: matthieu <matthieu@yuno.malio.fr> Reviewed-on: #58 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #58.
This commit is contained in:
@@ -162,7 +162,7 @@ describe('MalioButton', () => {
|
||||
it('applies correct dimensions', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.get('button').classes()).toContain('w-[240px]')
|
||||
expect(wrapper.get('button').classes()).toContain('w-[200px]')
|
||||
expect(wrapper.get('button').classes()).toContain('h-[40px]')
|
||||
})
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ const variantClasses = computed(() => {
|
||||
|
||||
const mergedButtonClass = computed(() =>
|
||||
twMerge(
|
||||
'inline-flex w-[240px] h-[40px] items-center justify-center gap-1 p-[10px] rounded-md text-base font-bold leading-[150%] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-m-primary/50',
|
||||
'inline-flex w-[200px] h-[40px] items-center justify-center gap-1 p-[10px] rounded-md text-base font-bold leading-[150%] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-m-primary/50',
|
||||
variantClasses.value,
|
||||
props.buttonClass,
|
||||
),
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
</label>
|
||||
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="mergedMessageClass"
|
||||
>
|
||||
@@ -121,7 +120,7 @@ const mergedLabelClass = computed(() =>
|
||||
|
||||
const mergedMessageClass = computed(() =>
|
||||
twMerge(
|
||||
'text-xs',
|
||||
'text-xs min-h-[1rem]',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
|
||||
@@ -85,11 +85,10 @@
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
|
||||
'mt-1 ml-[2px] text-xs',
|
||||
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
|
||||
@@ -126,6 +126,13 @@ describe('MalioInputText', () => {
|
||||
expect(wrapper.get('input').classes()).toContain('text-black/60')
|
||||
})
|
||||
|
||||
it('shows muted label color when disabled (matches border color)', () => {
|
||||
const wrapper = mountInput({label: 'Email', disabled: true, modelValue: 'foo@bar.com'})
|
||||
|
||||
expect(wrapper.get('label').classes()).toContain('text-m-muted')
|
||||
expect(wrapper.get('label').classes()).not.toContain('text-black/60')
|
||||
})
|
||||
|
||||
it('emits update:modelValue on input change', async () => {
|
||||
const wrapper = mountInput({modelValue: ''})
|
||||
|
||||
@@ -253,6 +260,15 @@ describe('MalioInputText', () => {
|
||||
expect(wrapper.get('p.text-m-muted').text()).toBe('Hint message test')
|
||||
})
|
||||
|
||||
it('reserves space for the message even when no hint/error/success is set', () => {
|
||||
const wrapper = mountInput({})
|
||||
|
||||
const p = wrapper.find('p')
|
||||
expect(p.exists()).toBe(true)
|
||||
expect(p.text()).toBe('')
|
||||
expect(p.classes()).toContain('min-h-[1rem]')
|
||||
})
|
||||
|
||||
it('does not render label when label prop is missing', () => {
|
||||
const wrapper = mountInput({labelClass: 'text-red-500'})
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -52,7 +51,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -109,7 +108,7 @@ const props = withDefaults(
|
||||
hint: '',
|
||||
error: '',
|
||||
success: '',
|
||||
iconSize: 24,
|
||||
iconSize: 20,
|
||||
iconColor: 'text-m-muted',
|
||||
},
|
||||
)
|
||||
@@ -153,12 +152,13 @@ const mergedLabelClass = computed(() =>
|
||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||
labelPositionClass.value,
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: disabled.value
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ type InputAutocompleteProps = {
|
||||
debounce?: number
|
||||
minSearchLength?: number
|
||||
allowCreate?: boolean
|
||||
localFilter?: boolean
|
||||
iconName?: string
|
||||
iconPosition?: 'left' | 'right'
|
||||
iconSize?: string | number
|
||||
@@ -427,4 +428,82 @@ describe('MalioInputAutocomplete', () => {
|
||||
|
||||
expect(wrapper.get('input').element.value).toBe('Custom')
|
||||
})
|
||||
|
||||
it('does not filter options when localFilter is false (default)', async () => {
|
||||
const wrapper = mountComponent({options})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
await wrapper.get('input').setValue('fr')
|
||||
|
||||
expect(wrapper.findAll('[data-test="option"]')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('filters options client-side when localFilter is true', async () => {
|
||||
const wrapper = mountComponent({options, localFilter: true})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
await wrapper.get('input').setValue('fr')
|
||||
|
||||
const items = wrapper.findAll('[data-test="option"]')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].text()).toBe('France')
|
||||
})
|
||||
|
||||
it('localFilter is case-insensitive and matches substrings', async () => {
|
||||
const wrapper = mountComponent({options, localFilter: true})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
await wrapper.get('input').setValue('GIQ')
|
||||
|
||||
const items = wrapper.findAll('[data-test="option"]')
|
||||
expect(items).toHaveLength(1)
|
||||
expect(items[0].text()).toBe('Belgique')
|
||||
})
|
||||
|
||||
it('localFilter shows all options when input is empty', async () => {
|
||||
const wrapper = mountComponent({options, localFilter: true})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
|
||||
expect(wrapper.findAll('[data-test="option"]')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('localFilter shows the no-results state when nothing matches', async () => {
|
||||
const wrapper = mountComponent({options, localFilter: true})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
await wrapper.get('input').setValue('zzzzz')
|
||||
|
||||
expect(wrapper.findAll('[data-test="option"]')).toHaveLength(0)
|
||||
expect(wrapper.find('[data-test="no-results-text"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the floating label at the same position whether focused or not (no jump)', async () => {
|
||||
const wrapper = mountComponent({options, label: 'Pays', modelValue: 'fr'})
|
||||
|
||||
// when a value is selected and the field is not focused, the label is already floated
|
||||
const labelClasses = wrapper.get('label').classes()
|
||||
expect(labelClasses).toContain('-translate-y-[1.25rem]')
|
||||
// and there is no extra peer-focus translate that would make it jump on click
|
||||
expect(labelClasses).not.toContain('peer-focus:-translate-y-[1.55rem]')
|
||||
})
|
||||
|
||||
it('does not shift inner text horizontally on focus (no focus:pl change)', () => {
|
||||
const wrapper = mountComponent({options})
|
||||
|
||||
const inputClasses = wrapper.get('input').classes()
|
||||
expect(inputClasses).not.toContain('focus:pl-[11px]')
|
||||
})
|
||||
|
||||
it('keeps the bottom border allocation when open (transparent, not zero)', async () => {
|
||||
const wrapper = mountComponent({options})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
|
||||
const inputClasses = wrapper.get('input').classes()
|
||||
// border-b-0 would shrink the bottom border to 0px and grow content area by 1px;
|
||||
// border-b-transparent keeps the 1px allocation but hides the line
|
||||
expect(inputClasses).not.toContain('!border-b-0')
|
||||
expect(inputClasses).toContain('!border-b-transparent')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
{{ minSearchText }}
|
||||
</li>
|
||||
<li
|
||||
v-else-if="options.length === 0"
|
||||
v-else-if="filteredOptions.length === 0"
|
||||
class="px-3 py-2 text-m-muted"
|
||||
data-test="no-results-text"
|
||||
>
|
||||
@@ -115,7 +115,7 @@
|
||||
</li>
|
||||
<template v-else>
|
||||
<li
|
||||
v-for="(opt, index) in options"
|
||||
v-for="(opt, index) in filteredOptions"
|
||||
:id="optionId(index)"
|
||||
:key="String(opt.value)"
|
||||
data-test="option"
|
||||
@@ -136,11 +136,10 @@
|
||||
</ul>
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
|
||||
'mt-1 ml-[2px] text-xs',
|
||||
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -180,6 +179,7 @@ const props = withDefaults(
|
||||
debounce?: number
|
||||
minSearchLength?: number
|
||||
allowCreate?: boolean
|
||||
localFilter?: boolean
|
||||
iconName?: string
|
||||
iconPosition?: 'left' | 'right'
|
||||
iconSize?: string | number
|
||||
@@ -207,6 +207,7 @@ const props = withDefaults(
|
||||
debounce: 300,
|
||||
minSearchLength: 0,
|
||||
allowCreate: false,
|
||||
localFilter: false,
|
||||
iconName: '',
|
||||
iconPosition: 'left',
|
||||
iconSize: 24,
|
||||
@@ -253,9 +254,18 @@ const showMinSearch = computed(() =>
|
||||
props.minSearchLength > 0 && inputValue.value.length < props.minSearchLength,
|
||||
)
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
if (!props.localFilter) return props.options
|
||||
const query = inputValue.value.trim().toLowerCase()
|
||||
if (query === '') return props.options
|
||||
return props.options.filter(opt =>
|
||||
opt.label.toLowerCase().includes(query),
|
||||
)
|
||||
})
|
||||
|
||||
const optionId = (index: number) => `${inputId.value}-option-${index}`
|
||||
const activeOptionId = computed(() =>
|
||||
activeIndex.value >= 0 && props.options[activeIndex.value]
|
||||
activeIndex.value >= 0 && filteredOptions.value[activeIndex.value]
|
||||
? optionId(activeIndex.value)
|
||||
: undefined,
|
||||
)
|
||||
@@ -294,11 +304,6 @@ const iconInputPaddingClass = computed(() => {
|
||||
return parts.join(' ')
|
||||
})
|
||||
|
||||
const focusPaddingClass = computed(() => {
|
||||
if (props.iconName && props.iconPosition === 'left') return 'focus:!pl-11'
|
||||
return 'focus:pl-[11px]'
|
||||
})
|
||||
|
||||
const labelPositionClass = computed(() =>
|
||||
props.iconName && props.iconPosition === 'left' ? 'left-11' : 'left-3',
|
||||
)
|
||||
@@ -315,10 +320,9 @@ const mergedInputClass = computed(() =>
|
||||
: hasSuccess.value
|
||||
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
|
||||
: 'focus:border-m-primary',
|
||||
isOpen.value ? '!rounded-b-none !border-b-0' : '',
|
||||
isOpen.value ? '!rounded-b-none !border-b-transparent' : '',
|
||||
props.inputClass,
|
||||
iconInputPaddingClass.value,
|
||||
focusPaddingClass.value,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -326,13 +330,14 @@ const mergedLabelClass = computed(() =>
|
||||
twMerge(
|
||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||
labelPositionClass.value,
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||
props.disabled ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] scale-90' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: props.disabled
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
@@ -432,8 +437,8 @@ const onKeydown = (event: KeyboardEvent) => {
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
if (activeIndex.value >= 0 && props.options[activeIndex.value]) {
|
||||
onSelect(props.options[activeIndex.value])
|
||||
if (activeIndex.value >= 0 && filteredOptions.value[activeIndex.value]) {
|
||||
onSelect(filteredOptions.value[activeIndex.value])
|
||||
return
|
||||
}
|
||||
if (props.allowCreate && inputValue.value !== '') {
|
||||
@@ -450,7 +455,7 @@ const onKeydown = (event: KeyboardEvent) => {
|
||||
if (!isOpen.value) {
|
||||
isOpen.value = true
|
||||
}
|
||||
activeIndex.value = Math.min(activeIndex.value + 1, props.options.length - 1)
|
||||
activeIndex.value = Math.min(activeIndex.value + 1, filteredOptions.value.length - 1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -481,12 +486,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.grow-height {
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
||||
}
|
||||
|
||||
.grow-height:focus {
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -50,7 +49,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -147,12 +146,13 @@ const mergedLabelClass = computed(() =>
|
||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||
labelPositionClass.value,
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: disabled.value
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -59,7 +58,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'text-xs ml-[2px] ',
|
||||
'text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -55,7 +54,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -155,12 +154,13 @@ const mergedLabelClass = computed(() =>
|
||||
'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' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: disabled.value
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -298,6 +298,41 @@ describe('MalioInputPhone', () => {
|
||||
expect(wrapper.get('input').classes()).toContain('!pr-10')
|
||||
})
|
||||
|
||||
it('shows default add button color when empty and unfocused', () => {
|
||||
const wrapper = mountComponent({addable: true})
|
||||
|
||||
expect(wrapper.get('[data-test="add-button"]').classes()).toContain('text-m-muted')
|
||||
expect(wrapper.get('[data-test="add-button"]').classes()).not.toContain('text-m-primary')
|
||||
})
|
||||
|
||||
it('shows primary add button color on focus', async () => {
|
||||
const wrapper = mountComponent({addable: true})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
|
||||
expect(wrapper.get('[data-test="add-button"]').classes()).toContain('text-m-primary')
|
||||
})
|
||||
|
||||
it('shows black add button color when filled and unfocused', () => {
|
||||
const wrapper = mountComponent({addable: true, modelValue: '+33612345678'})
|
||||
|
||||
expect(wrapper.get('[data-test="add-button"]').classes()).toContain('text-black')
|
||||
})
|
||||
|
||||
it('error overrides focus color on add button', async () => {
|
||||
const wrapper = mountComponent({addable: true, error: 'Numéro invalide'})
|
||||
|
||||
await wrapper.get('input').trigger('focus')
|
||||
|
||||
expect(wrapper.get('[data-test="add-button"]').classes()).toContain('text-m-danger')
|
||||
})
|
||||
|
||||
it('success applies to add button', () => {
|
||||
const wrapper = mountComponent({addable: true, success: 'Numéro valide'})
|
||||
|
||||
expect(wrapper.get('[data-test="add-button"]').classes()).toContain('text-m-success')
|
||||
})
|
||||
|
||||
it('applies mask via maska directive', async () => {
|
||||
const wrapper = mountComponent({mask: '+## # ## ## ## ##'})
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -68,7 +67,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -175,19 +174,21 @@ const mergedLabelClass = computed(() =>
|
||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||
labelPositionClass.value,
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: disabled.value
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
const mergedAddButtonClass = computed(() =>
|
||||
twMerge(
|
||||
'absolute right-[10px] top-1/2 -translate-y-1/2 cursor-pointer text-m-primary transition-opacity hover:opacity-70',
|
||||
'absolute right-[10px] top-1/2 -translate-y-1/2 cursor-pointer transition-opacity hover:opacity-70',
|
||||
iconStateClass.value,
|
||||
(props.disabled || props.readonly) ? 'cursor-not-allowed opacity-40 hover:opacity-40' : '',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -184,7 +184,6 @@
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${editorId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -192,7 +191,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px]',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
@@ -279,10 +278,11 @@ const mergedLabelClass = computed(() =>
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: isFocused.value
|
||||
? 'text-m-primary'
|
||||
: 'text-m-text',
|
||||
props.disabled ? 'text-black/60' : '',
|
||||
: props.disabled
|
||||
? 'text-m-muted'
|
||||
: isFocused.value
|
||||
? 'text-m-primary'
|
||||
: 'text-m-text',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -52,7 +51,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -158,12 +157,13 @@ const mergedLabelClass = computed(() =>
|
||||
'floating-label absolute top-2 mt-[5px] inline-block origin-left transition-transform duration-150 font-medium text-sm',
|
||||
labelPositionClass.value,
|
||||
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: disabled.value
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -149,4 +149,38 @@ describe('MalioInputTextArea', () => {
|
||||
expect(wrapper.find('p.text-m-success').exists()).toBe(false)
|
||||
expect(wrapper.get('p.text-m-danger').text()).toBe('Textarea error')
|
||||
})
|
||||
|
||||
it('renders as a single root element (works as a single grid item)', () => {
|
||||
const host = document.createElement('div')
|
||||
document.body.appendChild(host)
|
||||
const wrapper = mount(InputTextAreaForTest, {
|
||||
attachTo: host,
|
||||
})
|
||||
|
||||
// host > div[data-v-app] > component roots
|
||||
const app = host.firstElementChild as HTMLElement
|
||||
expect(app.children.length).toBe(1)
|
||||
|
||||
wrapper.unmount()
|
||||
host.remove()
|
||||
})
|
||||
|
||||
it('applies primary scrollbar class on focus', async () => {
|
||||
const wrapper = mount(InputTextAreaForTest)
|
||||
|
||||
expect(wrapper.get('textarea').classes()).not.toContain('textarea-scrollbar-primary')
|
||||
|
||||
await wrapper.get('textarea').trigger('focus')
|
||||
|
||||
expect(wrapper.get('textarea').classes()).toContain('textarea-scrollbar-primary')
|
||||
})
|
||||
|
||||
it('removes primary scrollbar class on blur', async () => {
|
||||
const wrapper = mount(InputTextAreaForTest)
|
||||
|
||||
await wrapper.get('textarea').trigger('focus')
|
||||
await wrapper.get('textarea').trigger('blur')
|
||||
|
||||
expect(wrapper.get('textarea').classes()).not.toContain('textarea-scrollbar-primary')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,79 +1,82 @@
|
||||
<template>
|
||||
<div :class="mergedGroupClass">
|
||||
<textarea
|
||||
:id="inputId"
|
||||
:name="name"
|
||||
<div class="relative w-full flex-1">
|
||||
<textarea
|
||||
:id="inputId"
|
||||
:name="name"
|
||||
|
||||
: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="[
|
||||
isFilled ? 'border-black' : 'border-m-muted',
|
||||
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
|
||||
hasError
|
||||
? 'border-m-danger focus:border-m-danger'
|
||||
: hasSuccess
|
||||
? 'border-m-success focus:border-m-success'
|
||||
: 'focus:border-m-primary',
|
||||
textInput,
|
||||
showCounterComputed ? 'pb-6' : '',
|
||||
rounded,
|
||||
]"
|
||||
:required="required"
|
||||
:maxlength="maxLength"
|
||||
:rows="rowsCount"
|
||||
:disabled="disabled"
|
||||
:value="currentValue"
|
||||
:readonly="readonly"
|
||||
:aria-invalid="hasError"
|
||||
:aria-describedby="describedBy"
|
||||
:style="textareaStyle"
|
||||
v-bind="attrs"
|
||||
placeholder="_"
|
||||
@input="onInput"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
/>
|
||||
<label
|
||||
v-if="label"
|
||||
:for="inputId"
|
||||
class="floating-label absolute left-3 top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
|
||||
:class="[
|
||||
shouldFloatLabel ? '-translate-y-[1.30rem] scale-90' : '',
|
||||
disabled ? 'text-black/60' : '',
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
|
||||
textLabel,
|
||||
]"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<span
|
||||
v-if="showCounterComputed"
|
||||
class="pointer-events-none absolute bottom-2 left-3 text-xs text-m-muted"
|
||||
>
|
||||
{{ currentLength }}/{{ maxLength }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasError || hasSuccess || hint"
|
||||
class="mt-1 flex items-center justify-between gap-2 text-xs"
|
||||
>
|
||||
<p
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
: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="[
|
||||
isFilled ? 'border-black' : 'border-m-muted',
|
||||
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
? 'border-m-danger focus:border-m-danger'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'ml-[2px]',
|
||||
? 'border-m-success focus:border-m-success'
|
||||
: 'focus:border-m-primary',
|
||||
isFocused ? 'textarea-scrollbar-primary' : '',
|
||||
textInput,
|
||||
showCounterComputed ? 'pb-6' : '',
|
||||
rounded,
|
||||
]"
|
||||
:required="required"
|
||||
:maxlength="maxLength"
|
||||
:rows="rowsCount"
|
||||
:disabled="disabled"
|
||||
:value="currentValue"
|
||||
:readonly="readonly"
|
||||
:aria-invalid="hasError"
|
||||
:aria-describedby="describedBy"
|
||||
:style="textareaStyle"
|
||||
v-bind="attrs"
|
||||
placeholder="_"
|
||||
@input="onInput"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false"
|
||||
/>
|
||||
<label
|
||||
v-if="label"
|
||||
:for="inputId"
|
||||
class="floating-label absolute left-3 top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
|
||||
:class="[
|
||||
shouldFloatLabel ? '-translate-y-[1.30rem] scale-90' : '',
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: disabled
|
||||
? 'text-m-muted'
|
||||
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
|
||||
textLabel,
|
||||
]"
|
||||
>
|
||||
{{ label }}
|
||||
</label>
|
||||
<span
|
||||
v-if="showCounterComputed"
|
||||
class="pointer-events-none absolute bottom-2 left-3 text-xs text-m-muted"
|
||||
>
|
||||
{{ currentLength }}/{{ maxLength }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 flex items-center justify-between gap-2 text-xs min-h-[1rem]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
</p>
|
||||
<p
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'ml-[2px]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -138,7 +141,7 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const mergedGroupClass = computed(() =>
|
||||
twMerge('relative w-full', props.groupClass),
|
||||
twMerge('flex flex-col w-full', props.groupClass),
|
||||
)
|
||||
|
||||
const attrs = useAttrs()
|
||||
@@ -188,4 +191,8 @@ const onInput = (event: Event) => {
|
||||
background: white;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.textarea-scrollbar-primary {
|
||||
scrollbar-color: rgb(var(--m-primary)) transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -58,7 +57,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 text-xs ml-[2px] ',
|
||||
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ hint || error || success }}
|
||||
@@ -144,12 +143,13 @@ const mergedLabelClass = computed(() =>
|
||||
'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' : '',
|
||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||
hasError.value
|
||||
? 'text-m-danger'
|
||||
: hasSuccess.value
|
||||
? 'text-m-success'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
: disabled.value
|
||||
? 'text-m-muted'
|
||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -207,4 +207,70 @@ describe('MalioSelect', () => {
|
||||
expect(wrapper.find('p.text-m-success').exists()).toBe(false)
|
||||
expect(wrapper.get('p.text-m-danger').text()).toBe('Selection error')
|
||||
})
|
||||
|
||||
it('shows muted chevron color when empty and closed', () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: null, options},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-muted')
|
||||
})
|
||||
|
||||
it('shows primary chevron color when open', async () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: null, options},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-primary')
|
||||
})
|
||||
|
||||
it('shows black chevron color when an option is selected and closed', () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: 'fr', options},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black')
|
||||
})
|
||||
|
||||
it('shows muted chevron color when disabled', () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: 'fr', options, disabled: true},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-muted')
|
||||
})
|
||||
|
||||
it('shows danger chevron color on error even when open', async () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: null, options, error: 'Selection error'},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-danger')
|
||||
})
|
||||
|
||||
it('shows success chevron color on success', () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: null, options, success: 'OK'},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success')
|
||||
})
|
||||
|
||||
it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => {
|
||||
const wrapper = mount(SelectForTest, {
|
||||
props: {modelValue: null, options},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
const buttonClasses = wrapper.get('button').classes()
|
||||
// !border-b-0 would shrink the bottom border to 0px and grow content area by 1px;
|
||||
// !border-b-transparent keeps the 1px allocation but hides the line
|
||||
expect(buttonClasses).not.toContain('!border-b-0')
|
||||
expect(buttonClasses).toContain('!border-b-transparent')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
hasError
|
||||
? isOpen
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border !border-m-danger !border-b-0'
|
||||
: 'rounded-t-none !border !border-m-danger !border-t-0'
|
||||
? 'rounded-b-none !border !border-m-danger !border-b-transparent'
|
||||
: 'rounded-t-none !border !border-m-danger !border-t-transparent'
|
||||
: 'border-m-danger'
|
||||
: hasSuccess
|
||||
? isOpen
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border !border-m-success !border-b-0'
|
||||
: 'rounded-t-none !border !border-m-success !border-t-0'
|
||||
? 'rounded-b-none !border !border-m-success !border-b-transparent'
|
||||
: 'rounded-t-none !border !border-m-success !border-t-transparent'
|
||||
: 'border-m-success'
|
||||
: isOpen
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border !border-m-primary !border-b-0'
|
||||
: 'rounded-t-none !border !border-m-primary !border-t-0'
|
||||
? 'rounded-b-none !border !border-m-primary !border-b-transparent'
|
||||
: 'rounded-t-none !border !border-m-primary !border-t-transparent'
|
||||
: isOptionSelected
|
||||
? 'border-black'
|
||||
: 'border-m-muted',
|
||||
@@ -73,13 +73,20 @@
|
||||
</span>
|
||||
|
||||
<span
|
||||
data-test="chevron"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2"
|
||||
:class="[
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-current'
|
||||
: disabled
|
||||
? 'text-m-muted'
|
||||
: isOpen
|
||||
? 'text-m-primary'
|
||||
: isOptionSelected
|
||||
? 'text-black'
|
||||
: 'text-m-muted'
|
||||
]"
|
||||
>
|
||||
<slot name="icon">
|
||||
@@ -145,7 +152,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${buttonId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -153,7 +159,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 ml-[2px] text-xs',
|
||||
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
@@ -330,12 +336,7 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
}
|
||||
|
||||
.grow-height {
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
||||
}
|
||||
|
||||
.grow-height:focus {
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -182,4 +182,70 @@ describe('MalioSelectCheckbox', () => {
|
||||
const root = wrapper.find('button').element.parentElement
|
||||
expect(root?.className).toContain('mt-4')
|
||||
})
|
||||
|
||||
it('shows muted chevron color when nothing is selected and closed', () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-muted')
|
||||
})
|
||||
|
||||
it('shows primary chevron color when open', async () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-primary')
|
||||
})
|
||||
|
||||
it('shows black chevron color when options are selected and closed', () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: ['fr'], options},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black')
|
||||
})
|
||||
|
||||
it('shows muted chevron color when disabled', () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: ['fr'], options, disabled: true},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-muted')
|
||||
})
|
||||
|
||||
it('shows danger chevron color on error even when open', async () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options, error: 'Selection error'},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-danger')
|
||||
})
|
||||
|
||||
it('shows success chevron color on success', () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options, success: 'OK'},
|
||||
})
|
||||
|
||||
expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success')
|
||||
})
|
||||
|
||||
it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options},
|
||||
})
|
||||
|
||||
await wrapper.get('button').trigger('click')
|
||||
|
||||
const buttonClasses = wrapper.get('button').classes()
|
||||
// !border-b-0 would shrink the bottom border to 0px and grow content area by 1px;
|
||||
// !border-b-transparent keeps the 1px allocation but hides the line
|
||||
expect(buttonClasses).not.toContain('!border-b-0')
|
||||
expect(buttonClasses).toContain('!border-b-transparent')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
hasError
|
||||
? isOpen
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border !border-m-danger !border-b-0'
|
||||
: 'rounded-t-none !border !border-m-danger !border-t-0'
|
||||
? 'rounded-b-none !border !border-m-danger !border-b-transparent'
|
||||
: 'rounded-t-none !border !border-m-danger !border-t-transparent'
|
||||
: 'border-m-danger'
|
||||
: hasSuccess
|
||||
? isOpen
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border !border-m-success !border-b-0'
|
||||
: 'rounded-t-none !border !border-m-success !border-t-0'
|
||||
? 'rounded-b-none !border !border-m-success !border-b-transparent'
|
||||
: 'rounded-t-none !border !border-m-success !border-t-transparent'
|
||||
: 'border-m-success'
|
||||
: isOpen
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border !border-m-primary !border-b-0'
|
||||
: 'rounded-t-none !border !border-m-primary !border-t-0'
|
||||
? 'rounded-b-none !border !border-m-primary !border-b-transparent'
|
||||
: 'rounded-t-none !border !border-m-primary !border-t-transparent'
|
||||
: isOptionSelected
|
||||
? 'border-black'
|
||||
: 'border-m-muted',
|
||||
@@ -101,13 +101,20 @@
|
||||
</span>
|
||||
|
||||
<span
|
||||
data-test="chevron"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2"
|
||||
:class="[
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-current'
|
||||
: disabled
|
||||
? 'text-m-muted'
|
||||
: isOpen
|
||||
? 'text-m-primary'
|
||||
: isOptionSelected
|
||||
? 'text-black'
|
||||
: 'text-m-muted'
|
||||
]"
|
||||
>
|
||||
<slot name="icon">
|
||||
@@ -194,7 +201,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${buttonId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -202,7 +208,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 ml-[2px] text-xs',
|
||||
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
@@ -409,12 +415,7 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
||||
}
|
||||
|
||||
.grow-height {
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
||||
}
|
||||
|
||||
.grow-height:focus {
|
||||
padding-top: 0.625rem;
|
||||
padding-bottom: 0.625rem;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError
|
||||
@@ -66,7 +65,7 @@
|
||||
: hasSuccess
|
||||
? 'text-m-success'
|
||||
: 'text-m-muted',
|
||||
'mt-1 ml-[2px] text-xs',
|
||||
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
|
||||
@@ -78,11 +78,10 @@
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="hint || hasError || hasSuccess"
|
||||
:id="`${inputId}-describedby`"
|
||||
:class="[
|
||||
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
|
||||
'mt-1 ml-[2px] text-xs',
|
||||
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||
]"
|
||||
>
|
||||
{{ error || success || hint }}
|
||||
|
||||
Reference in New Issue
Block a user