| 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:
@@ -10,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
<h1 class="text-[32px] text-m-primary font-bold">Ajouter un client</h1>
|
<h1 class="text-[32px] text-m-primary font-bold">Ajouter un client</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-[48px] grid grid-cols-3 gap-x-[80px] gap-y-8">
|
<div class="mt-[48px] grid grid-cols-3 gap-x-[80px] gap-y-5">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
label="Nom du client (Entreprise)"
|
label="Nom du client (Entreprise)"
|
||||||
/>
|
/>
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
<MalioSelectCheckbox
|
<MalioSelectCheckbox
|
||||||
v-model="multiselectValue"
|
v-model="multiselectValue"
|
||||||
|
error="test"
|
||||||
label="Catégorie"
|
label="Catégorie"
|
||||||
:options="[
|
:options="[
|
||||||
{label: 'Catégorie 1', value: 'Catégorie 1'},
|
{label: 'Catégorie 1', value: 'Catégorie 1'},
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
<div class="mt-[60px]">
|
<div class="mt-[60px]">
|
||||||
<MalioTabList :tabs="tabs" v-model="tabsValue">
|
<MalioTabList :tabs="tabs" v-model="tabsValue">
|
||||||
<template #information>
|
<template #information>
|
||||||
<div class="grid grid-cols-3 gap-x-[80px] gap-y-8 mt-12 shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] py-4 pl-[28px] pr-[60px]">
|
<div class="grid grid-cols-3 gap-x-[80px] gap-y-5 mt-12 shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] py-4 pl-[28px] pr-[60px]">
|
||||||
<MalioInputTextArea label="Descritpion" resize="none" groupClass="row-span-2" textInput="h-full"/>
|
<MalioInputTextArea label="Descritpion" resize="none" groupClass="row-span-2" textInput="h-full"/>
|
||||||
<MalioInputText v-model="concurrent" label="Concurrent"/>
|
<MalioInputText v-model="concurrent" label="Concurrent"/>
|
||||||
<MalioDate
|
<MalioDate
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #adresses>
|
<template #adresses>
|
||||||
<div class="relative grid grid-cols-3 gap-x-[80px] gap-y-8 mt-12 bg-white shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] py-4 pl-[28px] pr-[60px]">
|
<div class="relative grid grid-cols-3 gap-x-[80px] gap-y-5 mt-12 bg-white shadow-[0_4px_4px_0_rgba(0,0,0,0.25)] py-4 pl-[28px] pr-[60px]">
|
||||||
<MalioButtonIcon
|
<MalioButtonIcon
|
||||||
icon="mdi:delete-outline"
|
icon="mdi:delete-outline"
|
||||||
aria-label="Supprimer l'adresse"
|
aria-label="Supprimer l'adresse"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
v-model="simpleValue"
|
v-model="simpleValue"
|
||||||
label="Pays"
|
label="Pays"
|
||||||
:options="staticOptions"
|
:options="staticOptions"
|
||||||
|
local-filter
|
||||||
/>
|
/>
|
||||||
<p class="mt-2 text-sm text-m-muted">
|
<p class="mt-2 text-sm text-m-muted">
|
||||||
Valeur sélectionnée : <code>{{ simpleValue ?? 'null' }}</code>
|
Valeur sélectionnée : <code>{{ simpleValue ?? 'null' }}</code>
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
icon-name="mdi:magnify"
|
icon-name="mdi:magnify"
|
||||||
icon-position="left"
|
icon-position="left"
|
||||||
:options="staticOptions"
|
:options="staticOptions"
|
||||||
|
local-filter
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ Liste des évolutions de la librairie Malio layer UI
|
|||||||
* [#MUI-36] Création d'un composant modal (dialogue centré, focus-trap, scroll-lock, footer fixe)
|
* [#MUI-36] Création d'un composant modal (dialogue centré, focus-trap, scroll-lock, footer fixe)
|
||||||
* [#MUI-37] Création d'un composant accordéon
|
* [#MUI-37] Création d'un composant accordéon
|
||||||
* [#MUI-39] Création d'un sélecteur d'heure à molettes (MalioTimePicker) ; DateTime rebranché dessus (remplace l'input time natif intérimaire)
|
* [#MUI-39] Création d'un sélecteur d'heure à molettes (MalioTimePicker) ; DateTime rebranché dessus (remplace l'input time natif intérimaire)
|
||||||
|
* InputAutocomplete : prop `localFilter` pour le filtrage côté client des listes statiques (case-insensitive `label.includes(query)`), sans avoir à brancher `@search`
|
||||||
|
* InputTextArea : la scrollbar passe en primary (bleu) au focus, comme la liste du Select
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#MUI-35] Refonte du composant drawer : slots `#header`/`#footer`, prop `side` (droite/gauche), `dismissable`, `closeOnEscape`, classes d'override, focus-trap, scroll-lock et fermeture au clavier. **Breaking** : la prop `title` est remplacée par le slot `#header`.
|
* [#MUI-35] Refonte du composant drawer : slots `#header`/`#footer`, prop `side` (droite/gauche), `dismissable`, `closeOnEscape`, classes d'override, focus-trap, scroll-lock et fermeture au clavier. **Breaking** : la prop `title` est remplacée par le slot `#header`.
|
||||||
@@ -44,3 +46,11 @@ Liste des évolutions de la librairie Malio layer UI
|
|||||||
* Drawer : le slot `#footer` est désormais rendu hors de la zone scrollable (épinglé en bas, comme la modal) ; seul le body défile et la scrollbar ne s'étend plus derrière le footer
|
* Drawer : le slot `#footer` est désormais rendu hors de la zone scrollable (épinglé en bas, comme la modal) ; seul le body défile et la scrollbar ne s'étend plus derrière le footer
|
||||||
* Hauteur des boutons de pagination du datatable alignée sur le select (40px)
|
* Hauteur des boutons de pagination du datatable alignée sur le select (40px)
|
||||||
* Distribution de `tailwind.config.ts` aux projets consommateurs avec paths `content` absolus
|
* Distribution de `tailwind.config.ts` aux projets consommateurs avec paths `content` absolus
|
||||||
|
* Espace réservé (`min-h-[1rem]`) pour le paragraphe hint/error/success de 15 composants (Input*, Select*, Time*, CalendarField, Checkbox) — l'apparition d'une erreur ne décale plus les cellules voisines dans une grille
|
||||||
|
* InputPhone : la croix `+` (add button) suit la même cascade d'état que les autres icônes du champ (muted / primary en focus / black quand rempli / danger / success) au lieu d'être figée en primary
|
||||||
|
* Select / SelectCheckbox : le chevron suit l'état du champ (muted par défaut, primary à l'ouverture, black avec une option sélectionnée, danger / success en cas d'erreur ou succès) au lieu de `text-current`
|
||||||
|
* InputTextArea : composant single-root (était multi-root) — le wrapper du message ne prend plus sa propre cellule de grille, `row-span-2` fonctionne à nouveau
|
||||||
|
* Label désactivé en `text-m-muted` (gris des bordures) au lieu de `text-black/60` sur les inputs à floating-label (InputText, Email, Password, Amount, Phone, Upload, Autocomplete, TextArea, RichText)
|
||||||
|
* InputAutocomplete : suppression de 4 sources de saut visuel au focus / ouverture (extra translate label, padding `grow-height:focus`, `focus:pl-[11px]`, `!border-b-0` remplacé par `!border-b-transparent`)
|
||||||
|
* Select / SelectCheckbox : mêmes correctifs anti-saut (suppression du padding `grow-height:focus` et remplacement de `!border-b-0` / `!border-t-0` par leurs variantes `transparent`)
|
||||||
|
* MalioButton : largeur par défaut alignée sur `w-[200px]` (au lieu de `w-[240px]`) pour correspondre au sizing des formulaires de l'app
|
||||||
|
|||||||
+4
-3
@@ -146,7 +146,7 @@ Champ téléphone (`type="tel"` + `inputmode="tel"`) avec icône `mdi:phone-outl
|
|||||||
|
|
||||||
## MalioInputAutocomplete
|
## MalioInputAutocomplete
|
||||||
|
|
||||||
Champ de saisie assistée (typeahead / combobox) : l'utilisateur tape pour filtrer une liste d'options, ou pour déclencher une recherche côté parent (API). Le parent alimente `options` et `loading` en réponse à l'event `search` — c'est lui qui gère l'appel API, l'auth, la transformation et le cache.
|
Champ de saisie assistée (typeahead / combobox) : l'utilisateur tape pour filtrer une liste d'options, ou pour déclencher une recherche côté parent (API). Par défaut le parent alimente `options` et `loading` en réponse à l'event `search` — c'est lui qui gère l'appel API, l'auth, la transformation et le cache. Pour une liste **statique** courte, activer `localFilter` fait filtrer le composant lui-même (case-insensitive `label.includes(query)`) sans avoir à brancher `@search`.
|
||||||
|
|
||||||
| Prop | Type | Défaut | Description |
|
| Prop | Type | Défaut | Description |
|
||||||
|------|------|--------|-------------|
|
|------|------|--------|-------------|
|
||||||
@@ -159,6 +159,7 @@ Champ de saisie assistée (typeahead / combobox) : l'utilisateur tape pour filtr
|
|||||||
| `debounce` | `number` | `300` | Délai (ms) avant émission de `search` |
|
| `debounce` | `number` | `300` | Délai (ms) avant émission de `search` |
|
||||||
| `minSearchLength` | `number` | `0` | Caractères mini avant d'émettre `search` |
|
| `minSearchLength` | `number` | `0` | Caractères mini avant d'émettre `search` |
|
||||||
| `allowCreate` | `boolean` | `false` | Autorise la saisie libre validée par Entrée (émet `create`) |
|
| `allowCreate` | `boolean` | `false` | Autorise la saisie libre validée par Entrée (émet `create`) |
|
||||||
|
| `localFilter` | `boolean` | `false` | Filtre `options` côté client par sous-chaîne du label (case-insensitive). À utiliser pour les listes statiques courtes ; en mode API on laisse `false` et le parent répond à `@search`. |
|
||||||
| `iconName` | `string` | `''` | Icône Iconify décorative |
|
| `iconName` | `string` | `''` | Icône Iconify décorative |
|
||||||
| `iconPosition` | `'left' \| 'right'` | `'left'` | Position de l'icône décorative |
|
| `iconPosition` | `'left' \| 'right'` | `'left'` | Position de l'icône décorative |
|
||||||
| `iconSize` | `string \| number` | `24` | Taille de l'icône |
|
| `iconSize` | `string \| number` | `24` | Taille de l'icône |
|
||||||
@@ -185,8 +186,8 @@ Champ de saisie assistée (typeahead / combobox) : l'utilisateur tape pour filtr
|
|||||||
**Clavier :** `↓` / `↑` navigation, `Entrée` sélection (ou création), `Échap` ferme le dropdown.
|
**Clavier :** `↓` / `↑` navigation, `Entrée` sélection (ou création), `Échap` ferme le dropdown.
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- Usage statique -->
|
<!-- Usage statique (filtrage côté client via local-filter) -->
|
||||||
<MalioInputAutocomplete v-model="country" label="Pays" :options="countries" />
|
<MalioInputAutocomplete v-model="country" label="Pays" :options="countries" local-filter />
|
||||||
|
|
||||||
<!-- Usage API (parent gère le fetch) -->
|
<!-- Usage API (parent gère le fetch) -->
|
||||||
<MalioInputAutocomplete
|
<MalioInputAutocomplete
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ describe('MalioButton', () => {
|
|||||||
it('applies correct dimensions', () => {
|
it('applies correct dimensions', () => {
|
||||||
const wrapper = mountComponent()
|
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]')
|
expect(wrapper.get('button').classes()).toContain('h-[40px]')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const variantClasses = computed(() => {
|
|||||||
|
|
||||||
const mergedButtonClass = computed(() =>
|
const mergedButtonClass = computed(() =>
|
||||||
twMerge(
|
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,
|
variantClasses.value,
|
||||||
props.buttonClass,
|
props.buttonClass,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="mergedMessageClass"
|
:class="mergedMessageClass"
|
||||||
>
|
>
|
||||||
@@ -121,7 +120,7 @@ const mergedLabelClass = computed(() =>
|
|||||||
|
|
||||||
const mergedMessageClass = computed(() =>
|
const mergedMessageClass = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'text-xs',
|
'text-xs min-h-[1rem]',
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
|
|||||||
@@ -85,11 +85,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
|
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 }}
|
{{ error || success || hint }}
|
||||||
|
|||||||
@@ -126,6 +126,13 @@ describe('MalioInputText', () => {
|
|||||||
expect(wrapper.get('input').classes()).toContain('text-black/60')
|
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 () => {
|
it('emits update:modelValue on input change', async () => {
|
||||||
const wrapper = mountInput({modelValue: ''})
|
const wrapper = mountInput({modelValue: ''})
|
||||||
|
|
||||||
@@ -253,6 +260,15 @@ describe('MalioInputText', () => {
|
|||||||
expect(wrapper.get('p.text-m-muted').text()).toBe('Hint message test')
|
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', () => {
|
it('does not render label when label prop is missing', () => {
|
||||||
const wrapper = mountInput({labelClass: 'text-red-500'})
|
const wrapper = mountInput({labelClass: 'text-red-500'})
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -52,7 +51,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px] ',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
@@ -109,7 +108,7 @@ const props = withDefaults(
|
|||||||
hint: '',
|
hint: '',
|
||||||
error: '',
|
error: '',
|
||||||
success: '',
|
success: '',
|
||||||
iconSize: 24,
|
iconSize: 20,
|
||||||
iconColor: 'text-m-muted',
|
iconColor: 'text-m-muted',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -153,11 +152,12 @@ const mergedLabelClass = computed(() =>
|
|||||||
'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] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled.value
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type InputAutocompleteProps = {
|
|||||||
debounce?: number
|
debounce?: number
|
||||||
minSearchLength?: number
|
minSearchLength?: number
|
||||||
allowCreate?: boolean
|
allowCreate?: boolean
|
||||||
|
localFilter?: boolean
|
||||||
iconName?: string
|
iconName?: string
|
||||||
iconPosition?: 'left' | 'right'
|
iconPosition?: 'left' | 'right'
|
||||||
iconSize?: string | number
|
iconSize?: string | number
|
||||||
@@ -427,4 +428,82 @@ describe('MalioInputAutocomplete', () => {
|
|||||||
|
|
||||||
expect(wrapper.get('input').element.value).toBe('Custom')
|
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 }}
|
{{ minSearchText }}
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-else-if="options.length === 0"
|
v-else-if="filteredOptions.length === 0"
|
||||||
class="px-3 py-2 text-m-muted"
|
class="px-3 py-2 text-m-muted"
|
||||||
data-test="no-results-text"
|
data-test="no-results-text"
|
||||||
>
|
>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<li
|
<li
|
||||||
v-for="(opt, index) in options"
|
v-for="(opt, index) in filteredOptions"
|
||||||
:id="optionId(index)"
|
:id="optionId(index)"
|
||||||
:key="String(opt.value)"
|
:key="String(opt.value)"
|
||||||
data-test="option"
|
data-test="option"
|
||||||
@@ -136,11 +136,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
|
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 }}
|
{{ hint || error || success }}
|
||||||
@@ -180,6 +179,7 @@ const props = withDefaults(
|
|||||||
debounce?: number
|
debounce?: number
|
||||||
minSearchLength?: number
|
minSearchLength?: number
|
||||||
allowCreate?: boolean
|
allowCreate?: boolean
|
||||||
|
localFilter?: boolean
|
||||||
iconName?: string
|
iconName?: string
|
||||||
iconPosition?: 'left' | 'right'
|
iconPosition?: 'left' | 'right'
|
||||||
iconSize?: string | number
|
iconSize?: string | number
|
||||||
@@ -207,6 +207,7 @@ const props = withDefaults(
|
|||||||
debounce: 300,
|
debounce: 300,
|
||||||
minSearchLength: 0,
|
minSearchLength: 0,
|
||||||
allowCreate: false,
|
allowCreate: false,
|
||||||
|
localFilter: false,
|
||||||
iconName: '',
|
iconName: '',
|
||||||
iconPosition: 'left',
|
iconPosition: 'left',
|
||||||
iconSize: 24,
|
iconSize: 24,
|
||||||
@@ -253,9 +254,18 @@ const showMinSearch = computed(() =>
|
|||||||
props.minSearchLength > 0 && inputValue.value.length < props.minSearchLength,
|
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 optionId = (index: number) => `${inputId.value}-option-${index}`
|
||||||
const activeOptionId = computed(() =>
|
const activeOptionId = computed(() =>
|
||||||
activeIndex.value >= 0 && props.options[activeIndex.value]
|
activeIndex.value >= 0 && filteredOptions.value[activeIndex.value]
|
||||||
? optionId(activeIndex.value)
|
? optionId(activeIndex.value)
|
||||||
: undefined,
|
: undefined,
|
||||||
)
|
)
|
||||||
@@ -294,11 +304,6 @@ const iconInputPaddingClass = computed(() => {
|
|||||||
return parts.join(' ')
|
return parts.join(' ')
|
||||||
})
|
})
|
||||||
|
|
||||||
const focusPaddingClass = computed(() => {
|
|
||||||
if (props.iconName && props.iconPosition === 'left') return 'focus:!pl-11'
|
|
||||||
return 'focus:pl-[11px]'
|
|
||||||
})
|
|
||||||
|
|
||||||
const labelPositionClass = computed(() =>
|
const labelPositionClass = computed(() =>
|
||||||
props.iconName && props.iconPosition === 'left' ? 'left-11' : 'left-3',
|
props.iconName && props.iconPosition === 'left' ? 'left-11' : 'left-3',
|
||||||
)
|
)
|
||||||
@@ -315,10 +320,9 @@ const mergedInputClass = computed(() =>
|
|||||||
: 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',
|
: 'focus:border-m-primary',
|
||||||
isOpen.value ? '!rounded-b-none !border-b-0' : '',
|
isOpen.value ? '!rounded-b-none !border-b-transparent' : '',
|
||||||
props.inputClass,
|
props.inputClass,
|
||||||
iconInputPaddingClass.value,
|
iconInputPaddingClass.value,
|
||||||
focusPaddingClass.value,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -326,12 +330,13 @@ 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' : '',
|
||||||
props.disabled ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: props.disabled
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
@@ -432,8 +437,8 @@ const onKeydown = (event: KeyboardEvent) => {
|
|||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (activeIndex.value >= 0 && props.options[activeIndex.value]) {
|
if (activeIndex.value >= 0 && filteredOptions.value[activeIndex.value]) {
|
||||||
onSelect(props.options[activeIndex.value])
|
onSelect(filteredOptions.value[activeIndex.value])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (props.allowCreate && inputValue.value !== '') {
|
if (props.allowCreate && inputValue.value !== '') {
|
||||||
@@ -450,7 +455,7 @@ const onKeydown = (event: KeyboardEvent) => {
|
|||||||
if (!isOpen.value) {
|
if (!isOpen.value) {
|
||||||
isOpen.value = true
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,12 +486,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grow-height {
|
.grow-height {
|
||||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
|
||||||
|
|
||||||
.grow-height:focus {
|
|
||||||
padding-top: 0.625rem;
|
|
||||||
padding-bottom: 0.625rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -50,7 +49,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px] ',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
@@ -147,11 +146,12 @@ const mergedLabelClass = computed(() =>
|
|||||||
'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] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled.value
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -51,7 +51,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -59,7 +58,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'text-xs ml-[2px] ',
|
'text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -55,7 +54,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px] ',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
@@ -155,11 +154,12 @@ const mergedLabelClass = computed(() =>
|
|||||||
'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] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled.value
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -298,6 +298,41 @@ describe('MalioInputPhone', () => {
|
|||||||
expect(wrapper.get('input').classes()).toContain('!pr-10')
|
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 () => {
|
it('applies mask via maska directive', async () => {
|
||||||
const wrapper = mountComponent({mask: '+## # ## ## ## ##'})
|
const wrapper = mountComponent({mask: '+## # ## ## ## ##'})
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px] ',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
@@ -175,11 +174,12 @@ const mergedLabelClass = computed(() =>
|
|||||||
'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] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled.value
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
@@ -187,7 +187,8 @@ const mergedLabelClass = computed(() =>
|
|||||||
|
|
||||||
const mergedAddButtonClass = computed(() =>
|
const mergedAddButtonClass = computed(() =>
|
||||||
twMerge(
|
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' : '',
|
(props.disabled || props.readonly) ? 'cursor-not-allowed opacity-40 hover:opacity-40' : '',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -184,7 +184,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${editorId}-describedby`"
|
:id="`${editorId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -192,7 +191,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px]',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ error || success || hint }}
|
{{ error || success || hint }}
|
||||||
@@ -279,10 +278,11 @@ const mergedLabelClass = computed(() =>
|
|||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: props.disabled
|
||||||
|
? 'text-m-muted'
|
||||||
: isFocused.value
|
: isFocused.value
|
||||||
? 'text-m-primary'
|
? 'text-m-primary'
|
||||||
: 'text-m-text',
|
: 'text-m-text',
|
||||||
props.disabled ? 'text-black/60' : '',
|
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -52,7 +51,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px] ',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
@@ -158,11 +157,12 @@ const mergedLabelClass = computed(() =>
|
|||||||
'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] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled.value
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -149,4 +149,38 @@ describe('MalioInputTextArea', () => {
|
|||||||
expect(wrapper.find('p.text-m-success').exists()).toBe(false)
|
expect(wrapper.find('p.text-m-success').exists()).toBe(false)
|
||||||
expect(wrapper.get('p.text-m-danger').text()).toBe('Textarea error')
|
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,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="mergedGroupClass">
|
<div :class="mergedGroupClass">
|
||||||
|
<div class="relative w-full flex-1">
|
||||||
<textarea
|
<textarea
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
:name="name"
|
:name="name"
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'border-m-success focus:border-m-success'
|
? 'border-m-success focus:border-m-success'
|
||||||
: 'focus:border-m-primary',
|
: 'focus:border-m-primary',
|
||||||
|
isFocused ? 'textarea-scrollbar-primary' : '',
|
||||||
textInput,
|
textInput,
|
||||||
showCounterComputed ? 'pb-6' : '',
|
showCounterComputed ? 'pb-6' : '',
|
||||||
rounded,
|
rounded,
|
||||||
@@ -39,11 +41,12 @@
|
|||||||
class="floating-label absolute left-3 top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
|
class="floating-label absolute left-3 top-2 mt-1 inline-block origin-left transition-transform duration-150 font-medium"
|
||||||
:class="[
|
:class="[
|
||||||
shouldFloatLabel ? '-translate-y-[1.30rem] scale-90' : '',
|
shouldFloatLabel ? '-translate-y-[1.30rem] scale-90' : '',
|
||||||
disabled ? 'text-black/60' : '',
|
|
||||||
hasError
|
hasError
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled
|
||||||
|
? 'text-m-muted'
|
||||||
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
|
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
|
||||||
textLabel,
|
textLabel,
|
||||||
]"
|
]"
|
||||||
@@ -58,8 +61,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="hasError || hasSuccess || hint"
|
class="mt-1 flex items-center justify-between gap-2 text-xs min-h-[1rem]"
|
||||||
class="mt-1 flex items-center justify-between gap-2 text-xs"
|
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
@@ -75,6 +77,7 @@
|
|||||||
{{ error || success || hint }}
|
{{ error || success || hint }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -138,7 +141,7 @@ const props = withDefaults(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const mergedGroupClass = computed(() =>
|
const mergedGroupClass = computed(() =>
|
||||||
twMerge('relative w-full', props.groupClass),
|
twMerge('flex flex-col w-full', props.groupClass),
|
||||||
)
|
)
|
||||||
|
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
@@ -188,4 +191,8 @@ const onInput = (event: Event) => {
|
|||||||
background: white;
|
background: white;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textarea-scrollbar-primary {
|
||||||
|
scrollbar-color: rgb(var(--m-primary)) transparent;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -50,7 +50,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -58,7 +57,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 text-xs ml-[2px] ',
|
'mt-1 text-xs ml-[2px] min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ hint || error || success }}
|
{{ hint || error || success }}
|
||||||
@@ -144,11 +143,12 @@ const mergedLabelClass = computed(() =>
|
|||||||
'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] peer-focus:-translate-y-[1.55rem] scale-90' : '',
|
||||||
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
|
||||||
hasError.value
|
hasError.value
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess.value
|
: hasSuccess.value
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
|
: disabled.value
|
||||||
|
? 'text-m-muted'
|
||||||
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
props.labelClass,
|
props.labelClass,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -207,4 +207,70 @@ describe('MalioSelect', () => {
|
|||||||
expect(wrapper.find('p.text-m-success').exists()).toBe(false)
|
expect(wrapper.find('p.text-m-success').exists()).toBe(false)
|
||||||
expect(wrapper.get('p.text-m-danger').text()).toBe('Selection error')
|
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
|
hasError
|
||||||
? isOpen
|
? isOpen
|
||||||
? openDirection === 'down'
|
? openDirection === 'down'
|
||||||
? 'rounded-b-none !border !border-m-danger !border-b-0'
|
? 'rounded-b-none !border !border-m-danger !border-b-transparent'
|
||||||
: 'rounded-t-none !border !border-m-danger !border-t-0'
|
: 'rounded-t-none !border !border-m-danger !border-t-transparent'
|
||||||
: 'border-m-danger'
|
: 'border-m-danger'
|
||||||
: hasSuccess
|
: hasSuccess
|
||||||
? isOpen
|
? isOpen
|
||||||
? openDirection === 'down'
|
? openDirection === 'down'
|
||||||
? 'rounded-b-none !border !border-m-success !border-b-0'
|
? 'rounded-b-none !border !border-m-success !border-b-transparent'
|
||||||
: 'rounded-t-none !border !border-m-success !border-t-0'
|
: 'rounded-t-none !border !border-m-success !border-t-transparent'
|
||||||
: 'border-m-success'
|
: 'border-m-success'
|
||||||
: isOpen
|
: isOpen
|
||||||
? openDirection === 'down'
|
? openDirection === 'down'
|
||||||
? 'rounded-b-none !border !border-m-primary !border-b-0'
|
? 'rounded-b-none !border !border-m-primary !border-b-transparent'
|
||||||
: 'rounded-t-none !border !border-m-primary !border-t-0'
|
: 'rounded-t-none !border !border-m-primary !border-t-transparent'
|
||||||
: isOptionSelected
|
: isOptionSelected
|
||||||
? 'border-black'
|
? 'border-black'
|
||||||
: 'border-m-muted',
|
: 'border-m-muted',
|
||||||
@@ -73,13 +73,20 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
data-test="chevron"
|
||||||
class="absolute right-3 top-1/2 -translate-y-1/2"
|
class="absolute right-3 top-1/2 -translate-y-1/2"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-current'
|
: disabled
|
||||||
|
? 'text-m-muted'
|
||||||
|
: isOpen
|
||||||
|
? 'text-m-primary'
|
||||||
|
: isOptionSelected
|
||||||
|
? 'text-black'
|
||||||
|
: 'text-m-muted'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
@@ -145,7 +152,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${buttonId}-describedby`"
|
:id="`${buttonId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -153,7 +159,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 ml-[2px] text-xs',
|
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ error || success || hint }}
|
{{ error || success || hint }}
|
||||||
@@ -330,12 +336,7 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grow-height {
|
.grow-height {
|
||||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
|
||||||
|
|
||||||
.grow-height:focus {
|
|
||||||
padding-top: 0.625rem;
|
|
||||||
padding-bottom: 0.625rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -182,4 +182,70 @@ describe('MalioSelectCheckbox', () => {
|
|||||||
const root = wrapper.find('button').element.parentElement
|
const root = wrapper.find('button').element.parentElement
|
||||||
expect(root?.className).toContain('mt-4')
|
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
|
hasError
|
||||||
? isOpen
|
? isOpen
|
||||||
? openDirection === 'down'
|
? openDirection === 'down'
|
||||||
? 'rounded-b-none !border !border-m-danger !border-b-0'
|
? 'rounded-b-none !border !border-m-danger !border-b-transparent'
|
||||||
: 'rounded-t-none !border !border-m-danger !border-t-0'
|
: 'rounded-t-none !border !border-m-danger !border-t-transparent'
|
||||||
: 'border-m-danger'
|
: 'border-m-danger'
|
||||||
: hasSuccess
|
: hasSuccess
|
||||||
? isOpen
|
? isOpen
|
||||||
? openDirection === 'down'
|
? openDirection === 'down'
|
||||||
? 'rounded-b-none !border !border-m-success !border-b-0'
|
? 'rounded-b-none !border !border-m-success !border-b-transparent'
|
||||||
: 'rounded-t-none !border !border-m-success !border-t-0'
|
: 'rounded-t-none !border !border-m-success !border-t-transparent'
|
||||||
: 'border-m-success'
|
: 'border-m-success'
|
||||||
: isOpen
|
: isOpen
|
||||||
? openDirection === 'down'
|
? openDirection === 'down'
|
||||||
? 'rounded-b-none !border !border-m-primary !border-b-0'
|
? 'rounded-b-none !border !border-m-primary !border-b-transparent'
|
||||||
: 'rounded-t-none !border !border-m-primary !border-t-0'
|
: 'rounded-t-none !border !border-m-primary !border-t-transparent'
|
||||||
: isOptionSelected
|
: isOptionSelected
|
||||||
? 'border-black'
|
? 'border-black'
|
||||||
: 'border-m-muted',
|
: 'border-m-muted',
|
||||||
@@ -101,13 +101,20 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
data-test="chevron"
|
||||||
class="absolute right-3 top-1/2 -translate-y-1/2"
|
class="absolute right-3 top-1/2 -translate-y-1/2"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
? 'text-m-danger'
|
? 'text-m-danger'
|
||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-current'
|
: disabled
|
||||||
|
? 'text-m-muted'
|
||||||
|
: isOpen
|
||||||
|
? 'text-m-primary'
|
||||||
|
: isOptionSelected
|
||||||
|
? 'text-black'
|
||||||
|
: 'text-m-muted'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
@@ -194,7 +201,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${buttonId}-describedby`"
|
:id="`${buttonId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -202,7 +208,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 ml-[2px] text-xs',
|
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ error || success || hint }}
|
{{ error || success || hint }}
|
||||||
@@ -409,12 +415,7 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grow-height {
|
.grow-height {
|
||||||
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
|
||||||
|
|
||||||
.grow-height:focus {
|
|
||||||
padding-top: 0.625rem;
|
|
||||||
padding-bottom: 0.625rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError
|
hasError
|
||||||
@@ -66,7 +65,7 @@
|
|||||||
: hasSuccess
|
: hasSuccess
|
||||||
? 'text-m-success'
|
? 'text-m-success'
|
||||||
: 'text-m-muted',
|
: 'text-m-muted',
|
||||||
'mt-1 ml-[2px] text-xs',
|
'mt-1 ml-[2px] text-xs min-h-[1rem]',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ error || success || hint }}
|
{{ error || success || hint }}
|
||||||
|
|||||||
@@ -78,11 +78,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="hint || hasError || hasSuccess"
|
|
||||||
:id="`${inputId}-describedby`"
|
:id="`${inputId}-describedby`"
|
||||||
:class="[
|
:class="[
|
||||||
hasError ? 'text-m-danger' : hasSuccess ? 'text-m-success' : 'text-m-muted',
|
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 }}
|
{{ error || success || hint }}
|
||||||
|
|||||||
Reference in New Issue
Block a user