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:
5
.changeset/fn-6389-mobile-board-column-swipe.md
Normal file
5
.changeset/fn-6389-mobile-board-column-swipe.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Restored horizontal swiping on mobile kanban board columns while preserving page-level horizontal pan containment.
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user