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>
84 lines
3.2 KiB
TypeScript
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*;/);
|
|
});
|
|
});
|
|
});
|