FN-6500: fix tablet task detail modal sizing
Keeps tablet task detail modals wide enough and within the viewport.\n\n- Reconcile the tablet overlay offset with modal max-height so actions remain visible.\n- Widen the tablet task detail modal to use more horizontal viewport space.\n- Add CSS-focused coverage for tablet sizing plus unchanged desktop and mobile guards.\n- Add a patch changeset for the published Fusion package.\n\nFiles changed:\n .changeset/fn-6500-tablet-task-detail-modal.md | 5 ++\n .../task-detail-modal-tablet-width.test.ts | 14 +++--\n .../dashboard/app/components/TaskDetailModal.css | 15 +++--\n ...etailModal.responsive-and-dependencies.test.tsx | 64 ++++++++++++++++++++++\n .../__tests__/core-modals-mobile.test.tsx | 5 +-\n 5 files changed, 94 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-6500 Fusion-Task-Lineage: 016a2839-29f9-48de-b644-a5fcd27b35e3
This commit is contained in:
5
.changeset/fn-6500-tablet-task-detail-modal.md
Normal file
5
.changeset/fn-6500-tablet-task-detail-modal.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix the tablet task detail modal sizing so the action footer remains on-screen and the modal uses more viewport width.
|
||||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
|
||||||
describe("task detail modal tablet width (FN-5599)", () => {
|
describe("task detail modal tablet width (FN-5599, FN-6500)", () => {
|
||||||
const detailModalCss = readFileSync(
|
const detailModalCss = readFileSync(
|
||||||
resolve(__dirname, "../components/TaskDetailModal.css"),
|
resolve(__dirname, "../components/TaskDetailModal.css"),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
@@ -14,17 +14,23 @@ describe("task detail modal tablet width (FN-5599)", () => {
|
|||||||
expect(baseRuleMatch![0]).toContain("width: min(95vw, 800px);");
|
expect(baseRuleMatch![0]).toContain("width: min(95vw, 800px);");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defines a tablet breakpoint override for task detail modal width", () => {
|
it("defines a tablet breakpoint override for task detail modal width and height coupling", () => {
|
||||||
const tabletBlockMatch = detailModalCss.match(
|
const tabletBlockMatch = detailModalCss.match(
|
||||||
/@media\s*\(min-width:\s*769px\)\s*and\s*\(max-width:\s*1024px\)\s*\{([\s\S]*?)\n\}/,
|
/@media\s*\(min-width:\s*769px\)\s*and\s*\(max-width:\s*1024px\)\s*\{([\s\S]*?)\n\}/,
|
||||||
);
|
);
|
||||||
expect(tabletBlockMatch).toBeTruthy();
|
expect(tabletBlockMatch).toBeTruthy();
|
||||||
|
|
||||||
const tabletBlock = tabletBlockMatch![1];
|
const tabletBlock = tabletBlockMatch![1];
|
||||||
|
const overlayRuleMatch = tabletBlock.match(/\.modal-overlay:has\(\.task-detail-modal\)\s*\{[^}]*\}/s);
|
||||||
const modalRuleMatch = tabletBlock.match(/\.modal\.task-detail-modal\s*\{[^}]*\}/s);
|
const modalRuleMatch = tabletBlock.match(/\.modal\.task-detail-modal\s*\{[^}]*\}/s);
|
||||||
|
const overlayOffset = overlayRuleMatch?.[0].match(/--overlay-padding-top:\s*([^;]+);/)?.[1]?.trim();
|
||||||
|
const maxHeightOffset = modalRuleMatch?.[0].match(/max-height:\s*calc\(100dvh - var\(--overlay-padding-top,\s*([^)]+)\) - var\(--space-md\)\);/)?.[1]?.trim();
|
||||||
|
|
||||||
|
expect(overlayRuleMatch).toBeTruthy();
|
||||||
expect(modalRuleMatch).toBeTruthy();
|
expect(modalRuleMatch).toBeTruthy();
|
||||||
expect(modalRuleMatch![0]).toContain("width: min(96vw, 1024px);");
|
expect(maxHeightOffset).toBe(overlayOffset);
|
||||||
expect(modalRuleMatch![0]).toContain("max-width: 96vw;");
|
expect(modalRuleMatch![0]).toContain("width: 98vw;");
|
||||||
|
expect(modalRuleMatch![0]).toContain("max-width: 98vw;");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps mobile full-screen sheet width behavior", () => {
|
it("keeps mobile full-screen sheet width behavior", () => {
|
||||||
|
|||||||
@@ -962,13 +962,20 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
|
|||||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FN-5599: widen task detail modal on tablet viewports. */
|
/*
|
||||||
|
FNXC:TaskDetailModalResponsive 2026-06-16-19:13:
|
||||||
|
FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset and modal max-height subtraction must use the same `--overlay-padding-top` value so the `.modal-actions` footer remains on-screen, while the modal uses more of the tablet viewport to avoid a cramped layout.
|
||||||
|
*/
|
||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
@media (min-width: 769px) and (max-width: 1024px) {
|
||||||
|
.modal-overlay:has(.task-detail-modal) {
|
||||||
|
--overlay-padding-top: 6vh;
|
||||||
|
}
|
||||||
|
|
||||||
.modal.task-detail-modal {
|
.modal.task-detail-modal {
|
||||||
width: min(96vw, 1024px);
|
width: 98vw;
|
||||||
max-width: 96vw;
|
max-width: 98vw;
|
||||||
height: 92vh;
|
height: 92vh;
|
||||||
max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - 16px);
|
max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - var(--space-md));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
mockConfirmWithCheckbox,
|
mockConfirmWithCheckbox,
|
||||||
mockUsePluginUiSlots,
|
mockUsePluginUiSlots,
|
||||||
expectBaseRule,
|
expectBaseRule,
|
||||||
|
getCssRuleBlock,
|
||||||
readDashboardStylesSource,
|
readDashboardStylesSource,
|
||||||
setupTaskDetailModalHooks,
|
setupTaskDetailModalHooks,
|
||||||
} from "./TaskDetailModal.test-helpers";
|
} from "./TaskDetailModal.test-helpers";
|
||||||
@@ -20,6 +21,38 @@ import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
|
|||||||
|
|
||||||
setupTaskDetailModalHooks();
|
setupTaskDetailModalHooks();
|
||||||
|
|
||||||
|
function getCssAtRuleBlock(css: string, atRule: string, startAt = 0): { block: string; endIndex: number } {
|
||||||
|
const atRuleStart = css.indexOf(atRule, startAt);
|
||||||
|
expect(atRuleStart).toBeGreaterThanOrEqual(0);
|
||||||
|
const openingBrace = css.indexOf("{", atRuleStart);
|
||||||
|
expect(openingBrace).toBeGreaterThanOrEqual(0);
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = openingBrace; index < css.length; index += 1) {
|
||||||
|
const char = css[index];
|
||||||
|
if (char === "{") depth += 1;
|
||||||
|
if (char === "}") depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
return { block: css.slice(openingBrace + 1, index), endIndex: index + 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Missing closing brace for ${atRule}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCssAtRuleBlockContaining(css: string, atRule: string, selector: string): string {
|
||||||
|
let startAt = 0;
|
||||||
|
while (startAt < css.length) {
|
||||||
|
const { block, endIndex } = getCssAtRuleBlock(css, atRule, startAt);
|
||||||
|
if (block.includes(selector)) {
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
startAt = endIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Missing ${atRule} block containing ${selector}`);
|
||||||
|
}
|
||||||
|
|
||||||
describe("TaskDetailModal", () => {
|
describe("TaskDetailModal", () => {
|
||||||
describe("mobile responsive structure", () => {
|
describe("mobile responsive structure", () => {
|
||||||
it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => {
|
it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => {
|
||||||
@@ -52,6 +85,37 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
|
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
|
||||||
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/);
|
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamp-separator\s*\{[^}]*display:\s*none;/);
|
||||||
});
|
});
|
||||||
|
it("keeps desktop and mobile modal sizing guards unchanged", () => {
|
||||||
|
const css = readDashboardStylesSource();
|
||||||
|
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".modal-overlay:has(.task-detail-modal)");
|
||||||
|
const mobileOverlayBlock = getCssRuleBlock(mobileBlock, ".modal-overlay:has(.task-detail-modal)");
|
||||||
|
const mobileModalBlock = getCssRuleBlock(mobileBlock, ".modal.task-detail-modal");
|
||||||
|
|
||||||
|
expectBaseRule(css, ".modal.task-detail-modal", "width: min(95vw, 800px);");
|
||||||
|
expectBaseRule(css, ".modal.task-detail-modal", "height: 85vh;");
|
||||||
|
expect(mobileOverlayBlock).toContain("padding-top: 0;");
|
||||||
|
expect(mobileOverlayBlock).toContain("align-items: stretch;");
|
||||||
|
expect(mobileModalBlock).toContain("width: 100vw;");
|
||||||
|
expect(mobileModalBlock).toContain("height: 100dvh;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reconciles tablet overlay offset with task-detail max-height and widens the modal", () => {
|
||||||
|
const css = readDashboardStylesSource();
|
||||||
|
const tabletBlock = getCssAtRuleBlockContaining(css, "@media (min-width: 769px) and (max-width: 1024px)", ".modal.task-detail-modal");
|
||||||
|
const tabletOverlayBlock = getCssRuleBlock(tabletBlock, ".modal-overlay:has(.task-detail-modal)");
|
||||||
|
const tabletModalBlock = getCssRuleBlock(tabletBlock, ".modal.task-detail-modal");
|
||||||
|
const overlayOffset = tabletOverlayBlock.match(/--overlay-padding-top:\s*([^;]+);/)?.[1]?.trim();
|
||||||
|
const maxHeightOffset = tabletModalBlock.match(/max-height:\s*calc\(100dvh - var\(--overlay-padding-top,\s*([^)]+)\) - var\(--space-md\)\);/)?.[1]?.trim();
|
||||||
|
|
||||||
|
expect(overlayOffset).toBeTruthy();
|
||||||
|
expect(maxHeightOffset).toBe(overlayOffset);
|
||||||
|
expect(tabletModalBlock).toContain("width: 98vw;");
|
||||||
|
expect(tabletModalBlock).toContain("max-width: 98vw;");
|
||||||
|
expect(tabletModalBlock).toContain("height: 92vh;");
|
||||||
|
expect(tabletModalBlock).not.toContain("width: min(96vw, 1024px);");
|
||||||
|
expect(tabletModalBlock).not.toContain("16px");
|
||||||
|
});
|
||||||
|
|
||||||
it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => {
|
it("renders responsive structural classes (modal-lg, overlay, spacer, tabs, detail-body)", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ describe("core modals mobile css coverage", () => {
|
|||||||
const tabletRule = getLastRuleBlock(tabletBlock, ".modal.task-detail-modal");
|
const tabletRule = getLastRuleBlock(tabletBlock, ".modal.task-detail-modal");
|
||||||
expect(tabletRule).toContain("height: 92vh;");
|
expect(tabletRule).toContain("height: 92vh;");
|
||||||
expect(extractVhHeight(tabletRule)).toBeGreaterThan(extractVhHeight(baseRule));
|
expect(extractVhHeight(tabletRule)).toBeGreaterThan(extractVhHeight(baseRule));
|
||||||
expect(tabletRule).toContain("max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - 16px);");
|
expect(tabletRule).toContain("width: 98vw;");
|
||||||
|
expect(tabletRule).toContain("max-width: 98vw;");
|
||||||
|
expect(tabletBlock).toContain("--overlay-padding-top: 6vh;");
|
||||||
|
expect(tabletRule).toContain("max-height: calc(100dvh - var(--overlay-padding-top, 6vh) - var(--space-md));");
|
||||||
|
|
||||||
const mobileRule = getLastRuleBlock(mobileBlock, ".modal.task-detail-modal");
|
const mobileRule = getLastRuleBlock(mobileBlock, ".modal.task-detail-modal");
|
||||||
expect(mobileRule).toContain("height: 100dvh;");
|
expect(mobileRule).toContain("height: 100dvh;");
|
||||||
|
|||||||
Reference in New Issue
Block a user