FN-7317: fix mobile planning summary controls
Fix mobile Planning Mode summary controls so description actions stay tappable and accessible. - Move Markdown and Expand/Collapse controls outside the Description label to avoid mobile tap retargeting.\n- Add accessible labels and shared header/action styling for summary description controls.\n- Cover mobile expand/collapse behavior alongside the markdown preview toggle.\n- Add a patch changeset for the published CLI package.\n\nFiles changed:\n .changeset/fn-7317-mobile-planning-summary.md | 7 +++\n .../dashboard/app/components/PlanningModeModal.css | 26 ++++++++-\n .../dashboard/app/components/PlanningModeModal.tsx | 58 +++++++++++--------\n .../PlanningModeModal.ui-interactions.test.tsx | 65 ++++++++++++++++++++--\n 4 files changed, 128 insertions(+), 28 deletions(-) Fusion-Task-Id: FN-7317 Fusion-Task-Lineage: f13bc27a-ea4e-46cd-aebb-2fe0a3251496 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7317-mobile-planning-summary.md
Normal file
7
.changeset/fn-7317-mobile-planning-summary.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix mobile Planning Mode summary description Expand and Collapse controls.
|
||||
category: fix
|
||||
dev: Splits the summary description label from adjacent Markdown and Expand controls and adds mobile regression coverage.
|
||||
@@ -1127,13 +1127,25 @@ An empty footer must NOT reserve vertical space or paint its divider band. When
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.planning-expand-btn {
|
||||
.planning-summary-description-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.planning-summary-description-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.planning-expand-btn {
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--todo);
|
||||
font-size: 12px;
|
||||
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1561,6 +1573,16 @@ Tablet embedded Planning keeps the desktop two-pane shell, so the summary footer
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.planning-summary-description-header {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.planning-summary-description-actions {
|
||||
margin-left: 0;
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.planning-example-chips {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -2819,33 +2819,47 @@ function SummaryView({
|
||||
|
||||
<div className="planning-summary-form">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t("planning.description", "Description")}
|
||||
<button
|
||||
type="button"
|
||||
className="planning-expand-btn"
|
||||
aria-pressed={renderMarkdown}
|
||||
aria-label={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")}
|
||||
title={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")}
|
||||
data-testid="planning-description-markdown-toggle"
|
||||
onClick={() => setRenderMarkdown(!renderMarkdown)}
|
||||
>
|
||||
{renderMarkdown ? t("planning.plain", "Plain") : t("planning.markdown", "Markdown")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="planning-expand-btn"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? t("planning.collapse", "Collapse") : t("planning.expand", "Expand")}
|
||||
</button>
|
||||
</label>
|
||||
{/*
|
||||
FNXC:PlanningSummaryMobile 2026-06-30-12:00:
|
||||
Keep Description controls adjacent but outside the label. Mobile browsers can retarget taps from interactive descendants inside labels, which made the Expand/Collapse control appear inert while the textarea/preview state already supported expansion.
|
||||
*/}
|
||||
<div className="planning-summary-description-header">
|
||||
<label id="planning-summary-description-label" htmlFor="planning-summary-description">
|
||||
{t("planning.description", "Description")}
|
||||
</label>
|
||||
<div className="planning-summary-description-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="planning-expand-btn"
|
||||
aria-pressed={renderMarkdown}
|
||||
aria-label={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")}
|
||||
title={renderMarkdown ? t("planning.showRawText", "Show raw text") : t("planning.showFormattedMarkdown", "Show formatted markdown")}
|
||||
data-testid="planning-description-markdown-toggle"
|
||||
onClick={() => setRenderMarkdown(!renderMarkdown)}
|
||||
>
|
||||
{renderMarkdown ? t("planning.plain", "Plain") : t("planning.markdown", "Markdown")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="planning-expand-btn"
|
||||
aria-pressed={isExpanded}
|
||||
aria-label={isExpanded ? t("planning.collapseDescription", "Collapse description") : t("planning.expandDescription", "Expand description")}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? t("planning.collapse", "Collapse") : t("planning.expand", "Expand")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{renderMarkdown ? (
|
||||
<div className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}>
|
||||
<div
|
||||
className={`planning-description-preview markdown-body ${isExpanded ? "expanded" : ""}`}
|
||||
aria-labelledby="planning-summary-description-label"
|
||||
>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{summary.description}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<textarea
|
||||
id="planning-summary-description"
|
||||
ref={descriptionAutosizeRef}
|
||||
className={`planning-textarea ${isExpanded ? "expanded" : ""}`}
|
||||
value={summary.description}
|
||||
|
||||
@@ -104,6 +104,7 @@ vi.mock("../../api", () => ({
|
||||
rejectPlan: (...args: any[]) => mockRejectPlan(...args),
|
||||
refineTask: (...args: any[]) => mockRefineTask(...args),
|
||||
fetchSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }),
|
||||
fetchTaskEffectiveSettings: vi.fn().mockResolvedValue({ modelPresets: [], autoSelectModelPreset: false, defaultPresetBySize: {} }),
|
||||
fetchGlobalSettings: vi.fn().mockResolvedValue({}),
|
||||
fetchModels: (...args: any[]) => mockFetchModels(...args),
|
||||
fetchWorkflowSteps: vi.fn().mockResolvedValue([]),
|
||||
@@ -213,7 +214,8 @@ describe("PlanningModeModal", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("Definition")).toBeDefined();
|
||||
expect(screen.getByText("Example task")).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Plan" })).toBeDefined();
|
||||
expect(container.querySelector(".detail-body")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -937,12 +939,12 @@ describe("PlanningModeModal", () => {
|
||||
});
|
||||
|
||||
describe("Summary markdown preview toggle", () => {
|
||||
it("toggles description between plain textarea and formatted markdown preview", async () => {
|
||||
async function renderPlanningSummary(description: string) {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onSummary?.({
|
||||
...mockSummary,
|
||||
description: "## Heading\n\n- item\n\n**bold**",
|
||||
description,
|
||||
});
|
||||
}, 10);
|
||||
|
||||
@@ -952,7 +954,7 @@ describe("PlanningModeModal", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
const result = render(
|
||||
<PlanningModeModal
|
||||
isOpen={true}
|
||||
onClose={mockOnClose}
|
||||
@@ -971,6 +973,12 @@ describe("PlanningModeModal", () => {
|
||||
expect(screen.getByText("Planning Complete!")).toBeDefined();
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
it("toggles description between plain textarea and formatted markdown preview", async () => {
|
||||
const { container } = await renderPlanningSummary("## Heading\n\n- item\n\n**bold**");
|
||||
|
||||
expect(container.querySelector(".planning-textarea")).not.toBeNull();
|
||||
expect(container.querySelector(".planning-description-preview")).toBeNull();
|
||||
|
||||
@@ -988,6 +996,55 @@ describe("PlanningModeModal", () => {
|
||||
expect(container.querySelector(".planning-textarea")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("expands and collapses the mobile summary description without breaking the adjacent markdown toggle", async () => {
|
||||
mockViewport("mobile");
|
||||
const { container } = await renderPlanningSummary(
|
||||
"## Heading\n\n" +
|
||||
"This is a long planning description that should visibly change between collapsed and expanded summary states on mobile.\n\n" +
|
||||
"- Keep markdown available\n" +
|
||||
"- Keep expand available",
|
||||
);
|
||||
|
||||
const markdownToggle = screen.getByTestId("planning-description-markdown-toggle");
|
||||
const expandButton = screen.getByRole("button", { name: "Expand description" });
|
||||
expect(markdownToggle.closest("label")).toBeNull();
|
||||
expect(expandButton.closest("label")).toBeNull();
|
||||
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>(".planning-textarea");
|
||||
expect(textarea).not.toBeNull();
|
||||
expect(textarea?.classList.contains("expanded")).toBe(false);
|
||||
|
||||
fireEvent.click(expandButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Collapse description" })).toBeDefined();
|
||||
expect(container.querySelector(".planning-textarea")?.classList.contains("expanded")).toBe(true);
|
||||
});
|
||||
|
||||
fireEvent.click(markdownToggle);
|
||||
|
||||
await waitFor(() => {
|
||||
const preview = container.querySelector(".planning-description-preview");
|
||||
expect(preview).not.toBeNull();
|
||||
expect(preview?.classList.contains("expanded")).toBe(true);
|
||||
expect(screen.getByRole("heading", { level: 2, name: "Heading" })).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Collapse description" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Expand description" })).toBeDefined();
|
||||
expect(container.querySelector(".planning-description-preview")?.classList.contains("expanded")).toBe(false);
|
||||
});
|
||||
|
||||
fireEvent.click(markdownToggle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(".planning-textarea")).not.toBeNull();
|
||||
expect(container.querySelector(".planning-description-preview")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user