Repository navigation
Conversation
…ter the browser takes the touch A host that stopped feeding the finger at the takeover and still delivered the lift would have passed. Each transition now records the list's scrollTop, and the test expects a 'changed' while the list is scrolled (spec:AST-094 FR20: it follows the scroll to the lift).
…keover) into the host line
…d, as on iOS UIKit ignores every touch while a popover is presented: a tap outside 60 ms into the presentation neither dismisses it nor reaches the page, and the presentation finishes. Here the browser's light dismiss closed the popover mid-presentation, and it cannot be cancelled from an event. For the length of the present spring, the touch presentation places a transparent manual popover nested in the layer over the viewport (data-astryx-popover-hold). In the top layer it is above everything, and as a descendant of the layer a tap on it is a tap inside the popover, which the browser leaves open; it lets no pan start. It goes when the spring settles, when the popover is dismissed, and on reset. The mouse presentation is unchanged. The ignored tap leaves no trace: the hold cancels the compatibility mousedown and the touch's end, so the control the popover focused keeps focus and no click fires (before, the tap on the hold moved focus to the body). The hold's touch-action is pinch-zoom: no pan starts on it, and a pinch still zooms the page, as the draft touch-presentation record keeps it. (cherry picked from commit 25db2b9)
spec:AST-094 FR35 keeps a page pinch available while a Popover grows, and its verification lists a pinch during growth. A browser case now pinches with two fingers 60 ms into the presentation: both land on the hold, the page's visual viewport scales past 1.1, and the popover finishes opening and stays open. With the hold set to touch-action: none, the case fails (the scale stays 1). (cherry picked from commit 8828783)
…dule's own hold The popover placed its own hold: a manual popover inside the layer that swallowed the compatibility mouse events. The Layer module now owns one (`holdPresentation`, from the presentation hold), for every component whose layer presents under a finger, so Popover takes the shared one with scope `viewport` for the length of the present spring and keeps its release for a dismissal that cuts it short. What a tap meets is unchanged: the shield covers the viewport from inside the layer, so the browser's light dismiss counts it as inside the popover and leaves it open, no pan starts on it, a pinch still zooms the page, and the control the popover focused keeps focus. It goes further than the popover's own hold did: pointer and touch events are swallowed at the shield, not only the mouse pair, and the gesture host holds the layer's touches away from every recognizer. `POPOVER_HOLD_ATTRIBUTE` goes with the private hold. The unit tests and the touch proof read the shield through `isPresentationShield` and its shape. Where the browser has no popovers the shield is still placed, now outside the top layer, instead of no hold at all. (cherry picked from commit 79539c1)
…akes no input (AST-094 FR35) A tap outside 17 ms after the popover opens leaves it open with focus inside; once it has grown, the same tap light-dismisses it; a tap on the trigger during the growth does not close it; and a one-finger drag during the growth scrolls nothing. Chromium dismisses a touch on the gesture tap, so only a cancelled touchend (which the presentation hold's shield does) stops it. Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com> (cherry picked from commit c5ef889)
meta#8342 renamed `Layer/presentationHold` to `Layer/inputHold` (`holdInput`, `isInputShield`). Popover's sheet motion, its test and its Chromium spec take the new names; the comments that mean the runtime's shield say "input hold". Behavior is unchanged. Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com> (cherry picked from commit 5d3830f)
…ne's tap In WebKit the test failed before the hold was ever reached: it opened the popover with a scripted touch sequence, and Playwright's WebKit synthesises no click from one (the harness's documented engine limit), so the popover never opened and the tap outside landed on the page. In Chromium the same script opened it and the hold kept it open. The test now opens it through the engine's tap, as the test's other steps do, and records at each press whether the hold's shield was up: the trigger's press found none, and the tap outside landed while the popover presented. It passes in both engines, and with the hold disabled it fails in both, at that record and at the popover's being open. (cherry picked from commit ee457bd)
…of the document) into meta#8270
…sture system (spec:AST-094 FR24, FR43) utils/pressFeedback.ts reaches gestures/coarsePointer.ts, so a page whose only touch work is press feedback carries a gesture module, and a bundle check that counts gestures/ code finds it in the root and hooks entries. The coarse-pointer query belongs with the environment queries. This test walks the controller's import graph and fails until it reaches no module under gestures/.
…ries in utils/ (spec:AST-094 FR43) gestures/coarsePointer.ts becomes utils/coarsePointer.ts, beside utils/touchQueries.ts, with its test; the gesture host's selection lock, the press controller and the pointer test helper import it from there. The press controller's import graph now reaches no module under gestures/, so a page whose only touch work is press feedback carries none of the gesture system. No behavior changes.
CI's lint rejected the three Array<T> types the previous commit added (@typescript-eslint/array-type); the test now names its record once (WatchTransition) and reuses it.
…e arena's reset The arena resets a recognizer to possible after it ends, as UIKit does, and the transition listener records that too, so the last transition is possible: the test now asserts the ended-then-possible tail.
…nter query in utils/) into meta#8358
…nter query in utils/) into meta#8356 # Conflicts: # packages/core/src/gestures/GestureHost.ts # packages/core/src/gestures/fingerScroll.ts # packages/core/src/gestures/pageHold.ts # packages/core/src/gestures/selectionLock.ts # packages/core/src/utils/pressFeedback.ts
… layer at once Once the copy's dismissal started, exit() put the hidden element at rest with a set() that promotes, so the animator kept it promoted until PROMOTION_LINGER_MS (500 ms) after its last motion and then rewrote its will-change about 490 ms into the dismissal: an UpdateLayoutTree and a Layout inside the frames the dismissal proof counts, on an element that no longer paints. The element is now put at rest without promotion and detached from its animator in the exit's task, which demotes it there; the next presentation attaches it again.
… FR19, FR24) Under a fine primary pointer: each element that uses a gesture keeps one passive pointerdown listener and the page none while idle; a mouse or a pen pans, its page listeners lasting only the press; a press consumer takes a mouse press with no feedback-only mode (DEC-21); a pan writes its touch-action for a touchscreen laptop's finger (FR18); perEventPointer is taken as any attach, with no warning; a secondary button starts nothing. All but the last fail until the host attaches for every pointer.
… the scroll brake for a finger only (spec:AST-094 FR11, DEC-21) A mouse or a pen press paints at once, inside a scroller too; a mouse press on a scroller a flick set moving still presses, as the brake is a finger's. The brake case fails until the tracker brakes only a touch.
…'s selection An action group's press is a long press with no minimum duration, so it claims at the press's pointerdown, before any movement. The drag selection hold then cleared the page's selection, so a plain click on a group's button left its Copy or Cut with nothing to act on. The case fails on this head: the selection is gone after the pointerdown.
…age's selection The drag selection hold cleared the page's selection when any mouse or pen contact was claimed, but an action group's press claims at its pointerdown, before it has moved or selected anything, so a plain click on a group's button left its Copy or Cut with nothing to act on. A claim before any movement now keeps the page's selection: the hold still refuses a new one, and puts the page's back over a selection that appears while it runs. A claim after movement clears what its drag selected, as before (spec:AST-094 FR41). The case before the new one now waits for its hold to end a task after the lift, so no hold outlives that test.
…t every pointer does - GestureHost's header, the gestures README and coarsePointer.ts no longer describe the coarse-only host: a finger or a pen takes the touch-down lock under any primary pointer, a kept press's finger is kept during a fine pointer's press too, and recognizers attach for every pointer while the page listeners last a press (spec:AST-094 FR19, FR24, FR41, FR43). - The fine-pointer selection case asserted the replaced rule and passed only because it read iOS WebKit's inline lock without iOS WebKit: it now asserts the lock a fine pointer's finger and pen take, and that a mouse takes none. - FR41's sentence and its verification row say that a press claimed at its own pointerdown, before it moves, keeps the page's selection, while a drag that moved before its claim loses what it selected.
… guide The keyboard layout guide (this pull request) reads where focus moves to tell a keyboard that comes up from one that stays: meta#8559 removed the store's focus reports because nothing there read them, so they return here with their first reader. One pair of capture listeners on the document, attached while anything subscribes, reports each focusin and focusout with the current height; BottomSheet's reveal still acts on viewport reports only.
Conflicts were in this stack's own keyboard files: the browser-support sentence keeps ChatLayout's composer and takes meta#8559's page wording, BottomSheet's test imports follow the sheetKeyboard split, and the keyboard notes keep both the sheet's and ChatLayout's Chrome 124 findings. The search goldens are regenerated after the merge.
…op-level page The composer's tap route, its scroll-free focus and Done's park attach only where the keyboard is measurable, which excludes a page inside an iframe (spec:AST-099 FR7); there the browser's own focus applies. The docs and changeset now say so.
…ion, in the present tense The sentence said a drag that moved before its claim loses the selection it started, but the hold clears the page's selection then, whatever made it; and an unclaimed mouse drag kept "today's" behavior, a phrase that ages. It now says a claim after movement clears the page's selection, a press claimed at its own pointerdown keeps it, and an unclaimed mouse drag keeps its released behavior.
This pull request now sits on meta#8665, so a stack carrying it and meta#8665's other dependants (meta#8676) needs no second GestureHost resolution. Two hunks in GestureHost.ts's pointerdown, both this pull request's: - the second-finger check where fingers come from pointer events stays, followed by meta#8665's rule that a right click, a pen's barrel button or a macOS ctrl-click starts no press; - the check that runs again once the new finger is delivered stays, followed by meta#8665's selection lock call, which no longer takes the window.
…e Popover touch presentation AST-057 takes the dependencies' text, which is main's (meta#8646): the tuning bullet this branch once restored is superseded, and the Popover touch presentation does not touch row actions.
…e Popover presentation hold
… ChatLayout BottomSheet's useSheetKeyboard moved to keyboard/sheetKeyboard.ts, and it subscribes to the keyboard layout guide and rides Gboard with rideKeyboard as ChatLayout does, but the two modules' headers named only ChatLayout, and BottomSheetPanel's related-files list and the keyboard notes still pointed at keyboardHeight.ts. They now name the hook and its file.
…the Popover touch presentation
Once a component in the root entry holds its layer's input (a popover's presentation hold, meta#8223), Layer/inputHold's static imports of gestures/GestureHost and gestures/nativeScroll put eight gesture modules (about 47 KB) in the root entry, against architecture:react-component-runtime INV13. Bundle a component that imports the input hold and require no gesture bytes. Fails while the input hold imports from gestures/.
…e host asks it Layer/inputHold registered a touch gate with the gesture host (getGestureHost(doc).addTouchGate(isInHeldLayer)) and walked the flat tree through gestures/nativeScroll, so any component that holds its layer statically carried the gesture system into its bundle and the root entry (architecture:react-component-runtime INV13). Invert it: the host asks isInHeldLayer for every touch itself (gestures may import Layer), the hold registers nothing, and it walks the flat tree through utils/flatTree, where parentAcrossShadow already lives. addTouchGate stays for other gates. A host test holds a held layer's touches with no gate (it fails if the host stops asking the hold).
…e input hold imports nothing from gestures/
…ut hold imports nothing from gestures/) into the Popover presentation hold
There was a problem hiding this comment.
Approval is waiting on an engineering owner: against main this branch adds new normative text to three current records (architecture:layer-runtime INV13 and its Exit copy section, spec:AST-094 FR41's rule for claimed mouse and pen drags, and spec:AST-057's motion-role line), and it starts the FR35 input hold one frame after the open, which the description asks the owners to accept (no browser case yet shows what a tap that starts in that frame does). An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, or says what must change; the author may do so as an engineering owner. Whatever that decision is, these need changes before merge:
- The diff still carries #8223 and its stack (44,673 authored lines against
main), so please land or retarget the dependencies until only this change's 286 lines remain. - Popover's two springs play as literals (
popoverSurfaceMotion.ts:373,:440), whichspec:AST-094FR4 forbids, so register them as motion roles or sequence this after #8258. .changeset/layer-input-hold.mdsays no component uses the input hold although Popover does, and the no-requestAnimationFramefallback returns0, whichexit()later reads as a pending frame.
[Automated review]
| // The popover's two springs are this component's measured values, not | ||
| // motion roles (touch motion stays internal, spec:AST-094 DEC-11): they | ||
| // play as given, so no table is registered and no other row is bundled. | ||
| const animator = createAnimator(element, animatorOptions); |
There was a problem hiding this comment.
spec:AST-094 FR4 says a component “MUST read touch motion through these roles and MUST NOT play a literal”, and that a theme's knob values reach the motion; FR31 has a Popover grow and shrink “on its motion roles”. DEC-11 keeps roles internal without exempting a component, yet POPOVER_PRESENT_SPRING and POPOVER_DISMISS_SPRING play as given here (:373, :440) and stay out of registerSpringsTable's roles (motion/uikit/springs.ts:521-534), so a theme's tempo and character never reach this motion. Registering them through a Popover motion table (DEC-5), or sequencing this after #8258, would conform.
| ((callback: () => void) => | ||
| typeof view?.requestAnimationFrame === 'function' | ||
| ? view.requestAnimationFrame(() => callback()) | ||
| : (callback(), 0)); |
There was a problem hiding this comment.
Without requestAnimationFrame this runs the callback at once (it clears pendingFrame and presents) and then returns 0, which line 335 stores; exit() then reads 0 as a pending frame (line 391) and takes the early-close branch, so the shrink never plays. Returning no handle here, or storing the handle only while the callback has not run, avoids it.
| // The input hold starts with the presentation too, in that frame: a touch | ||
| // that starts in the one frame between the open and it (about 16 ms after | ||
| // the opening tap lifts) reaches the page, though its tap does not | ||
| // dismiss, since its lift lands on the hold. Holding from the open would | ||
| // cost two forced layouts in every touch opening (a top-layer cover's | ||
| // `showPopover()` lays the page out), the work this frame keeps out of | ||
| // the opening task. | ||
| pendingFrame = scheduleFrame(() => { |
There was a problem hiding this comment.
As this comment says, a touch that starts in the frame before the presentation is not held, while spec:AST-094 FR35 says a tap or a drag during the entrance must not scroll the page or reach anything beneath, and component:Popover FR9 says a touch during the growth is ignored. The claim here that such a tap still does not dismiss has no case, and under a finger the lift's pointerup goes to the element the pointerdown hit (implicit pointer capture), not to a shield shown afterwards, and the popover's light dismiss reads that pointerup. The description asks the owners to accept the window; if they do not, the hold moves back to the open, where #8223 placed it, and if they do, FR35 needs to say so.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: one input hold for every layer (`Layer/inputHold`). While a layer presents, a touch, a press or a click anywhere reaches nothing (keys and assistive technology still act, and a pinch still zooms the page); while it dismisses, the same, and the layer's content is inert. The gesture host holds the layer's touches through its touch gates, and `isInputShield` lets a hit test see past a hold's shield. Not exported from the package entry point; no component uses it yet. |
There was a problem hiding this comment.
Popover holds through this module at this head (popoverSurfaceMotion.ts:380), and .changeset/popover-touch-input-hold.md announces that hold, so “no component uses it yet” contradicts the release's own notes.
Review only this PR's change: vjeux/astryx@3f21fd4...3f3923a
Depends on #8223 (base 3f21fd4), which now carries #8342's input hold.
Owner question: FR35's hold starts in the presentation's first frame, about 16 ms after the opening tap lifts. Starting it at the open costs 2 forced layouts in every touch opening (a top-layer cover's
showPopover()), which this pull request exists to remove. A tap in that frame still doesn't dismiss, but its pointerdown reaches the page. Is that window acceptable, or should the hold start at the open?The popover's two springs play as motion roles once #8258 lands; that part changes then.
This pull request shows a merge conflict with main in the CLI's icon-set search goldens; it comes from the keyboard module below it (#8559), which resolves it, and this branch takes that fix with its next push.
Why
Opening a Popover with a finger did twice the layout work of opening it with a mouse, all inside the tap's own task: the page was laid out 8 times instead of 4 (about 21 ms against 11). On a busy phone that task crosses the 50 ms long-task line, which is when a tap visibly hitches; #8187's layout proof caught it in about 1 run in 50.
What led to it
We are bringing Astryx's touch components to the behavior of native iOS controls, and each presentation has a proof that nothing in it blocks the main thread from the key press to the end of the dismissal (
spec:AST-094M1). The touch presentation wrote styles, read the popover's boxes, and wrote again, all in the opening task; on an anchored element, each read after a write lays the page out again.Options and tradeoffs
Why this choice
It moves the work out of the tap with no visual difference: the popover still grows from the first painted frame. A presentation during a dismissal reads nothing and still starts at once.
How to check it
will-changeor timer) fail without this change and pass here.Evidence
iOS 26.2 simulator (Safari, a grey dot per finger):