add comment composer keyboard submit shortcut (#2941)

Co-authored-by: Siri-Ray <2667192167@qq.com>
This commit is contained in:
byte92 2026-05-29 16:46:15 +08:00 committed by GitHub
parent 0bd07b2a3d
commit cdf34897ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 109 additions and 1 deletions

View file

@ -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 (
<div
className={`comment-popover${docked ? ' comment-popover-docked' : ''}`}
@ -335,6 +339,25 @@ export function BoardComposerPopover({
aria-label={t('chat.comments.placeholder')}
placeholder={t('chat.comments.placeholder')}
onChange={(event) => 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();
}
}}
/>
<div className="comment-popover-actions">
<div className="comment-popover-actions-start">
@ -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')}

View file

@ -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(
<BoardComposerPopover
target={target}
existing={null}
draft="Tighten this heading"
notes={[]}
onDraft={() => {}}
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(
<BoardComposerPopover
target={target}
existing={null}
draft="Tighten this heading"
notes={[]}
onDraft={() => {}}
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();
});
});