diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 5c034bb..16cb82d 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568)) - Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572)) +- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580)) ## [1.7.15] - 2023-06-01 diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx index f0fbbb7..7b288ad 100644 --- a/packages/@headlessui-react/src/components/combobox/combobox.tsx +++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx @@ -778,9 +778,11 @@ function InputFn< // - By clicking `outside` of the Combobox useWatch( ([currentDisplayValue, state], [oldCurrentDisplayValue, oldState]) => { + // When the user is typing, we want to not touch the `input` at all. Especially when they are + // using an IME, we don't want to mess with the input at all. if (isTyping.current) return - let input = data.inputRef.current + let input = data.inputRef.current if (!input) return if (oldState === ComboboxState.Open && state === ComboboxState.Closed) { @@ -821,6 +823,10 @@ function InputFn< useWatch( ([newState], [oldState]) => { if (newState === ComboboxState.Open && oldState === ComboboxState.Closed) { + // When the user is typing, we want to not touch the `input` at all. Especially when they are + // using an IME, we don't want to mess with the input at all. + if (isTyping.current) return + let input = data.inputRef.current if (!input) return @@ -844,19 +850,12 @@ function InputFn< ) let isComposing = useRef(false) - let composedChangeEvent = useRef | null>(null) let handleCompositionStart = useEvent(() => { isComposing.current = true }) let handleCompositionEnd = useEvent(() => { d.nextFrame(() => { isComposing.current = false - - if (composedChangeEvent.current) { - actions.openCombobox() - onChange?.(composedChangeEvent.current) - composedChangeEvent.current = null - } }) }) @@ -885,6 +884,10 @@ function InputFn< case Keys.Enter: isTyping.current = false if (data.comboboxState !== ComboboxState.Open) return + + // When the user is still in the middle of composing by using an IME, then we don't want to + // submit this value and close the Combobox yet. Instead, we will fallback to the default + // behaviour which is to "end" the composition. if (isComposing.current) return event.preventDefault() @@ -983,12 +986,16 @@ function InputFn< }) let handleChange = useEvent((event: React.ChangeEvent) => { - if (isComposing.current) { - composedChangeEvent.current = event - return - } - actions.openCombobox() + // Always call the onChange listener even if the user is still typing using an IME (Input Method + // Editor). + // + // The main issue is Android, where typing always uses the IME APIs. Just waiting until the + // compositionend event is fired to trigger an onChange is not enough, because then filtering + // options while typing won't work at all because we are still in "composing" mode. onChange?.(event) + + // Open the combobox to show the results based on what the user has typed + actions.openCombobox() }) let handleBlur = useEvent(() => { diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index d818ab7..e894c1b 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568)) - Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572)) - Improve performance of `Combobox` component ([#2574](https://github.com/tailwindlabs/headlessui/pull/2574)) +- Ensure IME works on Android devices ([#2580](https://github.com/tailwindlabs/headlessui/pull/2580)) ## [1.7.14] - 2023-06-01 diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts index 5a39b16..f7e5d7a 100644 --- a/packages/@headlessui-vue/src/components/combobox/combobox.ts +++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts @@ -745,9 +745,11 @@ export let ComboboxInput = defineComponent({ watch( [currentDisplayValue, api.comboboxState], ([currentDisplayValue, state], [oldCurrentDisplayValue, oldState]) => { + // When the user is typing, we want to not touch the `input` at all. Especially when they + // are using an IME, we don't want to mess with the input at all. if (isTyping.value) return - let input = dom(api.inputRef) + let input = dom(api.inputRef) if (!input) return if (oldState === ComboboxStates.Open && state === ComboboxStates.Closed) { @@ -788,6 +790,10 @@ export let ComboboxInput = defineComponent({ // already in an open state. watch([api.comboboxState], ([newState], [oldState]) => { if (newState === ComboboxStates.Open && oldState === ComboboxStates.Closed) { + // When the user is typing, we want to not touch the `input` at all. Especially when they + // are using an IME, we don't want to mess with the input at all. + if (isTyping.value) return + let input = dom(api.inputRef) if (!input) return @@ -810,19 +816,12 @@ export let ComboboxInput = defineComponent({ }) let isComposing = ref(false) - let composedChangeEvent = ref<(Event & { target: HTMLInputElement }) | null>(null) function handleCompositionstart() { isComposing.value = true } function handleCompositionend() { disposables().nextFrame(() => { isComposing.value = false - - if (composedChangeEvent.value) { - api.openCombobox() - emit('change', composedChangeEvent.value) - composedChangeEvent.value = null - } }) } @@ -852,6 +851,10 @@ export let ComboboxInput = defineComponent({ case Keys.Enter: isTyping.value = false if (api.comboboxState.value !== ComboboxStates.Open) return + + // When the user is still in the middle of composing by using an IME, then we don't want + // to submit this value and close the Combobox yet. Instead, we will fallback to the + // default behaviour which is to "end" the composition. if (isComposing.value) return event.preventDefault() @@ -945,12 +948,16 @@ export let ComboboxInput = defineComponent({ } function handleInput(event: Event & { target: HTMLInputElement }) { - if (isComposing.value) { - composedChangeEvent.value = event - return - } - api.openCombobox() + // Always call the onChange listener even if the user is still typing using an IME (Input Method + // Editor). + // + // The main issue is Android, where typing always uses the IME APIs. Just waiting until the + // compositionend event is fired to trigger an onChange is not enough, because then filtering + // options while typing won't work at all because we are still in "composing" mode. emit('change', event) + + // Open the combobox to show the results based on what the user has typed + api.openCombobox() } function handleBlur() {