Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions pages/input/korean-ime.page.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);

const handleKeyDown = (event: CustomEvent<any>) => {
if (event.detail.keyCode === 13 && searchText.trim()) {
setSearchResults(prev => [
`Search executed: ${searchText}`,
...prev.slice(0, 9), // Keep last 10 results
]);
}
};

return (
<SimplePage
title="Input Korean IME Test"
subtitle="Type Korean characters and press Enter - should complete character first, not search"
>
<SpaceBetween size="m">
<Box variant="awsui-key-label">
<strong>Test Instructions:</strong>
<ol>
<li>Enable Korean 2-set keyboard</li>
<li>Type Korean character: ㄱ + ㅏ (forms 가)</li>
<li>Press Enter → Should complete character, NOT show search result</li>
<li>Press Enter again → Should show search result with 가</li>
</ol>
</Box>

<FormField
label="Search Input"
description="Watch the results below - Enter during composition shouldn't trigger search"
>
<Input
value={searchText}
onChange={({ detail }) => setSearchText(detail.value)}
onKeyDown={handleKeyDown}
ariaLabel="Korean IME test search"
placeholder="가족, 한글, etc."
type="search"
/>
</FormField>

<Box>
<strong>Current Input:</strong> {searchText || '(empty)'}
</Box>

<Box>
<strong>Search Results:</strong>
{searchResults.length === 0 ? (
<Box margin={{ top: 'xs' }} color="text-status-inactive">
No searches yet
</Box>
) : (
<Box margin={{ top: 'xs' }}>
{searchResults.map((result, index) => (
<Box
key={index}
padding={{ bottom: 'xs' }}
color={index === 0 ? 'text-status-success' : 'text-body-secondary'}
>
{result}
</Box>
))}
</Box>
)}
</Box>
</SpaceBetween>
</SimplePage>
);
}
27 changes: 27 additions & 0 deletions src/input/__tests__/input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
13 changes: 12 additions & 1 deletion src/input/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -108,6 +109,7 @@ function InternalInput(
};

const inputRef = useRef<HTMLInputElement>(null);
const { isComposing } = useIMEComposition(inputRef);
const searchProps = useSearchProps(type, disabled, readOnly, value, inputRef, handleChange);
__leftIcon = __leftIcon ?? searchProps.__leftIcon;
__rightIcon = __rightIcon ?? searchProps.__rightIcon;
Expand Down Expand Up @@ -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 ?? '',
Expand Down
Loading