feat(calendar): infinite horizontal day scroll in week view - #268
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
anchorDateis no longer the primary state — the scroll offset in pixels is. A2020-01-01day-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-virtualwithhorizontal: truerenders only the visible columns + overscan over a logically 100-year-wide strip. Column width is(containerWidth − gutterWidth) / 7tracked viaResizeObserver.Layout. Mirrored scroll containers — the body owns the 2D scroll, a header strip (
overflow-hidden) mirrors itsscrollLeft, and the time gutter (overflow-hidden) mirrors itsscrollTop. Simpler than sticky-top + sticky-left stacking in a single 2D container and keepsuseScrollToCurrentTimeworking unchanged.Flicker + scroll-back bug fixes. Two subtle issues surfaced during review:
onVisibleDayStartChangein itsuseEffectdeps, 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].onVisibleDayStartChangeinside asetVisibleDayStartupdater function. That breaks React 18 auto-batching — the hook'svisibleDayStartupdate and the page'ssetAnchorDatecould 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 alastEmittedAnchorRefin 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.
useTimeGridMarqueenow accepts an optionalgetColumnElementresolver — the virtualized grid uses adata-day-indexattribute lookup so drag-select and the quick-create popover anchor correctly against absolute day indices.Scrollbar. Body scrollbar hidden via the existing
.scrollbar-noneutility — functionality unchanged.Type
feat— new featureTest plan
dayIndexFromDate/dateFromDayIndexround-trip, DST boundary crossing, negative-index supportcalendar-week-scroll.e2e.tscovers scroll right without snap-back, scroll left from a forward position, Today button after scrolling far, and Prev/Next round-tripVerification run:
pnpm typecheck:webcleanpnpm lint0 errors (all warnings pre-existing in unrelated files)pnpm test6918 passed, 1 skippedChecklist