Repository navigation
fix(mobile): make swipe back smoother - #1793
Conversation
Reviewer's GuideMigrates mobile edge back navigation from React-driven pointer gestures to a compositor-friendly native scroll-snap pager, adding precise gesture routing for nested scrolling and taps, settle-time accessibility handling, mouse support, documentation, and comprehensive updated tests. Sequence diagram for a mobile edge swipe popsequenceDiagram
actor User
participant Edge as EdgeStrip
participant Pager as NativePager
participant Settle as watchScrollSettle
participant Below as RevealedPage
participant Router
User->>Edge: Start and drag horizontally
Edge->>Pager: Allow native horizontal scroll
Pager->>Pager: Snap to lower page
Pager->>Settle: Report settled scroll
Settle->>Below: Remove inert and aria-hidden
Settle->>Router: onPop()
Router-->>Pager: Re-render stack
Pager->>Pager: Pin scrollLeft to last page
Flow diagram for mobile gesture routingflowchart TD
Start[Gesture or tap in mobile screen]
Edge{Starts in left 20px edge strip?}
Horizontal{Inside data-scroll-x scroller?}
Vertical[Allow vertical pan]
Pager[Route horizontal drag to native pager]
Tap[Forward tap through strip]
Target[Focus field, click control, or place editor caret]
Start --> Edge
Edge -->|no| Vertical
Edge -->|yes| Horizontal
Horizontal -->|yes| Vertical
Horizontal -->|no horizontal drag| Pager
Horizontal -->|tap| Tap
Tap --> Target
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
8731ad0 to
0fb1dc9
Compare
0fb1dc9 to
c69bd03
Compare
c69bd03 to
9764ce2
Compare
9764ce2 to
cb9e434
Compare
# Conflicts: # apps/desktop/src/lib/scroll-settle.ts
# Conflicts: # pnpm-lock.yaml
# Conflicts: # pnpm-lock.yaml
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
# Conflicts: # apps/desktop/src/mobile/mobile-screen.test.tsx
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="apps/desktop/src/mobile/mobile-screen.test.tsx" line_range="979-983" />
<code_context>
- const stack = view.container.querySelector('.mobile-stack')!
- const card = stackLayers(view).at(-1)!
+ const scroller = pager(view)
+ await mouseDragFromStrip(view, 60)
+ await waitFor(() => expect(scroller.scrollLeft).toBe(scroller.clientWidth))
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ expect(view.getByRole('heading').element().textContent).toBe('Edit note')
+ expect(stackLayers(view)).toHaveLength(2)
+ })
</code_context>
<issue_to_address>
**Ignored mouse drags pass snap-back test**
When the mouse-drag path ignores a short drag but does not forward its click, `it('snaps a short mouse drag back without passing its click to the back button')` only checks that the note remains visible and the pager ends at its starting offset. A handler that ignores the drag but suppresses its click passes without ever scrolling or snapping back.
Assert that the pager moves during the drag before verifying that it returns to the last page.
</issue_to_address>There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
ocavuebot
left a comment
There was a problem hiding this comment.
Independent review by Claude (model ID: claude-opus-5-5) at 8b21d18e282d43fd88d473bb79532b22bc139437.
Summary:
- Simplification (inline,
use-back-pager.ts:41):pageCountis not needed. "Last page" is the end of the scroll range. - Possible logic bug (inline,
use-back-pager.ts:116): the resize re-pin ignores a finger that is down. The drawer in #1794 guards this case. - Question:
watchScrollSettlecounts a wheel as a user scroll, andtouch-actiondoes not apply to wheel input. So on an iPad with a trackpad (or in the browser preview), a two-finger horizontal scroll anywhere on a stacked screen moves the pager, and a long one pops. The PR text says only the edge strip starts a back swipe. If this is intended (iPadOS has a trackpad back swipe), please write it in the PR text. If not, ignore a wheel for the pop. - Edge case (no action needed if accepted):
forwardStripTaponly focuses aninput. A checkbox or radioinputunder the 20px strip gets focus but does not toggle. Long press (text selection, link preview) is also lost under the strip. Both are probably fine trade-offs, but the PR text does not list them.
ocavue
left a comment
There was a problem hiding this comment.
AI model ID: gpt-6 (Codex).
Reviewed commit: 8b21d18e282d43fd88d473bb79532b22bc139437.
Independent source review of the pager, stack, CSS, and settlement helper. No local tests or device checks were run.
[P2, known behavior trade-off] The edge strip removes vertical scrolling from the leftmost 20px. At apps/desktop/src/mobile/mobile-stack.tsx:387, the full-height strip receives the gesture and allows only horizontal panning. A vertical drag starting there cannot scroll a long note or Settings. I read the existing response: changing it to pan-x pan-y alone cannot fix this, because the content scroller is a sibling. This is a real UX regression that the current architecture accepts, not a missing defensive guard. Please make that limitation explicit in the PR's user-facing behavior and manual checks. If preserving edge vertical scrolling is required, the hit region needs to participate in the content scroller's ancestor chain, or needs deliberate gesture forwarding.
Simplification recommendation: keep the synthetic tap forwarding narrowly scoped. It already reconstructs clicks, field focus, and editor caret placement, but cannot preserve the full native pointer interaction. Avoid extending it into a general input dispatcher with more target-specific fallbacks. Resolve the hit-region layout first if more edge interactions must work.
I found no additional high-confidence navigation-state bug in the current diff. The once-only pop guard and programmatic-scroll rejection serve concrete purposes; I would retain them.
|
Before/after recording with Chrome CPU throttling at 20x ( Gestures (CDP touch events, 390x844 viewport), two rounds:
a3_cpu20_base_vs_pr.mp4 |
# Conflicts: # apps/desktop/src/mobile/mobile-stack.tsx
Makes the native scroll-snap pager the mobile edge back swipe and deletes the pointer-driven one. There is no flag and no second path.
What changed
MobileStackrenders its layers as pages of ascroll-snap-type: x mandatoryscroller. Every page isposition: sticky; left: 0and the current page is last, so a swipe moves only the current card while the page below stays put.useBackPagerpinsscrollLeftto the last page before paint whenever the page list changes, and on resize.touch-action: pan-ythrough a zero-specificity rule in@layer base. A 20px full-height edge strip (data-pager-edge,touch-action: pan-x) is the only place where a horizontal drag reaches the pager. Horizontal scrollers carrydata-scroll-x(meowdown sets it on code blocks and math and Mermaid previews, feat: mark horizontal scrollers withdata-scroll-xprosekit/meowdown#752, released in 0.82.3; swipe rows set it too), and they and everything inside them opt back intoautowithoverscroll-behavior-x: contain.elementsFromPointandcaretRangeFromPointwith the strip's pointer events off): fields focus, controls click, and the editor gets a caret.watchScrollSettlecommits the pop when a user scroll settles atscrollLeft <= 1. The revealed page dropsinertandaria-hiddenat once, before React re-renders it as current. A scroll that no finger, wheel, or mouse drag started pins the pager back.dragHandle). A plain click on the strip still forwards; the click that ends a drag does not.linkOpacityToScroll. Button back, wiki-link push, and other programmatic push and pop keep the existing CSS keyframes.position: fixedelement inside a stacked screen (it would stay put while the card moves), anddata-scroll-xfor any horizontal scroller.What was deleted
mobile/use-back-swipe.tsand its test (the pointer hook with its non-passivetouchmoveblocker, per-movesetState, and settle transition).mobile-stack.tsx: the dragging and settling styles, the drag scrim, and thetransitionendlistener.lib/pointer-drag.tsstays (the image dismiss drag uses it).What users feel
Before and after
Chromium 156.0.8078.4 (Playwright, headless,
isMobile, 390x844), 4x CPU throttle, dev build, n = 7 per row after 1 warmup. Baseb83dca13(old hook; the old path is byte-identical at the PR's base26f9058a), PR9764ce22. Real CDPInput.dispatchTouchEventgestures from (8, 420): pop = slow drag to x 330 and release; cancel = drag to x 100, hold 300ms, release; fling = 5 moves to x 150, 8ms apart. Light note:notes/sarah-chen.md(seed). Heavy note: a generated 3000-line note. "During" is touchStart to touchEnd; "after" is the 1.5s after touchEnd. LoAF max is the max over reps; other columns are p50. Script and task time come from CDPPerformance.getMetricsdeltas over the drag, divided by the deliveredtouchmovecount.p95FrameMsandhitchTimeRatiouse the perf HUD formula on rAF times from touchStart to touchEnd + 500ms. Correct means the expected outcome happened exactly once (oneback()for a pop, none for a cancel).How to test by hand
On an iPhone (or iPad) build, open a note from Daily:
?platform=ios): press on the left 20px and drag right: it goes back. Click the strip over the Back button: it goes back once. Drag on note text: it selects text and never swipes.Written by Claude Opus 5.5.
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by Sourcery
Adopt native scroll-snap paging for mobile edge back navigation to improve gesture responsiveness and preserve nested scrolling and underlying interactions.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests:
Chores:
Summary by CodeRabbit