From cdf34897ba4b0c265398ced95e2e3168892dabb3 Mon Sep 17 00:00:00 2001 From: byte92 <7370806+byte92@users.noreply.github.com> Date: Fri, 29 May 2026 16:46:15 +0800 Subject: [PATCH] add comment composer keyboard submit shortcut (#2941) Co-authored-by: Siri-Ray <2667192167@qq.com> --- .../src/components/BoardComposerPopover.tsx | 25 +++++- ...rdComposerPopover.keyboard-submit.test.tsx | 85 +++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 apps/web/tests/components/BoardComposerPopover.keyboard-submit.test.tsx diff --git a/apps/web/src/components/BoardComposerPopover.tsx b/apps/web/src/components/BoardComposerPopover.tsx index 268387fb9..29ff80e57 100644 --- a/apps/web/src/components/BoardComposerPopover.tsx +++ b/apps/web/src/components/BoardComposerPopover.tsx @@ -1,8 +1,10 @@ import type { CSSProperties } from 'react'; +import { useRef } from 'react'; import type { PreviewCommentSnapshot } from '../comments'; import type { Dict } from '../i18n/types'; import type { PreviewComment, PreviewCommentMember } from '../types'; +import { isImeComposing } from '../utils/imeComposing'; import { Icon } from './Icon'; @@ -262,6 +264,8 @@ export function BoardComposerPopover({ const pendingCount = notes.length + (draft.trim() ? 1 : 0); const hasCommentChange = !existing || draft.trim() !== existing.note.trim(); const podMembers = target.podMembers ?? []; + const composingRef = useRef(false); + const sendDisabled = pendingCount === 0 || sending; return (
onDraft(event.target.value)} + onCompositionStart={() => { + composingRef.current = true; + }} + onCompositionEnd={() => { + composingRef.current = false; + }} + onKeyDown={(event) => { + if (isImeComposing(event, composingRef.current)) return; + if ( + event.key === 'Enter' && + !event.shiftKey && + !event.altKey && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + if (sendDisabled) return; + void onSendBatch(); + } + }} />
@@ -386,7 +409,7 @@ export function BoardComposerPopover({ type="button" className="primary" data-testid="comment-add-send" - disabled={pendingCount === 0 || sending} + disabled={sendDisabled} onClick={() => void onSendBatch()} > {sending ? t('chat.comments.sending') : t('chat.comments.sendToChat')} diff --git a/apps/web/tests/components/BoardComposerPopover.keyboard-submit.test.tsx b/apps/web/tests/components/BoardComposerPopover.keyboard-submit.test.tsx new file mode 100644 index 000000000..6438279b2 --- /dev/null +++ b/apps/web/tests/components/BoardComposerPopover.keyboard-submit.test.tsx @@ -0,0 +1,85 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { BoardComposerPopover } from '../../src/components/BoardComposerPopover'; +import type { PreviewCommentSnapshot } from '../../src/comments'; + +afterEach(() => { + cleanup(); +}); + +const target: PreviewCommentSnapshot = { + filePath: 'index.html', + elementId: 'hero-title', + selector: '#hero-title', + label: 'Hero title', + text: '', + position: { x: 0, y: 0, width: 100, height: 24 }, + htmlHint: '', + selectionKind: 'element', +}; + +function renderPopover(onSendBatch: () => void, sending = false) { + return render( + {}} + onAddDraft={() => {}} + onRemoveQueuedNote={() => {}} + onClose={() => {}} + onSaveComment={() => {}} + onSendBatch={onSendBatch} + onRemoveMember={() => {}} + sending={sending} + t={((key: string) => String(key)) as never} + />, + ); +} + +describe('BoardComposerPopover keyboard submit', () => { + it('sends the drafted comment with the primary Enter shortcut', () => { + const onSendBatch = vi.fn(); + renderPopover(onSendBatch); + + fireEvent.keyDown(screen.getByTestId('comment-popover-input'), { key: 'Enter', metaKey: true }); + + expect(onSendBatch).toHaveBeenCalledTimes(1); + }); + + it('does not send while disabled or while IME text is composing', () => { + const onSendBatch = vi.fn(); + const { rerender } = renderPopover(onSendBatch, true); + const input = screen.getByTestId('comment-popover-input'); + + fireEvent.keyDown(input, { key: 'Enter', metaKey: true }); + expect(onSendBatch).not.toHaveBeenCalled(); + + rerender( + {}} + onAddDraft={() => {}} + onRemoveQueuedNote={() => {}} + onClose={() => {}} + onSaveComment={() => {}} + onSendBatch={onSendBatch} + onRemoveMember={() => {}} + sending={false} + t={((key: string) => String(key)) as never} + />, + ); + + fireEvent.compositionStart(input); + fireEvent.keyDown(input, { key: 'Enter', metaKey: true }); + + expect(onSendBatch).not.toHaveBeenCalled(); + }); +});