Skip to content

Commit 769d725

Browse files
authored
feat: Prevent onChange from firing in TextFields while user is typing via IME (#8519)
* 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 * fix lint * fix extra onChange when deleting IME input deleting IME input is essentially cancelling it so it shouldnt trigger another onChange * review comments
1 parent e677260 commit 769d725

File tree

1 file changed

+29
-5
lines changed

1 file changed

+29
-5
lines changed

packages/@react-aria/textfield/src/useTextField.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@
1111
*/
1212

1313
import {AriaTextFieldProps} from '@react-types/textfield';
14+
import {chain, filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils';
1415
import {DOMAttributes, ValidationResult} from '@react-types/shared';
15-
import {filterDOMProps, getOwnerWindow, mergeProps, useFormReset} from '@react-aria/utils';
1616
import React, {
1717
ChangeEvent,
1818
HTMLAttributes,
1919
type JSX,
2020
LabelHTMLAttributes,
2121
RefObject,
22+
useCallback,
2223
useEffect,
24+
useRef,
2325
useState
2426
} from 'react';
2527
import {useControlledState} from '@react-stately/utils';
@@ -122,9 +124,20 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
122124
isRequired = false,
123125
isReadOnly = false,
124126
type = 'text',
125-
validationBehavior = 'aria'
127+
validationBehavior = 'aria',
128+
onChange: onChangeProp
126129
} = props;
127-
let [value, setValue] = useControlledState<string>(props.value, props.defaultValue || '', props.onChange);
130+
131+
let isComposing = useRef(false);
132+
let onChange = useCallback((val) => {
133+
if (isComposing.current) {
134+
return;
135+
}
136+
137+
onChangeProp?.(val);
138+
}, [onChangeProp]);
139+
140+
let [value, setValue] = useControlledState<string>(props.value, props.defaultValue || '', onChange);
128141
let {focusableProps} = useFocusable<TextFieldHTMLElementType[T]>(props, ref);
129142
let validationState = useFormValidationState({
130143
...props,
@@ -165,6 +178,17 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
165178
}
166179
}, [ref]);
167180

181+
let onCompositionStart = useCallback(() => {
182+
isComposing.current = true;
183+
}, []);
184+
185+
let onCompositionEnd = useCallback((e) => {
186+
isComposing.current = false;
187+
if (e.data !== '') {
188+
onChangeProp?.(value);
189+
}
190+
}, [onChangeProp, value]);
191+
168192
return {
169193
labelProps,
170194
inputProps: mergeProps(
@@ -201,8 +225,8 @@ export function useTextField<T extends TextFieldIntrinsicElements = DefaultEleme
201225
onPaste: props.onPaste,
202226

203227
// Composition events
204-
onCompositionEnd: props.onCompositionEnd,
205-
onCompositionStart: props.onCompositionStart,
228+
onCompositionEnd: chain(onCompositionEnd, props.onCompositionEnd),
229+
onCompositionStart: chain(onCompositionStart, props.onCompositionStart),
206230
onCompositionUpdate: props.onCompositionUpdate,
207231

208232
// Selection events

0 commit comments

Comments
 (0)