feat(ui) : saisie clavier MalioDate + bouton « + » InputEmail + séparateurs InputAmount (#MUI-42) #68

Merged
tristan merged 38 commits from feature/MUI-42-fix-composants-apres-retour-erp into develop 2026-06-09 15:39:38 +00:00
2 changed files with 56 additions and 1 deletions
Showing only changes of commit 379f4a2d6f - Show all commits
+17
View File
@@ -18,6 +18,23 @@
outline: 2px solid rgb(var(--m-primary) / 1);
outline-offset: 2px;
}
/* Anneau de focus clavier pour un combobox ouvert (input + liste) : l'anneau
entoure le bloc entier d'un seul tenant. L'input porte le contour haut+côtés,
la liste le contour côtés+bas ; la jonction (bas de l'input / haut de la liste)
reste sans contour pour un raccord sans couture. */
.m-combo-ring-top {
box-shadow:
-2px 0 0 0 rgb(var(--m-primary) / 1),
2px 0 0 0 rgb(var(--m-primary) / 1),
0 -2px 0 0 rgb(var(--m-primary) / 1);
}
.m-combo-ring-bottom {
box-shadow:
-2px 0 0 0 rgb(var(--m-primary) / 1),
2px 0 0 0 rgb(var(--m-primary) / 1),
0 2px 0 0 rgb(var(--m-primary) / 1);
}
}
@layer base {
@@ -24,6 +24,7 @@
type="text"
@input="onInput"
@focus="onFocus"
@blur="onKbdBlur"
@click="onInputClick"
@keydown="onKeydown"
>
@@ -90,6 +91,7 @@
: hasSuccess
? 'border-m-success select-scrollbar-success'
: 'border-m-primary select-scrollbar-primary',
keyboardFocused ? 'm-combo-ring-bottom' : '',
]"
>
<li
@@ -150,13 +152,16 @@
</template>
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, useAttrs, useId, watch} from 'vue'
import {computed, nextTick, onBeforeUnmount, onMounted, ref, useAttrs, useId, watch} from 'vue'
import {Icon as IconifyIcon} from '@iconify/vue'
import {twMerge} from 'tailwind-merge'
import MalioRequiredMark from '../shared/RequiredMark.vue'
import {useKbdFocusRing} from '../shared/useKbdFocusRing'
defineOptions({name: 'MalioInputAutocomplete', inheritAttrs: false})
const {keyboardFocused, onFocus: onKbdFocus, onBlur: onKbdBlur} = useKbdFocusRing()
type Option = {
label: string
value: string | number
@@ -321,6 +326,7 @@ const labelPositionClass = computed(() =>
const mergedInputClass = computed(() =>
twMerge(
'floating-input peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none placeholder:text-transparent text-lg rounded-md',
keyboardFocused.value ? (isOpen.value ? 'm-combo-ring-top' : 'm-focus-ring-kbd') : '',
isReadonly.value ? '' : 'grow-height',
isReadonly.value
? 'border-black'
@@ -400,6 +406,7 @@ const onInput = (event: Event) => {
}
const onFocus = () => {
onKbdFocus()
if (props.disabled || props.readonly) return
isFocused.value = true
isOpen.value = true
@@ -446,7 +453,20 @@ const closeAndRevert = () => {
isFocused.value = false
}
// Garde l'option active visible dans la liste défilante quand on navigue au clavier
watch(activeIndex, async (index) => {
if (index < 0 || !isOpen.value) return
await nextTick()
document.getElementById(optionId(index))?.scrollIntoView({block: 'nearest'})
})
const onKeydown = (event: KeyboardEvent) => {
// Tab : laisse le focus partir mais ferme la liste (et valide la saisie courante)
if (event.key === 'Tab') {
if (isOpen.value) closeAndCommit()
return
}
if (event.key === 'Escape') {
event.preventDefault()
closeAndRevert()
@@ -479,7 +499,25 @@ const onKeydown = (event: KeyboardEvent) => {
if (event.key === 'ArrowUp') {
event.preventDefault()
// Liste fermée : ouvre et place sur la dernière option (APG)
if (!isOpen.value) {
isOpen.value = true
activeIndex.value = filteredOptions.value.length - 1
return
}
activeIndex.value = Math.max(activeIndex.value - 1, 0)
return
}
// Home / End : première / dernière option quand la liste est ouverte
if (isOpen.value && event.key === 'Home') {
event.preventDefault()
activeIndex.value = 0
return
}
if (isOpen.value && event.key === 'End') {
event.preventDefault()
activeIndex.value = filteredOptions.value.length - 1
}
}