diff --git a/apps/web/src/common/components/CommentInputNewline.test.tsx b/apps/web/src/common/components/CommentInputNewline.test.tsx new file mode 100644 index 0000000000..c4b29e7c2a --- /dev/null +++ b/apps/web/src/common/components/CommentInputNewline.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import LabCommentInput from '@modules/lab/model/Analyze/CommentDrawer/CommentInput'; +import SandboxCommentInput from '@modules/oh/DataView/HatchingTreatment/Sandbox/EvaluationInfo/MetricDrawer/CommentInput'; +import HatchCommentInput from '@modules/oh/DataView/HatchingTreatment/Hatch/EvaluationInfo/MetricDrawer/CommentInput'; +import GraduateCommentInput from '@modules/oh/DataView/HatchingTreatment/Graduate/EvaluationInfo/MetricDrawer/CommentInput'; + +jest.mock('next-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +jest.mock('@common/hooks/useImagePreview', () => ({ + __esModule: true, + default: () => ({ ref: jest.fn(), open: jest.fn(), close: jest.fn() }), +})); + +describe.each([ + { name: 'Lab', Component: LabCommentInput }, + { name: 'Sandbox', Component: SandboxCommentInput }, + { name: 'Hatch', Component: HatchCommentInput }, + { name: 'Graduate', Component: GraduateCommentInput }, +])('$name comment newlines', ({ Component }) => { + describe.each([ + { name: 'Shift+Enter', modifiers: { shiftKey: true } }, + { name: 'Ctrl+Enter', modifiers: { ctrlKey: true } }, + ])('$name', ({ modifiers }) => { + it.each([ + { position: 'start', start: 0, end: 0, expected: '\nabcdef' }, + { position: 'middle', start: 3, end: 3, expected: 'abc\ndef' }, + { position: 'selection', start: 2, end: 4, expected: 'ab\nef' }, + { position: 'end', start: 6, end: 6, expected: 'abcdef\n' }, + ])('inserts a newline at the $position', ({ start, end, expected }) => { + const onSubmit = jest.fn(); + render(); + const input = screen.getByRole('textbox') as HTMLTextAreaElement; + fireEvent.change(input, { target: { value: 'abcdef' } }); + input.focus(); + input.setSelectionRange(start, end); + + const accepted = fireEvent.keyDown(input, { + key: 'Enter', + code: 'Enter', + ...modifiers, + }); + + expect(input.value).toBe(expected); + expect(input.selectionStart).toBe(start + 1); + expect(input.selectionEnd).toBe(start + 1); + expect(onSubmit).not.toHaveBeenCalled(); + expect(accepted).toBe(false); + }); + }); + + it('continues submitting plain Enter', () => { + const onSubmit = jest.fn(); + render(); + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'abcdef' } }); + + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit).toHaveBeenCalledWith('abcdef', []); + }); + + it('does not insert a newline or submit while loading', () => { + const onSubmit = jest.fn(); + render(); + const input = screen.getByRole('textbox') as HTMLTextAreaElement; + fireEvent.change(input, { target: { value: 'abcdef' } }); + input.setSelectionRange(2, 2); + + fireEvent.keyDown(input, { key: 'Enter', shiftKey: true }); + fireEvent.keyDown(input, { key: 'Enter', ctrlKey: true }); + + expect(input.value).toBe('abcdef'); + expect(onSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/modules/lab/model/Analyze/CommentDrawer/CommentInput.tsx b/apps/web/src/modules/lab/model/Analyze/CommentDrawer/CommentInput.tsx index 6e74a3273f..df2a5ed491 100644 --- a/apps/web/src/modules/lab/model/Analyze/CommentDrawer/CommentInput.tsx +++ b/apps/web/src/modules/lab/model/Analyze/CommentDrawer/CommentInput.tsx @@ -5,6 +5,7 @@ import React, { forwardRef, useImperativeHandle, } from 'react'; +import { flushSync } from 'react-dom'; import { FiImage } from 'react-icons/fi'; import { BiLoaderAlt } from 'react-icons/bi'; import { toast } from 'react-hot-toast'; @@ -116,7 +117,19 @@ const CommentInput = forwardRef( if (loading) return; if (event.key == 'Enter' || event.code == 'Enter') { if (event.shiftKey || event.ctrlKey) { - setValue((p) => p + '\n'); + const input = event.currentTarget; + const { selectionStart, selectionEnd, value } = input; + flushSync(() => { + setValue( + value.slice(0, selectionStart) + + '\n' + + value.slice(selectionEnd) + ); + }); + input.setSelectionRange( + selectionStart + 1, + selectionStart + 1 + ); } else { if (value) { onSubmit(value, images); diff --git a/apps/web/src/modules/oh/DataView/HatchingTreatment/Graduate/EvaluationInfo/MetricDrawer/CommentInput.tsx b/apps/web/src/modules/oh/DataView/HatchingTreatment/Graduate/EvaluationInfo/MetricDrawer/CommentInput.tsx index 5aa8b0df60..34268a6de9 100644 --- a/apps/web/src/modules/oh/DataView/HatchingTreatment/Graduate/EvaluationInfo/MetricDrawer/CommentInput.tsx +++ b/apps/web/src/modules/oh/DataView/HatchingTreatment/Graduate/EvaluationInfo/MetricDrawer/CommentInput.tsx @@ -4,6 +4,7 @@ import React, { forwardRef, useImperativeHandle, } from 'react'; +import { flushSync } from 'react-dom'; import { BiLoaderAlt } from 'react-icons/bi'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'next-i18next'; @@ -124,7 +125,19 @@ const CommentInput = forwardRef( if (loading) return; if (event.key == 'Enter' || event.code == 'Enter') { if (event.shiftKey || event.ctrlKey) { - setValue((p) => p + '\n'); + const input = event.currentTarget; + const { selectionStart, selectionEnd, value } = input; + flushSync(() => { + setValue( + value.slice(0, selectionStart) + + '\n' + + value.slice(selectionEnd) + ); + }); + input.setSelectionRange( + selectionStart + 1, + selectionStart + 1 + ); } else { if (value) { onSubmit(value, images); diff --git a/apps/web/src/modules/oh/DataView/HatchingTreatment/Hatch/EvaluationInfo/MetricDrawer/CommentInput.tsx b/apps/web/src/modules/oh/DataView/HatchingTreatment/Hatch/EvaluationInfo/MetricDrawer/CommentInput.tsx index 18c409ea45..71096bcac7 100644 --- a/apps/web/src/modules/oh/DataView/HatchingTreatment/Hatch/EvaluationInfo/MetricDrawer/CommentInput.tsx +++ b/apps/web/src/modules/oh/DataView/HatchingTreatment/Hatch/EvaluationInfo/MetricDrawer/CommentInput.tsx @@ -5,6 +5,7 @@ import React, { forwardRef, useImperativeHandle, } from 'react'; +import { flushSync } from 'react-dom'; import { FiImage } from 'react-icons/fi'; import { BiLoaderAlt } from 'react-icons/bi'; import { toast } from 'react-hot-toast'; @@ -128,7 +129,19 @@ const CommentInput = forwardRef( if (loading) return; if (event.key == 'Enter' || event.code == 'Enter') { if (event.shiftKey || event.ctrlKey) { - setValue((p) => p + '\n'); + const input = event.currentTarget; + const { selectionStart, selectionEnd, value } = input; + flushSync(() => { + setValue( + value.slice(0, selectionStart) + + '\n' + + value.slice(selectionEnd) + ); + }); + input.setSelectionRange( + selectionStart + 1, + selectionStart + 1 + ); } else { if (value) { onSubmit(value, images); diff --git a/apps/web/src/modules/oh/DataView/HatchingTreatment/Sandbox/EvaluationInfo/MetricDrawer/CommentInput.tsx b/apps/web/src/modules/oh/DataView/HatchingTreatment/Sandbox/EvaluationInfo/MetricDrawer/CommentInput.tsx index 6240a07ce9..2020298d51 100644 --- a/apps/web/src/modules/oh/DataView/HatchingTreatment/Sandbox/EvaluationInfo/MetricDrawer/CommentInput.tsx +++ b/apps/web/src/modules/oh/DataView/HatchingTreatment/Sandbox/EvaluationInfo/MetricDrawer/CommentInput.tsx @@ -4,6 +4,7 @@ import React, { forwardRef, useImperativeHandle, } from 'react'; +import { flushSync } from 'react-dom'; import { BiLoaderAlt } from 'react-icons/bi'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'next-i18next'; @@ -86,7 +87,19 @@ const CommentInput = forwardRef( if (loading) return; if (event.key == 'Enter' || event.code == 'Enter') { if (event.shiftKey || event.ctrlKey) { - setValue((p) => p + '\n'); + const input = event.currentTarget; + const { selectionStart, selectionEnd, value } = input; + flushSync(() => { + setValue( + value.slice(0, selectionStart) + + '\n' + + value.slice(selectionEnd) + ); + }); + input.setSelectionRange( + selectionStart + 1, + selectionStart + 1 + ); } else { if (value) { onSubmit(value, images);