| ... | @@ -29,6 +29,7 @@ import { | ... | @@ -29,6 +29,7 @@ import { |
| 29 | useMemo, | 29 | useMemo, |
| 30 | useRef, | 30 | useRef, |
| 31 | useState, | 31 | useState, |
| | 32 | type KeyboardEvent as ReactKeyboardEvent, |
| 32 | } from "react"; | 33 | } from "react"; |
| 33 | import { EditorContent, useEditor } from "@tiptap/react"; | 34 | import { EditorContent, useEditor } from "@tiptap/react"; |
| 34 | | 35 | |
| ... | @@ -371,6 +372,16 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { | ... | @@ -371,6 +372,16 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 371 | .run(); | 372 | .run(); |
| 372 | }, [editor, nodePos]); | 373 | }, [editor, nodePos]); |
| 373 | | 374 | |
| | 375 | // Put the caret right after the chip so Enter sends and typing continues the prompt. |
| | 376 | const onRestoreFocus = useCallback(() => { |
| | 377 | if (!editor.isEditable) return; |
| | 378 | const pos = nodePos(); |
| | 379 | if (pos === null) return; |
| | 380 | const current = editor.state.doc.nodeAt(pos); |
| | 381 | if (!current || current.type.name !== "composer-citation") return; |
| | 382 | editor.commands.focus(pos + current.nodeSize); |
| | 383 | }, [editor, nodePos]); |
| | 384 | |
| 374 | return ( | 385 | return ( |
| 375 | <NodeViewWrapper | 386 | <NodeViewWrapper |
| 376 | as="span" | 387 | as="span" |
| ... | @@ -378,6 +389,23 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { | ... | @@ -378,6 +389,23 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 378 | contentEditable={false} | 389 | contentEditable={false} |
| 379 | spellCheck={false} | 390 | spellCheck={false} |
| 380 | data-composer-citation-chip="true" | 391 | data-composer-citation-chip="true" |
| | 392 | onKeyDown={(event: ReactKeyboardEvent<HTMLElement>) => { |
| | 393 | // Tab from the comment button returns to the caret after the chip. |
| | 394 | if ( |
| | 395 | !editor.isEditable || |
| | 396 | event.key !== "Tab" || |
| | 397 | event.shiftKey || |
| | 398 | event.altKey || |
| | 399 | event.metaKey || |
| | 400 | event.ctrlKey || |
| | 401 | !(event.target instanceof HTMLElement) || |
| | 402 | event.target.dataset.citationCommentTrigger === undefined |
| | 403 | ) { |
| | 404 | return; |
| | 405 | } |
| | 406 | event.preventDefault(); |
| | 407 | onRestoreFocus(); |
| | 408 | }} |
| 381 | > | 409 | > |
| 382 | <AssistantCitationChip | 410 | <AssistantCitationChip |
| 383 | citation={citation} | 411 | citation={citation} |
| ... | @@ -396,6 +424,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { | ... | @@ -396,6 +424,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 396 | commentContext.onSubmitAndSend(); | 424 | commentContext.onSubmitAndSend(); |
| 397 | return true; | 425 | return true; |
| 398 | }, | 426 | }, |
| | 427 | onRestoreFocus, |
| 399 | }} | 428 | }} |
| 400 | /> | 429 | /> |
| 401 | </NodeViewWrapper> | 430 | </NodeViewWrapper> |
| ... | @@ -856,6 +885,32 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { | ... | @@ -856,6 +885,32 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { |
| 856 | return true; | 885 | return true; |
| 857 | } | 886 | } |
| 858 | } | 887 | } |
| | 888 | // Shift+Tab from just after a citation reaches its comment button, which |
| | 889 | // native tab order skips because the chip lives inside the editor. |
| | 890 | if ( |
| | 891 | event.key === "Tab" && |
| | 892 | event.shiftKey && |
| | 893 | !event.altKey && |
| | 894 | !event.metaKey && |
| | 895 | !event.ctrlKey && |
| | 896 | view.state.selection.empty |
| | 897 | ) { |
| | 898 | const { $from } = view.state.selection; |
| | 899 | const citation = $from.nodeBefore; |
| | 900 | if (citation?.type.name === "composer-citation") { |
| | 901 | const chip = view.nodeDOM($from.pos - citation.nodeSize); |
| | 902 | const commentButton = |
| | 903 | chip instanceof HTMLElement |
| | 904 | ? chip.querySelector<HTMLElement>("[data-citation-comment-trigger]") |
| | 905 | : null; |
| | 906 | if (commentButton) { |
| | 907 | event.preventDefault(); |
| | 908 | event.stopPropagation(); |
| | 909 | commentButton.focus(); |
| | 910 | return true; |
| | 911 | } |
| | 912 | } |
| | 913 | } |
| 859 | if (event.key === "Enter" && (event.isComposing || event.keyCode === 229)) { | 914 | if (event.key === "Enter" && (event.isComposing || event.keyCode === 229)) { |
| 860 | event.stopPropagation(); | 915 | event.stopPropagation(); |
| 861 | return true; | 916 | return true; |