Repository navigation
spec(AST-094): touch presentations feel native by default - #8409
Conversation
Touch presentations get one native-feeling behavior by default on every touch device: measured values, compositor motion a finger can catch, recognizer-tracked gestures, two press profiles, Reduce Motion per design:reduced-motion, menus and popovers under a finger (FR26-FR35), and one release decision for every drag that commits (FR36-FR40). Touch motion is themeable through use-named roles, except motion in step with the on-screen keyboard. The current records it amends point to it. Retries #8394, whose review rounds were spent, carrying its last revision plus the owner's 2026-10-25 rulings: DEC-5 (FR4 cites the generated motion role table), DEC-6 (touch media-query CSS counts as after hydration), DEC-7 (watchers are passive, never cancelled, and follow the scroll), and interaction-modality INV10 (Lab reads release velocity only through Core's recognizers). FR17 and FR19 follow. Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
The exact-head preview is not available yet. Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
This spec is not ready to approve yet: no engineering owner has approved spec:AST-094 or its amendments to nine current records at this head. Please fix the two points below in a new commit; then an engineering owner (the author included, as one) records the decision by commenting /approve-spec <full head SHA> on this pull request.
- The rewritten
architecture:interaction-modalityINV10 has Lab import Core's recognizers from an internal module, but Lab reaches Core only through Core's package exports, whicharchitecture:public-component-apiINV12 makes public, while FR4 says the gesture machinery publishes no public API; please name the supported import path in INV10 (and narrow FR4 to match), or keep the Lab-local tracker sentence. spec:AST-094's Current-state impact says interaction-modality's "invariants are unchanged", and interaction-modality's Deciding specs section still says no other specification changes it; please name the INV10 change there and addspec:AST-094as a deciding spec.
[Automated review]
| threshold compares the tracker's release velocity. Lab reads release | ||
| velocity only through Core's recognizers (`velocity()` on a pan, or on a | ||
| scroll view's pan for a scrolling surface), from Core's internal gestures |
There was a problem hiding this comment.
Lab reaches Core only through Core's package exports (a peer dependency, no alias to Core's source). Reading the recognizers "from Core's internal gestures module" therefore needs a Core export, which architecture:public-component-api INV12 makes public, while AST-094 FR4 says this machinery publishes no public API. Please name the supported import path here (and narrow FR4 to match), or keep the sentence under which Lab keeps one local tracker with the same blend.
| - `architecture:interaction-modality`: the touch-presentation conditions | ||
| (FR13) sit beside the shared modality state; its invariants are unchanged. |
There was a problem hiding this comment.
This says interaction-modality's invariants are unchanged, but the bullet at line 464 and this pull request rewrite INV10. interaction-modality's Deciding specs section also still says no other specification changes it. Please name the INV10 change here and add spec:AST-094 to that section.
- interaction-modality is unchanged here: the INV10 amendment (Lab keeps no copy of the release-velocity filter) moves to a separate change, and Current-state impact says the record's invariants are unchanged. - FR17 is a plain pointer to spec:AST-100. - FR4 keeps "publishes no public API" without access or experimental wording. Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
Whether the motion and gesture machinery is public is spec:AST-017's to govern, not a touch behavior; FR4 keeps only the theme motion roles. Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
There was a problem hiding this comment.
This spec is not ready to approve yet: no engineering owner has approved spec:AST-094 and its amendments to current records at this head. An engineering owner (the author included, as one) records that decision by commenting /approve-spec <full head SHA> on this pull request. Two small record fixes are needed whatever the decision:
spec:AST-057now says the row's slide and spring motion plays on theme motion roles, but neither it nor FR4 names those roles or their sourced defaults; please name them, or keep the durations as source constants until they are measured.- FR19 and FR24 forbid recognizers and touch listeners "on a fine pointer" without saying whether that is the primary pointer, while
spec:AST-057reads the pointer per event; please say which condition governs a finger on a touchscreen whose primary pointer is fine.
[Automated review]
| - **Gesture tuning.** Axis-lock distance, commit ratio, fling velocity, | ||
| resistance past the panel: behavior constants in source, not public API and | ||
| not design tokens (the same ruling the touch press model received for its | ||
| clocks). The row's slide and spring motion plays on theme motion roles |
There was a problem hiding this comment.
This hands the row's slide and spring motion to theme motion roles, but FR4's table has no row-swipe role and this record measures none, so Item's swipe has no named role or sourced default to read once it may no longer play its constants. Please name which FR4 roles the slide and the spring-back play (or add the row's measured roles with sources), or keep the durations as source constants until they are measured.
| pan that shares its axis with native scrolling. `pinch-zoom` MUST stay | ||
| unless a pinch or rotation recognizer is attached. | ||
| - **FR19 — Claims are made in time and only by an owner.** Recognizers attach | ||
| only under a coarse pointer; on a fine pointer no recognizer attaches and no |
There was a problem hiding this comment.
"On a fine pointer" is not defined here: FR13 tests the primary pointer and lets a presentation ask for its own record's conditions, while spec:AST-057 reads the pointer per event (a touchscreen laptop gets the swipe). Read as the primary pointer, this withholds the row-swipe recognizer AST-057 grants there. Please say which condition FR19 and FR24 mean, and align AST-057's platform line if the answer changes it.
…ants - FR19 and FR24 mean the primary pointer: where it is fine, no recognizer attaches and a finger gets the browser's defaults for this record's behaviors (FR13, DEC-6; AST-017 FR59, DEC-25). spec:AST-057 keeps its per-event pointer read for the row swipe. - spec:AST-057 is unchanged: no row-swipe role is measured, so its slide and spring durations stay behavior constants; FR4 and Current-state impact no longer claim them. Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
Brings in current main (AST-025's tooltip exception merges cleanly beside this record's FR14 line) and drops the row swipe from FR36 and the Current-state impact: a row swipe keeps spec:AST-057's own release rule, and AST-057 is not amended. Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
There was a problem hiding this comment.
This spec is not ready to approve yet: no engineering owner has approved spec:AST-094 and its amendments to current records at this head. An engineering owner (the author included, as one) records that decision by commenting /approve-spec <full head SHA> on this pull request. Two record fixes are needed whatever the decision:
component:PopoverFR9 makes a pan outside close a Popover under a finger, but its Compatibility and migration section still says the released default is preserved and FR25 does not list it; please state the changed touch default and the caller's control (hasLightDismiss={false}) there, and add it to FR25 or say why it is touch-only behavior underarchitecture:public-component-apiINV16.- FR19 gives a finger on a touchscreen whose primary pointer is fine the browser's defaults for this record's behaviors, while FR26–FR28 apply whenever a finger drives a menu and
useMenuPresshands its finger rules to them; please say which rule a finger in a menu follows on such a device, for example by keeping the per-gesture pointer read as the row swipe does.
[Automated review]
| | FR6 | Component anatomy contains the caller trigger and content, one painted Popover surface, and the optional fallback close control. Popover owns no Header, Body, or separate shared-hook surface part. | Current source, docs target inventory, and tests | Accepted anatomy; stale consumer anatomy corrected by this contract | | ||
| | FR7 | The painted surface has one broad canonical target, `popover`; composed components may add one authoritative component-specific refinement target. | Owner direction on 2026-08-31 plus current target inventory | Accepted canonical contract; covered by focused target tests | | ||
| | FR8 | The surface padding is one spacing-scale rung applied on the painted surface. Popover defaults to step 3 (the released inset); `0` paints a flush surface; a direct `usePopover` composition paints none unless it asks for a rung. The rung sits where a theme's `padding` on the `popover` target replaces it. | PR #6683 and its Popover tests; `architecture:container-padding/INV8` (padding alone does not enroll) | Pending owner review (DEC-4); implemented and covered by the Popover surface-padding suite | | ||
| | FR9 | Under a finger, Popover grows out of its trigger and shrinks back into it, a pan outside dismisses it while its outside dismissal is on, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR32 and FR35 state. A mouse keeps today's entrance and dismissal. | `spec:AST-094` FR31, FR32, FR35 | Pending implementation; tests land with it | |
There was a problem hiding this comment.
This moves Popover's released touch dismissal: a pan outside now closes it while hasLightDismiss is on, where today the page scrolls and it stays open. Line 60 still says "Released default preserved: yes", and AST-094 FR25 lists only four moved defaults. Please state the touch default change and its caller control (hasLightDismiss={false}, which also stops tap-outside dismissal) under Compatibility and migration, and add Popover to FR25 or say there why it is touch-only behavior under architecture:public-component-api INV16.
| - **FR19 — Claims are made in time and only by an owner.** Recognizers attach only where the primary pointer | ||
| is coarse (FR13, DEC-6). Where the primary pointer is fine, no recognizer | ||
| attaches and no touch listener is registered (FR24), so a finger on a | ||
| touchscreen whose primary pointer is fine gets the browser's defaults for | ||
| this record's behaviors (`spec:AST-017` FR59, DEC-25). This rule governs | ||
| this record's behaviors only; `spec:AST-057` keeps its per-event pointer | ||
| read for the row swipe, as its record states. A recognizer that begins MUST claim its touches by cancelling |
There was a problem hiding this comment.
This gives a finger on a fine-primary touchscreen the browser's defaults for every behavior in this record, but FR26–FR28 apply whenever a finger or pen drives a menu, and useMenuPress FR1–FR2 now hand their finger halves to them while the module still reads the pointer type per event. Please say which rule a finger in a menu follows on such a device: keep the per-gesture read, as the row swipe keeps AST-057's, or name the change as a moved default. FR24's touch-only timer limit affects the held-finger open the same way.
…y pointer
- Popover: under a finger a pan outside now dismisses it while its outside
dismissal is on (FR32). Popover's compatibility section and FR25's
moved-defaults list name it, with hasLightDismiss={false} as the
caller's control.
- FR26-FR28 and FR30 apply when the primary pointer is coarse (FR19); on a
fine-primary touchscreen a finger in a menu keeps today's behavior
(AST-017 FR59, DEC-25), with no per-gesture pointer read.
Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
…hanged Popover keeps today's dismissal under a finger: FR32 covers anchored menus only, FR25's moved-defaults list and Current-state impact drop Popover, and Popover's record keeps its released default (its FR9 points only to FR31's grow and FR35's presentation hold). Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
There was a problem hiding this comment.
This spec is not ready to approve yet: no engineering owner has approved spec:AST-094 and its amendments to current records at this head. An engineering owner (the author included, as one) records that decision by commenting /approve-spec <full head SHA> on this pull request. Two record fixes are needed whatever the decision:
- On a touchscreen whose primary pointer is fine, the menu rules say a finger keeps today's behavior with no per-gesture pointer read, while
useMenuPressFR1–FR3, FR6, FR7 and FR9 still hand every finger to FR26–FR30 and FR24 rules out the timer behind today's 500 ms held-finger open; please state one rule for that finger (for example the module's per-event finger model) and either exempt it from FR24 or list the lost held open in FR25. - FR32 now closes every anchored menu on a pan outside, including one with
presentation="popover", but FR25 lists no such moved default and the Current-state impact and DropdownMenu's compatibility section saypresentation="popover"keeps the released behavior; please carve that presentation out of FR32 or record the change and narrow that wording.
[Automated review]
| and Selector's listbox, whose options count as rows) when a finger or pen | ||
| drives it and the primary pointer is coarse (FR19); where the primary pointer | ||
| is fine, a finger in a menu keeps today's behavior (`spec:AST-017` FR59, | ||
| DEC-25), and menus make no per-gesture pointer read; FR29 applies to DropdownMenu's trigger; FR34 applies to menus; |
There was a problem hiding this comment.
On a touchscreen whose primary pointer is fine, “keeps today's behavior” and “menus make no per-gesture pointer read” pull in opposite directions: today's press model reads the pointer type per gesture (it keeps a menu open after a finger's lift outside but closes it after a mouse's), and useMenuPress FR1–FR3, FR6, FR7 and FR9 still hand every finger to FR26–FR30. FR24 also rules out the timer behind today's 500 ms held-finger open there. Could this say which rule that finger follows, for example the module's per-event finger model as the row swipe keeps AST-057's, and either exempt it from FR24 or list the lost held open in FR25?
| closed. This is its exit under FR7 (`family:overlay-dismissal` AV6); under | ||
| reduced motion FR8 applies. A menu or popover presented as a bottom sheet | ||
| follows the sheet instead. | ||
| - **FR32 — A pan outside dismisses.** Under a finger, an open anchored menu MUST close as a touch outside it |
There was a problem hiding this comment.
This closes every anchored menu on a pan outside, including one with presentation="popover", where today native light dismiss leaves it open while the page scrolls. FR25 lists no such moved default, and the Current-state impact and DropdownMenu's compatibility section say presentation="popover" keeps the released behavior. Could FR32 carve that presentation out, or could FR25 and both menu records record the change and narrow that wording to the open and the lift?
- useMenuPress FR1-FR3, FR6, FR7 and FR9 hand a finger or pen to AST-094 FR26-FR30 only where the primary pointer is coarse; where it is fine, a finger keeps today's behavior, including the 500 ms held-finger open. FR24 exempts that released behavior explicitly, so FR25 needs no new entry. - FR29 applies under the same condition as FR26-FR28. - FR32: a menu whose caller passes presentation="popover" keeps its released outside dismissal (FR25). Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
|
/approve-spec 20eceaa |
Supersedes #8394, #8378, #8316 and #8052. It carries vjeux's record, credited as co-author, plus every owner ruling since. The record itself contains no pull request references.
Latest change: FR36 no longer lists the row swipe. A row swipe keeps
spec:AST-057's own release rule, andspec:AST-057is not amended. Current-state impact drops row swipes from the release-decision line. Currentmainis merged in.Exact decision needed
Approve
spec:AST-094: touch presentations get one native-feeling behavior by default on every touch device, iOS and Android alike. Open questions: OQ5 (the menu's drift allowance), OQ6–OQ8 (opening a sub-menu without a lift, the opening haptic, a withdrawn tentative open), OQ9 (where a menu sits on a compact touch screen) and OQ10 (a sub-menu with its parent visible). Platform look and feel is a non-goal tracked separately. Release velocity is governed byspec:AST-100.Existing authority searched
component:SelectorDEC-1 (theadaptivequery);spec:AST-013FR3 (an Android Chrome support row);spec:AST-025FR14 (nested scrolling);design:user-statesDR1/DR3;family:buttons;module:DropdownMenu/useMenuPressFR6.module:DropdownMenu/useMenuPressFR4;design:user-statesDR4;spec:AST-043FR1;spec:AST-012FR9 (compact);design:reduced-motion;architecture:public-component-apiINV14, INV16;spec:AST-017FR3, DEC-23;spec:AST-002FR3;architecture:react-component-runtimeINV5/INV7;family:overlay-dismissalAV6.Decision
design:reduced-motionDR2 and DR4.spec:AST-012FR9 compact definition where a presentation depends on size. The touch presentation applies after hydration.presentationchoice (orlongPressDelayMsforuseMenuPress). Touch work costs nothing where the primary pointer is fine, and a finger there gets the browser's defaults (AST-057's row swipe keeps its own per-event read).spec:AST-100.spec:AST-057's own release rule.Rejected alternatives
presentationis the way back, INV16 and DEC-23).Claim scope and non-goals
spec:AST-017), and how Lab reaches private modules.Evidence and implementation relationship
The values come from UIKit read on the simulator, Apple documentation, WWDC sessions and WebKit's source. Implementation stacks on this record. Implementations: #8078 (menu press and handoff), #8080 (menu grow and collapse), #8187 and #8198 (Popover grow and pan-outside dismissal), #8201 (ContextMenu lift), #7308 (ContextMenu under a finger), #8341 (the release decision), #8220 (sub-menus in place), #8223 (the presentation hold), #8195 (the menu cell profile); #8089 (menu placement on a phone) is OQ9.
Writing rubric results
Scope
architecture:knowledge-contractsINV18).Validation
pnpm check:knowledge: aligned.git diff --check: clean.