Repository navigation
fix: lock each swipe to one axis in the scrolling day and week views - #261
Conversation
The scrolling day, 3-day and week views scrolled the hours and the days in one element on both axes, with a mandatory x snap. Safari on iOS locks no direction in such a scroller, so a swipe down could page the days and a swipe sideways moved the hours. The hours now scroll in the viewport and the days sideways in an element inside it; the header follows the days' scrollLeft. calendar-dnd gains sidewaysScrollContainer, so a drag at the side edges scrolls the days while the top and bottom scroll the hours. Closes #260 Co-Authored-By: Claude Opus 5.5 <[email protected]>
The header followed the days through scroll events, which Safari runs at no more than 60 Hz while it scrolls at 120, so it trailed and froze during a swipe. Its days now move by a transform on a ScrollTimeline of the days' scroller, with scroll events as the fallback. All-day titles start on the first day shown instead of sticking. Also from review: contain sideways overscroll on the viewport, test focus moving into the header and drops through sidewaysScrollContainer, name the test helper's scroller, and list the option in architecture.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Run the header tests with and without scroll timelines, place a bar's title a whole number of days on, and rewrap the README. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Local gate: Review (Claude Opus 5.5, xhigh), two rounds. No open code blockers. Round 1 (5ca2c7b):
Between rounds: on the owner's iPhone, the header visibly trailed and froze during swipes. Scroll events run at about 60 Hz while Safari scrolls at 120. The header now moves by a Round 2 (a83ad6f):
|
Titles placed on the first day shown scrolled away with their bars mid swipe and came back only once the range moved. Each title now has an animation on the days' scroll timeline that holds it at the start of the days on screen, as position: sticky did, with scroll events as the fallback. TimeGridAllDayCells drops stickyLabels, which nothing else used. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Measure every title before writing, from layout offsets its own shift doesn't move, and keep a pin's animation while its title and bar stay put, so a render mid-swipe doesn't restart it. Share one scroll timeline per layout. A dragged bar's copy shows its title at its start. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The drag-copy test was lost in an earlier edit; it now drags a pinned all-day bar into a timed column and checks the copy's title. Kept pins also get their transform rewritten for the scroll-event fallback. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Local gate: Owner's iPhone recording after a83ad6f. The header's days tracked the columns, so the scroll-timeline transform works on the device. Two problems showed, broken down frame by frame:
Review round 3 (1549d12). No blockers.
Review round 4 (0bc8f14). No blockers.
|
… pinned WebKit sends the page's mouse a move at its spot whenever layout changes under it, which the drag took for its own pointer when both had id 1. The drag-copy test now uses another id and waits for the copy, and first checks the title it drags is pinned. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Local gate: Review round 5 (33538dc). No blockers.
|
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Review round 6 (49ca150): no blockers or should-fix findings. The last nit is done in a5450f9: the drag-copy test now asserts that the title has actually moved along its bar ( |
CI's Linux runners sometimes deliver the scroll event a frame after the days move, so the fallback header trails by that frame's distance. The test allows that in the fallback, keeps the timeline at a pixel, and still checks both at rest. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Outcome
Closes #260
Parent epic: #217 (found while checking #220 on a real iPhone)
On iPhone Safari, a swipe in the scrolling day, 3-day and week views now moves one axis, chosen by how it starts: a swipe down never pages the days, and a swipe sideways never moves the hours. The hours scroll in the viewport, and the days scroll sideways in an element inside it, so iOS locks each gesture to one of the two nested scrollers. Before, both axes scrolled in one element with
snap-x snap-mandatory, which iOS doesn't direction-lock, so a mostly vertical swipe's horizontal velocity flipped the page.Contract and scope
@moltenship/calendar-dnd(and@moltenship/calendar): new pointer optionsidewaysScrollContainer, a ref to an element insidescrollContainerthat scrolls sideways in its place at the left and right edges. Defaults toscrollContainer, so other views are unchanged. API report and the docs' options table updated.scrolling-time-grid-view:scrolling-time-grid-viewportnow scrolls only the hours, and a newscrolling-time-grid-dayselement scrolls the days (with the snap points). The header and all-day row are sticky above the days, and their days move by a transform on aScrollTimelineof the days' scroller, inside anoverflow: clipparent. The browser runs that animation with the scroll itself, off the main thread where it can. WithoutScrollTimeline, the header follows scroll events instead. The days element has no scrollbar, and the viewport hasoverscroll-x-contain.position: stickydid.stickydoesn't work under the transform, so each title has its own animation on the same scroll timeline. Drag copies show the title at their start.time-grid-view:TimeGridAllDayCellsdropsstickyLabels, which only this view used.scrolling-axis: a scroll it starts jumps instead of animating when focus is anywhere in the view's grid, not only in the scroller (focus can sit in the header now). Found by the WebKit run.calendar-dnd; registryscrolling-time-grid-view,calendar(scrolling-axis.tsx);architecture.md§13; docs API table.architecture.md§13 records the layout.Behaviour changes to note:
ScrollTimelineand keeps the scroll-event behaviour.main's demo.Acceptance criteria
Evidence
vp checkcalendar-dndunit test forsidewaysScrollContainer; newtools/a11y/tests/scrolling-axis-lock.a11y.tsx(18 tests); existing scrolling, touch, RTL and phone suites pass, with their helpers pointed at the new scrollers.vp run ready)readyruns the SSR fixture.scrollLeft.registry:smokeinready; no new files or dependencies.What the tests cover. The diagonal swipe doesn't reproduce in an automated browser: Chromium rails touch scrolls in the old layout too (a 60/300 px swipe kept the day), and Playwright's WebKit can't drag a finger. So the tests check the invariants instead:
vp run ready: every PR check passed (local, on 49ca150;vp run browsersin WebKit passes too). The header tests run twice, once with scroll timelines and once without (stubbed), in LTR and RTL, in Chromium and WebKit.Architecture invariants
Risk and rollout
sidewaysScrollContaineris optional and additive.🤖 Generated with Claude Code