From 07b84a25120e041d9a7bd3363a0db9f8689b5e44 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 23 Feb 2026 15:35:57 +0100 Subject: [PATCH] fix : modification du composant TimeSelect.vue pour pouvoir taper les heures au clavier --- frontend/components/ui/TimeSelect.vue | 131 +++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 15 deletions(-) diff --git a/frontend/components/ui/TimeSelect.vue b/frontend/components/ui/TimeSelect.vue index ed9d96a..1ef18e5 100644 --- a/frontend/components/ui/TimeSelect.vue +++ b/frontend/components/ui/TimeSelect.vue @@ -1,15 +1,36 @@ @@ -55,7 +75,9 @@ const emit = defineEmits<{ const root = ref(null) const trigger = ref(null) const menu = ref(null) +const inputRef = ref(null) const isOpen = ref(false) +const inputValue = ref('') const menuStyle = ref>({ top: '0px', left: '0px', @@ -73,7 +95,31 @@ const timeSlots = computed(() => { return slots }) -const displayValue = computed(() => props.modelValue || props.placeholder) +const filteredTimeSlots = computed(() => { + const query = inputValue.value.trim() + if (!query) return timeSlots.value + return timeSlots.value.filter((slot) => slot.includes(query)) +}) + +const applyTimeMask = (value: string): string => { + const digits = value.replace(/\D/g, '').slice(0, 4) + if (digits.length <= 2) return digits + return `${digits.slice(0, 2)}:${digits.slice(2)}` +} + +const normalizeTypedTime = (value: string): string | null => { + const trimmed = value.trim() + if (trimmed === '') return '' + + // Accepte HH:MM ou H:MM puis normalise en HH:MM. + const match = trimmed.match(/^(\d{1,2}):(\d{2})$/) + if (!match) return null + const hours = Number(match[1]) + const minutes = Number(match[2]) + if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return null + if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}` +} const updateMenuPosition = () => { const triggerEl = trigger.value @@ -103,10 +149,57 @@ const toggleOpen = () => { } } +const openMenu = () => { + if (props.disabled) return + if (!isOpen.value) { + isOpen.value = true + nextTick(updateMenuPosition) + } +} + +const openMenuAndFocusFirst = () => { + openMenu() +} + +const closeMenu = () => { + isOpen.value = false +} + +const commitInput = () => { + const normalized = normalizeTypedTime(inputValue.value) + if (normalized === null) { + inputValue.value = props.modelValue + closeMenu() + return + } + emit('update:modelValue', normalized) + inputValue.value = normalized + closeMenu() +} + +const onInput = (event: Event) => { + const target = event.target as HTMLInputElement + const masked = applyTimeMask(target.value) + if (masked !== inputValue.value) { + inputValue.value = masked + } + openMenu() +} + +const onInputBlur = () => { + // Laisse le temps au click menu de passer avant fermeture. + setTimeout(() => { + if (menu.value?.contains(document.activeElement)) return + commitInput() + }, 50) +} + const selectValue = (value: string) => { if (props.disabled) return emit('update:modelValue', value) + inputValue.value = value isOpen.value = false + nextTick(() => inputRef.value?.focus()) } const onDocumentClick = (event: MouseEvent) => { @@ -139,6 +232,14 @@ watch(() => props.disabled, (disabled) => { } }) +watch( + () => props.modelValue, + (value) => { + inputValue.value = value + }, + { immediate: true } +) + onMounted(() => { document.addEventListener('click', onDocumentClick) })