Files
fusion/packages/dashboard/app/__tests__/mobile-scroll-snap.test.ts
gsxdsm e01dc7d3d3 feat(board): speed up kanban scrolling and drop desktop snapping
Desktop board scrollers no longer snap: the browser's proximity-snap settle
animation was capturing wheel/trackpad pans and reading as a slow, sticky drag
toward a column center. Base `.board`, `.board-workflow-columns`, and
`.lane-columns` declare `scroll-snap-type: none`; proximity snap is re-declared
in phone-tier media blocks only, where the JS column pager owns paging.

On phones, the hook now owns post-lift motion instead of waiting it out. A
directional lift kills native inertia and animates to its target column via rAF
ease-out (~190-300ms), so the page starts moving on lift rather than after a
native fling that can coast for most of a second. Fling reach is preserved by
deriving a page count (1-3) from release velocity sampled off the board's own
scroll ticks, not from however far inertia happens to travel.

Guards: re-touch cancels the animation and hands the axis back to the finger;
reduced motion, missing rAF, and sub-2px distances fall back to the instant hard
jump; unmount mid-animation restores the frozen inline styles; a fast drag that
rests before lifting is not treated as a flick.

Tap-to-stop-during-momentum is gone as an interaction (no long coast remains to
interrupt). Its regression test is reframed around the equivalent seam: a drag
that interrupts the page animation wins over the pending page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:16:11 -07:00

84 lines
3.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { loadAllAppCss } from "../test/cssFixture";
const css = loadAllAppCss();
describe("scroll-snap CSS", () => {
describe("base (desktop) styles", () => {
/*
FNXC:BoardNavigation 2026-07-24-10:05:
Desktop board scrolling is free-panning — the base `.board` rule must declare
`scroll-snap-type: none` so no browser snap settle animation slows a wheel/trackpad pan.
*/
it("turns scroll snapping off on .board (desktop free-pan)", () => {
const boardBlock = css.match(/\.board\s*\{[^}]*\}/)?.[0];
expect(boardBlock).toBeDefined();
expect(boardBlock).toContain("scroll-snap-type: none");
expect(boardBlock).not.toContain("scroll-snap-type: x");
});
it("contains scroll-snap-align: center on .column", () => {
// The base .column rule should include scroll-snap-align: center
const columnBlockMatch = css.match(/\.column\s*\{[^}]*\}/);
expect(columnBlockMatch).not.toBeNull();
expect(columnBlockMatch![0]).toContain("scroll-snap-align: center");
});
it("contains scroll-padding-inline on .board", () => {
expect(css).toContain("scroll-padding-inline");
});
});
describe("mobile @media (max-width: 768px)", () => {
// Extract the mobile media block for scoped assertions
const mediaStart = css.search(
/@media\s*\([^)]*max-width:\s*768px[^)]*\)[^{]*\{/,
);
const afterMedia = css.slice(mediaStart);
it("contains scroll-snap-type: x proximity", () => {
expect(css).toContain("scroll-snap-type: x proximity");
});
it("contains scroll-snap-align: center (not start)", () => {
expect(afterMedia).toContain("scroll-snap-align: center");
expect(afterMedia).not.toContain("scroll-snap-align: start");
});
it("contains -webkit-overflow-scrolling: touch", () => {
expect(css).toContain("-webkit-overflow-scrolling: touch");
});
it("contains flex-shrink: 0", () => {
expect(css).toContain("flex-shrink: 0");
});
it("contains scroll-padding-inline in the media block", () => {
expect(afterMedia).toContain("scroll-padding-inline");
});
it("scroll-snap rules are inside a @media block", () => {
expect(mediaStart).toBeGreaterThanOrEqual(0);
expect(afterMedia).toContain("scroll-snap-type: x proximity");
expect(afterMedia).toContain("scroll-snap-align: center");
});
});
describe("card touch-action", () => {
it("allows horizontal panning so board swipes work when starting on a card", () => {
const cardBlockMatch = css.match(/\.card\s*\{[^}]*container-name:\s*task-card[^}]*\}/s)
?? css.match(/\.card\s*\{[^}]*touch-action:\s*pan-x pan-y[^}]*\}/s);
expect(cardBlockMatch).not.toBeNull();
expect(cardBlockMatch![0]).toContain("touch-action: pan-x pan-y");
});
it("does not use touch-action: pan-y alone (which blocks horizontal swipes)", () => {
const cardBlockMatch = css.match(/\.card\s*\{[^}]*container-name:\s*task-card[^}]*\}/s)
?? css.match(/\.card\s*\{[^}]*touch-action:\s*pan-x pan-y[^}]*\}/s);
expect(cardBlockMatch).not.toBeNull();
const cardBlock = cardBlockMatch![0];
expect(cardBlock).not.toMatch(/touch-action:\s*pan-y\s*;/);
});
});
});