From a4f6078bebcb3283e53e1cea247257136ce82cc8 Mon Sep 17 00:00:00 2001 From: MacKinley Smith Date: Fri, 25 Sep 2026 13:43:35 -0600 Subject: [PATCH 01/11] fix(web): return focus to the composer after saving a citation note (#13450) --- .../components/ComposerPromptEditorTiptap.tsx | 55 +++++++++++++++++++ .../components/chat/AssistantCitationChip.tsx | 20 +++++++ 2 files changed, 75 insertions(+) diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index 74e8736f04e7..2a5fe2ba0588 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -29,6 +29,7 @@ import { useMemo, useRef, useState, + type KeyboardEvent as ReactKeyboardEvent, } from "react"; import { EditorContent, useEditor } from "@tiptap/react"; @@ -371,6 +372,16 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { .run(); }, [editor, nodePos]); + // Put the caret right after the chip so Enter sends and typing continues the prompt. + const onRestoreFocus = useCallback(() => { + if (!editor.isEditable) return; + const pos = nodePos(); + if (pos === null) return; + const current = editor.state.doc.nodeAt(pos); + if (!current || current.type.name !== "composer-citation") return; + editor.commands.focus(pos + current.nodeSize); + }, [editor, nodePos]); + return ( ) => { + // Tab from the comment button returns to the caret after the chip. + if ( + !editor.isEditable || + event.key !== "Tab" || + event.shiftKey || + event.altKey || + event.metaKey || + event.ctrlKey || + !(event.target instanceof HTMLElement) || + event.target.dataset.citationCommentTrigger === undefined + ) { + return; + } + event.preventDefault(); + onRestoreFocus(); + }} > @@ -856,6 +885,32 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { return true; } } + // Shift+Tab from just after a citation reaches its comment button, which + // native tab order skips because the chip lives inside the editor. + if ( + event.key === "Tab" && + event.shiftKey && + !event.altKey && + !event.metaKey && + !event.ctrlKey && + view.state.selection.empty + ) { + const { $from } = view.state.selection; + const citation = $from.nodeBefore; + if (citation?.type.name === "composer-citation") { + const chip = view.nodeDOM($from.pos - citation.nodeSize); + const commentButton = + chip instanceof HTMLElement + ? chip.querySelector("[data-citation-comment-trigger]") + : null; + if (commentButton) { + event.preventDefault(); + event.stopPropagation(); + commentButton.focus(); + return true; + } + } + } if (event.key === "Enter" && (event.isComposing || event.keyCode === 229)) { event.stopPropagation(); return true; diff --git a/apps/web/src/components/chat/AssistantCitationChip.tsx b/apps/web/src/components/chat/AssistantCitationChip.tsx index 6baf14b18887..e03872553a88 100644 --- a/apps/web/src/components/chat/AssistantCitationChip.tsx +++ b/apps/web/src/components/chat/AssistantCitationChip.tsx @@ -39,10 +39,13 @@ export function AssistantCitationChip({ onCancel?: () => void; onSave: (comment: string) => boolean; onSaveAndSend?: (comment: string) => boolean; + /** Returns focus to the host editor when the popover closes instead of to the pencil trigger. */ + onRestoreFocus?: () => void; }; }) { const navigate = useNavigate(); const commentInputRef = useRef(null); + const commentPopupRef = useRef(null); const draftCommentRef = useRef(null); const [unavailableSourceAnchor, setUnavailableSourceAnchor] = useState(null); @@ -155,6 +158,7 @@ export function AssistantCitationChip({ > } >