|
1 | 1 | import { type ApprovalRequestId } from "@t3tools/contracts"; |
2 | | -import { memo, useCallback, useEffect, useRef } from "react"; |
| 2 | +import { memo, useEffect, useEffectEvent, useRef } from "react"; |
3 | 3 | import { type PendingUserInput } from "../../session-logic"; |
4 | 4 | import { |
5 | 5 | derivePendingUserInputProgress, |
@@ -61,32 +61,36 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( |
61 | 61 | const progress = derivePendingUserInputProgress(prompt.questions, answers, questionIndex); |
62 | 62 | const activeQuestion = progress.activeQuestion; |
63 | 63 | const autoAdvanceTimerRef = useRef<number | null>(null); |
| 64 | + const onAdvanceRef = useRef(onAdvance); |
| 65 | + useEffect(() => { |
| 66 | + onAdvanceRef.current = onAdvance; |
| 67 | + }, [onAdvance]); |
64 | 68 |
|
65 | | - // Clear auto-advance timer on unmount |
| 69 | + // Cancel a pending auto-advance on unmount, and whenever the active question |
| 70 | + // changes or a response goes in flight — otherwise a manual Next/Submit landing |
| 71 | + // inside the 200ms window leaves a stale timer that advances or submits again. |
66 | 72 | useEffect(() => { |
67 | 73 | return () => { |
68 | 74 | if (autoAdvanceTimerRef.current !== null) { |
69 | 75 | window.clearTimeout(autoAdvanceTimerRef.current); |
| 76 | + autoAdvanceTimerRef.current = null; |
70 | 77 | } |
71 | 78 | }; |
72 | | - }, []); |
| 79 | + }, [activeQuestion?.id, isResponding]); |
73 | 80 |
|
74 | | - const handleOptionSelection = useCallback( |
75 | | - (questionId: string, optionLabel: string) => { |
76 | | - onToggleOption(questionId, optionLabel); |
77 | | - if (activeQuestion?.multiSelect) { |
78 | | - return; |
79 | | - } |
80 | | - if (autoAdvanceTimerRef.current !== null) { |
81 | | - window.clearTimeout(autoAdvanceTimerRef.current); |
82 | | - } |
83 | | - autoAdvanceTimerRef.current = window.setTimeout(() => { |
84 | | - autoAdvanceTimerRef.current = null; |
85 | | - onAdvance(); |
86 | | - }, 200); |
87 | | - }, |
88 | | - [activeQuestion?.multiSelect, onAdvance, onToggleOption], |
89 | | - ); |
| 81 | + const handleOptionSelection = useEffectEvent((questionId: string, optionLabel: string) => { |
| 82 | + onToggleOption(questionId, optionLabel); |
| 83 | + if (activeQuestion?.multiSelect) { |
| 84 | + return; |
| 85 | + } |
| 86 | + if (autoAdvanceTimerRef.current !== null) { |
| 87 | + window.clearTimeout(autoAdvanceTimerRef.current); |
| 88 | + } |
| 89 | + autoAdvanceTimerRef.current = window.setTimeout(() => { |
| 90 | + autoAdvanceTimerRef.current = null; |
| 91 | + onAdvanceRef.current(); |
| 92 | + }, 200); |
| 93 | + }); |
90 | 94 |
|
91 | 95 | // Keyboard shortcut: digits toggle options for multi-select prompts and preserve |
92 | 96 | // the current auto-advance behavior for single-select questions. |
@@ -117,7 +121,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( |
117 | 121 | }; |
118 | 122 | document.addEventListener("keydown", handler); |
119 | 123 | return () => document.removeEventListener("keydown", handler); |
120 | | - }, [activeQuestion, handleOptionSelection, isResponding]); |
| 124 | + }, [activeQuestion, isResponding]); |
121 | 125 |
|
122 | 126 | if (!activeQuestion) { |
123 | 127 | return null; |
|
0 commit comments