Files
malio-layer-ui/app/story/input/inputAutocomplete.story.vue
tristan b2e3a83bb9 [#MUI-32] Création d'un composant saisie assistée (autocomplete) (#46)
| 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é

Reviewed-on: #46
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-13 06:59:13 +00:00

295 lines
9.2 KiB
Vue

<template>
<Story title="Input/Autocomplete">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Simple (statique)</h2>
<MalioInputAutocomplete
v-model="simpleValue"
label="Pays"
:options="staticOptions"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec icône à gauche</h2>
<MalioInputAutocomplete
v-model="leftIconValue"
label="Recherche"
icon-name="mdi:magnify"
icon-position="left"
:options="staticOptions"
/>
</div>
<div class="rounded-lg border p-4 md:col-span-2">
<h2 class="mb-4 text-xl font-bold">Branché sur une API (simulé)</h2>
<p class="mb-3 text-sm text-m-muted">
Tapez au moins 2 caractères. Le parent écoute <code>@search</code> et alimente <code>options</code> + <code>loading</code>.
</p>
<MalioInputAutocomplete
v-model="apiValue"
label="Client"
:options="apiOptions"
:loading="apiLoading"
:min-search-length="2"
icon-name="mdi:magnify"
icon-position="left"
@search="onSearchApi"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Création libre (allowCreate)</h2>
<MalioInputAutocomplete
v-model="createValue"
label="Catégorie"
:options="staticOptions"
allow-create
hint="Taper Entrée pour créer une nouvelle valeur"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Désactivé</h2>
<MalioInputAutocomplete
v-model="disabledValue"
label="Pays"
:options="staticOptions"
disabled
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Readonly</h2>
<MalioInputAutocomplete
v-model="readonlyValue"
label="Pays"
:options="staticOptions"
readonly
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Avec hint</h2>
<MalioInputAutocomplete
v-model="hintValue"
label="Pays"
:options="staticOptions"
hint="Sélectionne un pays dans la liste"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Erreur</h2>
<MalioInputAutocomplete
v-model="errorValue"
label="Pays"
:options="staticOptions"
error="Sélection invalide"
/>
</div>
<div class="rounded-lg border p-4">
<h2 class="mb-4 text-xl font-bold">Succès</h2>
<MalioInputAutocomplete
v-model="successValue"
label="Pays"
:options="staticOptions"
success="Sélection valide"
/>
</div>
</div>
</Story>
</template>
<docs lang="md">
# 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). Combine le pattern floating-label des autres inputs avec un dropdown inspiré de `MalioSelect`. Conçu pour les cas ERP la liste vient d'un appel API (auth, transformation, cache gérés par le parent).
------------------------------------------------------------------------
## Props détaillées
### id
- Type: string
- Description: Identifiant HTML de l'input. Auto-généré si non fourni (préfixe `malio-input-autocomplete-`).
### label
- Type: string
- Description: Texte affiché comme label flottant.
### name
- Type: string
- Description: Attribut name de l'input (formulaires).
### modelValue
- Type: `string | number | null | undefined`
- Description: Valeur sélectionnée. Doit correspondre à un `option.value` (ou être un texte libre si `allowCreate`).
### options
- Type: `{ label: string; value: string | number }[]`
- Défaut: `[]`
- Description: Liste affichée dans le dropdown. Le parent alimente cette liste (statique ou via API en réponse à l'event `search`).
### loading
- Type: boolean
- Défaut: `false`
- Description: Affiche un spinner à la place du chevron et un message dans le dropdown.
### debounce
- Type: number
- Défaut: `300`
- Description: Délai (ms) avant émission de l'event `search` après une frappe. Évite de spammer l'API.
### minSearchLength
- Type: number
- Défaut: `0`
- Description: Nombre minimum de caractères avant d'émettre `search`. Pratique pour API : ne pas appeler avec query vide.
### allowCreate
- Type: boolean
- Défaut: `false`
- Description: Si vrai, l'utilisateur peut valider (Entrée ou clic ailleurs) une valeur libre non présente dans `options` ; émet l'event `create`.
------------------------------------------------------------------------
## Icône
### iconName
- Type: string
- Défaut: `''` (aucune)
- Description: Nom Iconify de l'icône décorative.
### iconPosition
- Type: `'left' | 'right'`
- Défaut: `left`
- Description: Côté d'affichage de l'icône. À droite, l'icône s'aligne avec le chevron.
### iconSize / iconColor
- Type: number / string
- Défaut: `24` / `text-m-muted`
------------------------------------------------------------------------
## Textes du dropdown
### noResultsText
- Type: string
- Défaut: `Aucun résultat`
- Description: Affiché quand `options` est vide.
### loadingText
- Type: string
- Défaut: `Chargement…`
- Description: Affiché pendant que `loading=true`.
### minSearchText
- Type: string
- Défaut: `Tapez pour rechercher`
- Description: Affiché quand l'utilisateur n'a pas atteint `minSearchLength`.
------------------------------------------------------------------------
## Apparence & Style
### inputClass / labelClass / groupClass
- Type: string
- Description: Classes CSS appliquées respectivement à l'input, au label et au conteneur (fusionnées via `twMerge`).
------------------------------------------------------------------------
## Validation & Contraintes
### required / disabled / readonly
- Type: boolean
- Description: Attributs HTML standards. `disabled` et `readonly` empêchent l'ouverture du dropdown.
------------------------------------------------------------------------
## États & Messages
### hint / error / success
- Type: string
- Description: Messages affichés sous le champ. `error` est prioritaire et active `aria-invalid`.
------------------------------------------------------------------------
## Clavier
- `` / `` : naviguer dans les options
- `Entrée` : sélectionner l'option active (ou créer si `allowCreate`)
- `Échap` : fermer le dropdown et revenir à la dernière sélection
------------------------------------------------------------------------
## Accessibilité
- `role="combobox"` sur l'input avec `aria-expanded`, `aria-controls`, `aria-activedescendant`.
- `role="listbox"` sur le dropdown, `role="option"` sur chaque entrée, `aria-selected` reflète `modelValue`.
- `aria-invalid` activé si `error` existe.
- `aria-describedby` référence dynamiquement le message affiché.
------------------------------------------------------------------------
## Events
### update:modelValue
- Émis quand l'utilisateur sélectionne une option. Permet l'utilisation avec v-model.
### search
- Émis (après debounce + minSearchLength) avec la query texte tapée. C'est ce que le parent écoute pour lancer l'appel API.
### select
- Émis avec l'objet `Option` complet (ou `null` à la réinitialisation). Utile pour récupérer le `label` côté parent en plus du `value`.
### create
- Émis avec la chaîne saisie quand `allowCreate` est vrai et que l'utilisateur valide une valeur libre.
</docs>
<script setup lang="ts">
import {ref} from 'vue'
import MalioInputAutocomplete from '../../components/malio/input/InputAutocomplete.vue'
type Option = {label: string; value: string | number}
const staticOptions: Option[] = [
{label: 'France', value: 'fr'},
{label: 'Belgique', value: 'be'},
{label: 'Canada', value: 'ca'},
{label: 'Suisse', value: 'ch'},
{label: 'Luxembourg', value: 'lu'},
{label: 'Allemagne', value: 'de'},
]
const simpleValue = ref<string | number | null>('fr')
const leftIconValue = ref<string | number | null>(null)
const createValue = ref<string | number | null>(null)
const disabledValue = ref<string | number | null>('fr')
const readonlyValue = ref<string | number | null>('be')
const hintValue = ref<string | number | null>(null)
const errorValue = ref<string | number | null>('fr')
const successValue = ref<string | number | null>('fr')
const apiValue = ref<string | number | null>(null)
const apiOptions = ref<Option[]>([])
const apiLoading = ref(false)
const fakeClients: Option[] = [
{label: 'Yuno Malio', value: 1},
{label: 'Yuna Corp', value: 2},
{label: 'Yum Foods', value: 3},
{label: 'Acme Inc.', value: 4},
{label: 'Globex Corp', value: 5},
]
const onSearchApi = async (query: string) => {
apiLoading.value = true
await new Promise(resolve => setTimeout(resolve, 400))
apiOptions.value = fakeClients.filter(c =>
c.label.toLowerCase().includes(query.toLowerCase()),
)
apiLoading.value = false
}
</script>