FN-6389: restore mobile board column swiping

Restore horizontal touch panning for mobile board columns while keeping document-level drift containment.

- Opt board columns, column headers, and column bodies back into combined horizontal and vertical panning on mobile.
- Apply the same touch-action allowance to workflow and multi-lane column scrollers.
- Add CSS regression coverage for mobile board swipe targets, overscroll containment, and proximity snap behavior.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-6389-mobile-board-column-swipe.md    |   5 +
 .../__tests__/board-mobile-column-swipe.test.ts    | 123 +++++++++++++++++++++
 packages/dashboard/app/components/Lane.css         |   4 +
 packages/dashboard/app/styles.css                  |   7 ++
 4 files changed, 139 insertions(+)

Fusion-Task-Id: FN-6389

Fusion-Task-Lineage: 0d337b43-34b1-4ccd-a9d9-d25cc9326945
This commit is contained in:
gsxdsm
2026-06-13 14:07:22 -07:00
parent 537cc2ab7f
commit 14ed177d89
4 changed files with 139 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Restored horizontal swiping on mobile kanban board columns while preserving page-level horizontal pan containment.

View File

@@ -0,0 +1,123 @@
import { describe, expect, it } from "vitest";
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../test/cssFixture";
function extractMediaBlocks(content: string, pattern: RegExp): string {
const blocks: string[] = [];
for (const match of content.matchAll(pattern)) {
const start = match.index! + match[0].length;
let index = start;
let depth = 1;
while (index < content.length && depth > 0) {
if (content[index] === "{") depth++;
if (content[index] === "}") depth--;
index++;
}
expect(depth).toBe(0);
blocks.push(content.slice(start, index - 1));
}
expect(blocks.length).toBeGreaterThan(0);
return blocks.join("\n");
}
function stripCssComments(css: string): string {
return css.replace(/\/\*[\s\S]*?\*\//g, "");
}
function ruleBlocks(css: string, selector: string): string[] {
const blocks: string[] = [];
const rulePattern = /([^{}]+)\{([^{}]*)\}/g;
for (const match of stripCssComments(css).matchAll(rulePattern)) {
const selectorList = match[1]
.split(",")
.map((part) => part.trim())
.filter(Boolean);
if (selectorList.includes(selector)) {
blocks.push(`${match[1].trim()} {${match[2]}}`);
}
}
return blocks;
}
function ruleBlock(css: string, selector: string): string {
const blocks = ruleBlocks(css, selector);
expect(blocks.length, `missing CSS rule for ${selector}`).toBeGreaterThan(0);
return blocks[0];
}
function declarationValue(rule: string, property: string): string | null {
const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = rule.match(new RegExp(`${escaped}\\s*:\\s*([^;]+);`));
return match?.[1]?.trim() ?? null;
}
function expectTouchPanXY(css: string, selector: string): void {
const block = ruleBlock(css, selector);
expect(declarationValue(block, "touch-action")).toBe("pan-x pan-y");
expect(block).not.toMatch(/touch-action:\s*pan-y\s*;/);
}
function expectContainmentScroller(block: string): void {
expect(block).toContain("overflow-x: auto");
expect(block).toContain("overscroll-behavior-x: contain");
expect(block).toContain("scroll-snap-type: x proximity");
expect(block).not.toContain("scroll-snap-type: x mandatory");
}
describe("mobile board column swipe target containment (FN-6389)", () => {
const css = loadAllAppCss();
const baseCss = loadAllAppCssBaseOnly();
const mobileCss = extractMediaBlocks(css, /@media\s*\([^)]*max-width:\s*768px[^)]*\)[^{]*\{/g);
it("opts classic mobile board column interiors into horizontal panning", () => {
for (const selector of [".board > .column", ".column", ".column-header", ".column-body"]) {
expectTouchPanXY(mobileCss, selector);
}
const columnBodyBlock = ruleBlock(mobileCss, ".column-body");
expect(columnBodyBlock).not.toContain("overflow-y: hidden");
});
it("opts workflow and multi-lane board interiors into horizontal panning", () => {
for (const selector of [
".board.board-workflow-columns",
".board.board-workflow-columns > .column",
".lane-columns",
".lane-columns > .column",
]) {
expectTouchPanXY(baseCss, selector);
}
});
it("preserves the FN-6365 mobile document pan lock", () => {
const rootBlock = ruleBlock(mobileCss, "html");
const appRootBlock = ruleBlock(mobileCss, "#root");
const starBlocks = ruleBlocks(mobileCss, "*");
const defaultTouchBlock = starBlocks.find((block) => block.includes("touch-action: pan-y;")) ?? "";
const widthContainmentBlock = starBlocks.find((block) => block.includes("max-inline-size: 100%;")) ?? "";
for (const block of [rootBlock, appRootBlock]) {
expect(block).toContain("overflow-x: hidden;");
expect(block).toContain("overscroll-behavior-x: none;");
expect(block).toContain("touch-action: pan-y;");
}
expect(rootBlock).toContain("width: 100%;");
expect(rootBlock).toContain("max-width: 100%;");
expect(appRootBlock).toContain("min-width: 0;");
expect(declarationValue(defaultTouchBlock, "touch-action")).toBe("pan-y");
expect(widthContainmentBlock).toContain("max-width: 100%;");
expect(widthContainmentBlock).toContain("max-inline-size: 100%;");
});
it("preserves FN-6378 horizontal overscroll containment and proximity snap", () => {
expectContainmentScroller(ruleBlock(baseCss, ".board"));
expectContainmentScroller(ruleBlock(mobileCss, ".board"));
expectContainmentScroller(ruleBlock(baseCss, ".board.board-workflow-columns"));
expectContainmentScroller(ruleBlock(baseCss, ".lane-columns"));
});
});

View File

@@ -60,6 +60,7 @@
overflow-y: hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x proximity;
touch-action: pan-x pan-y;
}
.board.board-workflow-columns > .column {
@@ -68,6 +69,7 @@
height: 100%;
min-height: 0;
scroll-snap-align: center;
touch-action: pan-x pan-y;
}
.lane {
@@ -129,12 +131,14 @@
scrollbar-color: var(--border) transparent;
scrollbar-width: thin;
min-height: 0;
touch-action: pan-x pan-y;
}
.lane-columns > .column {
flex: 0 0 clamp(280px, 28vw, 340px);
/* Repo convention (mobile-scroll-snap test): snap-align must be `center`. */
scroll-snap-align: center;
touch-action: pan-x pan-y;
}
.lane-columns::-webkit-scrollbar {

View File

@@ -3417,6 +3417,13 @@ input[type="range"]:focus-visible {
display: none;
}
.board > .column,
.column,
.column-header,
.column-body {
touch-action: pan-x pan-y;
}
.board > .column {
width: 300px;
min-width: 300px;