FN-8501: align mobile task-detail footer actions
Align the mobile task-detail Move action with the footer edge. - Let the footer spacer absorb surplus mobile width instead of the Move dropdown. - Cover canonical footer order and triage actions-absent behavior. - Add a patch changeset for the mobile footer fix. Files changed: .changeset/fn-8501-mobile-footer-alignment.md | 7 ++++ .../dashboard/app/components/TaskDetailModal.css | 8 ++++- ...etailModal.responsive-and-dependencies.test.tsx | 40 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8501 Fusion-Task-Lineage: a87bb5af-d16e-4ca8-9111-3168c8440f14 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8501-mobile-footer-alignment.md
Normal file
7
.changeset/fn-8501-mobile-footer-alignment.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Align mobile task-detail Move actions with the footer edge.
|
||||||
|
category: fix
|
||||||
|
dev: The Task Detail spacer now owns mobile footer surplus width.
|
||||||
@@ -3402,6 +3402,12 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
|
|||||||
for overflow, so scoped tokenized packing, flex shrink guards, and label
|
for overflow, so scoped tokenized packing, flex shrink guards, and label
|
||||||
ellipsis let every text-bearing footer control yield while upward menus stay
|
ellipsis let every text-bearing footer control yield while upward menus stay
|
||||||
unclipped.
|
unclipped.
|
||||||
|
|
||||||
|
FNXC:TaskDetailModalResponsive 2026-07-22-17:12:
|
||||||
|
FN-8501 requires the flexible spacer—not the Move dropdown wrapper—to absorb
|
||||||
|
available mobile row space. Keeping the wrapper content-sized puts the primary
|
||||||
|
transition at the trailing edge while its controls retain the FN-8492 shrink
|
||||||
|
and ellipsis safeguards for dense review variants.
|
||||||
*/
|
*/
|
||||||
.task-detail-content .modal-actions {
|
.task-detail-content .modal-actions {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@@ -3422,7 +3428,7 @@ Live and Feed Activity expansion overlays the content instead of reserving a too
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-detail-content .detail-move-dropdown {
|
.task-detail-content .detail-move-dropdown {
|
||||||
flex: 1 1 auto;
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-detail-content .detail-move-actions-in-review {
|
.task-detail-content .detail-move-actions-in-review {
|
||||||
|
|||||||
@@ -901,6 +901,7 @@ describe("TaskDetailModal", () => {
|
|||||||
mobileBlock,
|
mobileBlock,
|
||||||
".task-detail-content .detail-actions-dropdown,\n .task-detail-content .detail-move-dropdown",
|
".task-detail-content .detail-actions-dropdown,\n .task-detail-content .detail-move-dropdown",
|
||||||
);
|
);
|
||||||
|
const desktopMoveDropdownBlock = getExactCssRuleBlock(css, ".detail-move-dropdown");
|
||||||
const expandedChatBlock = getExactCssRuleBlock(css, ".task-detail-content--chat-expanded .modal-actions");
|
const expandedChatBlock = getExactCssRuleBlock(css, ".task-detail-content--chat-expanded .modal-actions");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -917,6 +918,13 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(spacerBlock).toContain("min-width: 0;");
|
expect(spacerBlock).toContain("min-width: 0;");
|
||||||
expect(dropdownBlock).toContain("min-width: 0;");
|
expect(dropdownBlock).toContain("min-width: 0;");
|
||||||
expect(dropdownBlock).toContain("flex-shrink: 1;");
|
expect(dropdownBlock).toContain("flex-shrink: 1;");
|
||||||
|
// FNXC:TaskDetailModalResponsive 2026-07-22-17:12: The Actions + Move
|
||||||
|
// symptom regressed when Move grew into the spacer's middle region. This
|
||||||
|
// mobile-only contract leaves all surplus width to the spacer, making the
|
||||||
|
// move control trailing-aligned without changing desktop dropdown layout.
|
||||||
|
expect(mobileBlock).toMatch(/\.task-detail-content \.detail-move-dropdown\s*\{\s*flex:\s*0 1 auto;/);
|
||||||
|
expect(mobileBlock).not.toMatch(/\.task-detail-content \.detail-move-dropdown\s*\{\s*flex:\s*1 1 auto;/);
|
||||||
|
expect(desktopMoveDropdownBlock).not.toContain("flex:");
|
||||||
expect(inReviewBlock).toContain("display: flex;");
|
expect(inReviewBlock).toContain("display: flex;");
|
||||||
expect(inReviewBlock).toContain("flex-wrap: nowrap;");
|
expect(inReviewBlock).toContain("flex-wrap: nowrap;");
|
||||||
expect(inReviewBlock).toContain("min-width: 0;");
|
expect(inReviewBlock).toContain("min-width: 0;");
|
||||||
@@ -974,6 +982,38 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(standardFooter).toBeTruthy();
|
expect(standardFooter).toBeTruthy();
|
||||||
expect(standardFooter?.contains(screen.getByRole("button", { name: "Actions" }))).toBe(true);
|
expect(standardFooter?.contains(screen.getByRole("button", { name: "Actions" }))).toBe(true);
|
||||||
expect(standardFooter?.querySelector(".detail-move-btn")).toBeTruthy();
|
expect(standardFooter?.querySelector(".detail-move-btn")).toBeTruthy();
|
||||||
|
|
||||||
|
const footerChildren = Array.from(standardFooter?.children ?? []);
|
||||||
|
const actionsIndex = footerChildren.findIndex((child) => child.classList.contains("detail-actions-dropdown"));
|
||||||
|
const spacerIndex = footerChildren.findIndex((child) => child.classList.contains("modal-actions-spacer"));
|
||||||
|
const moveIndex = footerChildren.findIndex((child) => child.classList.contains("detail-move-dropdown"));
|
||||||
|
|
||||||
|
// The shared modal, embedded panel, dock, and mobile sheet all mount this
|
||||||
|
// canonical footer order: leading Actions, flexible spacer, trailing Move.
|
||||||
|
expect(actionsIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(spacerIndex).toBeGreaterThan(actionsIndex);
|
||||||
|
expect(moveIndex).toBeGreaterThan(spacerIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the triage footer usable when Actions is absent", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<TaskDetailModal
|
||||||
|
initialTab="definition"
|
||||||
|
task={makeTask({ column: "triage" as Column })}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={noopDelete}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const footer = container.querySelector(".modal-actions");
|
||||||
|
|
||||||
|
expect(footer?.querySelector(".detail-actions-dropdown")).toBeNull();
|
||||||
|
expect(footer?.querySelector(".modal-actions-spacer")).toBeTruthy();
|
||||||
|
expect(footer?.querySelector(".detail-move-dropdown .detail-move-btn")).toBeTruthy();
|
||||||
|
expect(screen.getByRole("button", { name: "Delete task" })).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("modal-actions contains Delete and Pause buttons for non-done tasks (via Actions dropdown)", () => {
|
it("modal-actions contains Delete and Pause buttons for non-done tasks (via Actions dropdown)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user