diff --git a/src/components/editor/EmojiSuggestionList.tsx b/src/components/editor/EmojiSuggestionList.tsx index 6105470..4bcd16e 100644 --- a/src/components/editor/EmojiSuggestionList.tsx +++ b/src/components/editor/EmojiSuggestionList.tsx @@ -58,7 +58,7 @@ export const EmojiSuggestionList = forwardRef< return true; } - if (event.key === "Enter") { + if (event.key === "Enter" && !event.ctrlKey && !event.metaKey) { if (items[selectedIndex]) { command(items[selectedIndex]); } diff --git a/src/components/editor/MentionEditor.tsx b/src/components/editor/MentionEditor.tsx index 6cbe7a9..a9423eb 100644 --- a/src/components/editor/MentionEditor.tsx +++ b/src/components/editor/MentionEditor.tsx @@ -4,8 +4,10 @@ import { useImperativeHandle, useMemo, useCallback, + useRef, } from "react"; import { useEditor, EditorContent, ReactRenderer } from "@tiptap/react"; +import { Extension } from "@tiptap/core"; import StarterKit from "@tiptap/starter-kit"; import Mention from "@tiptap/extension-mention"; import Placeholder from "@tiptap/extension-placeholder"; @@ -155,6 +157,9 @@ export const MentionEditor = forwardRef< }, ref, ) => { + // Ref to access handleSubmit from suggestion plugins (defined early so useMemo can access it) + const handleSubmitRef = useRef<(editor: any) => void>(() => {}); + // Create mention suggestion configuration for @ mentions const mentionSuggestion: Omit = useMemo( () => ({ @@ -166,9 +171,11 @@ export const MentionEditor = forwardRef< render: () => { let component: ReactRenderer; let popup: TippyInstance[]; + let editorRef: any; return { onStart: (props) => { + editorRef = props.editor; component = new ReactRenderer(ProfileSuggestionList, { props: { items: props.items, @@ -216,6 +223,16 @@ export const MentionEditor = forwardRef< return true; } + // Ctrl/Cmd+Enter submits the message + if ( + props.event.key === "Enter" && + (props.event.ctrlKey || props.event.metaKey) + ) { + popup[0]?.hide(); + handleSubmitRef.current(editorRef); + return true; + } + return component.ref?.onKeyDown(props.event) ?? false; }, @@ -242,9 +259,11 @@ export const MentionEditor = forwardRef< render: () => { let component: ReactRenderer; let popup: TippyInstance[]; + let editorRef: any; return { onStart: (props) => { + editorRef = props.editor; component = new ReactRenderer(EmojiSuggestionList, { props: { items: props.items, @@ -292,6 +311,16 @@ export const MentionEditor = forwardRef< return true; } + // Ctrl/Cmd+Enter submits the message + if ( + props.event.key === "Enter" && + (props.event.ctrlKey || props.event.metaKey) + ) { + popup[0]?.hide(); + handleSubmitRef.current(editorRef); + return true; + } + return component.ref?.onKeyDown(props.event) ?? false; }, @@ -375,11 +404,34 @@ export const MentionEditor = forwardRef< [onSubmit, serializeContent], ); + // Keep ref updated with latest handleSubmit + handleSubmitRef.current = handleSubmit; + // Build extensions array const extensions = useMemo(() => { + // Custom extension for keyboard shortcuts (runs before suggestion plugins) + const SubmitShortcut = Extension.create({ + name: "submitShortcut", + addKeyboardShortcuts() { + return { + // Ctrl/Cmd+Enter always submits + "Mod-Enter": ({ editor }) => { + handleSubmitRef.current(editor); + return true; + }, + // Plain Enter submits (Shift+Enter handled by hardBreak for newlines) + Enter: ({ editor }) => { + handleSubmitRef.current(editor); + return true; + }, + }; + }, + }); + const exts = [ + SubmitShortcut, StarterKit.configure({ - // Disable Enter to submit via Mod-Enter instead + // Shift+Enter inserts hard break (newline) hardBreak: { keepMarks: false, }, @@ -459,21 +511,7 @@ export const MentionEditor = forwardRef< editorProps: { attributes: { class: - "prose prose-sm max-w-none focus:outline-none min-h-[2rem] px-3 py-1.5", - }, - handleKeyDown: (view, event) => { - // Submit on Enter (without Shift) or Ctrl/Cmd+Enter - if ( - event.key === "Enter" && - (!event.shiftKey || event.ctrlKey || event.metaKey) - ) { - event.preventDefault(); - // Get editor from view state - const editorInstance = (view as any).editor; - handleSubmit(editorInstance); - return true; - } - return false; + "prose prose-sm max-w-none focus:outline-none min-h-[2rem] px-3 py-1.5 whitespace-nowrap", }, }, autofocus: autoFocus, @@ -513,9 +551,12 @@ export const MentionEditor = forwardRef< return (
- +
); }, diff --git a/src/components/editor/ProfileSuggestionList.tsx b/src/components/editor/ProfileSuggestionList.tsx index 58a0a3d..60c00c4 100644 --- a/src/components/editor/ProfileSuggestionList.tsx +++ b/src/components/editor/ProfileSuggestionList.tsx @@ -38,7 +38,7 @@ export const ProfileSuggestionList = forwardRef< return true; } - if (event.key === "Enter") { + if (event.key === "Enter" && !event.ctrlKey && !event.metaKey) { if (items[selectedIndex]) { command(items[selectedIndex]); }