Skip to content

spec(AST-094): touch presentations feel native by default - #8316

Closed
cixzhang wants to merge 5 commits into
mainfrom
spec/ast-094-touch-native-default
Closed

cixzhang wants to merge 5 commits into
mainfrom
spec/ast-094-touch-native-default

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #8052 (vjeux's record, credited as co-author). It carries his record plus the owner rulings recorded since, and fixes the two review findings left at #8052's last round. The record itself contains no pull request references.

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), and OQ9 (where a menu sits on a compact touch screen). Module-level experimental subpaths are closed: motion and gestures stay internal (FR4). Platform look and feel is a non-goal tracked separately. Release velocity is governed by spec:AST-100.

Existing authority searched

  • Current claims this record names as amended for touch presentations:
    • component:Selector DEC-1 (the adaptive query);
    • spec:AST-013 FR3 (an Android Chrome support row);
    • spec:AST-025 FR14 (nested scrolling);
    • spec:AST-057 (Item's swipe constants and FR2);
    • design:user-states DR1/DR3;
    • the press treatment of family:buttons;
    • module:DropdownMenu/useMenuPress FR6.
  • Current claims it keeps and cites:
    • module:DropdownMenu/useMenuPress FR4;
    • design:user-states DR4;
    • spec:AST-043 FR1;
    • spec:AST-012 FR9 (compact);
    • design:reduced-motion;
    • architecture:public-component-api INV14, INV16;
    • spec:AST-017 FR3, DEC-23;
    • spec:AST-002 FR3;
    • architecture:react-component-runtime INV5/INV7;
    • family:overlay-dismissal AV6.
  • Why none of them settles it: no current record states native touch behavior as the default.

Decision

  • One behavior on every touch device: measured values with sources, the same on every platform unless a measured, sourced difference exists. Touch motion is themeable, with the measured values as theme defaults; gesture thresholds stay internal.
  • Motion and gestures: compositor motion a finger can catch. Recognizer arbitration and readings follow the native model. Two press profiles. Exits as native.
  • Reduce Motion: governed by design:reduced-motion DR2 and DR4.
  • When it applies: one rule for when the touch presentation applies, a coarse pointer plus the spec:AST-012 FR9 compact definition where a presentation depends on size. The touch presentation applies after hydration.
  • Defaults and cost: a moved default is restored by the caller's presentation choice. Touch work costs nothing under a fine pointer.
  • Menus and popovers under a finger (FR26–FR35):
    • The lift decides, and the highlight follows the finger with one lit row.
    • A lift outside keeps a menu open.
    • A menu opens on press: at once in a fixed bar, or after 150 ms with no scroll inside a scroll view. The opening finger's handoff applies.
    • Edge autoscroll while the finger is held.
    • Menus and Popover grow from the trigger and shrink back.
    • A pan outside dismisses, and pinch stays available.
    • ContextMenu's long press and lift.
    • A sub-menu opens in place.
    • A presentation takes no input while it grows.
    • useMenuPress hands its finger rules here. DropdownMenu, ContextMenu and Popover point here.
  • Release decisions (FR36–FR40):
    • One decision at the release, from projected travel against a commit point.
    • Moving back cancels, and the settle continues the finger.
    • A keyboard and mouse path stays available.
    • Velocity comes from spec:AST-100.

Rejected alternatives

  • Per-platform behavior tables (one native-feeling behavior on every touch device is the ruling).
  • A generic restoring prop per moved default (presentation is the way back, INV16 and DEC-23).

Claim scope and non-goals

  • Out of scope: pointer presentations, scroll physics, which components adopt a touch presentation, and platform-matched look and feel (OQ3).
  • Open: module-level experimental subpaths (OQ4).

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

  • R1 narration: none. Amendments are stated as present-tense rules; pending approval lives only in the decision log and approved_by.
  • R2 research in the record: none.
  • R3 evidence naming a change: none.
  • R4 rejected alternatives over one line or outside a decision: none in the record.
  • R5 pull request or issue references remaining: none.
  • R6 ruled open questions still listing options: none. OQ1–OQ3 are unanswered; OQ4 is unanswered and keeps its two options.
  • Round 1 (pointers, role list, citation fixes): rubric re-run. R5 none; R6: OQ3 and OQ4 are now pointers with no option lists.
  • Round 2 (menu and popover gestures, release decisions, record pointers): rubric re-run. R1: amendments are stated as present-tense rules. R5: none in the records. R6: OQ4–OQ8 are unanswered.
  • Round 3 (sub-menus in place, presentation hold, OQ4 closed, review fixes): rubric re-run. R5: none in the records. R6: OQ5–OQ9 are unanswered.

Scope

  • This PR contains specification records only.
  • It records observable behavior and preserves equivalent internal implementations (FR1, FR9, FR13 and FR24 are stated as observable contracts, architecture:knowledge-contracts INV18).
  • Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
  • Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
  • It records one intentional durable decision, not a rescue for a separable tagalong.
  • The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
  • Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
  • Public text and artifacts contain no internal Meta context.

Validation

  • pnpm check:knowledge: aligned.
  • Prettier: clean.
  • git diff --check: clean.

Touch presentations get one native-feeling behavior by default on every
touch device, iOS and Android alike: measured values, compositor motion a
finger can catch, recognizer-tracked gestures with native arbitration, two
press profiles, and a Reduce Motion cross-fade. Touch motion is themeable,
with the measured values as theme defaults; gesture thresholds stay
internal. Size keys on the spec:AST-012 FR9 compact definition, a moved
default is restored by the caller's presentation choice (INV16, DEC-23),
and the record names the current claims it amends. Platform look and feel
(OQ3) and module-level experimental subpaths (OQ4) stay open.

Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 25, 2026 5:01am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 25, 2026, 5:30 AM UTC

@github-actions github-actions Bot added the spec-auto-merge Auto-merge was enabled by the spec owner gate label Oct 24, 2026
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current change has no visual or preview-producing scope. Visual checks are not applicable; preview links are not shown.

The exact-head preview is not available yet.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval of spec:AST-094 waits on an engineering owner's decision, because DEC-1, DEC-2 and FR1–FR24 add touch requirements that no current spec states and the record amends current claims in five records (spec:AST-057 FR2, spec:AST-025 FR14, design:user-states DR1 and DR3, module:DropdownMenu/useMenuPress FR6 and spec:AST-013 FR3). An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner; the decision should also settle whether FR4 may make touch motion roles public before the record lists their names and value shapes. Whatever that decision, these points need fixing:

  • FR7 sets aside "a rule that turns exits off by default" without naming it; no current record has one and the rule it answers is the open draft spec:AST-090 FR1, so drop the sentence and let whichever record lands second state the exception.
  • FR11 should say which sentence of useMenuPress FR6 it replaces and keep "At most one row of a menu is lit", because the cell profile ends its highlight only when a scroll wins.
  • FR13 cites architecture:public-component-api INV14 for the compact definition, but the compact claim is INV17.
  • #8052 is still open and adds the same file, so close it or mark it superseded.

[Automated review]

follows.
- **FR7 — Exits animate as the native counterpart's do.** A touch
presentation MUST play the exit its UIKit counterpart plays, on every touch
platform. A rule that turns exits off by default does not apply to touch presentations;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence sets aside a rule no current record states. The rule it answers is the open draft spec:AST-090 FR1 ("Exits are off by default"), which covers Dialog and the anchored layers and says nothing about touch presentations; if both land as written they contradict each other with no precedence (architecture:knowledge-contracts INV9). The first sentence of FR7 already requires the UIKit exit, so dropping this one lets whichever record lands second state the exception.

`design:user-states` DR1 (the pressed overlay during a hold) and DR3 (the
selected SegmentedControl item's held surface) for touch presentations, the
press treatment of `family:buttons`, and `module:DropdownMenu/useMenuPress`
FR6, whose rule that menu rows show no pressed paint under a finger gives way

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMenuPress FR6 has three sentences: "At most one row of a menu is lit", the press-start row losing its look once the pointer leaves it, and the :active-under-(hover: hover) rule. The cell profile above ends its highlight only when a scroll wins, so if all of FR6 gives way, a finger that slides from one row to another (FR1, FR2) leaves two rows lit. Naming the :active clause as the replaced sentence, and keeping the one-lit-row rule, removes the ambiguity.

its touch presentation MUST follow the same conditions in every component.
Touch behaviors key on a coarse primary pointer and are on by default there; layout keys on the available size through the one compact definition the
theme exports, the same on every platform (`spec:AST-012` FR9,
`architecture:public-component-api` INV14), and a component never restates

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV14 is "Shared breakpoint names keep one meaning". The compact-definition claim in architecture:public-component-api is INV17 ("A component that chooses a presentation by screen size uses the theme's compact definition").

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
The maintainers' ruling (`spec:AST-094`, meta#8316): touch behaviour is one
model on every touch device, iOS and Android alike, and nothing chooses a
platform. SearchField no longer gates its motion or its glass on the iOS
idiom: under a finger (a coarse primary pointer, `useTouchPresentation()`
in script and TOUCH_QUERY in CSS) it plays UIKit's activation and shows the
glass close button; under a mouse or a trackpad the parts take their
places at once and the button is plain, as before. Reduced motion stays
the animator's.

Proofs: an Android finger (Chromium with no iOS signals) plays the 383 ms
activation with the glass close button (it changed at once with a plain
button before); a mouse still changes at once. The iOS-idiom emulation is
gone from the proofs, and the jsdom role test's no-activation case is now
a mouse.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
…ed Motion

On meta#8076 @ 1203525 (AST-094 as meta#8316 states it): the back swipes are touch
behaviour under any finger, Android and iOS alike, keyed on a coarse
primary pointer (useTouchPresentation); a mouse goes back with the bar's
button. Reduced Motion is the animator's default (design:reduced-motion):
under a finger the screens fade in place in 200 ms with no dimming, under a
mouse they change at once, and what a finger's release starts fades on any
pointer (touchRelease). navigation.backChevronReducedMotionDelayMs retires
with its option; the motion roles and the docs golden are regenerated.
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

#8336 makes the gesture host the document's one touch owner. Against this record at 58cb828, some of its behaviors are forbidden by the current text and some aren't covered. Here are proposed amendments, each with the case that needs it. Accept, change or reject each one, and #8336 will follow.

  1. FR19, a page hold. "While a scroll lock under a modal presentation holds the page, it MAY cancel the moves of a one-finger touch that no recognizer owns, when that pan would scroll the page. A second finger is never cancelled, so pinch-zoom stays the browser's." Without it, useScrollLock keeps its own document listener, which does exactly this today.
  2. FR19, a tracking control keeps its touch. "A control that tracks a held finger across its own area (a stepper held to repeat, a slider's thumb) MAY keep the touch once its press is delivered. Its element registers a non-passive touchmove and cancels that finger's moves, as UIKit's scroll view never cancels a tracking control (touchesShouldCancel(in:) is false for a UIControl). This is opted into per control. Known limit: a native scroll that starts on such a control waits for a free main thread." Measured case: on Chrome for Android, a held stepper that slides onto its other button loses the touch to the page's pan at the first move, and its repeat stops.
  3. FR19 and FR20, a watcher. "A recognizer created as a watcher never claims. It registers only passive listeners, is simultaneous with every recognizer, and is not failed when the browser takes the touch (FR20), so it follows a scroll to the lift." This is for a component that reacts to the person's scrolling without delaying it, such as a tab bar that minimizes.
  4. New, one document touch owner. "Core has one document-level touch owner. Components ask it instead of listening to touches themselves: whether a scroll is a finger's (a scroll while a finger that landed inside the scroller is down, until that scroller's scrollend), how many fingers are down, and to hold touches away from a presentation's content until it settles (a touch gate)."

FR10 needs no change. #8336 now follows it: a second finger fails only a one-finger pan that has not claimed, and a claimed drag keeps its finger.

- **FR11 — Two press profiles.** Press feedback MUST follow one of two native
profiles. A control (UIControl) highlights on touch-down outside scroll
views, keeps tracking within its expanded bounds (70 pt), turns its highlight
off and on as the finger drags out and back, and activates on a lift inside.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR11's control profile can't be met on the web as written, and meeting it changes how a control is activated, so this needs a ruling on the spec before any code.

Measured in Chromium with touch emulation, a held finger on a button, on a page with nothing to scroll:

  • Default touch-action: by 20 px of drag the browser sends pointercancel. It claims the pan even with nothing to scroll (FR20), so the press is cancelled long before 70 pt, and a lift back on the button fires no click.
  • touch-action: pinch-zoom on the button: the pointer survives the drag, but once the finger has moved the browser fires no click, even for a lift inside the button.

So tracking out to 70 pt would only show a press the browser won't activate, unless two things change. Proposal, scoped to where UIKit tracks (a control with no scroller to take the touch: one in a fixed or sticky bar, as FR29 uses the term), under a coarse pointer only:

  1. A control in a bar gets touch-action: pinch-zoom, from static CSS decided before the touch. That matches UIKit, where a drag that starts on a toolbar button never scrolls the content, and it keeps page zoom (FR18).
  2. After a drag, a lift inside the 70 pt band activates the control from the press controller, and the browser's click for that gesture, if one fires, acts on nothing (the rule FR21 and FR26 already set for gestures and menu rows). React Aria's usePress activates this way.

Controls in scrolling content keep today's model: cancelled when the scroll takes the touch, as UIKit's are. A fine pointer is unchanged, and activation with detail === 0 always passes (FR21).

Question: should FR11 require this for controls in a bar? Or should it say that on the web a control's press ends when the browser takes the touch, and activation stays the browser's click?

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
The maintainer ruled one touch model on every touch device, iOS and
Android alike, with no platform idiom (meta#8316); a supports query keyed
to iOS is allowed only for styles WebKit itself provides or requires.
Dynamic Type is such a capability, so it is now gated by the
capability alone: the Dynamic Type CSS sits inside a supports query
for font: -apple-system-body, and the root Theme probes wherever that
keyword exists, with no touch-callout or pointer check.

Where the keyword exists but follows no reader setting, nothing
changes: macOS Safari's Body is a fixed 13px that names no category,
so no category is written and every scale stays 1. Chromium and
Playwright's Linux WebKit do not support the keyword.

The two tests that checked the iOS idiom now check the capability:
an engine with a touch callout but no font: -apple-system-body writes
nothing, one with the keyword and no touch callout follows the
reader's text size, and a fixed 13px Body writes no category. Docs,
the story readout and the changeset say where it applies.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
The maintainers ruled one touch model on every touch device, iOS and Android
alike, with no platform idiom (the touch resolver in meta#8062, now in meta#8076; AST-094
in meta#8316): touch behaviors key on a coarse primary pointer, and the resolver no
longer reports an idiom. The slider's touch presentation (the thumbs' touch
targets, UISlider's tracking and coast, the track's tap, the lift) now applies
under any finger; a mouse or a trackpad keeps the pointer handlers. The docs,
story and changeset say "under a finger" instead of "on iOS".

Tests: the Android cases that kept today's slider are flipped. The unit case
gives a coarse pointer with no iOS engine (no -webkit-touch-callout) the touch
targets and leaves a finger on the track to the touch presentation; it fails on
the previous head. The interaction cases run with no iOS-engine emulation, and
the Android case now checks the same model there: the lift, the travel past the
threshold and a track tap that sets the value on its lift (the Android Reduce
Motion case was the iOS one's duplicate and goes). The fine-pointer cases stay
as they were: no touch targets, and the desktop geometry case's pointer
styles.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
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, a Reduce Motion cross-fade
(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 purpose-named roles; thresholds stay internal.
useMenuPress, DropdownMenu, ContextMenu and Popover point to the new
requirements.

Retries meta#8316, whose review rounds were spent, carrying its last revision
plus the review fixes: the record is current with its deciders named, FR34
follows DropdownMenu's drill-in (a parent-visible sub-menu is OQ10), the
role list covers row swipes, Popover and sub-menus with purpose-led names,
FR2 admits an owner decision as a source, and the quick-tap and
longPressDelayMs wording is settled.

Co-authored-by: vjeux <197597+vjeux@users.noreply.github.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…th the menu beside it

The lift stays inside the layer runtime's viewport gutter (spec:AST-059),
read through a probe and carrying the LayerProvider's inset, instead of
UIKit's own margins.

The copy grows out of the trigger's rect and back on core's rect morph
(motion/rectMorph.ts), laid out at the trigger's size so its content scales.

The tint is the theme's modal overlay (--color-overlay), with no token of
its own.

The lift follows spec:AST-094 FR33 (meta#8316) and only applies with the touch
model: presentation="popover" keeps the menu where it opens.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…y touch device

One touch model (AST-094, meta#8316): the spring presentation keys on a coarse
primary pointer (`useTouchPresentation().isTouch`) instead of iOS's idiom,
so a finger on Android opens and closes the sheet on UIKit's spring, holding
input while it presents, as on iOS; a mouse keeps today's transitions. The
Android cases flip, and the reduced-motion fade runs for both.
@cixzhang cixzhang closed this Oct 25, 2026
@cixzhang
cixzhang deleted the spec/ast-094-touch-native-default branch October 25, 2026 17:37

This branch was successfully deployed

1 active deployment
Preview — 58cb828a Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants