Skip to content

Conversation

@alexisareyn
Copy link

Fix race condition where compositionend fires before onKeyDown handler, causing premature search submission when users press Enter to complete character composition.

  • Add useIMEComposition hook to track composition state with requestAnimationFrame
  • Modify autosuggest to use isComposing() check
  • Add comprehensive tests for IME composition scenarios
  • Ensure Enter key during composition completion doesn't trigger search

Description

This PR fixes a critical accessibility issue affecting Korean (and other IME) users where pressing Enter to complete character composition would accidentally trigger search submission instead of just completing the character.

Problem: When Korean users type characters like 가 (ga), pressing Enter to complete the character would incorrectly trigger search submission because the browser's compositionend event fires before the onKeyDown handler executes, creating a race condition.

Root Cause: The timing issue meant there was no reliable way to distinguish between "Enter pressed to complete character composition" vs "Enter pressed to trigger search".

Solution: Created a useIMEComposition hook that tracks composition state and uses requestAnimationFrame to maintain the composition flag briefly after compositionend, allowing the onKeyDown handler to correctly identify composition-related Enter keypresses and prevent inappropriate search triggering.

How has this been tested?

Automated Testing:

  • Added 13 comprehensive unit tests for the new useIMEComposition hook covering character formation, multiple composition cycles, edge cases, and proper cleanup
  • Modified existing autosuggest integration tests to verify IME scenarios
  • All tests pass: npm test

Manual Testing:

  • Start the dev server: npm start

  • Navigate to the autosuggest search page at http://localhost:8080

  • Switch to Korean 2-set keyboard input method

  • Type Korean characters (e.g., ㄱ + ㅏ to form 가) and press Enter to complete - should NOT trigger search

  • Type regular text and press Enter - should trigger search normally

  • Run automated tests: npm test

Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • [N/A] Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@alexisareyn alexisareyn requested a review from a team as a code owner January 13, 2026 00:55
@alexisareyn alexisareyn requested review from Harsh-Anand-Singh and removed request for a team January 13, 2026 00:55
Fix race condition where compositionend fires before onKeyDown handler,
causing premature search submission when users press Enter to
complete character composition.

- Add useIMEComposition hook to track composition state with requestAnimationFrame
- Modify autosuggest to use isComposing() check
- Add comprehensive tests for IME composition scenarios
- Ensure Enter key during composition completion doesn't trigger search
@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.15%. Comparing base (e4cf4d5) to head (908a46a).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4182   +/-   ##
=======================================
  Coverage   97.15%   97.15%           
=======================================
  Files         874      875    +1     
  Lines       25660    25683   +23     
  Branches     9285     9286    +1     
=======================================
+ Hits        24929    24952   +23     
- Misses        684      725   +41     
+ Partials       47        6   -41     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Harsh-Anand-Singh
Copy link
Member

Thanks for the contribution! Great PR description the problem & solution breakdown made this easy to review. LGTM 👍

@Harsh-Anand-Singh Harsh-Anand-Singh added this pull request to the merge queue Jan 14, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 14, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 14, 2026
break;
}
case KeyCode.enter: {
if (isComposing()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try to check event.detail.shiftKey and event.detail.isComposing instead of introducing this hook?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially tried event.detail.isComposing but it doesn't solve the race condition. The core issue is that compositionend fires immediately when Enter is pressed, clearing the composition state before onKeyDown executes. So by the time we check event.detail.isComposing, it's already false even for composition-ending Enter keys.

The useIMEComposition hook bridges this timing gap by using requestAnimationFrame to delay clearing the composition flag, ensuring onKeyDown can reliably detect composition-related Enter presses.

{ value: '_gak_word_', label: '각도 (Angle)', description: 'Starts with 각' },
{ value: '_gan_word_', label: '간단 (Simple)', description: 'Starts with 간' },
{ value: '_gar_word_', label: '가방 (Bag)', description: 'Korean word starting with 가' },
{ value: '_gae_word_', label: '개념 (Concept)', description: 'Starts with 개' },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[non-blocking ] Could also be useful to introduce an integ test in the __integ__ folder to ensure a real browser behavior

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will add one with other fixes for comments below, thanks

const { wrapper, container } = renderAutosuggest(
<Autosuggest {...defaultProps} onChange={onChange} onSelect={onSelect} />
);
const input = container.querySelector('input')!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use wrapper.findNativeInput().getElement() instead

expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);
});

test('selects option on enter keydown when not composing', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks will do

const { wrapper, container } = renderAutosuggest(
<Autosuggest {...defaultProps} onChange={onChange} onSelect={onSelect} />
);
const input = container.querySelector('input')!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

expect(onChange).not.toHaveBeenCalled();
expect(onSelect).not.toHaveBeenCalled();

jest.restoreAllMocks();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be moved to beforeEach

const { wrapper, container } = renderAutosuggest(
<Autosuggest {...defaultProps} onChange={onChange} onSelect={onSelect} />
);
const input = container.querySelector('input')!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

getInputElement: () => HTMLInputElement | null;
}

const Component = React.forwardRef((props, ref: React.Ref<Ref>) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use our internal renderHook to achieve pretty much the same https://github.com/cloudscape-design/components/blob/main/src/__tests__/render-hook.tsx#L18

};
}, [elementRef, handleCompositionStart, handleCompositionEnd]);

const isComposing = useCallback(() => wasComposingRef.current, []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you don't get benefits of using useCallback as the ref is a mutable object

wasComposingRef.current = true;
}, []);

const handleCompositionEnd = useCallback(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you don't get benefits of using useCallback as the ref is a mutable object

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants