From c7840845f20321a26b66827f132510664845f904 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 24 Oct 2026 15:21:08 -0700 Subject: [PATCH 01/17] spec(AST-094): touch presentations feel native by default 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> --- .../specs/AST-094-touch-presentations/spec.md | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 docs/specs/AST-094-touch-presentations/spec.md diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md new file mode 100644 index 0000000000000..469509af16807 --- /dev/null +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -0,0 +1,397 @@ +--- +schema_version: 4 +template_version: 2 +kind: system-spec +id: spec:AST-094 +authority: draft +archive_reason: null +superseded_by: null +approved_by: null +approved_at: null +phase: proposed +owners: [vjeux] +affects_architecture: + [ + architecture:interaction-modality, + architecture:theme-tokens, + architecture:theme-authoring-contract, + ] +affects_families: [family:buttons] +affects_contributing: [] +affects_consumer_docs: [] +--- + +# Touch presentations feel native system spec + +## Intent + +A person using an Astryx product on a phone or tablet gets controls, sheets, +menus, lists and navigation with native-feeling touch behavior: measured +physics, timings and gesture thresholds instead of desktop motion stretched to +a finger. This behavior is the default on every touch device, iOS and Android +alike, with no opt-in, and its motion keeps running at display rate while the +page's JavaScript is busy. + +The behavior comes from one measured table. Its values are measured from UIKit +on a named reference release, UIKit on iOS 26.2 today; each number names the +release it was taken from, moving the reference to a newer release re-measures +the table, and differences on older releases are noted, not targeted. Android +touch devices get the same behavior. How touch behavior differs between the +platforms is being explored, and the table records a separate value only where +a measured, sourced difference exists. + +## Non-goals + +- Pointer (desktop) presentations. Their timing and easing stay with the + theme's duration and easing tokens (`architecture:theme-tokens`). +- Platform-matched look and feel: glass versus material surfaces, glyphs and + chrome. This record governs behavior only (OQ3). +- WebKit embedded in a native app and the native Swift package. This record + governs the web components themselves. +- Scroll physics. Scrolling stays native on every platform: on iOS an overflow + area or the page is a UIKit scroll view already. +- Which components get a touch presentation, and when. Each component's own + record owns its presentation. +- Equivalent internal implementations remain valid when they satisfy this + contract. The modules, files and algorithms of the motion and gesture + foundations belong to their architecture records. + +## Requirements + +### Physics + +- **FR1 — Measured values, the same everywhere.** Every spring, duration, + deceleration rate, rubber-band coefficient, gesture threshold and haptic + moment a touch presentation uses MUST default to the measured native value + for that use (FR2), and every component that plays the same motion or + gesture MUST use the same value; a theme MAY override a motion role (FR4). + No component uses a value of its own where a measured value exists. The same + values apply on every touch platform; a platform gets a value of its own only + where a measured, sourced difference exists (FR2). +- **FR2 — Every value has a source.** Each measured value MUST name its source next to it: Apple's documentation, a WWDC session, WebKit's own + source where WebKit states it matches UIKit, or a measurement of UIKit on + the iOS simulator naming the app, the iOS version, the method and the date. + A value without one of these MUST be labelled a guess and name the + measurement that will replace it. A decompilation of UIKit MAY aim a + measurement and MUST NOT be a source. +- **FR3 — Native physics.** Springs MUST follow UIKit's spring model + (response and damping ratio, mass, stiffness and damping, and the release + velocity), with overdamping clamped to critical damping wherever UIKit + clamps it. A momentum coast MUST decay like a UIKit scroll view (0.998 per + millisecond, 0.99 for the fast rate), a drag past an edge MUST resist with + UIKit's rubber band `(1 − 1 ÷ (x·c ÷ d + 1))·d` with the coefficient its use + measures (0.55 for a scroll view and a sheet), and a release MUST pick its + rest point by projecting its velocity the way its UIKit counterpart does: + with a deceleration rate, or with a measured projection time. +- **FR4 — Touch motion is themeable.** Every spring, duration and curve a + touch presentation plays MUST be a theme motion role, named by its table path + (`sheet.present`, `timing.curves.easeInOut`). The role's default value is + its table value, and a theme MAY override it through `motion`, at its root or + in an adaptation rule. A component MUST read touch motion through these roles + and MUST NOT play a literal. A theme value MUST reach the motion at its + start, without changing how the motion runs (FR5, FR6). Gesture thresholds, + the velocity filter, the timings that decide who gets a touch (the + content-touch delay, the scroll brake, momentum memory) and haptics are not + motion: they stay internal constants from the table and are not public + concepts (`spec:AST-002` FR3). This adds touch motion roles to the motion + configuration of `architecture:theme-authoring-contract` and to + `architecture:theme-tokens` beside the existing duration and easing tokens, whose rules are unchanged. It + amends `spec:AST-057` for touch presentations: Item's swipe springs and + durations become theme motion roles instead of constants in source, and its + swipe travel is a transform the recognizer writes (FR9) instead of a custom + property on the root (its FR2). It departs from the draft `design:motion` DR3 + wherever the native motion settles with an overshoot or stretches with a + rubber band. + +### Motion + +- **FR5 — Motion runs on the compositor.** Every release and every + programmatic animation of a touch presentation MUST animate `transform`, the + individual `translate`, `scale` and `rotate` properties, or `opacity` + (a dialog's `::backdrop` included) through Web Animations that the browser + runs off the main thread, + and MUST keep advancing at display rate through a main-thread block. It MUST + NOT animate layout properties, and MUST NOT step a frame from script when the + compositor can run the curve. +- **FR6 — Motion is interruptible.** A finger MUST be able to catch any + animation mid-flight at the value on screen. That value and its velocity + MUST come from the curve, not from a layout read. A new target MUST keep the + current velocity, and a release MUST hand its velocity to the spring that + 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; + FR8 governs their reduced-motion form. Each overlay's own record states its + exit, since members own their animation (`family:overlay-dismissal` AV6). +- **FR8 — Reduce Motion cross-fades.** When the person asks for reduced + motion (`prefers-reduced-motion: reduce`), a touch presentation MUST reach + each change of state by a cross-fade: it animates `opacity` only, with no + travel, no scale and no spring, over at most 200 ms + (`design:reduced-motion`). An entrance lands in place and fades in, an exit + fades out in place, and any other move fades through. A drag still follows + the finger while it is down; after the lift the presentation reaches its + committed state by the cross-fade. A core component MUST NOT keep its motion + under reduced motion (no `reducedMotion: 'keep'`). The cross-fade's duration + is a theme motion role whose default is at most 200 ms (FR4); a theme MAY + shorten it. This is iOS's Reduce Motion with Prefer Cross-Fade Transitions + on. `design:reduced-motion` owns the rule for every presentation, including + the pointer presentations this record leaves out, which change state at + once. + +### Gestures + +- **FR9 — A finger is tracked off the render path.** A drag MUST follow the + finger with one visual update per move, with no component render per move + and no layout read after the gesture begins. + Native scrolling MUST be used only where the component is a scroll view + (lists, scroll areas, the page). A component MAY track a finger with native + scroll snapping only with simulator evidence that it tracks through a + main-thread block, ends on the native release curve, ends correctly when the + finger lifts during a block, and lets a finger catch the content mid-flight. +- **FR10 — A second finger never breaks the first.** A second finger MUST NOT + cancel or redirect a drag a recognizer has claimed. Page pinch-zoom MUST stay + the browser's whenever no pinch recognizer claims the fingers and no drag is + claimed (WCAG 1.4.4). A menu's press-tracking slide keeps + `module:DropdownMenu/useMenuPress` FR4: a second pointer or `pointercancel` + ends its gesture with nothing acting, which keeps the page zoomable over an + open menu. +- **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. + A cell (a table or collection cell) highlights 150 ms after touch-down inside + a scroll view, drops the highlight when the scroll wins, and activates on a + lift inside it with no scroll. A control inside a scroll view (an overflow + area or the scrolling page) takes the same 150 ms delay before its highlight + and then follows its control profile; the 150 ms delay applies only inside + scroll views. A control whose own record keeps its press through a drag + keeps it: Slider's overlay stays on the dragged thumb (`design:user-states` + DR4). On every touch platform this amends + `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 + to the cell profile for menu rows. +- **FR12 — Long press and rubber band are per use.** A long press MUST use the + thresholds its UIKit counterpart uses: 0.5 s and 10 pt for a plain long + press, the measured values for context menus and menus. A rubber band MUST + use the coefficient its use measures. +- **FR13 — One rule for the touch presentation.** Whether a component shows + 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 + its values or keeps a parallel breakpoint table. A presentation asks for + exactly the conditions its own record names: one that depends on both (a + sheet instead of a popover) asks for both, and the date and time sheet, whose + record asks for a coarse pointer with no size condition (`spec:AST-043` FR1), + asks for the pointer only. No component answers these conditions differently + from another. A server render and the hydrating render produce the pointer + presentation, and the touch presentation applies after hydration + (`spec:AST-013` FR9). +- **FR14 — Nested scrolling follows the native rule.** Who gets a drag that + starts inside a scroll area MUST be decided once, when the drag begins. If + the area sits at its edge and the drag moves toward that edge, the + surrounding surface (a sheet) takes the drag; otherwise the area keeps it for + the whole gesture and rubber-bands at its edge. The drag MUST NOT change + hands midway, in either direction. This amends `spec:AST-025` FR14 (scroll + chaining by default) for touch presentations on every touch platform. +- **FR15 — Recognizers arbitrate like UIKit's.** Recognizers that see the + same touch sequence MUST be exclusive by default: the first to recognize + wins and the others fail, unless both allow simultaneous recognition. A + recognizer that requires another to fail MUST hold its recognition until + that one fails, and MUST fail if that one recognizes. When several recognize + on the same event, the innermost element's recognizer MUST win. +- **FR16 — Gestures keep the event clock.** Recognizers MUST read time from the + input events' timestamps. Durations (a long press, the interval between + taps) MUST be deadlines on that clock, so a blocked main thread delays the + report and never the moment reported. +- **FR17 — Native readings.** A recognizer's thresholds and readings MUST + follow UIKit's and come from the table. A pan begins at 10 pt of travel on + either axis, and its translation excludes that travel; its velocity is + UIKit's estimate, 0.2 × the newest event's instantaneous velocity + 0.8 × + the previous event's, never decayed through a rest. +- **FR18 — `touch-action` leaves the browser the rest.** An element with touch + recognizers MUST get a `touch-action` that leaves the browser exactly what + its recognizers do not use: the other axis for an axis-limited pan or swipe, + `manipulation` for taps and long presses, everything for a one-way pan or a + 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 + touch listener is registered (FR24). A recognizer that begins MUST claim its + touches by cancelling `touchmove` through a non-passive listener on its + element. A `touchmove` of a touch no recognizer owns MUST NOT be cancelled. A + recognizer that competes with native scrolling MUST decide within 10 px of + travel, before the browser commits to the scroll. Only an element with a + recognizer that must claim mid-gesture registers that blocking listener; an + element whose recognizers decide only at the lift keeps its `touchmove` + listener passive, and its `touchend` listener stays cancelable so the + gesture can cancel the click (FR21). Known limit: a native scroll that starts on an element with a + blocking listener waits for a free main thread in iOS Safari and Chromium. + That is never worse than UIKit, whose scrolling stops on a busy main thread, + and a passive listener, or one added at `touchstart`, would hand diagonal + drags to the scroll. +- **FR20 — The browser can take a touch.** `pointercancel`, an uncancelable + `touchmove`, or a native scroll under the touch MUST count as the browser + taking the touch: possible recognizers that share it fail, active ones are + cancelled, and the view's press is cancelled without a release. +- **FR21 — Clicks follow the gesture.** When a gesture takes a touch, its + `touchend` MUST be cancelled, and no click or compatibility mouse event + reaches the page. A click held for a recognizer that may still recognize (a + tap that a double tap may claim) MUST be replayed on its target if that + recognizer fails. Activation with `detail === 0` (keyboard, assistive + technology) MUST always pass. A tap MUST NOT wait for the browser's + double-tap delay. +- **FR22 — The platform keeps its edge gestures.** iOS Safari's back swipe + from the screen edge and Android's system back gesture MUST stay the + browser's and the system's; a touch presentation never overrides them. + +### Haptics + +- **FR23 — Haptics where the native counterpart plays them.** A touch + presentation MUST play a haptic only at the moments its UIKit counterpart + does. In iOS Safari the only haptic a page can produce is a light impact from + a real tap landing on a `