fix(dashboard): keep Planning Mode and Settings typable after floating-window migration
FN-8606 declared the window shell as a component inside PlanningModeModal and SettingsModal render. A component declared in render is a new element type on every render, so React remounted the whole subtree on each keystroke, destroying the focused input: Planning Mode dropped everything after the first character and Settings text fields did the same. Replace ModalShell with a plain renderModalShell(children) call so the returned element types stay stable, and add a Planning regression test that types per-character across both the modal and embedded surfaces (fireEvent.change cannot observe this class of bug, which is why it shipped). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/fix-planning-settings-composer-remount.md
Normal file
7
.changeset/fix-planning-settings-composer-remount.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix Planning Mode and Settings dropping typed text after the first character.
|
||||
category: fix
|
||||
dev: The FN-8606 floating-window migration declared `ModalShell` as a component inside `PlanningModeModal`/`SettingsModal` render, so each render produced a new element type and remounted the whole subtree, destroying the focused input. Replaced with a plain `renderModalShell(children)` call so element types stay stable.
|
||||
@@ -3540,7 +3540,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
);
|
||||
|
||||
/* FNXC:ModalTouchGeometry 2026-07-26-14:25: Preserve Planning Mode's pre-migration responsive desktop shell as the FloatingWindow seed so shared persistence does not shrink the surface. */
|
||||
const ModalShell = ({ children }: { children: ReactNode }) => isEmbedded ? (
|
||||
/*
|
||||
FNXC:ModalTouchGeometry 2026-07-26-19:40:
|
||||
The shell MUST be a plain render function, never a component declared inside this render. A nested component is a
|
||||
brand-new element type on every render, so React unmounts and remounts the entire Planning subtree on each keystroke:
|
||||
the composer textarea is recreated, loses DOM focus, and typing appears dead after the first character. Keep the
|
||||
returned element types (div / FloatingWindow) stable by calling this directly.
|
||||
*/
|
||||
const renderModalShell = (children: ReactNode) => isEmbedded ? (
|
||||
<div className="planning-view open" data-testid="planning-view" role="region" aria-label={t("planning.title", "Planning Mode")}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -3564,8 +3571,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
</FloatingWindow>
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalShell>
|
||||
return renderModalShell(
|
||||
(
|
||||
<div className={isEmbedded ? "modal modal-lg planning-modal planning-modal--embedded" : "modal modal-lg planning-modal"} ref={modalRef}>
|
||||
{/*
|
||||
FNXC:PlanningMode 2026-06-22-00:00:
|
||||
@@ -4250,7 +4257,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4619,7 +4619,13 @@ export function SettingsModal({
|
||||
FNXC:Settings 2026-06-22-00:00:
|
||||
Embedded settings is a main-content destination, not a dialog. It drops the fixed `.modal-overlay` backdrop and the inner card chrome (modal-overlay/modal/settings-modal classes), and instead uses `settings-embedded right-dock-embedded-view` (host) + `settings-modal--embedded` (panel) to fill the pane flush like other embedded views (Planning, Command Center). The modal path stays byte-identical.
|
||||
*/
|
||||
const ModalShell = ({ children }: { children: ReactNode }) => isEmbedded ? (
|
||||
/*
|
||||
FNXC:ModalTouchGeometry 2026-07-26-19:40:
|
||||
The shell MUST stay a plain render function, never a component declared inside this render. A nested component is a
|
||||
new element type on every render, so React remounts the whole Settings subtree on each keystroke and text inputs lose
|
||||
focus after one character. Keep the returned element types (div / FloatingWindow) stable by calling this directly.
|
||||
*/
|
||||
const renderModalShell = (children: ReactNode) => isEmbedded ? (
|
||||
<div
|
||||
className="settings-embedded right-dock-embedded-view"
|
||||
data-testid="settings-view"
|
||||
@@ -4648,8 +4654,8 @@ export function SettingsModal({
|
||||
</FloatingWindow>
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalShell>
|
||||
return renderModalShell(
|
||||
<>
|
||||
<div
|
||||
className={isEmbedded ? "modal modal-lg settings-modal settings-modal--embedded" : "modal modal-lg settings-modal"}
|
||||
style={isEmbedded ? undefined : keyboardStyle}
|
||||
@@ -5391,7 +5397,7 @@ export function SettingsModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,27 @@ describe("PlanningModeModal autosize", () => {
|
||||
expect(screen.getByText("Generating initial plan…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:PlanningMode 2026-07-26-19:40:
|
||||
Typing into a Planning composer must keep the SAME textarea node mounted and focused. The FN-8606 floating-window
|
||||
migration declared the modal/embedded shell as a component inside render, so every keystroke produced a new element
|
||||
type, remounted the whole subtree, and dropped focus after one character — Planning Mode became untypable. Assert the
|
||||
invariant with real per-character typing (fireEvent.change cannot see it) across both presentation surfaces.
|
||||
*/
|
||||
it.each(["modal", "embedded"] as const)("keeps the %s composer mounted and focused across keystrokes", async (presentation) => {
|
||||
mockCreatePlanningDraft.mockReturnValue(new Promise(() => {}));
|
||||
const user = userEvent.setup();
|
||||
render(<PlanningModeModal isOpen={true} onClose={vi.fn()} onTaskCreated={vi.fn()} onTasksCreated={vi.fn()} tasks={mockTasks} presentation={presentation} />);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(/Build a user authentication/i) as HTMLTextAreaElement;
|
||||
await user.click(textarea);
|
||||
await user.type(textarea, "auth flow");
|
||||
|
||||
expect(screen.getByPlaceholderText(/Build a user authentication/i)).toBe(textarea);
|
||||
expect(textarea.value).toBe("auth flow");
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
});
|
||||
|
||||
it("grows initial planning textarea and caps at max", async () => {
|
||||
// This assertion owns the blank composer; do not let its draft debounce replace it with a
|
||||
// session view while verifying the textarea's height contract.
|
||||
|
||||
Reference in New Issue
Block a user