Skip to content

feat(calendar): infinite horizontal day scroll in week view - #268

Merged
h4yfans merged 1 commit into
mainfrom
stupefied-archimedes-5bb631
Apr 17, 2026
Merged

feat(calendar): infinite horizontal day scroll in week view#268
h4yfans merged 1 commit into
mainfrom
stupefied-archimedes-5bb631

Conversation

@h4yfans

@h4yfans h4yfans commented Apr 17, 2026

Copy link
Copy Markdown
Collaborator

What

Replace the fixed 7-day CSS grid in Calendar Week view with a continuously-scrollable horizontal day strip. Trackpad horizontal swipes (and Shift + mouse wheel) pan the visible range day-by-day; the toolbar month/year label updates live as columns slide. Prev/Next/Today buttons smooth-scroll to their target and share one source of truth with the gesture scroll.

Why

The old week view was a dead-end pager — seven columns, click Next to advance seven days. On a trackpad-centric macOS app users expect to scroll horizontally through time the same way they scroll vertically through hours. "Infinite" horizontal scroll also unifies the gesture + button navigation paths so the header label, fetch range, and scroll position never desync.

How

Core state shift. anchorDate is no longer the primary state — the scroll offset in pixels is. A 2020-01-01 day-index epoch keeps virtualization math simple (all dates become non-negative integer column indices). visibleDayStart = floor(scrollLeft / columnWidth); header label and fetch range derive from it.

Virtualization. @tanstack/react-virtual with horizontal: true renders only the visible columns + overscan over a logically 100-year-wide strip. Column width is (containerWidth − gutterWidth) / 7 tracked via ResizeObserver.

Layout. Mirrored scroll containers — the body owns the 2D scroll, a header strip (overflow-hidden) mirrors its scrollLeft, and the time gutter (overflow-hidden) mirrors its scrollTop. Simpler than sticky-top + sticky-left stacking in a single 2D container and keeps useScrollToCurrentTime working unchanged.

Flicker + scroll-back bug fixes. Two subtle issues surfaced during review:

  1. Hook's scroll listener had onVisibleDayStartChange in its useEffect deps, and the shell passed an inline arrow — every parent re-render unregistered + re-added the listener, losing scroll events mid-gesture → flicker. Fixed by storing the callback in a ref so the effect deps are just [columnWidth].
  2. The scroll handler was calling onVisibleDayStartChange inside a setVisibleDayStart updater function. That breaks React 18 auto-batching — the hook's visibleDayStart update and the page's setAnchorDate could commit in separate renders, and in the intermediate render the anchor-sync effect saw a mismatch and smooth-scrolled back to today. Fixed by calling both as siblings in the same native event handler + adding a lastEmittedAnchorRef in the view so self-initiated anchor updates don't re-trigger the anchor-sync effect.

Data. Week view fetch range widened to [weekStart − 7, weekStart + 14) so the React Query cache keeps ±1 week around the visible range loaded. Single expanded query is simpler than three parallel queries and the cache behavior is the same in practice.

Marquee. useTimeGridMarquee now accepts an optional getColumnElement resolver — the virtualized grid uses a data-day-index attribute lookup so drag-select and the quick-create popover anchor correctly against absolute day indices.

Scrollbar. Body scrollbar hidden via the existing .scrollbar-none utility — functionality unchanged.

Type

  • feat — new feature

Test plan

  • Unit tests added/updated — 6 new tests for dayIndexFromDate / dateFromDayIndex round-trip, DST boundary crossing, negative-index support
  • Integration tests added/updated
  • E2E tests added — calendar-week-scroll.e2e.ts covers scroll right without snap-back, scroll left from a forward position, Today button after scrolling far, and Prev/Next round-trip
  • Manual testing — trackpad horizontal swipe, Shift + mouse wheel, Prev/Next/Today buttons, window resize stability

Verification run:

  • pnpm typecheck:web clean
  • pnpm lint 0 errors (all warnings pre-existing in unrelated files)
  • pnpm test 6918 passed, 1 skipped
  • E2E suite needs a fresh electron-vite build before running (per CLAUDE.md gotchas) — not executed in CI-blocking form from this worktree

Checklist

  • Self-reviewed the diff
  • No hardcoded secrets or credentials
  • Files stay under ~500 LOC
  • Follows immutable data patterns

Continuous day-by-day horizontal scroll replaces the fixed 7-day grid.
Trackpad swipes and Shift+wheel pan the visible range; toolbar label
updates live as columns slide. Prev/Next/Today smooth-scroll to target
and share one source of truth with the gesture-driven scroll offset.

- Horizontal virtualization via @tanstack/react-virtual with a
  2020-01-01 day-index epoch; only visible columns mount
- Ref-backed visible-day callback + dedup ref in the hook so the
  scroll listener never re-registers and setState/notify run as
  siblings in one batched commit — kills scroll-back-to-today race
- lastEmittedAnchorRef in the view guards against self-initiated
  anchor updates triggering a spurious scroll-back via the
  anchor-sync effect
- Mirrored header + time-gutter scroll containers synced to the
  body scroll, avoiding sticky-top/sticky-left stacking contexts
- Fetch window for week view widened to [weekStart-7, weekStart+14)
  for ±1 week prefetch via the existing useCalendarRange query key
- useTimeGridMarquee accepts a getColumnElement resolver so drag
  selection + quick-create popover anchor work against absolute
  day indices in the virtualized grid
- Body scrollbar hidden via existing .scrollbar-none utility
- New e2e tests cover scroll right, scroll left, Today button,
  and Prev/Next button round-trip (no snap-back assertions)
- 6 new unit tests for dayIndexFromDate / dateFromDayIndex
  round-trip, DST boundary, and negative-index support
@h4yfans
h4yfans merged commit 1e872d0 into main Apr 17, 2026
5 checks passed
@h4yfans
h4yfans deleted the stupefied-archimedes-5bb631 branch May 6, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant