Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/runtime/components/InputDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,56 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputDate ||

const inputsRef = ref<ComponentPublicInstance[]>([])

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
Expand Down Expand Up @@ -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() }}
</DateField.Input>
Expand Down
29 changes: 29 additions & 0 deletions src/runtime/components/PinInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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'
Expand Down Expand Up @@ -144,6 +170,9 @@ defineExpose({
:disabled="disabled"
@blur="onBlur"
@focus="emitFormFocus"
@input.capture="onInput"
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd"
/>
</PinInputRoot>
</template>
Loading