fix(web): harden ime composition submit handling

This commit is contained in:
-LAN-
2026-03-01 06:02:29 +08:00
parent c0f78e921b
commit 4e7a3e07ae
2 changed files with 66 additions and 4 deletions

View File

@@ -245,6 +245,50 @@ describe('Question component', () => {
expect(onRegenerate).not.toHaveBeenCalled()
})
it('should not confirm editing when Enter is pressed during IME composition', () => {
const onRegenerate = vi.fn() as unknown as OnRegenerate
renderWithProvider(makeItem(), onRegenerate)
fireEvent.click(screen.getByTestId('edit-btn'))
const textbox = screen.getByRole('textbox')
fireEvent.compositionStart(textbox)
fireEvent.keyDown(textbox, { key: 'Enter', code: 'Enter' })
expect(onRegenerate).not.toHaveBeenCalled()
})
it('should keep Enter suppressed if a new composition starts before previous composition-end timer finishes', () => {
vi.useFakeTimers()
try {
const onRegenerate = vi.fn() as unknown as OnRegenerate
renderWithProvider(makeItem(), onRegenerate)
fireEvent.click(screen.getByTestId('edit-btn'))
const textbox = screen.getByRole('textbox')
fireEvent.compositionStart(textbox)
fireEvent.compositionEnd(textbox)
fireEvent.compositionStart(textbox)
vi.advanceTimersByTime(50)
fireEvent.keyDown(textbox, { key: 'Enter', code: 'Enter' })
expect(onRegenerate).not.toHaveBeenCalled()
fireEvent.compositionEnd(textbox)
vi.advanceTimersByTime(50)
fireEvent.keyDown(textbox, { key: 'Enter', code: 'Enter' })
expect(onRegenerate).toHaveBeenCalledTimes(1)
}
finally {
vi.useRealTimers()
}
})
it('should switch siblings when prev/next buttons are clicked', async () => {
const user = userEvent.setup()
const switchSibling = vi.fn()

View File

@@ -57,6 +57,7 @@ const Question: FC<QuestionProps> = ({
const [contentWidth, setContentWidth] = useState(0)
const contentRef = useRef<HTMLDivElement>(null)
const isComposingRef = useRef(false)
const compositionEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const handleEdit = useCallback(() => {
setIsEditing(true)
@@ -84,15 +85,26 @@ const Question: FC<QuestionProps> = ({
handleResend()
}, [handleResend])
const handleCompositionStart = useCallback(() => {
isComposingRef.current = true
const clearCompositionEndTimer = useCallback(() => {
if (!compositionEndTimerRef.current)
return
clearTimeout(compositionEndTimerRef.current)
compositionEndTimerRef.current = null
}, [])
const handleCompositionStart = useCallback(() => {
clearCompositionEndTimer()
isComposingRef.current = true
}, [clearCompositionEndTimer])
const handleCompositionEnd = useCallback(() => {
setTimeout(() => {
clearCompositionEndTimer()
compositionEndTimerRef.current = setTimeout(() => {
isComposingRef.current = false
compositionEndTimerRef.current = null
}, 50)
}, [])
}, [clearCompositionEndTimer])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev') {
@@ -122,6 +134,12 @@ const Question: FC<QuestionProps> = ({
}
}, [])
useEffect(() => {
return () => {
clearCompositionEndTimer()
}
}, [clearCompositionEndTimer])
return (
<div className="mb-2 flex justify-end last:mb-0">
<div className={cn('group relative mr-4 flex max-w-full items-start overflow-x-hidden pl-14', isEditing && 'flex-1')}>