Repository navigation
feat(core): a pan follows a trackpad where its owner allows it, never a mouse wheel; a pinch takes a ctrl-wheel (spec:AST-094 FR43) - #8676
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
…allows; the wheel latch; FR43) into uikit/c25-toast-every-pointer
…ft: the base; right: this change), Chromium desktop
There was a problem hiding this comment.
This pull request is on hold for an owner decision: spec:AST-094 on main has no FR43, so neither the wheel and ctrl-wheel input nor the every-pointer gesture host it relies on has a governing spec yet (today's FR19 keeps recognizers off fine pointers). An engineering owner needs to decide whether this waits until #8646 lands FR43 with its wheel questions answered, or to accept now the readings it takes: a pan opts in through allowedScrollTypes, every pinch takes a ctrl-wheel at exp(-deltaY / 100), and a wheel gesture ends 100 ms after its last wheel. They record that decision by commenting /approve-spec followed by this pull request's full head SHA, and the author may do so if they are an engineering owner. Whatever that decision, these changes are needed:
- Reduce the diff to this change: about 28,700 of its 30,372 authored lines are the open #8665 stack (land it first or retarget onto it), and the
astryxStylexfix belongs in its own pull request. - Make
scrollsByinGestureHost.tsRTL-aware: it comparesMath.abs(scrollLeft)against LTR bounds, so in RTL an opted-in pan takes the wheels a horizontal scroller should get. - Resolve the wheel's target with
landedOnso that scrollers inside a shadow tree count, and updateuseLongPress.doc.mjs, which still documents the per-axis rule.
[Automated review]
| const position = Math.abs( | ||
| isHorizontal ? element.scrollLeft : element.scrollTop, | ||
| ); | ||
| return delta > 0 ? position < room - 1 : position > 0; |
There was a problem hiding this comment.
In an RTL scroller, scrollLeft runs from 0 at the start edge down to -room, so comparing Math.abs(scrollLeft) against LTR bounds inverts both edges. At the start edge, a wheel to the right (deltaX > 0) reads as scrollable, so the pan never takes it. A wheel into the content (deltaX < 0) reads as blocked, so an opted-in pan takes and prevents it, and the list never scrolls. canMoveX in nativeScroll.ts already handles this with entry.isRtl, and isRtlElement is imported at the top of this file:
const value = isHorizontal ? element.scrollLeft : element.scrollTop;
const [min, max] =
isHorizontal && isRtlElement(element as HTMLElement) ? [-room, 0] : [0, room];
return delta > 0 ? value < max - 1 : value > min;An RTL case beside the scroller test in wheel.test.ts would cover it.
| const isHorizontal = Math.abs(dx) > Math.abs(dy); | ||
| // Every scroller under the pointer, the page included, as a finger's | ||
| // probe reads them, without touch-action, which a wheel ignores. | ||
| const scroll = createNativeScrollProbe(wheel.target, { |
There was a problem hiding this comment.
Seen from a listener outside an open shadow tree, wheel.target is the shadow host. So this probe, the walk below and scrollerTakes never see a scroller inside that tree, and an opted-in pan around a web component with its own scroller takes the wheels meant for it. The pointer and touch paths already resolve this with landedOn(event); starting beginWheel from landedOn(wheel) would match them.
| ) { | ||
| // A distance, as UIKit measures `allowableMovement`: a (7, 7) drift | ||
| // keeps the press, a (7.2, 7.2) one ends it. | ||
| if (Math.hypot(point.x - from.x, point.y - from.y) > moveCancelPx) { |
There was a problem hiding this comment.
This makes moveCancelPx a distance, but the hook's public docs in useLongPress.doc.mjs (lines 48 and 97) still say "Movement past this distance on either axis cancels the press". Could they be updated to the distance rule so the docs match the hook?
PR Analysis Report🔎 Preview ·
|
There was a problem hiding this comment.
This pull request is on hold for an owner decision: spec:AST-094 on main has no FR43, so the wheel and ctrl-wheel input, and the every-pointer gesture host it relies on, have no governing spec yet (today's FR19 keeps recognizers off fine pointers). An engineering owner needs to decide whether this waits until #8646 lands FR43 with its wheel questions answered, or to accept now the readings it takes: a pan opts in through allowedScrollTypes, every pinch takes a ctrl-wheel at exp(-deltaY / 100), and a wheel gesture ends 100 ms after its last wheel while following macOS momentum; they record that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever that decision, these changes are needed:
- Reduce the diff to this change: about 29,000 of its 30,737 authored lines are the open #8665 stack (land it first or retarget onto it), and the
astryxStylexfix belongs in its own pull request. - Mirror the edge tolerance in
scrollsBy(GestureHost.ts): at the end of an RTL list, where Chromium stops a pixel short ofscrollWidth - clientWidth, it reads the list as still scrollable, so an opted-in pan never gets the wheel there; an RTL end-edge test would cover it. - Reconcile
.changeset/gesture-host-owner.mdand the host's header comment with the every-pointer behaviour that ships.
[Automated review]
| const min = isRtl ? -room : 0; | ||
| const max = isRtl ? 0 : room; | ||
| const position = isHorizontal ? element.scrollLeft : element.scrollTop; | ||
| return delta > 0 ? position < max - 1 : position > min; |
There was a problem hiding this comment.
In RTL the end edge is min, which gets no tolerance here, while an LTR list's end (max) gets 1 px. Chromium stops an RTL list a pixel short of scrollWidth - clientWidth (a 200 px list of 300.6 px content ends at scrollLeft -100 with a computed room of 101), so at that list's end this reads as still scrollable and an opted-in pan never gets the wheel, while the mirrored LTR list reads as at its edge. Mirroring the tolerance reads both correctly:
return delta > 0 ? position < max - 1 : position > min + 1;| * once that press's contacts end (FR24). There a finger's claim rests on | ||
| * `touch-action`: an element's blocking `touchmove` listener stays a | ||
| * coarse pointer's, so the element keeps its one listener. |
There was a problem hiding this comment.
This says an element's blocking touchmove listener stays a coarse pointer's, but refresh (lines 1015-1031) adds it for every pointer once a recognizer claims while moving. Could the comment follow the code?
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: the gesture host becomes the one document-level touch owner: the finger count, touch gates, and a kept press for continuous-tracking controls, whose element gets `touch-action: manipulation` where the pointer is coarse. A scroll lock's page hold and the finger-scroll watch keep their own listeners and ask the host only about the touches it owns. A second finger fails a one-finger pan that has not claimed, so the page can pinch. A scroll-edge pan holds a lone finger from the first move that pulls away from an edge every scroller under it is at, so those moves are cancelled before it begins; with a second finger anywhere on the screen the page keeps its pinch. Where the primary pointer is fine, the host attaches nothing and registers no listener, following the pointer when it changes; a recognizer that reads each event's pointer itself opts out with `perEventPointer`, and a press tracker with it paints a finger's press feedback only, with no control event and no mouse or pen press. A press tracker whose hold acts (a stepper's hold-repeat) can bring a selection lock for its presses, as a recognizer can. Internal, for the touch components that adopt them; not exported from the package entry point. |
There was a problem hiding this comment.
This says that where the primary pointer is fine the host attaches nothing and registers no listener, while gestures-every-pointer.md in the same release says gestures attach for every pointer and that perEventPointer does nothing. [chore] entries publish under Other Changes, so both would reach the changelog; could this one be amended to match?
There was a problem hiding this comment.
This pull request waits on an engineering owner's decision: it carries three spec sentences from #8360 that the current records do not state yet (spec:AST-094 FR11's tap on moving content, FR19's document touchmove during compositor motion, and architecture:react-component-runtime INV13's shared window listener). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. These fixes are needed whatever the decision:
- Land #8360 first or retarget onto it, so the diff is the wheel change (19,564 changed lines today, 17,100 of them from #8360).
- Keep a followed pan's post-lift momentum prevented, end a stream a scroller keeps on FR43's 180 or 220 ms pause rather than 100 ms, and keep ctrl-wheel pinches out of the trackpad splitter.
- Date the release of a pan detached or disabled mid-swipe at its last wheel, not 180–220 ms later, so the next press keeps its own clock (FR16).
- Route the wheel latch and the motion
touchmovethroughsubscribeInput, letuseFingerScrollcalluseAttachment, retitle to the trackpad-only pan, and update the comments that still call the ends provisional.
[Automated review]
| ) { | ||
| // Every recognizer that took it has left (detached, disabled or | ||
| // cancelled): the rest of the gesture is the page's. | ||
| this.endWheel(); |
There was a problem hiding this comment.
After the lift ends the pan (:2202-2212), its recognizer is back in possible, so the next momentum wheel fails wheelHasTaker and reaches this branch: the rest of the stream becomes a kept gesture and is never prevented, and the f.lifted branch at :2199 never runs. A fling of four 10 px wheels then shrinking ones, 16 ms apart, left all twelve wheels after the 112 ms lift unprevented. FR43: “momentum after the lift never moves a surface or continues a gesture”. Could a lifted follow stay latched and prevented until a splitter boundary or its end timer, with defaultPrevented asserted on post-lift momentum in wheel.test.ts?
| gesture.timer = this.clock.setTimeout(() => { | ||
| this.segmenter.end(); | ||
| this.endWheel(); | ||
| }, gesture.follow?.endMs ?? WHEEL_END_MS); |
There was a problem hiding this comment.
A stream the page keeps has no follow, so this timer (and the stale check at :2137) ends it 100 ms after its last wheel, and the next wheel lets beginWheel decide again. A slow vertical drag a list kept for three wheels, paused 130 ms at the list's edge, then had its next three wheels prevented and taken by the pan. FR43 ends a moving gesture only after 180 ms (vertical) or 220 ms (horizontal). Could a kept trackpad stream use the axis's splitter bound too?
| horizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS, | ||
| ); | ||
| this.wheelSegment = seg; | ||
| if (seg.boundary != null) { |
There was a problem hiding this comment.
Every wheel reaches the splitter here, a ctrl-wheel pinch's included, so a pinch that slows for four deltas reads as momentum and a reversal or a 50 ms rest ends it. A pinch of −3, −4, −5, −4, −3, −2.5, −2, −1.5 then +1…+4 ended at the reversal at scale 1.189 and a second pinch began from 1; resting 60 ms ended it too. FR43: “A pinch takes a ctrl-wheel and ends 100 ms after its last event”; DEC-24 gives the splitter to every pan. Could the splitter read only a pan's (non-ctrl) stream?
| // move that input's times too (FR16). | ||
| const end = gesture.last + (gesture.follow?.endMs ?? WHEEL_END_MS); | ||
| const time = | ||
| endTime == null ? end : Math.max(gesture.last, Math.min(end, endTime)); |
There was a problem hiding this comment.
The detach path (:894-903) and the taker-left path (:2149) call endWheel() with no time, so this stamps the release at the last wheel plus 180 or 220 ms and moves the arena's clock there. After a wheel at 0 ms and a detach at 10 ms the arena read 180 ms, and a long press landing at 20 ms fired at 680 ms instead of 520 ms (FR16). Could those two paths pass the last wheel's (or the current wheel's) time?
| /** Route every wheel on the page to the live wheel gesture, from its claim. */ | ||
| private latchWheel(): void { | ||
| if (!this.isWheelLatched) { | ||
| this.doc.addEventListener('wheel', this.onWheel, { |
There was a problem hiding this comment.
This is a document listener outside subscribeInput (utils/inputStream.ts, “the one entry for document-wide input listeners in core”), which the host's other page input uses and this diff's INV13 sentence names. Could the latch subscribe with subscribeInput(view, 'wheel', this.onWheel, {cancels: true}) and unsubscribe in endWheel?
| return true; | ||
| this.isBlockingForMovers = needed; | ||
| if (needed) { | ||
| this.doc.addEventListener('touchmove', this.onBlockingTouchMove, { |
There was a problem hiding this comment.
The compositor-motion touchmove is also a document listener beside the shared input stream; pageHold.ts routes its cancelling touchmove through subscribeInput(…, {cancels: true}). Could this go the same way, so an app's window-capture stopPropagation affects all of the host's input alike?
| const attachedRef = useRef<{node: Node; detach: () => void} | null>(null); | ||
| // Follows the node on every commit: a ref that now points at another | ||
| // element moves the watch to it. | ||
| useIsomorphicLayoutEffect(() => { |
There was a problem hiding this comment.
These two layout Effects (here and :70-76) copy useAttachment (useAttachment.ts:80-98: compare the node, release the old attachment, attach the new one, release at unmount). Could useAttachment take a Node or a document, so this hook makes that one call and a node-replacement fix reaches both?
|
|
||
| /** | ||
| * How long after its last event a wheel gesture ends, ms: the web reports no | ||
| * wheel phases, so a quiet wheel is a lifted one. Provisional (owner |
There was a problem hiding this comment.
These ends are no longer provisional: current FR43 and DEC-24 state the 180 and 220 ms pauses and the pinch's 100 ms end. Could this cite them, and could onWheel's (:2117-2119) and endWheel's (:2470-2472) comments and wheel.test.ts:10-11 describe the 180 and 220 ms ends and the still release?
There was a problem hiding this comment.
This pull request waits on an engineering owner's decision: it carries three spec sentences from #8360 that the current records do not state yet (spec:AST-094 FR11's tap on moving content, FR19's document touchmove during compositor motion with its verification row, and architecture:react-component-runtime INV13's shared window listener). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner. These fixes are needed whatever the decision:
- Land #8360 first or retarget onto it, so the diff is the wheel change (19,585 changed lines today, 17,121 of them from #8360).
- Keep a followed pan's post-lift momentum prevented, end a stream a scroller keeps on FR43's 180 or 220 ms pause rather than 100 ms, and keep ctrl-wheel pinches out of the trackpad splitter.
- Date the release of a pan detached or disabled mid-swipe at its last wheel, not 180–220 ms later, so the next press keeps its own clock (FR16).
- Route the wheel latch through
subscribeInput, letuseFingerScrollcalluseAttachment, retitle to the trackpad-only pan, and update the comments that still call the ends provisional.
[Automated review]
There was a problem hiding this comment.
Changes are needed: the wheel path follows spec:AST-094 FR43 and DEC-24 in shape, but five behaviours on this head contradict current spec, each reproducible with a short jsdom case.
- Keep a followed pan's post-lift momentum prevented; today every momentum wheel after the lift reaches the page (FR43).
- End a trackpad stream a scroller keeps on FR43's 180 or 220 ms pause rather than 100 ms, and keep ctrl-wheel pinches out of the trackpad splitter so they end 100 ms after their last wheel.
- Date the release of a pan detached or disabled mid-swipe at its last wheel, so a press right after keeps its own clock (FR16).
- Route the wheel latch through
subscribeInputasarchitecture:react-component-runtimeINV13 requires, and callnativeScroll.ts's edge test instead of the copiedscrollsBy. - Retitle to the trackpad-only pan this ships, declare the 2,476-line change a behavior-module PR in the description, and update the comments that still call the wheel ends provisional.
[Automated review]
| if ( | ||
| gesture != null && | ||
| gesture.ids.length > 0 && | ||
| !this.wheelHasTaker(gesture) | ||
| ) { | ||
| // Every recognizer that took it has left (detached, disabled or | ||
| // cancelled): the rest of the gesture is the page's. | ||
| this.endWheel(); |
There was a problem hiding this comment.
After the lift ends the pan (:2210-2221), its recognizer is back in possible, so the next momentum wheel fails wheelHasTaker and lands here: the rest of the stream becomes a kept gesture and is never prevented, and the f.lifted branch at :2207 never runs. A fling of four 10 px wheels and then shrinking ones, 16 ms apart, leaves all twelve wheels after the 112 ms lift unprevented. FR43: “momentum after the lift never moves a surface or continues a gesture”. Could a lifted follow stay latched and prevented until a splitter boundary or its end timer, with defaultPrevented asserted on post-lift momentum?
| gesture.timer = this.clock.setTimeout(() => { | ||
| this.segmenter.end(); | ||
| this.endWheel(); | ||
| }, gesture.follow?.endMs ?? WHEEL_END_MS); |
There was a problem hiding this comment.
A stream the page keeps has no follow, so this timer (and the stale check at :2145) ends it 100 ms after its last wheel and resets the splitter; the next wheel lets beginWheel decide again. A slow vertical drag a list kept for three wheels, paused 130 ms at the list's end, then had its next three wheels prevented and taken by the pan. FR43 ends a moving gesture only after 180 ms (vertical) or 220 ms (horizontal). Could a kept trackpad stream use the axis's bound too?
| horizontal ? WHEEL_FOLLOW_END_MS : WHEEL_FOLLOW_END_V_MS, | ||
| ); | ||
| this.wheelSegment = seg; | ||
| if (seg.boundary != null) { |
There was a problem hiding this comment.
Every wheel reaches the splitter here, a ctrl-wheel pinch's included, so a pinch that slows for four deltas reads as momentum and a reversal or a 50 ms rest ends it. A pinch of −3, −4, −5, −4, −3, −2.5, −2, −1.5 then +1…+4 ends at the reversal at scale 1.189, and a second pinch begins from 1; resting 60 ms ends it too. FR43: “A pinch takes a ctrl-wheel and ends 100 ms after its last event”. Could the splitter read only a pan's (non-ctrl) stream?
| // move that input's times too (FR16). | ||
| const end = gesture.last + (gesture.follow?.endMs ?? WHEEL_END_MS); | ||
| const time = | ||
| endTime == null ? end : Math.max(gesture.last, Math.min(end, endTime)); |
There was a problem hiding this comment.
The detach path (:897-903) and the taker-left path (:2157) call endWheel() with no time, so this stamps the release at the last wheel plus 180 or 220 ms and moves the arena's clock there. After a wheel at 0 ms and a detach (or a disable) at 10 ms, a long press landing at 20 ms fires at 680 ms instead of 520 ms (FR16). Could those two paths pass the last wheel's time?
| /** Route every wheel on the page to the live wheel gesture, from its claim. */ | ||
| private latchWheel(): void { | ||
| if (!this.isWheelLatched) { | ||
| this.doc.addEventListener('wheel', this.onWheel, { |
There was a problem hiding this comment.
This is a document listener outside subscribeInput (utils/inputStream.ts), while architecture:react-component-runtime INV13 says the gesture host hears document-wide input through one capture listener per event type on the window, before any document listener. An app's window-capture stopPropagation hides a latched wheel from this listener, so it scrolls the page. Could the latch subscribe with subscribeInput(view, 'wheel', this.onWheel, {cancels: true}) and unsubscribe in endWheel?
| } | ||
|
|
||
| /** Whether `element` scrolls natively by `delta` along one axis now. */ | ||
| function scrollsBy( |
There was a problem hiding this comment.
This restates canMoveX/canMoveY in nativeScroll.ts (overflow, room, RTL range, edge tolerance) with different numbers: 1 px at the ends and any room above 0 here, 0.5 px and more than 1 px of room there. Both feed beginWheel's decision (:2357-2359), and a finger reads the probe's, so a scroller 0.5 to 1 px from its end reads differently to a finger and a trackpad. Could nativeScroll.ts export the per-element edge test (or let the probe stop at a boundary) and scrollerTakes call it?
…trl-wheel (spec:AST-094 FR43) meta#8676's reviewed change, rebuilt as one commit on meta#8360's head: a wheel gesture drives the recognizers that allow its scroll type as a contact moving with the content, a pan along its own axis once every scroller between it and the page is at its edge, and a ctrl-wheel drives a pinch. A screen-edge pan stays a finger's.
…ain input reads as a mouse wheel, and a pan can take a swipe as one step (spec:AST-094 FR43) The round-4 trackpad behaviors, as a draft for the owner to try each one: - wheelSegmenter.ts splits the wheel stream into gestures and reads a trackpad's lift: once a run of shrinking deltas has lost a fifth of its size, momentum began. A pan the host follows ends there at the fingers' travel, released at their last move with their speed, and the momentum after it never moves the pan. Fingers that stop release still, 180 ms (vertical) or 220 ms (horizontal) after their last wheel. - Input the deltas cannot place reads as a mouse wheel: a delta a hair off a whole pixel, a scroll with no legacy wheelDelta (an engine that sends none), a legacy wheelDelta that is not three times the delta, or a 120 tick over a 40 px delta. - A pan created with wheelStep takes a trackpad swipe as one step: a short flick once the swipe travels 24 px, a slow swipe's travel carried across pauses, its slow tail absorbed, and one undo back to rest. A mouse wheel's gesture and a pinch keep their 100 ms end. In Chromium the harness's wheels arrive as notches (wheelDeltaY -120 whatever the delta), which now read as a mouse wheel, so the two trackpad-only browser cases run in WebKit, whose driver sends a trackpad's precise deltas. The lift's release is stamped at the fingers' last move: stamped at the lift, three held wheels later, it read as a stop and lost the fling.
…speed until the splitter decides, so it never stalls before a lift (spec:AST-094 FR43) The splitter reads a lift only after four shrinking deltas, so a pan that held them stood still for that stretch (about 64 to 130 ms) and then let go at the fingers' speed: a stop, then a go. Through the undecided stretch the pan now moves on from where it last was at the fingers' last speed, never past what that speed projects for the time since. At the lift it keeps that travel and lets go there, still moving at that speed, and nothing more moves. When the fingers carry on, their moves absorb what the stretch showed past their travel or short of it, half a move at a time, so the pan never steps back and ends on their travel; fingers that stop without a lift end on their travel where it runs ahead.
…t, and never a mouse wheel (spec:AST-094 FR43, DEC-24) The merged record decides it: a trackpad's two-finger scroll is direct manipulation, so a pan follows it where its record allows it; a mouse wheel's notches are a scrolling control, so a pan takes none, and a scroll without the trackpad signature, Shift+wheel included, counts as a wheel's. - A pan's allowedScrollTypes takes only 'continuous', and none by default, as UIKit's allowedScrollTypesMask: the sheet, the pager and swipe rows opt in. The host never feeds a wheel's notches to a pan, whatever a caller passes. ScrollType keeps 'discrete' for the pinch, which takes a ctrl-wheel from a trackpad or a mouse. - A scroll with Shift held reads as a mouse wheel's. - wheelStep goes: DEC-24 rejects a discrete step per trackpad swipe for rows and paging, and nothing used it. - The splitter's verification streams: ten swipes that each start in the last one's momentum, ten whose momentum runs out, and a slow drag with 150 ms pauses count ten, ten and one gestures.
After a trackpad's lift released the pan, the pan was back in possible, so the next momentum wheel read as every taker gone and the rest of the stream went to the page unprevented. A lifted follow now stays latched and prevented until the splitter's next gesture or the end timer, and moves nothing (spec:AST-094 FR43). A fling of four 10 px wheels and sixteen shrinking ones 16 ms apart left all twelve wheels after the 112 ms lift unprevented before; they are prevented now, and the fingers' next swipe is a new gesture. In WebKit, the trackpad proof now also checks that no wheel of the stream reached the page, the momentum after the lift included; it saw four after the lift before. The latch never strands the page: it lets go with the stream's end window (220 ms after its last wheel), after which a wheel is decided afresh, and when the pan it released is detached. A case guards each.
…nd a ctrl-wheel pinch stays out of the splitter A trackpad stream the page or a scroller kept had no follow, so it ended 100 ms after its last wheel and the next wheel was decided afresh: a slow vertical drag a list kept for three wheels, resting 130 ms at the list's end, had its next three wheels taken by the pan. Every wheel gesture now carries its own end window: a trackpad pan stream the bound for its axis (180 ms vertical, 220 ms horizontal) whoever keeps it, and 100 ms for a pinch or a mouse wheel's notches (spec:AST-094 FR43). Every wheel also reached the trackpad splitter, a ctrl-wheel pinch's included, so a pinch that slowed read as momentum, and a reversal or a 50 ms rest split it in two. The splitter now reads a pan's stream only; a pinch ends 100 ms after its last wheel. Both cases failed before. The right-to-left scroller case takes its second stream after the 220 ms bound, as the bound now applies to the stream the scroller kept.
…ast wheel The detach path and the taker-left path ended the wheel gesture with no time, so its release was stamped at the last wheel plus the end window (180 or 220 ms) and moved the arena's clock there: after a wheel at 0 ms and a detach, or a disable followed by the fingers' next wheel, a long press landing at 20 ms fired at 680 ms instead of 520 ms (spec:AST-094 FR16). Both paths now stamp the release at the last wheel. A detached and a disabled case failed before.
…stream, and a scroller's ends read as a finger's
The latch was a document capture listener outside subscribeInput, so an
app's window-capture stopPropagation of a wheel that left the moved element
hid it from the host and the page scrolled. It now subscribes to the
window's wheel stream with {cancels: true}, as the page hold does, and
leaves it when the gesture ends (architecture:react-component-runtime
INV13).
The wheel path also restated nativeScroll.ts's edge test with other numbers
(1 px at the ends, any room above 0), so a scroller 0.5 to 1 px short of its
end read as at its end to a trackpad and as scrollable to a finger. The
wheel now calls nativeScroll's canElementScroll, the probe's own per-element
test. The right-to-left case uses the measured end instead: WebKit scrolls
a fractional scroller a pixel past its reported overflow, which both tests
read as its end; Chromium's end is the reported overflow (neither stops a
pixel short of it). The latch, its window listener at a detach, and the
0.75 px edge cases failed before.
…them The comments on the three wheel end windows still called them provisional, pending an owner question; spec:AST-094 FR43 and DEC-24 now state them (100 ms for a pinch or a mouse wheel's notches, 180 ms vertical and 220 ms horizontal for a trackpad scroll, whoever keeps it). No code changes.
…s it is, in short sentences, without new record citations Main's no-comment-provenance rule refuses a record or claim citation in a new or changed source comment, and source comments use short, one-topic sentences. The comments this change added or changed in the host's wheel path, the stream splitter, the recognizers' scroll options, native scrolling's edge test and the scroll types cited the record they follow, and many ran long. They now keep only the reason, in short sentences. The screen-edge pan keeps the one citation that is its only link to the record, and the provenance baseline drops the four citation entries the other edited comments carried. The README and the wheel handler's doc now describe the latch as the shared input stream's window-capture listener, and the end window as the stream's, whoever keeps it. Comments and docs only.
|
All five are fixed, one commit each, red first, on main 1667ea8. The feature's four commits have the same changed lines. A last commit, comments and docs only, drops the record citations from the comments this change added or edited and shortens their sentences.
|
A trackpad pan has no lift, so a window blur or a hidden page left it mid-gesture: its end timer released it later, and its latch kept taking the page's wheels until then. Under a fine pointer the host is not installed during a wheel gesture, so its own blur and visibilitychange listeners were absent. The latch now listens for both while it holds a gesture, and the host's cancel of every touch also cancels a wheel gesture a recognizer still drives. The pan reads cancelled, never ended, and releases without momentum, as a cancelled touch does; the next wheel is decided afresh. A pan its lift already released keeps its momentum latched until the stream ends. spec:AST-094 FR43 says so in one sentence, and its Verification row lists the case. A blur, a hidden page, and a blur under a coarse primary pointer each failed before.
There was a problem hiding this comment.
Changes requested: the wheel path now follows spec:AST-094 FR43, DEC-24 and FR16, and the earlier momentum, end-window, pinch, release-stamp and latch findings are fixed with regression cases. One item remains: panTakesMove in GestureHost.ts keeps its own copy of the pan's direction rule and ignores axisRatio, so a pan with a non-default axisRatio reads a trackpad's direction differently from a finger's. Please call the pan's own rule (for example through recognizerInternals, as family is) and add a wheel case with a non-default axisRatio; the two comment notes inline are optional.
[Automated review]
| } | ||
|
|
||
| /** Whether a pan's `direction` takes a contact moving by (cx, cy). */ | ||
| function panTakesMove( |
There was a problem hiding this comment.
This restates the pan's own direction rule (PanGestureRecognizer.classify, PanGestureRecognizer.ts:308-326) without its axisRatio. With axisRatio: 0.5, a 50° mouse drag pans while the same trackpad stream scrolls the page; with axisRatio: 2, a 35° stream's first wheel is prevented before the pan fails. Could the host ask the pan through recognizerInternals (as it does for family) and drop this copy, with a wheel case for a non-default axisRatio?
| : 'continuous'; | ||
| } | ||
|
|
||
| /** Whether a recognizer is a scroll-edge pan. */ |
There was a problem hiding this comment.
Non-blocking (comment style): this restates the function's name. If anything, keep the reason, for example: “Read without the pan class, which a page with no pan never ships.”
| return 'pinch'; | ||
| } | ||
|
|
||
| /** A pinch takes a trackpad's pinch and ctrl and a mouse wheel. */ |
There was a problem hiding this comment.
Non-blocking (comment style): “a trackpad's pinch and ctrl and a mouse wheel” reads ambiguously. Suggested: “A ctrl-wheel drives a pinch, from a trackpad or a mouse.”
…isRatio included The host chose a trackpad stream's pan by its own copy of the pan's direction rule, which ignored axisRatio. So with an axisRatio of 0.5, a 50 degree mouse drag panned while the same trackpad swipe scrolled the page; with 2, a 35 degree swipe's first wheel was prevented before the pan failed. The host now asks the pan's own rule through the gesture system's internal view, and a swipe the rule leaves undecided stays the page's. A wheel case for each axisRatio failed before. Two comments say less: the scroll-edge read gives its reason, and the pinch's scroll types read plainly.
… to the page After a window blur or a hidden page cancelled a trackpad pan, the next wheel of the same stream started a new gesture: the splitter was reset, the pan was possible again, and it began within 10 px. The fingers' later wheels and the momentum after their lift could then move it. The rest of a cancelled pan's stream is now the page's until the stream ends, as a touch the browser takes is; only the next stream is decided afresh. The blur, hidden-page and coarse-pointer cases now send the rest of the stream, fingers and momentum, and failed before. A new case moves focus between elements mid-swipe and cancels nothing.
|
Fixed at 1a0cd94 (commit e8c3a28). This head also carries the spec owners' ruling on a blur: a window blur or a hidden page cancels a trackpad pan, as the browser taking a touch cancels a finger's. That is one |
|
/approve-spec 1a0cd94 |
There was a problem hiding this comment.
This pull request waits on one engineering-owner decision; everything else checks out, including the earlier panTakesMove finding, now fixed with axisRatio cases.
- The
spec:AST-094FR43 sentence approved at this head says a window blur or a hidden page cancels a trackpad pan, butdropDrivenWheelinGestureHost.tsalso cancels a ctrl-wheel pinch mid-pinch and leaves the rest of that ctrl-wheel stream to the page, which then zooms. - If an engineering owner wants that for the pinch, FR43 should say so, and an engineering owner records the decision by commenting
/approve-spec <full head SHA>on that head (the author may do this as an engineering owner); otherwise, limit the cancel to pans and add a pinch case beside the blur cases.
The comment trims marked non-blocking are optional.
[Automated review]
| * is. A gesture the page keeps stays as it is. So does a pan its lift | ||
| * released: its momentum stays latched and prevented until the stream ends. | ||
| */ | ||
| private dropDrivenWheel(): ReadonlyArray<number> { |
There was a problem hiding this comment.
This also drops a driven ctrl-wheel pinch. On this head, a mouse ctrl-wheel pinch reads cancelled at a window blur, and the next ctrl-wheel is not prevented, so the page zooms for the rest of that stream. The approved FR43 sentence names only a trackpad pan. Either record the pinch reading in FR43, or return [] here for a pinch gesture and add a case beside the blur cases in wheel.test.ts.
| */ | ||
| private fingerTargets: ReadonlyArray<Node> = []; | ||
| private nextId = 1; | ||
| /** The wheel gesture in progress. */ |
There was a problem hiding this comment.
Non-blocking (comment style): this restates the field name wheel; it can go.
| private stopWheelLatch: (() => void) | null = null; | ||
| /** Splits the wheel stream into gestures and reads a trackpad's lift. */ | ||
| private readonly segmenter = new WheelSegmenter(); | ||
| /** The latest wheel's segment. */ |
There was a problem hiding this comment.
Non-blocking (comment style): this restates the field name wheelSegment; it can go.
… keeps its stream The blur and hidden-page cancel also cancelled a ctrl-wheel pinch mid-pinch and left the rest of that stream to the page, which then zoomed the whole page. It now cancels only a trackpad pan, as the spec:AST-094 FR43 sentence says; a pinch keeps its stream and ends 100 ms after its last wheel. A pinch case for a blur, a hidden page and a blur under a coarse primary pointer failed before.
|
Done at 5a38c0c: a blur or a hidden page now cancels only a trackpad pan, as the FR43 sentence says. A ctrl-wheel pinch keeps its stream through a blur and ends 100 ms after its last wheel, so the page never zooms for it. A pinch case for a blur, a hidden page, and a blur under a coarse primary pointer fails before it and passes now. |
|
/approve-spec 5a38c0c |
There was a problem hiding this comment.
Recommend approval: the wheel and trackpad path follows spec:AST-094 FR43 and DEC-24, and the FR43 blur sentence is owner-approved for this head. A blur or a hidden page now cancels only a trackpad pan, and the new pinch cases cover the ctrl-wheel stream. Optionally, drop the few new comments in GestureHost.ts that restate their names (see the inline notes).
[Automated review]
| hasTouchEndListener: boolean; | ||
| /** Whether the element carries the host's shared blocking touchmove handler. */ | ||
| hasTouchMoveListener: boolean; | ||
| /** Whether the element carries the host's wheel handler. */ |
There was a problem hiding this comment.
Non-blocking (comment style, INV29): this restates hasWheelListener; it can go.
| */ | ||
| private fingerTargets: ReadonlyArray<Node> = []; | ||
| private nextId = 1; | ||
| /** The wheel gesture in progress. */ |
There was a problem hiding this comment.
Non-blocking (comment style, INV29): this restates the field name wheel; it can go.
| private stopWheelLatch: (() => void) | null = null; | ||
| /** Splits the wheel stream into gestures and reads a trackpad's lift. */ | ||
| private readonly segmenter = new WheelSegmenter(); | ||
| /** The latest wheel's segment. */ |
There was a problem hiding this comment.
Non-blocking (comment style, INV29): this restates the field name wheelSegment; it can go.
| // A trackpad scroll waits out a slow drag's pauses, whoever | ||
| // keeps it; a pinch or a mouse wheel's notches 100 ms. |
There was a problem hiding this comment.
Non-blocking (comment style, INV29): wheelEndMs's doc and the endMs field doc already state this end-window rule; this third copy can go.
On main: fourteen commits (the feature's four, then ten from review and the owners' rulings).
This behavior-module pull request adds the gesture host's wheel and trackpad module and the recognizer options that opt in; no component or entry point changes. Of 3,076 changed lines, 1,031 are source and 1,965 proofs.
Why
A trackpad swipe over an Astryx pan, such as a sheet, never reaches it: the page scrolls. The merged record: a pan follows a trackpad's two-finger scroll where its record allows it, never a mouse wheel's notches (
spec:AST-094FR43, DEC-24).Records checked:
spec:AST-094FR43, DEC-24, FR16, FR19, FR20, FR24;architecture:react-component-runtimeINV13;architecture:public-component-apiINV25;architecture:knowledge-contractsINV29. The record gains one FR43 sentence, in the owners' words, and its Verification case.Why this choice
One host decides each gesture by the pan's own direction rule, after every scroller under the pointer reaches its edge, and latches it through the shared input stream. Only the trackpad signature of Chromium and WebKit counts. One splitter reads a pan's stream (DEC-24): a 180 or 220 ms pause ends it, the fourth shrinking delta marks the lift, and momentum never moves a surface. A ctrl-wheel pinch ends 100 ms after its last wheel. A blur or a hidden page cancels a trackpad pan; the rest of that stream is the page's.
API changes
Internal only:
ScrollType,allowedScrollTypes(default none; a pan takes only'continuous'),pinchinRecognizerFamily,createNativeScrollProbe's{readsTouchAction}, and the internalcanElementScrollandRecognizerInternals.classify.What the review fixes change
Each fix is a commit with cases that fail before it:
axisRatioreads a trackpad's direction as a finger's.A pan's own swipe keeps its release velocity.
Evidence
Testing
Local gates at 5a38c0c, on main 1667ea8 (14 commits; 20 files):
no-comment-provenance), Prettier, the private-attribute baseline andcheck:*(check:i18n-runtimeincluded): clean. The provenance baseline check still reports drift in four files this change does not touch (ChatComposer.tsx,Item.tsx,layerSurfacePress.ts,Token.tsx), as it does on main.CI=1,LP_NUM_THREADS=4, one worker), ten runs: 550 passed and 100 skipped (the Chromium-only cases), none failed.pnpm test:ui: 720 files, 17,880 tests; node: 845 files. The reds were main's own:motion/roles.test.ts(spec(ContextMenu): touch-opened menus lift with a tint, grow from the finger, and use named springs #9423, fixed by chore(core): the touch motion tables register FR4's context-menu springs as roles #9432) and fix(cli): a crashed run's release install is recognized by the Node it ran on, whatever its name #8520's tworelease/cache.test.mjscases.Ten runs each: WebKit at 3 CPUs (
LP_NUM_THREADS=3) and one worker, 550 passed; Chromium pinned to 2 cores, 610 passed at four workers and 790 (with the animator's busy-main-thread proofs) at one.At 16 workers on 3 CPUs (
LP_NUM_THREADS=3; not a required profile), 36 of 550 runs fail, all in four trackpad cases: momentum after the lift 10, absorbing the stretch 10, a real trackpad scroll 9, the latch 7. Run side by side at that profile, those cases failed 29 of 40 times on 9f9dc80 and 26 of 40 on this code, so the fixes do not change the rate.The cause, measured on 9f9dc80: the runner's wheel steps reach the page further apart than the end window in force, and the splitter ends the gesture there, as recorded. A run that logged when each step reached the page found such a gap in every failing run and in no passing run:
Sending each stream as one burst on a frame's schedule only renamed it: WebKit coalesced the steps the starved page was still handling.
Accessibility, both suites whole: WebKit, in CI's container profile, 138 passed and 10 skipped; Chromium 2,278 passed, and one
TableCellMenuslong-press case failed, which passed 20 of 20 side by side on main and on this code. Core's and Lab's entries bundle none of this change's files.Writing rubric results
R1–R6: none. The FR43 sentence is in the present tense, the Verification row names cases, and the owners ruled the question.