diff --git a/.changeset/fluffy-flowers-grab.md b/.changeset/fluffy-flowers-grab.md new file mode 100644 index 00000000000..7fe2f3b1d75 --- /dev/null +++ b/.changeset/fluffy-flowers-grab.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/ui-react-ai': patch +--- + +fix(ui-react-ai): Addressing cross-browser inconsistencies in AIConversation IME input handling diff --git a/packages/react-ai/src/components/AIConversation/views/default/Form.tsx b/packages/react-ai/src/components/AIConversation/views/default/Form.tsx index 8f7aaadaf92..930688a8007 100644 --- a/packages/react-ai/src/components/AIConversation/views/default/Form.tsx +++ b/packages/react-ai/src/components/AIConversation/views/default/Form.tsx @@ -110,7 +110,21 @@ export const Form: Required['Form'] = ({ value={input?.text ?? ''} testId="text-input" onCompositionStart={() => setComposing(true)} - onCompositionEnd={() => setComposing(false)} + onCompositionUpdate={(e) => { + const composedText = e?.currentTarget?.value || ''; + setInput?.((prevValue) => ({ + ...prevValue, + text: composedText, + })); + }} + onCompositionEnd={(e) => { + setComposing(false); + const composedText = e?.currentTarget?.value || ''; + setInput?.((prevValue) => ({ + ...prevValue, + text: composedText, + })); + }} onKeyDown={(e) => { // Submit on enter key if shift is not pressed also const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing; diff --git a/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx b/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx index 121d2e23131..4be0c75488e 100644 --- a/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx +++ b/packages/react-ai/src/components/AIConversation/views/default/__tests__/Form.spec.tsx @@ -1,11 +1,21 @@ +/* eslint-disable no-console */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { + render, + screen, + fireEvent, + waitFor, + within, +} from '@testing-library/react'; import { Form } from '../Form'; const setInput = jest.fn(); const input = {}; const handleSubmit = jest.fn(); const onValidate = jest.fn(); +const onCompositionStart = jest.fn(); +const onCompositionEnd = jest.fn(); +const onKeyDown = jest.fn(); const defaultProps = { allowAttachments: true, @@ -13,6 +23,9 @@ const defaultProps = { input, handleSubmit, onValidate, + onCompositionStart, + onCompositionEnd, + onKeyDown, }; describe('Form', () => { @@ -57,4 +70,54 @@ describe('Form', () => { expect(fileInput.files).not.toBeNull(); expect(fileInput.files![0]).toStrictEqual(testFile); }); + + it('updates IME input with composition completion', async () => { + const input = { currentTarget: { value: '你' } }; + const updatedInput = { currentTarget: { value: '你好' } }; + + const result = render(
); + expect(result.container).toBeDefined(); + + const textFieldContainer = screen.getByTestId('text-input'); + + const textInput = + textFieldContainer.querySelector('textarea') ?? + within(textFieldContainer).getByRole('textbox'); + + await waitFor(() => { + fireEvent.compositionStart(textInput); + fireEvent.compositionEnd(textInput, input); + }); + + await waitFor(() => { + fireEvent.compositionEnd(textInput, updatedInput); + }); + + expect(setInput).toHaveBeenCalledTimes(2); + }); + + it('updates IME input with composition update', async () => { + const input = { currentTarget: { value: 'しあわせ' } }; + const updatedInput = { currentTarget: { value: '幸せならおkです' } }; + + const result = render(); + expect(result.container).toBeDefined(); + + const textFieldContainer = screen.getByTestId('text-input'); + + const textInput = + textFieldContainer.querySelector('textarea') ?? + within(textFieldContainer).getByRole('textbox'); + + await waitFor(() => { + fireEvent.compositionStart(textInput); + fireEvent.compositionUpdate(textInput, input); + }); + + await waitFor(() => { + fireEvent.compositionUpdate(textInput, updatedInput); + }); + + expect(setInput).toHaveBeenCalledTimes(2); + }); });