| ... | ... | @@ -29,6 +29,7 @@ import { |
| 29 | 29 | useMemo, |
| 30 | 30 | useRef, |
| 31 | 31 | useState, |
| 32 | type KeyboardEvent as ReactKeyboardEvent, |
| 32 | 33 | } from "react"; |
| 33 | 34 | import { EditorContent, useEditor } from "@tiptap/react"; |
| 34 | 35 | |
| ... | ... | @@ -371,6 +372,16 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 371 | 372 | .run(); |
| 372 | 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 | 385 | return ( |
| 375 | 386 | <NodeViewWrapper |
| 376 | 387 | as="span" |
| ... | ... | @@ -378,6 +389,23 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 378 | 389 | contentEditable={false} |
| 379 | 390 | spellCheck={false} |
| 380 | 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 | 410 | <AssistantCitationChip |
| 383 | 411 | citation={citation} |
| ... | ... | @@ -396,6 +424,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 396 | 424 | commentContext.onSubmitAndSend(); |
| 397 | 425 | return true; |
| 398 | 426 | }, |
| 427 | onRestoreFocus, |
| 399 | 428 | }} |
| 400 | 429 | /> |
| 401 | 430 | </NodeViewWrapper> |
| ... | ... | @@ -856,6 +885,32 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { |
| 856 | 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 | 914 | if (event.key === "Enter" && (event.isComposing || event.keyCode === 229)) { |
| 860 | 915 | event.stopPropagation(); |
| 861 | 916 | return true; |