Cancel composer focus frames from root cleanup (#3452)

This commit is contained in:
Neil
2026-05-30 14:42:54 -04:00
committed by GitHub
parent b1aec76b2d
commit 9c0f8a34de
3 changed files with 37 additions and 4 deletions
@@ -41,6 +41,7 @@ type RepoOption = React.ComponentProps<typeof RepoCombobox>['repos'][number]
type NewWorkspaceComposerCardProps = {
containerClassName?: string
composerRef?: React.RefObject<HTMLDivElement | null>
onComposerNodeChange?: (node: HTMLDivElement | null) => void
nameInputRef?: React.RefObject<HTMLInputElement | null>
quickAgent: TuiAgent | null
onQuickAgentChange: (agent: TuiAgent | null) => void
@@ -205,6 +206,7 @@ function useComposerFileDragOver(): {
export default function NewWorkspaceComposerCard({
containerClassName,
composerRef,
onComposerNodeChange,
nameInputRef,
quickAgent,
onQuickAgentChange,
@@ -281,7 +283,19 @@ export default function NewWorkspaceComposerCard({
nameInputFocusFrameRef.current = null
}, [])
React.useEffect(() => cancelNameInputFocusFrame, [cancelNameInputFocusFrame])
const setComposerNode = React.useCallback(
(node: HTMLDivElement | null): void => {
// Why: the queued repo-picker focus is only valid while this composer exists.
if (!node) {
cancelNameInputFocusFrame()
}
if (composerRef) {
composerRef.current = node
}
onComposerNodeChange?.(node)
},
[cancelNameInputFocusFrame, composerRef, onComposerNodeChange]
)
const focusNameInput = React.useCallback(() => {
// Why: after the repo picker commits a choice, moving focus to the name
@@ -313,7 +327,7 @@ export default function NewWorkspaceComposerCard({
return (
<div
ref={composerRef}
ref={setComposerNode}
// Why: preload classifies native OS file drops by the nearest
// `data-native-file-drop-target` marker in the composedPath. Tagging
// the composer root makes drops anywhere on the card route to the
@@ -100,7 +100,14 @@ function QuickTabBody({
active: boolean
}): React.JSX.Element {
const settings = useAppStore((s) => s.settings)
const { cardProps, composerRef, nameInputRef, submitQuick, createDisabled } = useComposerState({
const {
cardProps,
composerRef,
onComposerNodeChange,
nameInputRef,
submitQuick,
createDisabled
} = useComposerState({
initialName: modalData.prefilledName ?? '',
// Why: the modal is quick-create only now, so prompt-prefill state is
// intentionally ignored even if older callers still send it.
@@ -219,6 +226,7 @@ function QuickTabBody({
</DialogHeader>
<NewWorkspaceComposerCard
composerRef={composerRef}
onComposerNodeChange={onComposerNodeChange}
nameInputRef={nameInputRef}
quickAgent={quickAgent}
onQuickAgentChange={handleQuickAgentChange}
+12 -1
View File
@@ -212,6 +212,7 @@ export type UseComposerStateResult = {
/** Ref the consumer should attach to the composer wrapper so the global
* Enter-to-submit handler can scope its behavior to the visible composer. */
composerRef: React.RefObject<HTMLDivElement | null>
onComposerNodeChange: (node: HTMLDivElement | null) => void
promptTextareaRef: React.RefObject<HTMLTextAreaElement | null>
nameInputRef: React.RefObject<HTMLInputElement | null>
submit: () => Promise<void>
@@ -514,7 +515,16 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
promptCaretFrameRef.current = null
}, [])
useEffect(() => cancelPromptCaretFrame, [cancelPromptCaretFrame])
const handleComposerNodeChange = useCallback(
(node: HTMLDivElement | null): void => {
// Why: the queued caret restoration targets composer descendants and
// must be canceled as soon as the composer root leaves the DOM.
if (!node) {
cancelPromptCaretFrame()
}
},
[cancelPromptCaretFrame]
)
const hookCheckRef = useRef<{
key: string
@@ -2329,6 +2339,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
return {
cardProps,
composerRef,
onComposerNodeChange: handleComposerNodeChange,
promptTextareaRef,
nameInputRef,
submit,