From df6a1c15212337e6910b7bd1d45630090657c381 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 29 May 2026 23:09:20 -0700 Subject: [PATCH] fix: cancel review dialog focus frames (#3493) --- .../src/components/GitHubItemDialog.tsx | 53 +++++++++++++++++-- .../src/components/PullRequestPage.tsx | 53 +++++++++++++++++-- 2 files changed, 96 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index 9ca15a482bf..8144b3bbc45 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -478,6 +478,34 @@ function PRReviewersPanel({ const patchWorkItem = useAppStore((s) => s.patchWorkItem) const settings = useAppStore((s) => s.settings) const reviewerInputRef = useRef(null) + const reviewerInputFocusFrameRef = useRef(null) + const reviewerPanelMountedRef = useRef(true) + + const cancelReviewerInputFocusFrame = useCallback((): void => { + if (reviewerInputFocusFrameRef.current !== null) { + cancelAnimationFrame(reviewerInputFocusFrameRef.current) + reviewerInputFocusFrameRef.current = null + } + }, []) + + const scheduleReviewerInputFocus = useCallback((): void => { + if (!reviewerPanelMountedRef.current) { + return + } + cancelReviewerInputFocusFrame() + reviewerInputFocusFrameRef.current = requestAnimationFrame(() => { + reviewerInputFocusFrameRef.current = null + reviewerInputRef.current?.focus() + }) + }, [cancelReviewerInputFocusFrame]) + + useEffect(() => { + reviewerPanelMountedRef.current = true + return () => { + reviewerPanelMountedRef.current = false + cancelReviewerInputFocusFrame() + } + }, [cancelReviewerInputFocusFrame]) useEffect(() => { setLocalReviewRequests(item.reviewRequests ?? []) @@ -736,7 +764,7 @@ function PRReviewersPanel({ await (selectedReviewerLogins.has(reviewer.login.toLowerCase()) ? handleRemoveReviewers([reviewer.login]) : handleRequestReview([reviewer.login])) - requestAnimationFrame(() => reviewerInputRef.current?.focus()) + scheduleReviewerInputFocus() } const handleReviewerPickerOpenChange = (nextOpen: boolean): void => { @@ -745,7 +773,7 @@ function PRReviewersPanel({ } setOpen(nextOpen) if (nextOpen) { - requestAnimationFrame(() => reviewerInputRef.current?.focus()) + scheduleReviewerInputFocus() return } setReviewerInput('') @@ -2318,6 +2346,7 @@ function ConversationTab({ const [bodyEditing, setBodyEditing] = useState(false) const [bodySaving, setBodySaving] = useState(false) const bodyTextareaRef = useRef(null) + const bodyTextareaFocusFrameRef = useRef(null) const repoAssignees = useRepoAssignees(repoPath, item.repoId) const commentCounts = useMemo(() => getPRCommentAudienceCounts(comments), [comments]) const visibleComments = useMemo( @@ -2336,6 +2365,13 @@ function ConversationTab({ [comments, detailsParticipants, item, repoAssignees.data] ) + const cancelBodyTextareaFocusFrame = useCallback((): void => { + if (bodyTextareaFocusFrameRef.current !== null) { + cancelAnimationFrame(bodyTextareaFocusFrameRef.current) + bodyTextareaFocusFrameRef.current = null + } + }, []) + useEffect(() => { if (replyingTo !== null && !visibleComments.some((comment) => comment.id === replyingTo)) { setReplyingTo(null) @@ -2349,10 +2385,17 @@ function ConversationTab({ }, [body, bodyEditing, item.id]) useEffect(() => { - if (bodyEditing) { - requestAnimationFrame(() => bodyTextareaRef.current?.focus()) + if (!bodyEditing) { + cancelBodyTextareaFocusFrame() + return cancelBodyTextareaFocusFrame } - }, [bodyEditing]) + cancelBodyTextareaFocusFrame() + bodyTextareaFocusFrameRef.current = requestAnimationFrame(() => { + bodyTextareaFocusFrameRef.current = null + bodyTextareaRef.current?.focus() + }) + return cancelBodyTextareaFocusFrame + }, [bodyEditing, cancelBodyTextareaFocusFrame]) const bodySlug = useMemo(() => parseOwnerRepoFromItemUrl(item.url), [item.url]) const markdownGitHubRepo = useMemo( diff --git a/src/renderer/src/components/PullRequestPage.tsx b/src/renderer/src/components/PullRequestPage.tsx index 47766a5c40e..1ac3c4ce549 100644 --- a/src/renderer/src/components/PullRequestPage.tsx +++ b/src/renderer/src/components/PullRequestPage.tsx @@ -478,6 +478,34 @@ function PRReviewersPanel({ const patchWorkItem = useAppStore((s) => s.patchWorkItem) const settings = useAppStore((s) => s.settings) const reviewerInputRef = useRef(null) + const reviewerInputFocusFrameRef = useRef(null) + const reviewerPanelMountedRef = useRef(true) + + const cancelReviewerInputFocusFrame = useCallback((): void => { + if (reviewerInputFocusFrameRef.current !== null) { + cancelAnimationFrame(reviewerInputFocusFrameRef.current) + reviewerInputFocusFrameRef.current = null + } + }, []) + + const scheduleReviewerInputFocus = useCallback((): void => { + if (!reviewerPanelMountedRef.current) { + return + } + cancelReviewerInputFocusFrame() + reviewerInputFocusFrameRef.current = requestAnimationFrame(() => { + reviewerInputFocusFrameRef.current = null + reviewerInputRef.current?.focus() + }) + }, [cancelReviewerInputFocusFrame]) + + useEffect(() => { + reviewerPanelMountedRef.current = true + return () => { + reviewerPanelMountedRef.current = false + cancelReviewerInputFocusFrame() + } + }, [cancelReviewerInputFocusFrame]) useEffect(() => { setLocalReviewRequests(item.reviewRequests ?? []) @@ -736,7 +764,7 @@ function PRReviewersPanel({ await (selectedReviewerLogins.has(reviewer.login.toLowerCase()) ? handleRemoveReviewers([reviewer.login]) : handleRequestReview([reviewer.login])) - requestAnimationFrame(() => reviewerInputRef.current?.focus()) + scheduleReviewerInputFocus() } const handleReviewerPickerOpenChange = (nextOpen: boolean): void => { @@ -745,7 +773,7 @@ function PRReviewersPanel({ } setOpen(nextOpen) if (nextOpen) { - requestAnimationFrame(() => reviewerInputRef.current?.focus()) + scheduleReviewerInputFocus() return } setReviewerInput('') @@ -2447,6 +2475,7 @@ function ConversationTab({ const [bodyEditing, setBodyEditing] = useState(false) const [bodySaving, setBodySaving] = useState(false) const bodyTextareaRef = useRef(null) + const bodyTextareaFocusFrameRef = useRef(null) const repoAssignees = useRepoAssignees(repoPath, item.repoId) const commentCounts = useMemo(() => getPRCommentAudienceCounts(comments), [comments]) const visibleComments = useMemo( @@ -2465,6 +2494,13 @@ function ConversationTab({ [comments, detailsParticipants, item, repoAssignees.data] ) + const cancelBodyTextareaFocusFrame = useCallback((): void => { + if (bodyTextareaFocusFrameRef.current !== null) { + cancelAnimationFrame(bodyTextareaFocusFrameRef.current) + bodyTextareaFocusFrameRef.current = null + } + }, []) + useEffect(() => { if (replyingTo !== null && !visibleComments.some((comment) => comment.id === replyingTo)) { setReplyingTo(null) @@ -2478,10 +2514,17 @@ function ConversationTab({ }, [body, bodyEditing, item.id]) useEffect(() => { - if (bodyEditing) { - requestAnimationFrame(() => bodyTextareaRef.current?.focus()) + if (!bodyEditing) { + cancelBodyTextareaFocusFrame() + return cancelBodyTextareaFocusFrame } - }, [bodyEditing]) + cancelBodyTextareaFocusFrame() + bodyTextareaFocusFrameRef.current = requestAnimationFrame(() => { + bodyTextareaFocusFrameRef.current = null + bodyTextareaRef.current?.focus() + }) + return cancelBodyTextareaFocusFrame + }, [bodyEditing, cancelBodyTextareaFocusFrame]) const bodySlug = useMemo(() => parseOwnerRepoFromItemUrl(item.url), [item.url]) const markdownGitHubRepo = useMemo(