From 8c2ed143a1ef465986496c89860eb1dfc53868e8 Mon Sep 17 00:00:00 2001 From: k90325248 Date: Thu, 23 Apr 2026 16:25:15 +0800 Subject: [PATCH 1/2] fix(PinInput, InputDate): resolve double input and segment issues in IME mode --- src/runtime/components/InputDate.vue | 49 ++++++++++++++++++++++++++++ src/runtime/components/PinInput.vue | 27 +++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/runtime/components/InputDate.vue b/src/runtime/components/InputDate.vue index 4f311fd536..f507de0cd7 100644 --- a/src/runtime/components/InputDate.vue +++ b/src/runtime/components/InputDate.vue @@ -119,6 +119,51 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputDate || const inputsRef = ref([]) +const isComposing = ref(false) + +function onCompositionStart() { + isComposing.value = true +} + +function onCompositionEnd(event: Event) { + isComposing.value = false + const target = event.target as HTMLInputElement + const data = (event as CompositionEvent).data + + if (target && data) { + // Dispatch fake keydown and input events for reka-ui to recognize numeric input during IME composition. + for (const char of data) { + target.dispatchEvent(new KeyboardEvent('keydown', { + key: char, + code: `Digit${char}`, + bubbles: true, + cancelable: true + })) + + target.dispatchEvent(new InputEvent('input', { + data: char, + bubbles: true, + cancelable: true + })) + } + } +} + +function onKeydown(event: KeyboardEvent) { + // Prevent IME keydown (229) from interfering with reka-ui's segment logic. + if (isComposing.value || event.keyCode === 229) { + event.stopPropagation() + event.stopImmediatePropagation() + } +} + +function onInput(event: Event) { + // Prevent browser from inserting characters directly into segments during IME composition. + if (isComposing.value || (event as InputEvent).isComposing) { + event.stopImmediatePropagation() + } +} + function setInputRef(index: number, el: Element | ComponentPublicInstance | null) { // @ts-expect-error - ComponentPublicInstance type mismatch in Nuxt module augmentation inputsRef.value[index] = el @@ -173,6 +218,10 @@ defineExpose({ data-slot="segment" :class="ui.segment({ class: uiProp?.segment })" :data-segment="segment.part" + @input.capture="onInput" + @keydown.capture="onKeydown" + @compositionstart="onCompositionStart" + @compositionend="onCompositionEnd" > {{ segment.value.trim() }} diff --git a/src/runtime/components/PinInput.vue b/src/runtime/components/PinInput.vue index 52a254c82f..ad762b7ac3 100644 --- a/src/runtime/components/PinInput.vue +++ b/src/runtime/components/PinInput.vue @@ -89,6 +89,30 @@ function setInputRef(index: number, el: Element | ComponentPublicInstance | null inputsRef.value[index] = el } +const isComposing = ref(false) + +function onCompositionStart() { + isComposing.value = true +} + +function onCompositionEnd(event: Event) { + isComposing.value = false + // Some browsers may not fire an input event after compositionend, or reka-ui might miss it. + // We manually dispatch an input event to ensure reka-ui receives the final value. + const target = event.target as HTMLInputElement + if (target) { + target.dispatchEvent(new Event('input', { bubbles: true })) + } +} + +function onInput(event: Event) { + // Prevent reka-ui from shifting focus too early during IME composition, + // which causes subsequent characters to be entered into the next input field. + if (isComposing.value || (event as InputEvent).isComposing) { + event.stopImmediatePropagation() + } +} + const completed = ref(false) function onComplete(value: string[] | number[]) { // @ts-expect-error - 'target' does not exist in type 'EventInit' @@ -144,6 +168,9 @@ defineExpose({ :disabled="disabled" @blur="onBlur" @focus="emitFormFocus" + @input.capture="onInput" + @compositionstart="onCompositionStart" + @compositionend="onCompositionEnd" /> From a309ff0837513e93430ca262d4a06a44e2f83cc4 Mon Sep 17 00:00:00 2001 From: k90325248 Date: Thu, 23 Apr 2026 17:19:33 +0800 Subject: [PATCH 2/2] fix(InputDate): prevent invalid segment values during IME composition --- src/runtime/components/InputDate.vue | 15 ++++++++++----- src/runtime/components/PinInput.vue | 8 +++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/runtime/components/InputDate.vue b/src/runtime/components/InputDate.vue index f507de0cd7..392f04493b 100644 --- a/src/runtime/components/InputDate.vue +++ b/src/runtime/components/InputDate.vue @@ -127,20 +127,25 @@ function onCompositionStart() { function onCompositionEnd(event: Event) { isComposing.value = false - const target = event.target as HTMLInputElement const data = (event as CompositionEvent).data - if (target && data) { - // Dispatch fake keydown and input events for reka-ui to recognize numeric input during IME composition. + if (data) { + // Process each character from IME composition by dispatching fake events. + // We use document.activeElement to follow potential focus shifts between segments. for (const char of data) { - target.dispatchEvent(new KeyboardEvent('keydown', { + if (!/^\d$/.test(char)) continue + + const currentTarget = document.activeElement as HTMLInputElement + if (!currentTarget) break + + currentTarget.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: `Digit${char}`, bubbles: true, cancelable: true })) - target.dispatchEvent(new InputEvent('input', { + currentTarget.dispatchEvent(new InputEvent('input', { data: char, bubbles: true, cancelable: true diff --git a/src/runtime/components/PinInput.vue b/src/runtime/components/PinInput.vue index ad762b7ac3..d36ef0a794 100644 --- a/src/runtime/components/PinInput.vue +++ b/src/runtime/components/PinInput.vue @@ -97,10 +97,12 @@ function onCompositionStart() { function onCompositionEnd(event: Event) { isComposing.value = false - // Some browsers may not fire an input event after compositionend, or reka-ui might miss it. - // We manually dispatch an input event to ensure reka-ui receives the final value. + // Some browsers (like Chrome/Safari) may not fire an input event after compositionend. + // Firefox DOES fire a native input event after compositionend, so we must skip the synthetic dispatch. + const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('firefox') + const target = event.target as HTMLInputElement - if (target) { + if (target && !isFirefox) { target.dispatchEvent(new Event('input', { bubbles: true })) } }