From b09a96cc5352adfb57503ce823b7df9588fe97c7 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 25 Jun 2026 10:37:17 +0200 Subject: [PATCH] =?UTF-8?q?fix(input)=20:=20InputAutocomplete=20garde=20la?= =?UTF-8?q?=20valeur=20coll=C3=A9e=20apr=C3=A8s=20s=C3=A9lection=20(MUI-48?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Après avoir sélectionné une suggestion dans la liste, un collage qui remplace tout (Ctrl+A puis Ctrl+V, sans re-cliquer dans le champ) vidait le champ au lieu de prendre la valeur collée. Cause : onSelect repassait isFocused à false alors que l'input gardait le focus DOM (option cliquée en mousedown.prevent). Au collage, onInput émet update:modelValue(null) et le watch de synchronisation, protégé par le seul isFocused, remettait inputValue à ''. onInput resynchronise désormais isFocused (un évènement input prouve l'édition). - test de non-régression colocalisé (séquence sélection → Ctrl+A/Ctrl+V) - page playground : section allowCreate + BAN dédiée au test - CHANGELOG : entrée Fixed MUI-48 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../composant/input/inputAutocomplete.vue | 61 +++++++++++++++++++ CHANGELOG.md | 1 + .../malio/input/InputAutocomplete.test.ts | 38 +++++++++++- .../malio/input/InputAutocomplete.vue | 5 ++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/.playground/pages/composant/input/inputAutocomplete.vue b/.playground/pages/composant/input/inputAutocomplete.vue index dfac49b..89a2119 100644 --- a/.playground/pages/composant/input/inputAutocomplete.vue +++ b/.playground/pages/composant/input/inputAutocomplete.vue @@ -47,6 +47,31 @@

+
+

allowCreate + BAN (test MUI-48)

+

+ Tapez au moins 3 caractères → suggestions de la Base Adresse Nationale. + Repro : sélectionnez une adresse dans la liste, puis + Ctrl+A et Ctrl+V pour coller une autre valeur + par-dessus. La valeur collée doit rester (le champ ne doit ni se vider, ni faire redescendre le label). +

+ +

+ v-model : {{ banValue ?? 'null' }} +

+
+

Avec création (allowCreate)

{ const onSelectApi = (option: Option | null) => { apiSelected.value = option } + +// allowCreate + BAN (test MUI-48) : recherche nationale sur la Base Adresse Nationale. +const banValue = ref(null) +const banOptions = ref([]) +const banLoading = ref(false) +let banFetchId = 0 + +const onSearchBan = async (query: string) => { + if (query.length < 3) { + banOptions.value = [] + banLoading.value = false + return + } + const requestId = ++banFetchId + banLoading.value = true + try { + const params = new URLSearchParams({q: query, limit: '8'}) + const response = await fetch(`https://api-adresse.data.gouv.fr/search/?${params.toString()}`) + const data = await response.json() as {features: {properties: {label: string}}[]} + if (requestId !== banFetchId) return + banOptions.value = data.features.map(f => ({ + label: f.properties.label, + value: f.properties.label, + })) + } catch (err) { + if (requestId !== banFetchId) return + banOptions.value = [] + console.error('Erreur lors du chargement des adresses BAN', err) + } finally { + if (requestId === banFetchId) banLoading.value = false + } +} + +const onCreateBan = (value: string) => { + console.log('BAN — valeur libre créée :', value) +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 107ab34..5d277ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Liste des évolutions de la librairie Malio layer UI * [#MUI-42] Button / ButtonIcon : l'anneau de focus passe du halo `ring-2 ring-m-primary/50` à l'anneau standard `.m-focus-ring` (outline plein, offset 2px), pour l'homogénéité avec les autres composants. ### Fixed +* [#MUI-48] InputAutocomplete : après avoir **sélectionné une suggestion** dans la liste, un **collage qui remplace tout** (Ctrl+A puis Ctrl+V, sans re-cliquer dans le champ) **vidait le champ** au lieu de prendre la valeur collée (label qui redescend, dropdown « Tapez pour rechercher »). Cause : `onSelect` repassait `isFocused` à `false` alors que l'input gardait le focus DOM (option cliquée en `mousedown.prevent`) ; le `watch` de synchronisation, non protégé, remettait `inputValue` à `''`. `onInput` resynchronise désormais `isFocused`. La sélection à la souris n'était pas affectée (re-clic réalignant l'état). * [#MUI-47] Sidebar : la **bande de ~4px en haut/bas d'un lien** (padding du `
  • ` qui porte le fond de hover) était survolée mais **non cliquable**. Le padding vertical passe du `
  • ` à l'`` (`py-1`), si bien que toute la zone survolée devient cliquable — sans changement visuel. Les côtés n'étaient pas affectés (`` en `block`, pas de padding horizontal sur le `
  • `). * Sidebar : le **lien actif** reste actif sur les **sous-routes** (match par préfixe via `useRoute().path` au lieu de l'`active-class` de NuxtLink qui dépendait de l'imbrication des routes) — ex. `/supplier` reste surligné sur `/supplier/1/edit`. Nouvelle option `exact: true` par item pour forcer le match strict. * Famille Date (CalendarField) : le **clic sur le picto calendrier** ouvre désormais le popover (le `` en overlay absolu interceptait le clic sans le traiter, et ne le laissait pas retomber sur l'input). Couvre Date, DateTime, DateRange, DateWeek. La croix d'effacement conserve son comportement (efface sans ouvrir). diff --git a/app/components/malio/input/InputAutocomplete.test.ts b/app/components/malio/input/InputAutocomplete.test.ts index ebc59fe..96cf735 100644 --- a/app/components/malio/input/InputAutocomplete.test.ts +++ b/app/components/malio/input/InputAutocomplete.test.ts @@ -1,6 +1,6 @@ import {describe, expect, it, vi} from 'vitest' import {mount} from '@vue/test-utils' -import type {DefineComponent} from 'vue' +import {defineComponent, nextTick, ref, type DefineComponent} from 'vue' import {Icon as IconifyIcon} from '@iconify/vue' import InputAutocomplete from './InputAutocomplete.vue' @@ -569,4 +569,40 @@ describe('MalioInputAutocomplete', () => { expect(msg.exists()).toBe(true) expect(msg.classes()).not.toContain('min-h-[1rem]') }) + + // MUI-48 : après avoir sélectionné une option dans la liste, le champ garde le focus DOM + // mais isFocused interne passe à false (clic option en mousedown.prevent). Un collage qui + // remplace tout (Ctrl+A puis Ctrl+V) déclenche update:modelValue(null) ; le watch ne doit + // PAS vider la valeur collée. Régression : le champ se vidait au lieu de prendre le texte collé. + it('MUI-48 : un collage après sélection dans la liste remplace la valeur (ne la vide pas)', async () => { + const Harness = defineComponent({ + components: {InputAutocomplete}, + setup() { + const val = ref(null) + const opts = ref([{label: '10 Rue de la Paix', value: '10 Rue de la Paix'}]) + return {val, opts} + }, + template: '', + }) + + const wrapper = mount(Harness, { + attachTo: document.body, + global: {stubs: {IconifyIcon: {template: ''}}}, + }) + const input = wrapper.get('input') + + // saisie puis sélection d'une suggestion (commit, focus DOM conservé) + await input.trigger('focus') + await input.setValue('10') + await wrapper.findAll('[data-test="option"]')[0].trigger('click') + await nextTick() + expect(input.element.value).toBe('10 Rue de la Paix') + + // Ctrl+A puis Ctrl+V : input toujours focalisé DOM, aucun nouvel évènement focus + await input.setValue('25 Avenue Victor Hugo') + await nextTick() + + expect(input.element.value).toBe('25 Avenue Victor Hugo') + wrapper.unmount() + }) }) diff --git a/app/components/malio/input/InputAutocomplete.vue b/app/components/malio/input/InputAutocomplete.vue index 1d907c2..780ed73 100644 --- a/app/components/malio/input/InputAutocomplete.vue +++ b/app/components/malio/input/InputAutocomplete.vue @@ -393,6 +393,11 @@ const scheduleSearch = () => { const onInput = (event: Event) => { const target = event.target as HTMLInputElement + // Un évènement input prouve que le champ est en cours d'édition : on resynchronise + // isFocused, qu'une sélection précédente (onSelect) a pu passer à false tout en gardant + // le focus DOM (clic option en mousedown.prevent). Sans ça, le watch ci-dessous remettrait + // inputValue à '' au collage et la valeur collée serait perdue (MUI-48). + isFocused.value = true inputValue.value = target.value if (!isOpen.value) isOpen.value = true activeIndex.value = -1