From dc11e0a20af8a78670c2c148e115f98a0605c50a Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 9 Jul 2025 17:08:17 -0700 Subject: [PATCH 1/4] feat: block onChange from firing until composition end this is for IMEs, we only want onChange to fire once the user has confirmed the final word/phrase they wanna enter --- .../@react-aria/textfield/src/useTextField.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index e232514d052..3756b3ba2c5 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -12,14 +12,16 @@ import {AriaTextFieldProps} from '@react-types/textfield'; import {DOMAttributes, ValidationResult} from '@react-types/shared'; -import {filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils'; +import {chain, filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils'; import React, { ChangeEvent, HTMLAttributes, type JSX, LabelHTMLAttributes, RefObject, + useCallback, useEffect, + useRef, useState } from 'react'; import {useControlledState} from '@react-stately/utils'; @@ -122,9 +124,20 @@ export function useTextField(props.value, props.defaultValue || '', props.onChange); + + let isComposing = useRef(false); + let onChange = useCallback((val) => { + if (isComposing.current) { + return; + } + + onChangeProp && onChangeProp(val); + }, [onChangeProp]); + + let [value, setValue] = useControlledState(props.value, props.defaultValue || '', onChange); let {focusableProps} = useFocusable(props, ref); let validationState = useFormValidationState({ ...props, @@ -165,6 +178,15 @@ export function useTextField { + isComposing.current = true; + }, []); + + let onCompositionEnd = useCallback(() => { + isComposing.current = false; + onChangeProp && onChangeProp(value); + }, [onChangeProp, value]); + return { labelProps, inputProps: mergeProps( @@ -201,8 +223,8 @@ export function useTextField Date: Wed, 9 Jul 2025 17:08:46 -0700 Subject: [PATCH 2/4] fix lint --- packages/@react-aria/textfield/src/useTextField.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index 3756b3ba2c5..27814b34973 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -11,8 +11,8 @@ */ import {AriaTextFieldProps} from '@react-types/textfield'; -import {DOMAttributes, ValidationResult} from '@react-types/shared'; import {chain, filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils'; +import {DOMAttributes, ValidationResult} from '@react-types/shared'; import React, { ChangeEvent, HTMLAttributes, From 56c40559da0a8c81da81749609ae3392ec6140ed Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 9 Jul 2025 17:23:33 -0700 Subject: [PATCH 3/4] fix extra onChange when deleting IME input deleting IME input is essentially cancelling it so it shouldnt trigger another onChange --- packages/@react-aria/textfield/src/useTextField.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index 27814b34973..2f886dfde5f 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -182,9 +182,11 @@ export function useTextField { + let onCompositionEnd = useCallback((e) => { isComposing.current = false; - onChangeProp && onChangeProp(value); + if (e.data !== '') { + onChangeProp && onChangeProp(value); + } }, [onChangeProp, value]); return { From 1d464a3892e09f0166c1d8a49c7db91ccf0602c7 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 9 Jul 2025 17:24:39 -0700 Subject: [PATCH 4/4] review comments --- packages/@react-aria/textfield/src/useTextField.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/textfield/src/useTextField.ts b/packages/@react-aria/textfield/src/useTextField.ts index 2f886dfde5f..8616abeb94b 100644 --- a/packages/@react-aria/textfield/src/useTextField.ts +++ b/packages/@react-aria/textfield/src/useTextField.ts @@ -134,7 +134,7 @@ export function useTextField(props.value, props.defaultValue || '', onChange); @@ -185,7 +185,7 @@ export function useTextField { isComposing.current = false; if (e.data !== '') { - onChangeProp && onChangeProp(value); + onChangeProp?.(value); } }, [onChangeProp, value]);