feat(web): enhance HomeHero component to support IME composition handling

- Added a `composingRef` to track IME composition state, preventing submission during text composition.
- Implemented `isImeComposing` function to check if the input is currently being composed.
- Updated event handlers to ensure that submissions and plugin selections are blocked while composing.
- Added tests to verify that submissions and plugin picks do not occur during IME composition, improving user experience for non-Latin input methods.

This update enhances the input handling in the HomeHero component, ensuring a smoother experience for users utilizing IME for text input.
This commit is contained in:
pftom 2026-05-13 11:26:10 +08:00
parent c36609c47d
commit f7a13c7b15
2 changed files with 76 additions and 1 deletions

View file

@ -7,7 +7,8 @@
// composed with the recent-projects strip and plugins section
// without owning their data lifecycles.
import { forwardRef, useMemo, useState } from 'react';
import { forwardRef, useMemo, useRef, useState } from 'react';
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
import type { InstalledPluginRecord } from '@open-design/contracts';
import { Icon } from './Icon';
import {
@ -59,6 +60,7 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
ref,
) {
const [selectedIndex, setSelectedIndex] = useState(0);
const composingRef = useRef(false);
const canSubmit = prompt.trim().length > 0 && !submitDisabled;
const placeholder = activePluginTitle
? 'Edit the example query or write your own…'
@ -135,7 +137,14 @@ export const HomeHero = forwardRef<HTMLTextAreaElement, Props>(function HomeHero
onPromptChange(e.target.value);
setSelectedIndex(0);
}}
onCompositionStart={() => {
composingRef.current = true;
}}
onCompositionEnd={() => {
composingRef.current = false;
}}
onKeyDown={(e) => {
if (isImeComposing(e, composingRef.current)) return;
if (pickerOpen && pickerOptions.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault();
@ -292,6 +301,11 @@ function replaceMentionToken(value: string, mention: PluginMention): string | nu
return next.length > 0 ? next : null;
}
function isImeComposing(event: ReactKeyboardEvent<HTMLTextAreaElement>, composing: boolean): boolean {
const nativeEvent = event.nativeEvent as KeyboardEvent & { keyCode?: number };
return composing || nativeEvent.isComposing || nativeEvent.keyCode === 229;
}
function getPluginSourceLabel(plugin: InstalledPluginRecord): string {
return plugin.sourceKind === 'bundled' ? 'Community' : 'My plugin';
}

View file

@ -72,4 +72,65 @@ describe('HomeHero plugin picker', () => {
'Make',
);
});
it('does not submit while an IME composition is confirming text with Enter', () => {
const onSubmit = vi.fn();
render(
<HomeHero
prompt="做一个中文官网"
onPromptChange={() => undefined}
onSubmit={onSubmit}
activePluginTitle={null}
activeChipId={null}
onClearActivePlugin={() => undefined}
pluginOptions={[]}
pluginsLoading={false}
pendingPluginId={null}
pendingChipId={null}
onPickPlugin={() => undefined}
onPickChip={() => undefined}
contextItemCount={0}
error={null}
/>,
);
const input = screen.getByTestId('home-hero-input');
fireEvent.compositionStart(input);
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onSubmit).not.toHaveBeenCalled();
fireEvent.compositionEnd(input);
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it('does not pick a plugin while an IME composition is active', () => {
const onPickPlugin = vi.fn();
const onSubmit = vi.fn();
render(
<HomeHero
prompt="Make @sam"
onPromptChange={() => undefined}
onSubmit={onSubmit}
activePluginTitle={null}
activeChipId={null}
onClearActivePlugin={() => undefined}
pluginOptions={[makePlugin('sample-plugin', 'Sample Plugin')]}
pluginsLoading={false}
pendingPluginId={null}
pendingChipId={null}
onPickPlugin={onPickPlugin}
onPickChip={() => undefined}
contextItemCount={0}
error={null}
/>,
);
const input = screen.getByTestId('home-hero-input');
fireEvent.compositionStart(input);
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onPickPlugin).not.toHaveBeenCalled();
expect(onSubmit).not.toHaveBeenCalled();
});
});