diff --git a/src/runtime/components/InputDate.vue b/src/runtime/components/InputDate.vue index 4f311fd536..392f04493b 100644 --- a/src/runtime/components/InputDate.vue +++ b/src/runtime/components/InputDate.vue @@ -119,6 +119,56 @@ 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 data = (event as CompositionEvent).data + + 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) { + 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 + })) + + currentTarget.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 +223,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..d36ef0a794 100644 --- a/src/runtime/components/PinInput.vue +++ b/src/runtime/components/PinInput.vue @@ -89,6 +89,32 @@ 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 (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 && !isFirefox) { + 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 +170,9 @@ defineExpose({ :disabled="disabled" @blur="onBlur" @focus="emitFormFocus" + @input.capture="onInput" + @compositionstart="onCompositionStart" + @compositionend="onCompositionEnd" />