diff --git a/pages/input/korean-ime.page.tsx b/pages/input/korean-ime.page.tsx new file mode 100644 index 0000000000..8acda4889e --- /dev/null +++ b/pages/input/korean-ime.page.tsx @@ -0,0 +1,81 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; + +import { Box, FormField, SpaceBetween } from '~components'; +import Input from '~components/input'; + +import { SimplePage } from '../app/templates'; + +export default function InputKoreanIMEPage() { + const [searchText, setSearchText] = useState(''); + const [searchResults, setSearchResults] = useState([]); + + const handleKeyDown = (event: CustomEvent) => { + if (event.detail.keyCode === 13 && searchText.trim()) { + setSearchResults(prev => [ + `Search executed: ${searchText}`, + ...prev.slice(0, 9), // Keep last 10 results + ]); + } + }; + + return ( + + + + Test Instructions: +
    +
  1. Enable Korean 2-set keyboard
  2. +
  3. Type Korean character: ㄱ + ㅏ (forms 가)
  4. +
  5. Press Enter → Should complete character, NOT show search result
  6. +
  7. Press Enter again → Should show search result with 가
  8. +
+
+ + + setSearchText(detail.value)} + onKeyDown={handleKeyDown} + ariaLabel="Korean IME test search" + placeholder="가족, 한글, etc." + type="search" + /> + + + + Current Input: {searchText || '(empty)'} + + + + Search Results: + {searchResults.length === 0 ? ( + + No searches yet + + ) : ( + + {searchResults.map((result, index) => ( + + {result} + + ))} + + )} + +
+
+ ); +} diff --git a/src/input/__tests__/input.test.tsx b/src/input/__tests__/input.test.tsx index 417627135a..da8c637f12 100644 --- a/src/input/__tests__/input.test.tsx +++ b/src/input/__tests__/input.test.tsx @@ -515,4 +515,31 @@ describe('Input', () => { expect(input).toHaveClass('additional-class'); }); }); + + describe('IME composition', () => { + test('does not trigger onKeyDown handler when Enter pressed during active IME composition', () => { + const onKeyDown = jest.fn(); + const { wrapper, input } = renderInput({ value: '가', onKeyDown }); + + input.dispatchEvent(new CompositionEvent('compositionstart')); + wrapper.findNativeInput().keydown({ keyCode: 13, isComposing: false }); + + expect(onKeyDown).not.toHaveBeenCalled(); + + input.dispatchEvent(new CompositionEvent('compositionend', { data: '가' })); + }); + + test('allows onKeyDown handler after IME composition ends', async () => { + const onKeyDown = jest.fn(); + const { wrapper, input } = renderInput({ value: '가', onKeyDown }); + + input.dispatchEvent(new CompositionEvent('compositionstart')); + input.dispatchEvent(new CompositionEvent('compositionend', { data: '가' })); + + await new Promise(resolve => requestAnimationFrame(() => resolve(null))); + + wrapper.findNativeInput().keydown({ keyCode: 13 }); + expect(onKeyDown).toHaveBeenCalled(); + }); + }); }); diff --git a/src/input/internal.tsx b/src/input/internal.tsx index 7c798d294e..f0ae30e290 100644 --- a/src/input/internal.tsx +++ b/src/input/internal.tsx @@ -18,6 +18,7 @@ import { FormFieldValidationControlProps, useFormFieldContext } from '../interna import { fireKeyboardEvent, fireNonCancelableEvent, NonCancelableEventHandler } from '../internal/events'; import { InternalBaseComponentProps } from '../internal/hooks/use-base-component'; import { useDebounceCallback } from '../internal/hooks/use-debounce-callback'; +import { useIMEComposition } from '../internal/hooks/use-ime-composition'; import WithNativeAttributes, { SkipWarnings } from '../internal/utils/with-native-attributes'; import { GeneratedAnalyticsMetadataInputClearInput, @@ -108,6 +109,7 @@ function InternalInput( }; const inputRef = useRef(null); + const { isComposing } = useIMEComposition(inputRef); const searchProps = useSearchProps(type, disabled, readOnly, value, inputRef, handleChange); __leftIcon = __leftIcon ?? searchProps.__leftIcon; __rightIcon = __rightIcon ?? searchProps.__rightIcon; @@ -148,7 +150,16 @@ function InternalInput( step, inputMode, spellCheck: spellcheck, - onKeyDown: onKeyDown && (event => fireKeyboardEvent(onKeyDown, event)), + onKeyDown: + onKeyDown && + (event => { + // Prevent keydown event during IME composition to avoid race conditions + if (isComposing() && event.key === 'Enter') { + event.preventDefault(); + return; + } + fireKeyboardEvent(onKeyDown, event); + }), onKeyUp: onKeyUp && (event => fireKeyboardEvent(onKeyUp, event)), // We set a default value on the component in order to force it into the controlled mode. value: value ?? '',