diff --git a/web/app/components/workflow/comment/mention-input.tsx b/web/app/components/workflow/comment/mention-input.tsx index 7bcd782978..31fc706ed0 100644 --- a/web/app/components/workflow/comment/mention-input.tsx +++ b/web/app/components/workflow/comment/mention-input.tsx @@ -1,10 +1,12 @@ 'use client' -import type { FC, ReactNode } from 'react' +import type { ReactNode } from 'react' import { + forwardRef, memo, useCallback, useEffect, + useImperativeHandle, useLayoutEffect, useMemo, useRef, @@ -34,7 +36,7 @@ type MentionInputProps = { autoFocus?: boolean } -export const MentionInput: FC = memo(({ +const MentionInputInner = forwardRef(({ value, onChange, onSubmit, @@ -45,7 +47,7 @@ export const MentionInput: FC = memo(({ className, isEditing = false, autoFocus = false, -}) => { +}, forwardedRef) => { const params = useParams() const { t } = useTranslation() const appId = params.appId as string @@ -55,6 +57,9 @@ export const MentionInput: FC = memo(({ const actionRightRef = useRef(null) const baseTextareaHeightRef = useRef(null) + // Expose textarea ref to parent component + useImperativeHandle(forwardedRef, () => textareaRef.current!, []) + const workflowStore = useWorkflowStore() const mentionUsersFromStore = useStore(state => ( appId ? state.mentionableUsersCache[appId] : undefined @@ -630,4 +635,6 @@ export const MentionInput: FC = memo(({ ) }) -MentionInput.displayName = 'MentionInput' +MentionInputInner.displayName = 'MentionInputInner' + +export const MentionInput = memo(MentionInputInner) diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index 9c978abc4f..c9325bbcb3 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -163,10 +163,44 @@ export const CommentThread: FC = memo(({ const [editingReply, setEditingReply] = useState<{ id: string; content: string }>({ id: '', content: '' }) const [deletingReplyId, setDeletingReplyId] = useState(null) + // Focus management refs + const replyInputRef = useRef(null) + const threadRef = useRef(null) + useEffect(() => { setReplyContent('') }, [comment.id]) + // P0: Auto-focus reply input when thread opens or comment changes + useEffect(() => { + const timer = setTimeout(() => { + if (replyInputRef.current && !editingReply.id && onReply) + replyInputRef.current.focus() + }, 100) + + return () => clearTimeout(timer) + }, [comment.id, editingReply.id, onReply]) + + // P2: Handle Esc key to close thread + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Don't intercept if actively editing a reply + if (editingReply.id) return + + // Don't intercept if mention dropdown is open (let MentionInput handle it) + if (document.querySelector('[data-mention-dropdown]')) return + + if (e.key === 'Escape') { + e.preventDefault() + e.stopPropagation() + onClose() + } + } + + document.addEventListener('keydown', handleKeyDown, true) + return () => document.removeEventListener('keydown', handleKeyDown, true) + }, [onClose, editingReply.id]) + const handleReplySubmit = useCallback(async (content: string, mentionedUserIds: string[]) => { if (!onReply || replySubmitting) return @@ -174,6 +208,11 @@ export const CommentThread: FC = memo(({ try { await onReply(content, mentionedUserIds) + + // P0: Restore focus to reply input after successful submission + setTimeout(() => { + replyInputRef.current?.focus() + }, 0) } catch (error) { console.error('Failed to send reply', error) @@ -195,6 +234,11 @@ export const CommentThread: FC = memo(({ const handleCancelEdit = useCallback(() => { setEditingReply({ id: '', content: '' }) + + // P1: Restore focus to reply input after canceling edit + setTimeout(() => { + replyInputRef.current?.focus() + }, 0) }, []) const handleEditSubmit = useCallback(async (content: string, mentionedUserIds: string[]) => { @@ -203,6 +247,11 @@ export const CommentThread: FC = memo(({ if (!trimmed) return await onReplyEdit(editingReply.id, trimmed, mentionedUserIds) setEditingReply({ id: '', content: '' }) + + // P1: Restore focus to reply input after saving edit + setTimeout(() => { + replyInputRef.current?.focus() + }, 0) }, [editingReply, onReplyEdit]) const replies = comment.replies || [] @@ -272,9 +321,20 @@ export const CommentThread: FC = memo(({ transform: 'translateY(-20%)', }} > -
+
-
{t('workflow.comments.panelTitle')}
+
+ {t('workflow.comments.panelTitle')} +
= memo(({ />