fix(natural-date-input): preserve input on edit, use reliable update event

Two reliability bugs in the natural-language date input:

1. Clicking Edit on a saved-date pill and changing the value immediately
   re-showed the saved value. clearAndEdit pre-fills the input with the
   formatted saved date so the admin doesn't start over, but chrono
   parses that string on the very first keystroke, re-sets parsedDate,
   and the auto-hide template flips the pill back. Added an isEditing
   flag that keeps the input visible across re-parses and clears on
   blur once we have a valid parse.

2. Typing "tomorrow at 2pm" sometimes committed "tomorrow at <current
   time>". UInput's template spreads \$attrs onto the inner <input>
   alongside its own @input="onInput", and Vue's listener-array merge
   intermittently drops the fall-through @input mid-typing — in the
   reproduction, the final 'm' never reached parseNaturalInput, so
   chrono's last successful read was "tomorrow at 2p" matching just
   "tomorrow". Switched to @update:model-value (a declared emit on
   UInput, so it goes through the reliable component-emit path) and
   made onBlur always re-parse the final value as a backup.
This commit is contained in:
Jennie Robinson Faber 2026-05-19 12:19:35 +01:00
parent 9e4030ccfd
commit 96470a604a

View file

@ -1,8 +1,8 @@
<template>
<div class="space-y-2">
<div v-if="!parsedDate || hasError" class="relative">
<div v-if="!parsedDate || hasError || isEditing" class="relative">
<UInput
v-model="naturalInput"
:model-value="naturalInput"
:placeholder="placeholder"
:color="
hasError && naturalInput.trim()
@ -11,7 +11,7 @@
? 'success'
: undefined
"
@input="parseNaturalInput"
@update:model-value="onNaturalInputChange"
@blur="onBlur"
>
<template #trailing>
@ -32,7 +32,7 @@
</div>
<div
v-if="parsedDate && isValidParse"
v-if="parsedDate && isValidParse && !isEditing"
class="text-sm px-3 py-2"
style="color: var(--candle); background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
>
@ -108,6 +108,7 @@ const isValidParse = ref(false);
const hasError = ref(false);
const errorMessage = ref("");
const datetimeValue = ref("");
const isEditing = ref(false);
// Initialize with current value
onMounted(() => {
@ -132,6 +133,7 @@ watch(
datetimeValue.value = formatForDatetimeLocal(date);
isValidParse.value = true;
naturalInput.value = ""; // Clear natural input when set externally
isEditing.value = false;
}
} else if (!newValue) {
reset();
@ -175,11 +177,23 @@ const parseNaturalInput = () => {
}
};
// UInput's fall-through @input listener fires unreliably (the Nuxt UI Input
// template merges $attrs with an explicit @input and can drop keystrokes).
// update:model-value is a declared emit and fires for every change.
const onNaturalInputChange = (value) => {
naturalInput.value = value;
parseNaturalInput();
};
const onBlur = () => {
// If we have a valid parse but the input changed, try to parse again
if (naturalInput.value.trim() && !isValidParse.value) {
// Always re-parse on blur so the final typed value wins, even if an
// intermediate state produced a stale parse.
if (naturalInput.value.trim()) {
parseNaturalInput();
}
if (isValidParse.value) {
isEditing.value = false;
}
};
const onDatetimeChange = () => {
@ -190,6 +204,7 @@ const onDatetimeChange = () => {
isValidParse.value = true;
hasError.value = false;
naturalInput.value = ""; // Clear natural input when using traditional picker
isEditing.value = false;
emit("update:modelValue", datetimeValue.value);
}
} else {
@ -227,6 +242,9 @@ const clearAndEdit = () => {
parsedDate.value = null;
isValidParse.value = false;
hasError.value = false;
// Prevent auto-hide while editing: chrono re-parses the pre-filled string
// on the first keystroke, which would otherwise hide the input.
isEditing.value = true;
};
const formatForDatetimeLocal = (date) => {