From a8a805bbed6939608e2bfbb2556854ae0c03f531 Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:48:17 -0800 Subject: [PATCH 01/11] spec(AST-094): gesture-driven motion plays a spring on every pointer --- .../specs/AST-094-touch-presentations/spec.md | 157 +++++++++++++++--- .../AlertDialog/AlertDialog.spec.md | 42 ++--- .../ContextMenu/ContextMenu.spec.md | 29 ++-- .../components/DateInput/DateInput.spec.md | 4 +- .../DropdownMenu/DropdownMenu.spec.md | 2 +- .../components/Lightbox/Lightbox.spec.md | 38 ++--- docs/specs/components/Popover/Popover.spec.md | 2 +- .../ProgressBar/ProgressBar.spec.md | 53 +++--- docs/specs/components/Slider/Slider.spec.md | 29 ++-- docs/specs/components/Switch/Switch.spec.md | 133 ++++++++------- .../components/TreeList/TreeList.spec.md | 22 +-- 11 files changed, 320 insertions(+), 191 deletions(-) diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 0732ea85d80ea..75a91e20cd5d0 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -35,10 +35,13 @@ affects_consumer_docs: [] 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. +physics and gesture thresholds instead of desktop motion stretched to a +finger. What a person drags follows their hand, and what they throw settles on +a spring from the speed of the throw, under every pointer. Motion that a tap, +a click, a key or the program starts plays the theme's default motion (FR44). +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 @@ -50,9 +53,12 @@ a measured, sourced difference exists. ## Non-goals -- Pointer (desktop) presentations' timing and easing, which stay with the - theme's duration and easing tokens (`architecture:theme-tokens`). Gestures - and press tracking themselves apply to every pointer (FR43). +- The timing and easing of motion that no gesture drives. Motion that a tap, + a click, a hold, a key or the program starts, such as an entrance, an exit + or a change of value, plays the theme's duration and easing tokens + (`architecture:theme-tokens`) under every pointer, a finger included + (FR44). Gestures, press tracking, a part that follows a pointer, and the + spring that settles a gesture apply to every pointer (FR43, FR44). - Platform-matched look and feel: glass versus material surfaces, glyphs and chrome. This record governs behavior, and touch sizing only as FR42 states. - WebKit embedded in a native app and the native Swift package. This record @@ -123,7 +129,7 @@ a measured, sourced difference exists. 310.7–329.9 pt/s; releases at rest stay at 45–48.5 % and advance at 55–57.5 % (DEC-25). - **FR4 — Touch motion uses named roles.** Every spring, duration and curve a - touch presentation plays MUST be a motion role named for its use + touch presentation plays for a gesture (FR44) MUST be a motion role named for its use (`architecture:theme-tokens` INV3). The roles this record names are listed below with their defaults and sources; further roles a component's own record measures join through the generated motion role table (DEC-5). With tempo 1 and character @@ -173,8 +179,20 @@ a measured, sourced difference exists. | `timing.curves.move` | cubic-bezier(0.42, 0, 0.58, 1) | measured: UIKit's ease-in-out, iOS 26.2 simulator, 2026-10-23 | The three `springs.contextMenu*` rows are measured for ContextMenu's lift, - and `component:ContextMenu` owns them (DEC-5). They play only in a - ContextMenu that a touch opens. + and `component:ContextMenu` owns them (DEC-5). + + A role plays only for motion a gesture drives (FR44): a release's settle, a + flick, a pan's or a pinch's settle, a rubber band's return, and a follow. + The theme's `character` normalizes a gesture's spring and its `tempo` + scales the spring's duration (`spec:AST-108` FR3); the follow itself and + the rubber band stay fixed (`spec:AST-108` FR4). The rows measured for + motion that a tap, a hold, a key or the program starts + (`springs.menuOpen`, `springs.menuClose`, `springs.subMenuOpen`, + `springs.subMenuBack`, the three `springs.contextMenu*` rows, + `timing.stateChangeMs` and the four `timing.curves.*` rows) stay as the + measured native reference, and no motion plays them: that motion plays the + theme's duration and easing tokens (FR44). A component's own record states + which of its roles a gesture drives. Implementation gap: the pager's measured paging motion (its settle integrator, its 300 ms animated scroll and that scroll's sinusoidal curve, @@ -201,10 +219,33 @@ a measured, sourced difference exists. (UIKit zeroes steady releases at 175 and 200 pt/s and keeps 249 pt/s, iOS 26.2 simulator, 2026-10-23) until a measurement replaces it (FR2). A page change during a drag starts from rest (DEC-25). -- **FR7 — Exits animate as the native counterpart's do.** A touch - presentation MUST play the exit its UIKit counterpart plays, on every touch - platform. FR8 governs their reduced-motion form. Each overlay's own record states its - exit, since members own their animation (`family:overlay-dismissal` AV6). +- **FR7 — Exits keep the native counterpart's look.** A touch presentation's + exit MUST keep the look of its UIKit counterpart's exit, on every touch + platform: a menu shrinks into its trigger, and an alert fades. An exit that + a tap, a key, an outside press or the program starts plays on the theme's + exit duration and easing tokens (`spec:AST-090`, FR44); an exit that a drag + commits springs from the release velocity (FR38). FR8 governs their + reduced-motion form. Each overlay's own record states its exit, since + members own their animation (`family:overlay-dismissal` AV6). +- **FR44 — A gesture's motion springs; other motion plays the theme's + default motion.** Motion that a gesture drives MUST play a spring that + starts at the gesture's velocity, under every pointer (FR43). A gesture is + a continuous pointer movement whose velocity carries into the motion: a + drag's release, a flick, a swipe, a pan's settle, a pinch, a rubber band's + return, and a follow and its settle. The theme's `character` normalizes the + spring's parameters and its `tempo` scales the spring's duration + (`spec:AST-108` FR3, FR4). A part that follows a pointer follows it under + every pointer (FR43). Every other motion, which a tap, a click, a hold, a + key or the program starts, MUST play the theme's default motion, its + duration and easing tokens (`architecture:theme-tokens`, `spec:AST-108` + FR2), under every pointer, a finger included. A tap has no velocity, so it + is not a gesture. A touch presentation keeps its own look for that motion + (FR13), such as a menu that grows from its trigger or a thumb that lifts, + and plays it on those tokens. A press that starts as a tap or a hold and + continues as a drag springs from the drag's release. Gesture thresholds, + the press model's clocks and haptics are not motion and stay as FR4 states. + Under reduced motion, `design:reduced-motion` governs both kinds (FR8, + FR38, FR43). - **FR8 — Reduce Motion.** Under reduced motion, touch presentations follow `design:reduced-motion` DR2 and DR4. @@ -271,6 +312,12 @@ a measured, sourced difference exists. (the date and time sheet asks for the pointer only, `spec:AST-043` FR1). A style keyed on the literal touch media query conforms, since the server and hydrating markup are identical (DEC-6). + A touch presentation is the look and press timing a component adds under + this condition: a lift, a sheet in place of a popover, a grow from the + trigger, hit-area growth (FR42), touch press timing (FR11) and haptics + (FR23). It never decides whether a part follows a pointer or whether a + gesture's settle springs (FR43, FR44), and its other motion plays the + theme's duration and easing tokens (FR44). - **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 @@ -433,9 +480,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and - **FR31 — Menus and popovers grow from their trigger and shrink back.** Under a finger, on every touch device, an anchored menu or a Popover MUST open by growing out of its trigger and close by shrinking back into it, on - its motion roles (FR4; a menu on `springs.menuOpen` and - `springs.menuClose`; a lifted ContextMenu's menu grows on - `springs.contextMenuMorph`). The shrink plays in full after the surface has + the theme's entrance and exit duration and easing tokens (FR44). The shrink plays in full after the surface has 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. @@ -456,6 +501,8 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and finger opens on a block trigger MUST lift the trigger above the page and show the menu beside it, with no row lit. A tap outside closes the menu and reaches nothing beneath it, and closing returns the trigger to its place. + The lift and the return play on the theme's duration and easing tokens + (FR44). Under reduced motion FR8 applies. An inline trigger keeps today's menu, a menu presented as a bottom sheet follows the sheet, and `presentation="popover"` keeps the released menu without the lift (`architecture:public-component-api` @@ -465,8 +512,8 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and sub-menu's rows and a leading "Back to " row replace the menu's rows in the same surface, no flyout opens, and Back returns to the row that opened it (`component:DropdownMenu` FR11 and FR20). The change between - levels plays on the motion roles `springs.subMenuOpen` and - `springs.subMenuBack` (FR4); under reduced motion FR8 applies. + levels plays on the theme's move duration and easing tokens (FR44); under + reduced motion FR8 applies. - **FR35 — A presentation takes no input while it enters.** Under a finger, a tap or a drag that lands while a touch presentation is still entering, a Popover or an anchored menu growing out of its trigger, an alert @@ -493,9 +540,11 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and `spec:AST-057`'s own release rule. - **FR37 — Moving back cancels.** A release whose velocity points back toward the start MUST cancel, whatever the travel so far. -- **FR38 — The settle continues the finger.** The commit or the spring-back - MUST start at the release velocity (FR6). Under reduced motion it follows - `design:reduced-motion` DR2. +- **FR38 — The settle continues the pointer.** The commit or the spring-back + MUST start at the release velocity (FR6), under every pointer (FR43), on a + spring that the theme's `character` normalizes and its `tempo` scales + (FR44). Under reduced motion it follows `design:reduced-motion` DR2, and a + release from a mouse, a trackpad or a pen follows FR43. - **FR39 — The action never needs a drag.** Every action a drag commits MUST also be reachable without a drag, by keyboard and by mouse (`architecture:interaction-modality` INV4). @@ -579,7 +628,12 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and component or module record sets on purpose, such as `component:Thumbnail` AR4's, is unaffected. Mobile size increases belong to theme adaptations (DEC-20). - **FR43 — Gestures work on the web.** Gestures attach for every pointer, a - mouse, a pen, and a finger, not only a coarse primary pointer. These stay + mouse, a pen, and a finger, not only a coarse primary pointer. A part that + follows a pointer's drag, such as a switch's or a slider's thumb, a sheet, a + pager, a picker wheel or a row, follows a mouse, a pen, a trackpad and a + finger alike, with the same rubber band past its ends, wherever its record + lets that pointer drag it, and a release from any of them settles on FR44's + spring from the release velocity. These stay off: row swipe actions under a mouse drag, which would take the browser's text selection and native drag, while a trackpad's horizontal scroll swipes a row (`spec:AST-057` AR2, DEC-7); a @@ -704,9 +758,9 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and stretch for the same travel; a mouse wheel scrolls its content and never moves it. Implementation gap: a finger drags it one for one below rest. - The shared pager (Carousel's paging presentation, Lightbox's viewer): a - trackpad swipe follows FR43's pager rule in Lightbox's viewer under every - pointer (`component:Lightbox` FR11), and in Carousel's paging presentation - where the primary pointer is coarse. + trackpad swipe follows FR43's pager rule in Lightbox's viewer and in + Carousel's paging presentation, under every pointer (`component:Lightbox` + FR11). - `architecture:react-component-runtime` INV13: gestures attach for every pointer, within FR24's listener budget. - `design:user-states` DR1, DR3 and `family:buttons`: press timing follows @@ -733,6 +787,28 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and - BottomSheet, Toast, Lightbox, NavigationStack and keyboard dismissal: a drag that commits follows FR36–FR40. - `spec:AST-100` owns release velocity, which FR17 reads. +- Under FR44, motion that a tap, a hold, a key or the program starts plays + the theme's duration and easing tokens under a finger as under a mouse. + The records that state such motion carry it: `component:Switch` FR9, FR10 + and FR15, `component:Slider` FR7, `component:ContextMenu` FR5, + `component:DropdownMenu` FR21, `component:Popover` FR9, + `component:AlertDialog` FR6, `component:Lightbox` FR5, FR12 and FR16, + `component:TreeList` FR8 and `component:ProgressBar` FR4. A touch exit that + a tap starts plays on `spec:AST-090`'s exit tokens, which are off by + default, so it closes at once unless the theme sets an exit duration + (FR7). Implementation gap: Switch, Slider and ProgressBar still play their + measured roles for a tap or a value change under a coarse pointer. +- Under FR44, `spec:AST-105` FR4's glide, which `component:SideNav` FR6 and + `component:AppShell` FR17 ride, and `spec:AST-107`'s `settleTo` and an + `animate` call without a velocity play a spring for motion that no gesture + drives. Their records keep their springs until each carries FR44. +- Under FR43 and FR44, a part that follows a pointer follows every pointer + and its release springs from the release velocity under every pointer: + `component:Switch` FR15's thumb under a mouse or a pen, `component:Slider` + FR7's coast after a mouse or a pen release, and `component:DateInput` + FR15's wheel under a trackpad wherever the sheet shows. Implementation gap: + Switch's thumb does not follow a mouse or a pen, and Slider's mouse and pen + releases do not coast. - `design:user-states` DR1 and DR3, `spec:AST-025` FR14 and `spec:AST-013` FR3 each carry an amendment for touch presentations (FR11, FR14, Platform support); `design:motion` DR3 and DR5 (draft) are departed from or met as @@ -753,6 +829,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and | FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring; a retarget before the browser reports the call's start | a layout read, a jump, or a lost velocity fails | | FR6 pager | Unit and browser tests of a pager's settle start | releases just under and over 225 pt/s; a catch mid-settle; a programmatic page change mid-settle | a release under the floor whose settle starts moving, a release over it whose settle starts from rest, a catch that loses the on-screen velocity, or a programmatic page change mid-settle that starts from rest fails; a page change during a drag starts from rest | | FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | +| FR44 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | | FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | | FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | | FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | @@ -1083,6 +1160,34 @@ applies unchanged. Rejected: projecting a paging release with a deceleration rate; handing a slow paging release's velocity to the settle. +### DEC-26 — A gesture's motion springs from its velocity; other motion plays the theme's tokens + +**Reference:** `spec:AST-094/DEC-26` +**Decider:** `cixzhang`, `2026-11-02` + +Motion that a gesture drives continues the person's hand, so it starts at the +gesture's speed and settles on a spring under every pointer: a flick with a +mouse throws as a flick with a finger does. Motion that a tap, a click, a key +or the program starts has no speed to carry, so it plays the theme's default +motion, its duration and easing tokens, under every pointer, and a theme's +timing reaches it as it reaches every other timed motion. A part that follows +a pointer is not timed motion, so it follows every pointer. The theme's +`character` normalizes a gesture's spring and its `tempo` scales it, so one +theme feels the same for both kinds of motion. + +The owner decided this on 2026-11-02, in four steps: motion stays with the +theme's duration, but elements that follow an interaction are outside those +timings; a mouse release settles on a spring from the release speed, so a +flick keeps its throw; the theme's character normalizes those springs and its +tempo scales them, and any interaction that involves a gesture uses a spring; +and a tap has no velocity, so an interaction without a gesture uses the +default motion, which applies the theme's motion tokens. This narrows DEC-1's +measured native timing to the motion a gesture drives, and extends the +Non-goals' theme timing to every pointer. + +Rejected: keeping the measured springs for motion a tap starts under a finger; +a theme curve for a mouse release; following a pointer only under a finger. + ## Open questions - **OQ1 — Scroll view bounce-back curve.** What curve brings a scroll view diff --git a/docs/specs/components/AlertDialog/AlertDialog.spec.md b/docs/specs/components/AlertDialog/AlertDialog.spec.md index 4d2e3646a011a..9de55e41450e7 100644 --- a/docs/specs/components/AlertDialog/AlertDialog.spec.md +++ b/docs/specs/components/AlertDialog/AlertDialog.spec.md @@ -44,15 +44,15 @@ system_specs: ## Contract at a glance -| Area | Contract | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Public contract | `AlertDialog`, `AlertDialogProps`, and `useImperativeAlertDialog` are exported from `@astryxdesign/core/AlertDialog`; `title`, `description`, `actionLabel`, `onAction`, and `onOpenChange` are required with `isOpen`, plus cancel label, action variant, loading, width, and an inline preview mode. | -| Behavior | A modal confirmation that interrupts: it opens through the native dialog, cannot be dismissed by a click outside, closes on Escape or Cancel through `onOpenChange(false)`, and runs `onAction` on the action button. | -| End-user impact | A keyboard or screen-reader user is told a decision is needed (the title and description are announced), lands on Cancel, and cannot lose the dialog by clicking past it. On a touch screen the alert scales in and fades out as iOS's does and ignores taps while it enters (FR6, FR7), and on a phone it is 320 px wide (FR9); under every pointer, mouse included, a press that slides from Cancel to the action runs the action it lifts on (FR8). | -| Builder impact | None. | -| Compatibility/readiness | FR1–FR5 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6–FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. | -| Review checks | Reject a modal AlertDialog without `role="alertdialog"`, a title or description not linked by id, initial focus on the destructive action, or a backdrop click that dismisses it. | -| Governing rules | `architecture:public-component-api/INV1,INV5–INV8`; `architecture:component-theming-surface/INV3–INV7`; `architecture:layer-runtime`; `component:Dialog/AR1–AR3`; `spec:AST-020/FR15`; `spec:AST-094` FR4, FR13, FR35, DEC-15, DEC-17; `family:action-groups`; `design:reduced-motion`; `spec:AST-017`. | +| Area | Contract | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Public contract | `AlertDialog`, `AlertDialogProps`, and `useImperativeAlertDialog` are exported from `@astryxdesign/core/AlertDialog`; `title`, `description`, `actionLabel`, `onAction`, and `onOpenChange` are required with `isOpen`, plus cancel label, action variant, loading, width, and an inline preview mode. | +| Behavior | A modal confirmation that interrupts: it opens through the native dialog, cannot be dismissed by a click outside, closes on Escape or Cancel through `onOpenChange(false)`, and runs `onAction` on the action button. | +| End-user impact | A keyboard or screen-reader user is told a decision is needed (the title and description are announced), lands on Cancel, and cannot lose the dialog by clicking past it. On a touch screen the alert scales in and fades out with iOS's look, on the theme's motion tokens, and ignores taps while it enters (FR6, FR7), and on a phone it is 320 px wide (FR9); under every pointer, mouse included, a press that slides from Cancel to the action runs the action it lifts on (FR8). | +| Builder impact | None. | +| Compatibility/readiness | FR1–FR5 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6–FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. | +| Review checks | Reject a modal AlertDialog without `role="alertdialog"`, a title or description not linked by id, initial focus on the destructive action, or a backdrop click that dismisses it. | +| Governing rules | `architecture:public-component-api/INV1,INV5–INV8`; `architecture:component-theming-surface/INV3–INV7`; `architecture:layer-runtime`; `component:Dialog/AR1–AR3`; `spec:AST-020/FR15`; `spec:AST-094` FR4, FR13, FR35, DEC-15, DEC-17; `family:action-groups`; `design:reduced-motion`; `spec:AST-017`. | This table is a review projection; the body below is authoritative. @@ -120,17 +120,17 @@ Consumer migration instructions belong in consumer docs and release notes. ## Behavioral and layout contract -| ID | Invariant | Basis | -| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| FR1 | A modal AlertDialog opens through Dialog's native modal path and a click on the backdrop does not dismiss it. | shipped source (`purpose="form"`); Alert Dialog accessibility contract (pending) | -| FR2 | Escape and the Cancel button both call `onOpenChange(false)`; the action button calls `onAction` and leaves closing to the caller. | shipped source and focused tests | -| FR3 | The anatomy is title, description, then the buttons: Cancel then action on wider screens, the action stacked above Cancel on small screens, Cancel keeping initial focus either way; the action may show a loading state while `isActionLoading`. | shipped source and focused tests | -| FR4 | `isInline` renders the same anatomy without modality and without the alert-dialog role. | shipped source and public docs | -| FR5 | `useImperativeAlertDialog` returns `show`, `hide`, and the element to render, and owns `isOpen`/`onOpenChange` for its caller. | shipped source and focused tests | -| FR6 | Under a touch presentation (`spec:AST-094` FR13), a modal AlertDialog presents by scaling from 1.2 to 1 with its opacity and its backdrop's dimming rising from 0 to 1 on the motion role `springs.alertPresent`, started at the browser's commit, with the larger start kept inside the viewport; it dismisses by a fade alone on `springs.alertDismiss` and closes at the fade's end; a close during the presentation, or a reopen during the dismissal, springs from where it is. The roles are internal, with UIKit's measured values at `tempo` 1 and `'standard'`, and follow the theme's motion knobs (`spec:AST-094` FR4, DEC-15). Under Reduce Motion both are the shared cross-fade (`design:reduced-motion` DR1–DR3). A fine primary pointer keeps today's motion (`spec:AST-094` FR13 and its Non-goals). Implementation gap: not shipped yet (Current-state impact). | proposed from measured iOS 26.2 behavior | -| FR7 | Under a touch presentation, pointer input to the alert is held while it enters (`spec:AST-094` FR35); keyboard and assistive-technology input are never held, and the entrance never makes the content inert. While the alert leaves, its content is inert for the dismissal fade. The hold's timing stays internal (`spec:AST-002` FR3). Implementation gap: not shipped yet (Current-state impact). | `spec:AST-094` FR35; owner decision (DEC-1) | -| FR8 | AlertDialog's actions are an action group (`family:action-groups`): a press that slides across them runs the action it lifts on, and a lift outside them runs nothing, under every pointer; keyboard and assistive-technology behavior is unchanged. Implementation gap: not shipped yet (Current-state impact). | `family:action-groups`; `spec:AST-094` DEC-17; DEC-3 | -| FR9 | On a phone, a compact window (`spec:AST-012` FR9: narrower than `md` or shorter than 500 px) under a coarse primary pointer, the alert is 320 px wide in every theme, as iOS's is; no theme role sets it. A caller's explicit `width` still wins there (`architecture:public-component-api` INV5), and Dialog's viewport clamp still applies on a screen narrower than the alert. Elsewhere it keeps today's width. Implementation gap: not shipped yet (Current-state impact). | owner decision (DEC-2) | +| ID | Invariant | Basis | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| FR1 | A modal AlertDialog opens through Dialog's native modal path and a click on the backdrop does not dismiss it. | shipped source (`purpose="form"`); Alert Dialog accessibility contract (pending) | +| FR2 | Escape and the Cancel button both call `onOpenChange(false)`; the action button calls `onAction` and leaves closing to the caller. | shipped source and focused tests | +| FR3 | The anatomy is title, description, then the buttons: Cancel then action on wider screens, the action stacked above Cancel on small screens, Cancel keeping initial focus either way; the action may show a loading state while `isActionLoading`. | shipped source and focused tests | +| FR4 | `isInline` renders the same anatomy without modality and without the alert-dialog role. | shipped source and public docs | +| FR5 | `useImperativeAlertDialog` returns `show`, `hide`, and the element to render, and owns `isOpen`/`onOpenChange` for its caller. | shipped source and focused tests | +| FR6 | Under a touch presentation (`spec:AST-094` FR13), a modal AlertDialog presents by scaling from 1.2 to 1 with its opacity and its backdrop's dimming rising from 0 to 1 on the theme's entrance duration and easing tokens, started at the browser's commit, with the larger start kept inside the viewport; it dismisses by a fade alone on the theme's exit tokens (`spec:AST-090`) and closes at the fade's end; a close during the presentation, or a reopen during the dismissal, turns back from where it is (`spec:AST-094` FR7, FR44). `springs.alertPresent` and `springs.alertDismiss` stay as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4). Under Reduce Motion both are the shared cross-fade (`design:reduced-motion` DR1–DR3). A fine primary pointer keeps today's motion (`spec:AST-094` FR13 and its Non-goals). Implementation gap: not shipped yet (Current-state impact). | proposed from measured iOS 26.2 behavior | +| FR7 | Under a touch presentation, pointer input to the alert is held while it enters (`spec:AST-094` FR35); keyboard and assistive-technology input are never held, and the entrance never makes the content inert. While the alert leaves, its content is inert for the dismissal fade. The hold's timing stays internal (`spec:AST-002` FR3). Implementation gap: not shipped yet (Current-state impact). | `spec:AST-094` FR35; owner decision (DEC-1) | +| FR8 | AlertDialog's actions are an action group (`family:action-groups`): a press that slides across them runs the action it lifts on, and a lift outside them runs nothing, under every pointer; keyboard and assistive-technology behavior is unchanged. Implementation gap: not shipped yet (Current-state impact). | `family:action-groups`; `spec:AST-094` DEC-17; DEC-3 | +| FR9 | On a phone, a compact window (`spec:AST-012` FR9: narrower than `md` or shorter than 500 px) under a coarse primary pointer, the alert is 320 px wide in every theme, as iOS's is; no theme role sets it. A caller's explicit `width` still wins there (`architecture:public-component-api` INV5), and Dialog's viewport clamp still applies on a screen narrower than the alert. Elsewhere it keeps today's width. Implementation gap: not shipped yet (Current-state impact). | owner decision (DEC-2) | ### Allowed variation @@ -226,7 +226,7 @@ The dialog surface (the Dialog element AlertDialog renders) carries `alert-dialo | Contract | Verification | Representative states | Mutation or failure expectation | Audit section | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | FR1–FR5 | `AlertDialog.test.tsx` ("calls onOpenChange(false) on Escape", "does not call onOpenChange when action is clicked", "does not call onAction on Escape", "does not disable the cancel button while the action is loading", "exposes a named group instead, with no aria-modal") and `useImperativeAlertDialog.test.tsx` ("opens on show()", "closes on hide()") | open, loading, inline, imperative | Escape not cancelling, the action closing by itself, or the inline path claiming the role fails. | `audit:AlertDialog/behavior` | -| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a spring-curve oracle, a busy-main-thread case, and real-device clips on iOS Safari and Android Chrome | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | +| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle, a busy-main-thread case, and real-device clips on iOS Safari and Android Chrome | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | | FR7 | Browser tests under a coarse pointer, with keyboard and assistive-technology input | a tap during the presentation, Enter and Escape during it, a screen reader's activation, a tap during the dismissal; a mouse click during the presentation under a fine pointer | a held key or assistive-technology action, content made inert during the entrance, a tap that reaches a leaving alert, or a mouse click held under a fine pointer fails. No test exists yet; FR7 is not shipped. | `audit:AlertDialog/behavior` | | FR8 | Pointer-stream tests with a mouse, a pen, and a finger, and keyboard tests | a press on Cancel slid onto the action and lifted, and a press lifted outside the actions, each with a mouse, a pen, and a finger; Enter and Space on each action | the press-start action running after a slide, any action running on a lift outside, or a changed keyboard or assistive-technology activation fails. No test exists yet; FR8 is not shipped. | `audit:AlertDialog/behavior` | | FR9 | Browser tests at a phone width and at a desktop width, in the default and an alternate theme | a phone width and a desktop width, in the default and an alternate theme; a caller `width` at a phone width; a screen narrower than 320 px; a narrow window under a fine pointer; a coarse screen that is not compact | a phone-width alert other than 320 px in any theme without a caller `width`, a caller `width` overridden, a desktop width changed, an alert wider than the viewport, or a 320 px alert in a narrow fine-pointer window or on a non-compact coarse screen fails. No test exists yet; FR9 is not shipped. | `audit:AlertDialog/behavior` | diff --git a/docs/specs/components/ContextMenu/ContextMenu.spec.md b/docs/specs/components/ContextMenu/ContextMenu.spec.md index 9ed24d8152150..1673ff841a5a2 100644 --- a/docs/specs/components/ContextMenu/ContextMenu.spec.md +++ b/docs/specs/components/ContextMenu/ContextMenu.spec.md @@ -97,18 +97,18 @@ Consumer props, content modes, and presentation policy remain documented in ## Behavioral and layout contract -| ID | Invariant | Basis | -| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| FR1 | Every render contains a caller-provided Trigger area; with `isDisabled` no invocation opens a menu and the native context menu is left alone. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | -| FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source | -| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | -| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | -| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The lift, the grow and their springs apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The trigger lifts on `springs.contextMenuLift` and returns on `springs.contextMenuDismiss`, and the menu grows on `springs.contextMenuMorph` (`spec:AST-094` FR4). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 | -| FR6 | With `triggerAs="span"` the Trigger area is an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | DEC-1 | -| FR7 | Sub-menu rows follow `component:DropdownMenu` FR11 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. A sub-menu with a header or footer shows its rows alone here (`component:DropdownMenu` FR20). | `component:DropdownMenu/DEC-4` | -| FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. Arrow keys and typeahead skip disabled rows, shipped behavior that is not a requirement until `component:DropdownMenu` OQ1 is ruled. | Current source and tests | -| FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | -| FR10 | ContextMenu MUST be built on the public headless context menu (`spec:AST-107` FR8), with its invocation, open state, and cleanup the factory's and no behavior of its own beside the factory's. Implementation gap: no headless context menu ships; ContextMenu owns its invocation, open state, outside-press listener, and cleanup itself. | `spec:AST-107` FR8 | +| ID | Invariant | Basis | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| FR1 | Every render contains a caller-provided Trigger area; with `isDisabled` no invocation opens a menu and the native context menu is left alone. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | +| FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source | +| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | +| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | +| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The lift and the grow apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The trigger's lift and return and the menu's grow play on the theme's duration and easing tokens (`spec:AST-094` FR44); the three `springs.contextMenu*` roles stay as the measured reference (`spec:AST-094` FR4). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 | +| FR6 | With `triggerAs="span"` the Trigger area is an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | DEC-1 | +| FR7 | Sub-menu rows follow `component:DropdownMenu` FR11 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. A sub-menu with a header or footer shows its rows alone here (`component:DropdownMenu` FR20). | `component:DropdownMenu/DEC-4` | +| FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. Arrow keys and typeahead skip disabled rows, shipped behavior that is not a requirement until `component:DropdownMenu` OQ1 is ruled. | Current source and tests | +| FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | +| FR10 | ContextMenu MUST be built on the public headless context menu (`spec:AST-107` FR8), with its invocation, open state, and cleanup the factory's and no behavior of its own beside the factory's. Implementation gap: no headless context menu ships; ContextMenu owns its invocation, open state, outside-press listener, and cleanup itself. | `spec:AST-107` FR8 | ### Allowed variation @@ -229,7 +229,8 @@ anchor is positioning infrastructure rather than consumer anatomy. - `spec:AST-094` owns the touch press, lift, grow, and dismissal rules (FR5). - `spec:AST-094` FR4 lists the three motion roles this component owns, measured for its lift: `springs.contextMenuLift`, `springs.contextMenuDismiss` and - `springs.contextMenuMorph`. + `springs.contextMenuMorph`. They stay as the measured reference: a long press + is not a gesture, so the lift plays the theme's tokens (`spec:AST-094` FR44). - `family:overlay-dismissal` owns shared Escape and platform-close ordering. ContextMenu's pointer presentation is a shared-stack adopter; the touch presentation delegates dismissal to BottomSheet, which is also a shared-stack @@ -245,7 +246,7 @@ anchor is positioning infrastructure rather than consumer anatomy. | FR5 | `ContextMenu.test.tsx` "acts on the row under a finger release, once, and swallows the stray click" and outside-dismissal cases; `ContextMenuLongPress.a11y.chromium.spec.ts` | Finger release; long press lift; tap, mouse, pen, and scroll outside | A row acting on the press row, a stray click that acts, a long press that closes its own menu, or a tap or mouse press outside that leaves it open fails. Evidence gap: no test asserts the FR31 grow, the opening-finger handoff, or the FR33 lift; the scroll-outside and pen cases assert the current gaps. | `audit:ContextMenu/behavior` | | FR5, DD1 touch scope | Browser tests on the touch story, the menu opened by a finger under a coarse pointer and by a mouse and by Shift+F10 under a fine pointer | Touch-opened, mouse-opened and keyboard-opened menus on a block trigger | A mouse- or keyboard-opened menu that lifts its trigger, tints the page or grows, or a touch-opened menu that blurs the page, grows a second surface, or animates anything but transform and opacity, fails. Evidence gap: the lift and the grow are unbuilt, so no test asserts either. | `audit:ContextMenu/behavior` | | FR5 Reduce Motion | Browser tests on the touch story with reduced motion | A touch-opened menu opening and closing under Reduce Motion | A lift, grow or shrink that moves rather than playing an opacity-only cross-fade of 200 ms or less (`spec:AST-094` FR8) fails. Evidence gap: no test exists. | `audit:ContextMenu/behavior` | -| FR5 motion roles | Role registry tests over `spec:AST-094` FR4 | `springs.contextMenuLift`, `springs.contextMenuDismiss` and `springs.contextMenuMorph` at tempo 1 and `'standard'` | A literal spring in the lift, or a role off its FR4 table value, fails. | `audit:ContextMenu/behavior` | +| FR5 motion roles | Role registry tests over `spec:AST-094` FR4 | `springs.contextMenuLift`, `springs.contextMenuDismiss` and `springs.contextMenuMorph` at tempo 1 and `'standard'` | A lift, return or grow that plays a spring or a literal instead of the theme's duration and easing tokens, or a role off its FR4 table value, fails. | `audit:ContextMenu/behavior` | | DEC-2 | Element-count browser test on the touch story (`spec:AST-017` FR60) | At rest and lifted, fine and coarse pointer | A lift element at rest, a copy of the trigger, or an open count beyond the layer, the shadow, the anchor and the placeholder fails. Evidence gap: the lift is unbuilt, so no test exists. | `audit:ContextMenu/anatomy` | | FR6, AR1, ORD1 | `ContextMenu.test.tsx` "inline trigger (triggerAs)" and cursor-anchor cases; "does not put aria-haspopup on the role-less trigger wrapper (menus-15)"; `ContextMenu.triggerProps.test.tsx`; `ContextMenuCursorAnchor.a11y.browser.spec.ts` | `span` in a paragraph; default `div`; pointer and keyboard anchors | A block trigger in prose, a trigger with a role or `aria-haspopup`, or an anchor outside the trigger fails. | `audit:ContextMenu/behavior` | | FR7 | `ContextMenu.test.tsx` "drills into nested data items inside the BottomSheet", "renders a submenu from a nested items array and keyboard-reaches an item after it", and "shows a sub-menu with a header or footer as its rows alone, and says why in development" | Sheet drill-in; pointer sub-menu; regions under ContextMenu | A missing sub-menu, an unreachable row after it, or a sub-menu dialog under ContextMenu fails. Evidence gap: no test here asserts the pointer drill-in on a phone. | `audit:ContextMenu/behavior` | diff --git a/docs/specs/components/DateInput/DateInput.spec.md b/docs/specs/components/DateInput/DateInput.spec.md index bcf1212a5fb26..5a8aa86084728 100644 --- a/docs/specs/components/DateInput/DateInput.spec.md +++ b/docs/specs/components/DateInput/DateInput.spec.md @@ -141,8 +141,8 @@ Consumer migration instructions belong in consumer docs and release notes. | FR11 | Clear MUST emit `undefined` through the ordinary change pipeline. The pointer and bottom-sheet surfaces restore field focus using their shipped timing; the native surface MUST NOT reclaim focus because doing so reopens the OS picker. Implementation gap: when the field becomes disabled or busy while its clear control holds focus, the control unmounts and focus falls to the document body; `spec:AST-085/FR4` requires focus to stay or move to a destination this record names. | Current source and focused clear tests; `spec:AST-085/FR4` | | FR12 | No surface renders native `disabled`. A disabled field MUST stay focusable with `aria-disabled`, block typing, picking, clearing, and opening, describe its disabled reason when one is given, and take disabled paint. While busy (`isLoading` or a pending Action) the pointer and bottom-sheet surfaces expose `aria-disabled` and `aria-busy`, stay focusable, and block new commits; the native surface does the same for `isLoading`, and during a pending Action stays editable so the OS picker remains attached but rejects additional commits until the Action settles. A busy field that is not disabled keeps its ordinary paint and Spinner, shows no hover ring while commits are blocked, and keeps its keyboard focus ring. Caller handlers other than focus and blur MUST NOT run while the field is disabled (`spec:AST-085` FR2). Implementation gap: only a caller `onClick` is skipped while disabled; caller key, paste, and pointer handlers (such as `onKeyDown`, `onKeyUp`, `onPaste`, and `onPointerDown`) still run on every surface, the native field included. | `spec:AST-085/FR1–FR6`; `family:input-fields/FR4–FR6`; `family:buttons/FR4`; current source and focused tests | | FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source | -| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it. | UIDatePicker on the iOS 26.2 simulator; current source | -| FR15 | Under a coarse primary pointer, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13. A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source | +| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR44). | UIDatePicker on the iOS 26.2 simulator; current source | +| FR15 | Wherever the sheet shows, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13, settling on a spring from that speed (`spec:AST-094` FR44). A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source | ### Allowed variation diff --git a/docs/specs/components/DropdownMenu/DropdownMenu.spec.md b/docs/specs/components/DropdownMenu/DropdownMenu.spec.md index 2eb77ab775fad..b166582bc4bb0 100644 --- a/docs/specs/components/DropdownMenu/DropdownMenu.spec.md +++ b/docs/specs/components/DropdownMenu/DropdownMenu.spec.md @@ -139,7 +139,7 @@ component-local concepts; each keeps its released default. | FR16 | Checkbox rows follow `component:DropdownMenuCheckboxItem`. | Current source | | FR17 | Arrow keys and typeahead skip disabled rows, including checkbox and radio rows. Shipped behavior, not a requirement until OQ1 is ruled. | Current source and tests | | FR20 | With `header` or `footer` on a sub-menu (`DropdownMenuSubMenu`), its surface is a modal dialog named by its row (AR7), holding the header, the sub-menu's `role="menu"` (rows only), then the footer. It sits in a host `DropdownMenu` renders beside its menu, never inside a menu; under a root with no such host (`ContextMenu`) the sub-menu shows its rows alone. On a fine pointer the dialog is the flyout; on a compact touch display, the drilled-in view, which replaces the root's menu and regions until popped. A region keeps its own keys and presses. The rows are one Tab stop: Tab and Shift+Tab walk the header's controls, the rows, then the footer's, and wrap; from a deeper flyout, that flyout closes and the walk goes on from the rows. An open lands as for any sub-menu (FR11, FR12), not on a region. Escape in the dialog, outside an IME composition and unless a control there handled it, returns focus to the row and leaves the root open. A pointer leaving the flyout keeps it open until focus leaves, and it closes with the flyout holding its row. Without either prop nothing changes. | Current behavior; FR15; AR5 | -| FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button and collapse back into it (`spec:AST-094` FR31), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own. | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6 | +| FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button and collapse back into it on the theme's duration and easing tokens (`spec:AST-094` FR31, FR44), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own. | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6 | ### Allowed variation diff --git a/docs/specs/components/Lightbox/Lightbox.spec.md b/docs/specs/components/Lightbox/Lightbox.spec.md index 8c28a713c8e00..e80f579f2f9e5 100644 --- a/docs/specs/components/Lightbox/Lightbox.spec.md +++ b/docs/specs/components/Lightbox/Lightbox.spec.md @@ -131,24 +131,24 @@ FR21). Consumer props and usage remain documented in `Lightbox.doc.mjs`. ## Behavioral and layout contract -| ID | Candidate invariant | Basis | Draft review state | -| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------- | -| FR1 | The viewer contains one active Media item and a Close button; galleries additionally contain Previous and Next buttons plus a Counter, and Caption renders only when supplied. | Current source, docs, and tests | settled | -| FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled | -| FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled | -| FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap | -| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring, duration, and curve they play is a named motion role (zoom settle, page settle, dismiss, pinch close, controls fade, zoom transition) read through the motion role table, never a literal. Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap | -| FR6 | With `hasZoom`, on an image, a pinch zooms about the fingers and pans while it zooms; past the minimum or maximum the photo resists and eases back without overshoot. A double tap zooms to the larger of filling the viewer and 2×, about the tapped point; a second double tap returns to the fitted size. A zoomed photo pans with momentum and rubber-band edges. A pinch on the controls or the caption still zooms the page. | `spec:AST-094` FR3, FR10 | settled; implementation gap | -| FR7 | In a gallery, a horizontal drag moves the row of items, which sit apart by the measured gap. The release lands on at most one neighbouring item by `spec:AST-094` FR3's paging rule (DEC-25). Paging applies over a video item's frame as over a photo; a drag that starts on the video's own controls, such as its scrubber or volume, stays the video's. A zoomed photo pans to its edge before a new drag pages. A pinch never pages. Previous, Next, and the arrow keys keep paging. A controlled `index` the caller refuses springs back. `onIndexChange` reports a swipe. | `spec:AST-094` FR3, FR14, FR36–FR39, DEC-25 | settled; implementation gap | -| FR8 | On a fitted image, a downward drag moves the photo with the finger, shrinks it, and fades the backdrop. The release decides once: a projected travel past the commit point closes the viewer and reports `onOpenChange(false)` at the lift; a release moving back springs back. Video is unchanged. | `spec:AST-094` FR36–FR40 | settled; implementation gap | -| FR9 | With `hasZoom`, a pinch in from the fitted size is a release decision: below the measured fraction of the fitted size the release closes the viewer; opening the fingers again by the measured amount before the lift is moving back and cancels. A pinch that starts zoomed in never closes the viewer. | `spec:AST-094` FR36, FR37 | settled; implementation gap | -| FR10 | With `hasZoom`, under a coarse pointer only, a tap on the photo fades the controls and caption and darkens the backdrop. While they are hidden, the next tap on the photo, a tap beside the photo, or a cancelled drag brings them back, and that tap beside the photo does not close the viewer (ORD3). AR5 governs keyboard and assistive-technology access while they are hidden. | `architecture:public-component-api` INV16 | settled; implementation gap | -| FR11 | A mouse, a pen, and a trackpad also use FR7's drag paging, FR8's drag to close, a ctrl-wheel or trackpad pinch over the photo for FR6's zoom and FR9's close, and a wheel to pan a zoomed photo; over the photo these replace the browser's page zoom and scroll. A double-click zooms 2× about the clicked point. A click on the photo keeps the controls, and the viewer opens and closes at once. | `spec:AST-094` FR43; `spec:AST-017` FR59 | settled; implementation gap | -| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens and returns into it when it closes: the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap | -| FR13 | `hasZoom={false}` turns off FR6, FR9, and FR10. FR7 and FR8 add to the buttons, keys, and Escape and remove none of them, so Lightbox takes no `presentation` prop. | `architecture:public-component-api` INV16 | settled | -| FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap | -| FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled | -| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on its named motion role. `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap | +| ID | Candidate invariant | Basis | Draft review state | +| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------- | +| FR1 | The viewer contains one active Media item and a Close button; galleries additionally contain Previous and Next buttons plus a Counter, and Caption renders only when supplied. | Current source, docs, and tests | settled | +| FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled | +| FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled | +| FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap | +| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the zoom settle, the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR44). Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap | +| FR6 | With `hasZoom`, on an image, a pinch zooms about the fingers and pans while it zooms; past the minimum or maximum the photo resists and eases back without overshoot. A double tap zooms to the larger of filling the viewer and 2×, about the tapped point; a second double tap returns to the fitted size. A zoomed photo pans with momentum and rubber-band edges. A pinch on the controls or the caption still zooms the page. | `spec:AST-094` FR3, FR10 | settled; implementation gap | +| FR7 | In a gallery, a horizontal drag moves the row of items, which sit apart by the measured gap. The release lands on at most one neighbouring item by `spec:AST-094` FR3's paging rule (DEC-25). Paging applies over a video item's frame as over a photo; a drag that starts on the video's own controls, such as its scrubber or volume, stays the video's. A zoomed photo pans to its edge before a new drag pages. A pinch never pages. Previous, Next, and the arrow keys keep paging. A controlled `index` the caller refuses springs back. `onIndexChange` reports a swipe. | `spec:AST-094` FR3, FR14, FR36–FR39, DEC-25 | settled; implementation gap | +| FR8 | On a fitted image, a downward drag moves the photo with the finger, shrinks it, and fades the backdrop. The release decides once: a projected travel past the commit point closes the viewer and reports `onOpenChange(false)` at the lift; a release moving back springs back. Video is unchanged. | `spec:AST-094` FR36–FR40 | settled; implementation gap | +| FR9 | With `hasZoom`, a pinch in from the fitted size is a release decision: below the measured fraction of the fitted size the release closes the viewer; opening the fingers again by the measured amount before the lift is moving back and cancels. A pinch that starts zoomed in never closes the viewer. | `spec:AST-094` FR36, FR37 | settled; implementation gap | +| FR10 | With `hasZoom`, under a coarse pointer only, a tap on the photo fades the controls and caption and darkens the backdrop. While they are hidden, the next tap on the photo, a tap beside the photo, or a cancelled drag brings them back, and that tap beside the photo does not close the viewer (ORD3). AR5 governs keyboard and assistive-technology access while they are hidden. | `architecture:public-component-api` INV16 | settled; implementation gap | +| FR11 | A mouse, a pen, and a trackpad also use FR7's drag paging, FR8's drag to close, a ctrl-wheel or trackpad pinch over the photo for FR6's zoom and FR9's close, and a wheel to pan a zoomed photo; over the photo these replace the browser's page zoom and scroll. A double-click zooms 2× about the clicked point. A click on the photo keeps the controls, and the viewer opens and closes at once. | `spec:AST-094` FR43; `spec:AST-017` FR59 | settled; implementation gap | +| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens and returns into it when it closes, on the theme's duration and easing tokens (`spec:AST-094` FR44): the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap | +| FR13 | `hasZoom={false}` turns off FR6, FR9, and FR10. FR7 and FR8 add to the buttons, keys, and Escape and remove none of them, so Lightbox takes no `presentation` prop. | `architecture:public-component-api` INV16 | settled | +| FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap | +| FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled | +| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on the theme's exit tokens (`spec:AST-090`; `spec:AST-094` FR7, FR44). `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap | ### Allowed variation @@ -229,7 +229,7 @@ under `spec:AST-020` FR1; reduced motion cites `design:reduced-motion`. touch drag starts them (DR2), including a mouse, pen, or trackpad release where the primary pointer is coarse (`spec:AST-094` FR43). Otherwise they complete at once (DR3). The momentum coast of a thrown zoomed photo is direct - movement and continues (DR2). These motions read the named motion roles (FR5), which carry the + movement and continues (DR2). These motions read the named motion roles or the theme's tokens (FR5), which carry the reduced-motion path. - **AR7 — Focus visible and not obscured (WCAG 2.2 2.4.7, 2.4.11).** Every focusable control and the zoom target show the shared focus indicator. A diff --git a/docs/specs/components/Popover/Popover.spec.md b/docs/specs/components/Popover/Popover.spec.md index a92a2cca5c40e..7fe0476bded0e 100644 --- a/docs/specs/components/Popover/Popover.spec.md +++ b/docs/specs/components/Popover/Popover.spec.md @@ -187,7 +187,7 @@ table owns their semantic effect. | 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, and a touch during its growth is ignored, as `spec:AST-094` FR31 and FR35 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it | +| FR9 | Under a finger, Popover grows out of its trigger and shrinks back into it on the theme's duration and easing tokens, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR35 and FR44 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it | ### Allowed variation diff --git a/docs/specs/components/ProgressBar/ProgressBar.spec.md b/docs/specs/components/ProgressBar/ProgressBar.spec.md index 5c18bd6f57b32..7e827b0313e3c 100644 --- a/docs/specs/components/ProgressBar/ProgressBar.spec.md +++ b/docs/specs/components/ProgressBar/ProgressBar.spec.md @@ -58,7 +58,7 @@ system_specs: | Area | Contract | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Public contract | `label`, `value`, `max`, `isLabelHidden`, `hasValueLabel`, `formatValueLabel`, `variant`, `isIndeterminate`, `marks`, `isDisabled`, root `BaseProps`, and a root `ref`. | -| Behavior | A labelled `progressbar` whose fill shows the clamped value against a muted track, moving at UIKit's speed under a finger, or slides while indeterminate and pulses in place under reduced motion, with optional value text and labelled target marks. | +| Behavior | A labelled `progressbar` whose fill shows the clamped value against a muted track, moving on the theme's duration and easing under every pointer, or slides while indeterminate and pulses in place under reduced motion, with optional value text and labelled target marks. | | End-user impact | People see how far an operation has gone, and assistive technology reads the name and value; marks reveal their labels on hover or focus. | | Builder impact | A builder's `data-variant` replaces the reflected variant today, so theme rules for the bar's real variant stop matching (FR8 gap); otherwise none. | | Compatibility/readiness | No API or default changes. Implementation gaps are listed under Current-state impact. | @@ -80,9 +80,10 @@ Consumer syntax and examples belong in `ProgressBar.doc.mjs`. - Released default preserved: `yes` — determinate, `value` 0, `max` 100, `accent`, visible label, no value text, no marks. -- Compatibility class: records shipped behavior plus two motion changes with - no API, default, or value change: a determinate fill's timing under a coarse - primary pointer (FR4) and the indeterminate fill under reduced motion (FR6); +- Compatibility class: records shipped behavior plus one motion change with + no API, default, or value change: the indeterminate fill under reduced + motion (FR6); a determinate fill moves on the theme's eased duration under + every pointer (FR4); the four canonical `progress-bar*` targets are the only target classes. - Controlled/uncontrolled behavior: not applicable; `value` is caller-owned. - Migration decision: none @@ -158,17 +159,17 @@ Consumer migration instructions belong in consumer docs and release notes. ## Behavioral and layout contract -| ID | Invariant | Basis | -| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| FR1 | Every determinate presentation ProgressBar provides is a sufficient standalone visual for distinguishing completed from remaining progress; external visible text may supplement it but is never required, and supplementary text or graphics never substitute for that distinction. ProgressBar does not make visual correctness depend on caller-declared external content it cannot verify or associate. | `component:ProgressBar/DEC-1`; `spec:AST-002` | -| FR2 | New public API that adjusts the visual distinction is considered only after the component has a correct base behavior and a stable caller-owned distinction still remains. | `component:ProgressBar/DEC-1`; `spec:AST-002` | -| FR3 | The root holds a label row and a track. The label names the track (`role="progressbar"`) through `aria-labelledby`; `isLabelHidden` hides it visually only. When determinate, the track exposes `aria-valuenow` (the clamped value), `aria-valuemin` 0, `aria-valuemax` (`max`), and `aria-valuetext` from `formatValueLabel`, which defaults to the rounded percentage; `hasValueLabel` shows the same text beside the label. A zero or non-finite `max` gives 0% and never renders `NaN`. Astryx-owned value text comes from one catalog message (`architecture:internationalization` INV1a). Implementation gap: the default text is assembled in code (Current-state impact). | Current source and tests | -| FR4 | The track is 8px high, fully rounded, in `--color-background-muted`, and clips the fill. The determinate fill spans the track and slides along it so its leading edge sits at the clamped percentage, mirrored by the bar's own writing direction, moving over `--duration-medium` when the value changes. Under a coarse primary pointer (`spec:AST-094` FR13) it instead moves linearly from where it is drawn when the value changes, for the `progress.fillTrackMs` role (1000ms, UIKit's progress view for a whole-track change) times the share of the track the value moved, so 10% to 90% takes 800ms; above zero it draws at least 16px long, a mark that length covers counts as on the fill (FR7), and at zero it paints nothing. That duration and its linear curve, `progress.curves.fill`, are roles in the generated motion role table, and a theme's tempo scales the duration (`spec:AST-094` FR4, DEC-1, DEC-5; `spec:AST-108` FR3); any other pointer keeps the theme's eased duration. Under reduced motion a new value lands at once on every pointer (`design:reduced-motion` DR1). The fill paints `--color-accent`, `--color-success`, `--color-warning`, or `--color-error`, or `--color-text-disabled` for `neutral` and disabled bars; a theme-added variant has a safe theme-independent fill (`architecture:component-theming-surface` INV14). Implementation gap: a theme-added variant paints no fill and no on-fill mark color until a theme styles it (Current-state impact). In forced colors the fill is sized to the value instead of sliding and keeps a 1px `CanvasText` outline so its shape stays visible (`architecture:theme-tokens` INV11). | Current source and tests; UIKit measurement (iOS 26.2 simulator); `component:ProgressBar/DEC-2` | -| FR5 | `isDisabled` mutes the fill, label, value text, and marks and adds no ARIA state. | Current source and tests | -| FR6 | `isIndeterminate` removes every value attribute and the value text. Without reduced motion, on every pointer, a 40% fill slides across the clipped track, eased, 1.5s a pass, mirrored by the bar's own rendered direction (`architecture:internationalization` INV11). Under reduced motion, on every pointer, the fill spans the whole track as one solid bar, with no segment and no travel, and only its opacity pulses: from 1 down to 0.84 and back, each way taking the `progress.reducedMotionPulseMs` role (2000ms) on the `timing.curves.move` curve; the track is unchanged. The pulse is a busy loop, so tempo does not scale it (`spec:AST-094` FR4, `spec:AST-108` FR4). It applies `design:reduced-motion` DR1's no-travel rule, replacing the travel with an opacity change, to a loop that record leaves to its component. At the 0.84 floor the fill keeps at least 3:1 against the track in both themes for every tone that reaches 3:1 at full opacity (AR4). Implementation gap: the slide, and the centering of marks, mirror under any right-to-left ancestor rather than the bar's own direction, so in a left-to-right region of a right-to-left page they run against the label (Current-state impact). | Current source and tests; `component:ProgressBar/DEC-2` | -| FR7 | Each finite mark renders, outside the `progressbar` element, a tick at its clamped position whose tooltip shows its label on hover and focus; marks inside the filled part take the fill's contrasting color, and marks do not render while indeterminate. A focusable mark exposes its label as its accessible name (WCAG 2.2 SC 4.1.2). Implementation gap: marks are focusable `span`s with no role or name, labelled only as a description once the tooltip loads (Current-state impact). | Current source and tests | -| FR8 | The root carries `progress-bar` reflecting `variant`, the track `progress-bar-track`, the fill `progress-bar-fill` reflecting the resolved fill variant, and each mark `progress-bar-mark` reflecting the resolved fill variant and placement; the label and value text carry no target. Reflected state stays component-owned (`architecture:public-component-api` INV5). Implementation gap: a caller `data-variant` replaces the root's reflected variant (Current-state impact). | Current source and tests | -| FR9 | `ref` and `data-testid` reach the root; supported DOM, data, ARIA, and event props reach the root, while the reflected theme attributes stay component-owned (FR8); `xstyle`, `className`, and `style` compose after component styles. | Current source and tests; `architecture:public-component-api` INV5–INV8 | +| ID | Invariant | Basis | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | +| FR1 | Every determinate presentation ProgressBar provides is a sufficient standalone visual for distinguishing completed from remaining progress; external visible text may supplement it but is never required, and supplementary text or graphics never substitute for that distinction. ProgressBar does not make visual correctness depend on caller-declared external content it cannot verify or associate. | `component:ProgressBar/DEC-1`; `spec:AST-002` | +| FR2 | New public API that adjusts the visual distinction is considered only after the component has a correct base behavior and a stable caller-owned distinction still remains. | `component:ProgressBar/DEC-1`; `spec:AST-002` | +| FR3 | The root holds a label row and a track. The label names the track (`role="progressbar"`) through `aria-labelledby`; `isLabelHidden` hides it visually only. When determinate, the track exposes `aria-valuenow` (the clamped value), `aria-valuemin` 0, `aria-valuemax` (`max`), and `aria-valuetext` from `formatValueLabel`, which defaults to the rounded percentage; `hasValueLabel` shows the same text beside the label. A zero or non-finite `max` gives 0% and never renders `NaN`. Astryx-owned value text comes from one catalog message (`architecture:internationalization` INV1a). Implementation gap: the default text is assembled in code (Current-state impact). | Current source and tests | +| FR4 | The track is 8px high, fully rounded, in `--color-background-muted`, and clips the fill. The determinate fill spans the track and slides along it so its leading edge sits at the clamped percentage, mirrored by the bar's own writing direction, moving from where it is drawn over `--duration-medium` with the theme's easing when the value changes, under every pointer, a finger included (`spec:AST-094` FR44). Under a coarse primary pointer (`spec:AST-094` FR13), above zero it draws at least 16px long, a mark that length covers counts as on the fill (FR7), and at zero it paints nothing. `progress.fillTrackMs` (1000ms, UIKit's progress view for a whole-track change) and its linear curve, `progress.curves.fill`, stay in the generated motion role table as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4, DEC-5). Implementation gap: under a coarse primary pointer the fill still moves linearly for `progress.fillTrackMs` times the share of the track the value moved. Under reduced motion a new value lands at once on every pointer (`design:reduced-motion` DR1). The fill paints `--color-accent`, `--color-success`, `--color-warning`, or `--color-error`, or `--color-text-disabled` for `neutral` and disabled bars; a theme-added variant has a safe theme-independent fill (`architecture:component-theming-surface` INV14). Implementation gap: a theme-added variant paints no fill and no on-fill mark color until a theme styles it (Current-state impact). In forced colors the fill is sized to the value instead of sliding and keeps a 1px `CanvasText` outline so its shape stays visible (`architecture:theme-tokens` INV11). | Current source and tests; UIKit measurement (iOS 26.2 simulator); `component:ProgressBar/DEC-2` | +| FR5 | `isDisabled` mutes the fill, label, value text, and marks and adds no ARIA state. | Current source and tests | +| FR6 | `isIndeterminate` removes every value attribute and the value text. Without reduced motion, on every pointer, a 40% fill slides across the clipped track, eased, 1.5s a pass, mirrored by the bar's own rendered direction (`architecture:internationalization` INV11). Under reduced motion, on every pointer, the fill spans the whole track as one solid bar, with no segment and no travel, and only its opacity pulses: from 1 down to 0.84 and back, each way taking the `progress.reducedMotionPulseMs` role (2000ms) on the `timing.curves.move` curve; the track is unchanged. The pulse is a busy loop, so tempo does not scale it (`spec:AST-094` FR4, `spec:AST-108` FR4). It applies `design:reduced-motion` DR1's no-travel rule, replacing the travel with an opacity change, to a loop that record leaves to its component. At the 0.84 floor the fill keeps at least 3:1 against the track in both themes for every tone that reaches 3:1 at full opacity (AR4). Implementation gap: the slide, and the centering of marks, mirror under any right-to-left ancestor rather than the bar's own direction, so in a left-to-right region of a right-to-left page they run against the label (Current-state impact). | Current source and tests; `component:ProgressBar/DEC-2` | +| FR7 | Each finite mark renders, outside the `progressbar` element, a tick at its clamped position whose tooltip shows its label on hover and focus; marks inside the filled part take the fill's contrasting color, and marks do not render while indeterminate. A focusable mark exposes its label as its accessible name (WCAG 2.2 SC 4.1.2). Implementation gap: marks are focusable `span`s with no role or name, labelled only as a description once the tooltip loads (Current-state impact). | Current source and tests | +| FR8 | The root carries `progress-bar` reflecting `variant`, the track `progress-bar-track`, the fill `progress-bar-fill` reflecting the resolved fill variant, and each mark `progress-bar-mark` reflecting the resolved fill variant and placement; the label and value text carry no target. Reflected state stays component-owned (`architecture:public-component-api` INV5). Implementation gap: a caller `data-variant` replaces the root's reflected variant (Current-state impact). | Current source and tests | +| FR9 | `ref` and `data-testid` reach the root; supported DOM, data, ARIA, and event props reach the root, while the reflected theme attributes stay component-owned (FR8); `xstyle`, `className`, and `style` compose after component styles. | Current source and tests; `architecture:public-component-api` INV5–INV8 | ### Allowed variation @@ -179,14 +180,14 @@ Consumer migration instructions belong in consumer docs and release notes. ### Representative states -| State | Required invariant | Allowed variation | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | -| Determinate, partial | Completed and remaining progress are distinguishable from the graphic alone; value attributes present. | Variant. | -| With value text | The graphic still satisfies FR1; the text supplements it. | Formatter. | -| Label hidden | The bar keeps its accessible name. | — | -| Determinate, touch | Under a coarse primary pointer the fill moves linearly from where it is drawn, for the role's share; at least 16px above zero; nothing at zero. | Variant. | -| Indeterminate | No value attributes; sliding fill; under reduced motion a full-width fill pulses its opacity in place, with no travel. | Variant. | -| With marks | Ticks outside the `progressbar`, labelled by tooltip. | Mark labels. | +| State | Required invariant | Allowed variation | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | +| Determinate, partial | Completed and remaining progress are distinguishable from the graphic alone; value attributes present. | Variant. | +| With value text | The graphic still satisfies FR1; the text supplements it. | Formatter. | +| Label hidden | The bar keeps its accessible name. | — | +| Determinate, touch | Under a coarse primary pointer the fill moves from where it is drawn on the theme's duration and easing; at least 16px above zero; nothing at zero. | Variant. | +| Indeterminate | No value attributes; sliding fill; under reduced motion a full-width fill pulses its opacity in place, with no travel. | Variant. | +| With marks | Ticks outside the `progressbar`, labelled by tooltip. | Mark labels. | ### Transformation and precedence order @@ -275,7 +276,7 @@ those parts stay untargeted. | FR4 | `ProgressBar.test.tsx` track-clip, transform, variant, and forced-colors tests; `ProgressBarMotion.a11y.chromium.spec.ts` | Every variant; value changes; RTL region; reduced motion; forced colors | A track that does not clip, a fill sized by width outside forced colors, a lost forced-colors outline, a leading edge off the value or unmirrored in an RTL region, a value change that does not land at once under reduced motion, or per-frame layout while moving fails. Gap: the variant colors have no direct check. Gap: the "fixed 8px track height" test only checks that the bar renders, so the track height has no check; no test covers a theme-added variant. | `audit:ProgressBar/visual` | | FR5 | `ProgressBar.test.tsx` disabled and disabled-mark tests | Disabled | A missing label or undimmed marks fail. Gap: the muted fill and label colors have no direct check. | `audit:ProgressBar/visual` | | FR6 | `ProgressBar.test.tsx` indeterminate suite | Every variant; RTL | Value attributes or value text while indeterminate, a lost label, an unclipped track, or an unmirrored slide fails. No test covers the slide in a left-to-right region inside a right-to-left page. Gap: the RTL test asserts the right-to-left ancestor selector, so it pins the FR6 gap. | `audit:ProgressBar/behavior` | -| FR4 touch timing | `ProgressBarTouch.a11y.chromium.spec.ts`; the `ProgressBar.test.tsx` touch suite | Coarse vs fine pointer; 10% to 90%; a new value mid-move; 1% on a narrow track; zero; reduced motion | A coarse-pointer move that eases, starts from the old value instead of the drawn position, or lasts other than `progress.fillTrackMs` times the share moved; a fine-pointer move that leaves the theme's eased duration; a non-zero fill shorter than 16px; a painted fill at zero; or a duration played as a literal instead of the role fails. | `audit:ProgressBar/behavior` | +| FR4 touch timing | `ProgressBarTouch.a11y.chromium.spec.ts`; the `ProgressBar.test.tsx` touch suite | Coarse vs fine pointer; 10% to 90%; a new value mid-move; 1% on a narrow track; zero; reduced motion | A move under any pointer that leaves the theme's eased duration or starts from the old value instead of the drawn position; a coarse-pointer non-zero fill shorter than 16px; or a painted fill at zero fails. | `audit:ProgressBar/behavior` | | FR6 reduced motion | The reduced-motion pulse cases of `ProgressBarMotion.a11y.chromium.spec.ts`; the `ProgressBar.test.tsx` pulse suite | Every variant; coarse and fine pointer | A segment, any transform or travel, a fill narrower than the track, an animated property other than the fill's opacity, an opacity below 0.84, a period other than `progress.reducedMotionPulseMs` each way, or a changed track fails. | `audit:ProgressBar/behavior` | | AR4 | The pulse-floor contrast case of `ProgressBarMotion.a11y.chromium.spec.ts` | Every variant; light and dark default themes; body and surface backgrounds | A tone that reaches 3:1 at full opacity and falls below 3:1 at the floor fails. Gap: warning and neutral in light are below 3:1 at full opacity (OQ1). | `audit:ProgressBar/a11y` | | FR7 | `ProgressBar.test.tsx` target-mark suite | None; one; many; clamped; non-finite; on fill; indeterminate; disabled; tooltip | A missing, misplaced, or clipped mark, a mark inside the `progressbar`, a lost tooltip, or a wrong on-fill color fails. Gap: the test asserts that marks have no role and no `aria-label`, which pins the FR7 gap. | `audit:ProgressBar/a11y` | @@ -311,7 +312,9 @@ verify. Under a finger a determinate fill moves the way UIKit's progress view does, because touch presentations default to measured native timing -(`spec:AST-094` DEC-1); a mouse keeps the theme's motion. The indeterminate +(`spec:AST-094` DEC-1); a mouse keeps the theme's motion. `spec:AST-094` +DEC-26 supersedes this touch fill timing: a value change is not a gesture, so +the fill moves on the theme's duration and easing under every pointer. The indeterminate bar keeps its 40% eased slide on every pointer. Under reduced motion a travelling segment still moves, so the bar stops travelling and pulses slowly in place on every pointer, at a floor that keeps it visible; the slow 2000ms diff --git a/docs/specs/components/Slider/Slider.spec.md b/docs/specs/components/Slider/Slider.spec.md index 6c1da6ce42ee1..da52f29b39e55 100644 --- a/docs/specs/components/Slider/Slider.spec.md +++ b/docs/specs/components/Slider/Slider.spec.md @@ -49,7 +49,7 @@ an additive target for the interactive control surface. - Released default preserved: `no` — FR5 changes the released `onChangeEnd` payload and FR7 how every pointer drags a thumb and when `onChangeEnd` fires - after a finger's drag; every other released default is unchanged + after a drag that coasts; every other released default is unchanged - Compatibility class: additive target and state reflection; default styling, DOM semantics, and public props remain unchanged, and the one keyboard change AR1 names — the horizontal arrows follow the track's direction under RTL — @@ -68,6 +68,9 @@ an additive target for the interactive control surface. moves is when a touch flick's `onChangeEnd` fires, about 0.6 s after the lift, once the coast settles; an owner that acts at the release keeps doing so through `onChangeEnd` and receives it after the coast. +- A mouse's or a pen's flick (FR7): it coasts as a finger's does, so its + `onChangeEnd` fires about 0.6 s after the release, once the coast settles; a + slow drag still ends at the release (`spec:AST-094/DEC-26`). - Controlled/uncontrolled behavior: unchanged; Slider remains controlled - Migration decision: an owner that moves `value` only from `onChangeEnd` moves it from `onChange` instead (FR5); an owner that needs the value at a @@ -104,15 +107,15 @@ modes, states, and usage remain documented in `Slider.doc.mjs`. ## Behavioral and layout contract -| ID | Candidate invariant | Basis | Review state | -| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided | -| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and DEC-1 | Approved additive target contract | -| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy | -| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary | -| FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled (after a finger's release, once its coast settles; FR7), or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved | -| FR6 | When both thumbs of a range share a value, a pointer press takes the thumb that can travel toward the pointer: a press on the track beside them takes the thumb on its side, and a press on the thumbs or on their shared value, such as its mark label, moves neither until the pointer moves, when the direction of that first movement picks the thumb, which takes the drag's focus, pressed state and value tooltip. Until then the upper thumb, drawn over the lower one, holds the press, its focus and its pressed state, so a press that never moves changes no value and leaves focus on the upper thumb. A drag that began on the shared thumbs and comes back past their value moves the other thumb: the dragged thumb stops against it and the other follows the pointer, so with both at 80 a drag to 95 and back to 50 ends at [50, 80]. | Current source and focused tests | Accepted contract | -| FR7 | Every pointer — a finger, a pen, or a mouse — drags a thumb by its travel. A press on a thumb changes no value. Once the pointer has moved `spec:AST-094/FR19`'s 10 px from where it pressed — a distance the pointer travels, not a zone around the thumb — the value moves by the pointer's further travel along the track over the track's length, from wherever the thumb was grabbed, so the thumb never jumps to the pointer; each move is written to the thumb with no component render (`spec:AST-094/FR9`). Between coincident range thumbs, FR6's first movement is the one that crosses the 10 px. A finger or a pen claims the drag only along the track, so a swipe across it scrolls the page; a mouse claims in any direction. Under a coarse primary pointer, after hydration (`spec:AST-094/FR13`), each thumb takes a finger or a pen through a 48 px touch target that grows toward a range's other thumb only to half the gap (`spec:AST-094/FR42`), and the held thumb lifts on the `springs.sliderLift` motion role and drops back on `springs.sliderDrop`. A finger's or a pen's drag that brings the value to either end plays UISlider's end haptic where the browser can vibrate, and a mouse's drag plays none; iOS Safari has no haptic for that moment (`spec:AST-094/FR23`). After a finger lifts, the thumb coasts toward 0.4 of the release's momentum projection and settles on the `springs.sliderRelease` motion role, about 0.6 s, with `onChange` firing through the coast; the three roles are internal, each with UISlider's measured value at `tempo` 1 and `'standard'`, and follow the theme's motion knobs (`spec:AST-094/FR4`). A mouse's or a pen's drag ends at its release, with no coast. A tap or a click on the track still sets the value: a finger's tap when it lifts, and a mouse press at once, where a primary click lands; a secondary click and a wheel or trackpad scroll change nothing (`spec:AST-094/FR43`). Under reduced motion the thumb does not lift, a tap's value applies at once, and the coast continues (`design:reduced-motion/DR2`). | UIKit measurements and maintainer decision (DEC-4) | Approved | +| ID | Candidate invariant | Basis | Review state | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | -------------------------------------------------- | +| FR1 | The current render places a filled range, one or two thumbs, and optional tick marks over the background track. | Current source, docs, and focused tests | Verified current behavior; no new behavior decided | +| FR2 | `Slider`, `Interactive control`, `Track`, and `Thumb` carry the `slider`, `slider-control`, `slider-track`, and `slider-thumb` targets respectively. | Current source, public docs, and DEC-1 | Approved additive target contract | +| FR3 | Filled range, tick marks, mark labels, and adjacent text value display are stable rendered parts without their own current Slider target. | Current source and public docs | Verified current asymmetry; not ratified as policy | +| FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary | +| FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled (after a release, once its coast settles; FR7), or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved | +| FR6 | When both thumbs of a range share a value, a pointer press takes the thumb that can travel toward the pointer: a press on the track beside them takes the thumb on its side, and a press on the thumbs or on their shared value, such as its mark label, moves neither until the pointer moves, when the direction of that first movement picks the thumb, which takes the drag's focus, pressed state and value tooltip. Until then the upper thumb, drawn over the lower one, holds the press, its focus and its pressed state, so a press that never moves changes no value and leaves focus on the upper thumb. A drag that began on the shared thumbs and comes back past their value moves the other thumb: the dragged thumb stops against it and the other follows the pointer, so with both at 80 a drag to 95 and back to 50 ends at [50, 80]. | Current source and focused tests | Accepted contract | +| FR7 | Every pointer — a finger, a pen, or a mouse — drags a thumb by its travel. A press on a thumb changes no value. Once the pointer has moved `spec:AST-094/FR19`'s 10 px from where it pressed — a distance the pointer travels, not a zone around the thumb — the value moves by the pointer's further travel along the track over the track's length, from wherever the thumb was grabbed, so the thumb never jumps to the pointer; each move is written to the thumb with no component render (`spec:AST-094/FR9`). Between coincident range thumbs, FR6's first movement is the one that crosses the 10 px. A finger or a pen claims the drag only along the track, so a swipe across it scrolls the page; a mouse claims in any direction. Under a coarse primary pointer, after hydration (`spec:AST-094/FR13`), each thumb takes a finger or a pen through a 48 px touch target that grows toward a range's other thumb only to half the gap (`spec:AST-094/FR42`), and the held thumb lifts and drops back on the theme's duration and easing tokens (`spec:AST-094/FR44`). A finger's or a pen's drag that brings the value to either end plays UISlider's end haptic where the browser can vibrate, and a mouse's drag plays none; iOS Safari has no haptic for that moment (`spec:AST-094/FR23`). After any pointer's release, a finger's, a pen's or a mouse's, the thumb coasts toward 0.4 of the release's momentum projection and settles on the `springs.sliderRelease` motion role, about 0.6 s, with `onChange` firing through the coast (`spec:AST-094/FR44`); the role is internal, with UISlider's measured value at `tempo` 1 and `'standard'`, and follows the theme's motion knobs (`spec:AST-094/FR4`). `springs.sliderLift` and `springs.sliderDrop` stay as UISlider's measured reference, and no motion plays them. Implementation gap: under a coarse primary pointer the thumb still lifts on `springs.sliderLift` and drops on `springs.sliderDrop`, and a mouse's or a pen's drag ends at its release, with no coast. A tap or a click on the track still sets the value: a finger's tap when it lifts, and a mouse press at once, where a primary click lands; a secondary click and a wheel or trackpad scroll change nothing (`spec:AST-094/FR43`). Under reduced motion the thumb does not lift, a tap's value applies at once, and the coast continues (`design:reduced-motion/DR2`). | UIKit measurements and maintainer decision (DEC-4) | Approved | ### Observed current target asymmetry @@ -263,7 +266,7 @@ separately listed value tooltip retains Tooltip's target. | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` | | FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` | | FR6 | `Slider.test.tsx` coincident range thumbs cases | Both thumbs at the minimum, the maximum and mid-range; a track press on each side; a press on the thumbs moved either way; a press that never moves, on the thumbs and on the mark label of their value; a drag that passes back over the shared value; right to left | A press or drag on coincident thumbs moves neither thumb, moves the thumb that cannot travel toward the pointer, keeps its focus, pressed state or tooltip on a thumb the drag has left, or leaves a press that never moves on the lower thumb | `audit:Slider/anatomy` | -| FR7 | `Slider.test.tsx`, `useSliderTouch.test.tsx`, and the touch and fine-pointer browser tests (`SliderTouch.interaction.browser.spec.ts`, `SliderFinePointer.interaction.browser.spec.ts`) in Chromium and WebKit | Drag, threshold, grab offset, coast, slow release, track tap and swipe, mouse click, secondary click, wheel, close range thumbs, vertical, right to left, reduced motion | The thumb jumps to the pointer, a press moves the value, a swipe across is trapped, a drag waits on the owner's render, a mouse or pen release coasts, or `onChangeEnd` fires before a finger's coast stops | `audit:Slider/anatomy` | +| FR7 | `Slider.test.tsx`, `useSliderTouch.test.tsx`, and the touch and fine-pointer browser tests (`SliderTouch.interaction.browser.spec.ts`, `SliderFinePointer.interaction.browser.spec.ts`) in Chromium and WebKit | Drag, threshold, grab offset, coast, slow release, track tap and swipe, mouse click, secondary click, wheel, close range thumbs, vertical, right to left, reduced motion | The thumb jumps to the pointer, a press moves the value, a swipe across is trapped, a drag waits on the owner's render, a flick's release from any pointer that does not coast, or `onChangeEnd` fires before a finger's coast stops | `audit:Slider/anatomy` | | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` | The focused Slider suite asserts the `slider-control` target's orientation and @@ -311,6 +314,10 @@ A thumb that jumps to the pointer changes the value on a press nobody meant as a Rejected: a mouse keeping the jump to the pointer, which gives one Slider two drags by device. +`spec:AST-094/DEC-26` supersedes this decision's finger-only coast and its +measured lift: a mouse's or a pen's release coasts as a finger's does, and +the lift and drop play on the theme's duration and easing tokens. + ## Open questions - **OQ1 — After a joint ProgressBar and Slider family audit, should any shared diff --git a/docs/specs/components/Switch/Switch.spec.md b/docs/specs/components/Switch/Switch.spec.md index 1e6f2570d4747..22c05a7ddadb8 100644 --- a/docs/specs/components/Switch/Switch.spec.md +++ b/docs/specs/components/Switch/Switch.spec.md @@ -58,6 +58,7 @@ system_specs: spec:AST-094/FR6, spec:AST-094/FR18, spec:AST-094/FR24, + spec:AST-094/FR44, ] --- @@ -73,15 +74,15 @@ system_specs: ## Contract at a glance -| Area | Contract | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Public contract | None changes. Switch takes `label`, `value`, `onChange`, `changeAction`, `isLoading`, `isDisabled`, `disabledMessage`, `isRequired`, `isOptional`, `status`, `size`, `labelPosition`, `labelSpacing`, field content, and base props. | -| Behavior | A native checkbox with `role="switch"` drawn as a track and thumb. A change calls `onChange`, then an optional `changeAction` with the optimistic value; while busy the switch shows a Spinner in the thumb and refuses changes. Its press and drag work for every pointer, and under a finger it follows the finger as the iOS switch does (FR13–FR15). | -| End-user impact | People flip a setting that takes effect at once and hear when it is saving. Keyboard and screen-reader users can reach a disabled switch and its reason. | -| Builder impact | None. | -| Compatibility/readiness | Under a finger a hold and a press slid off now toggle the switch, and under a mouse or a pen a drag past the flip distance now flips it (FR15); a coarse pointer's hit area grows toward 44 px (FR10); no API, default, or DOM change. Caller handlers other than `onClick` run while disabled, and caller ARIA lands on the field root; both are listed under Current-state impact. | -| Review checks | Reject a lost switch role or name, a change while disabled or busy, focus moving on change, a ring painted anywhere but the track, a thumb that travels the wrong way under RTL, or a disabled switch that submits. | -| Governing rules | `spec:AST-021` switch pattern; `spec:AST-085` FR1–FR3, FR5, FR6; `spec:AST-086` FR8; `design:user-states` DR1, DR2 (pressed overlay); `design:reduced-motion` DR1; WCAG 2.5.2, 3.2.2, 1.4.11; `spec:AST-094` FR4, FR11, FR13, FR24, FR42, FR43; `spec:AST-020` FR9 (DEC-5). | +| Area | Contract | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Public contract | None changes. Switch takes `label`, `value`, `onChange`, `changeAction`, `isLoading`, `isDisabled`, `disabledMessage`, `isRequired`, `isOptional`, `status`, `size`, `labelPosition`, `labelSpacing`, field content, and base props. | +| Behavior | A native checkbox with `role="switch"` drawn as a track and thumb. A change calls `onChange`, then an optional `changeAction` with the optimistic value; while busy the switch shows a Spinner in the thumb and refuses changes. Its press and drag work for every pointer: the thumb follows a drag and settles from the release on a spring, and a tap or a key moves it on the theme's motion tokens (FR15). | +| End-user impact | People flip a setting that takes effect at once and hear when it is saving. Keyboard and screen-reader users can reach a disabled switch and its reason. | +| Builder impact | None. | +| Compatibility/readiness | Under a finger a hold and a press slid off now toggle the switch, and a tap moves the thumb on the theme's motion tokens; under a mouse or a pen the thumb now follows a drag, and a drag past the flip distance flips it (FR15); a coarse pointer's hit area grows toward 44 px (FR10); no API, default, or DOM change. Caller handlers other than `onClick` run while disabled, and caller ARIA lands on the field root; both are listed under Current-state impact. | +| Review checks | Reject a lost switch role or name, a change while disabled or busy, focus moving on change, a ring painted anywhere but the track, a thumb that travels the wrong way under RTL, or a disabled switch that submits. | +| Governing rules | `spec:AST-021` switch pattern; `spec:AST-085` FR1–FR3, FR5, FR6; `spec:AST-086` FR8; `design:user-states` DR1, DR2 (pressed overlay); `design:reduced-motion` DR1; WCAG 2.5.2, 3.2.2, 1.4.11; `spec:AST-094` FR4, FR11, FR13, FR24, FR42, FR43; `spec:AST-020` FR9 (DEC-5). | This table is a review projection; the body below is authoritative. @@ -90,23 +91,25 @@ This table is a review projection; the body below is authoritative. Switch presents a labelled boolean setting whose change takes effect immediately. It owns the switch control, its change and Action pipeline, busy and disabled behavior, label placement, and the field's theming anatomy. -Its press and drag work for a mouse, a pen and a finger (FR15). Under a touch -presentation it follows a finger the way the iOS switch does (FR13–FR15); -under a fine primary pointer it keeps its released look, and a keyboard or -assistive technology keeps its released behavior. Consumer usage lives in `Switch.doc.mjs`. +Its press and drag work for a mouse, a pen and a finger: the thumb follows a +drag and settles from the release on a spring under every pointer, and a tap, +a key or a change of value moves it on the theme's motion tokens (FR15). +Under a touch presentation the thumb also lifts while a finger presses +(FR13–FR15); a keyboard or assistive technology keeps its released behavior. Consumer usage lives in `Switch.doc.mjs`. ## Compatibility and migration - Released default preserved: `no` under a finger, where a hold and a press slid off now toggle the switch (FR15), and under a mouse or a pen, where a - drag past the flip distance now flips it (FR15); `yes` for a keyboard and + drag moves the thumb and past the flip distance flips it (FR15); `yes` for a keyboard and assistive technology; `size="md"`, `labelPosition="end"`, `labelSpacing="hug"` -- Compatibility class: a touch presentation adds the finger behavior - (FR13–FR15), a mouse or a pen drags the switch by the same flip rule - (FR15), and a coarse pointer's hit area grows toward 44 × 44 px (FR10); the - look under a fine pointer and every public prop, default, export, and DOM - element stay as they were +- Compatibility class: a touch presentation adds the lift and the finger's + hold and slide-off (FR13–FR15), every pointer drags the thumb by the same + follow and flip rule and settles it on a spring (FR15), a tap under a finger + moves the thumb on the theme's motion tokens (FR15), and a coarse pointer's + hit area grows toward 44 × 44 px (FR10); every public prop, default, + export, and DOM element stays as it was - Controlled/uncontrolled behavior: controlled only; `value` is required, and the switch shows an optimistic value only while a `changeAction` is pending - Migration decision: none @@ -146,38 +149,47 @@ Consumer migration instructions belong in consumer docs and release notes. ## Behavioral and layout contract -| ID | Invariant | Basis | -| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | The current render places a thumb inside the switch track and arranges that control with its label: a visually hidden native `` sits over a decorative track, with the label after it by default or before it with `labelPosition="start"`. | `spec:AST-021`; current source, docs, and focused tests | -| FR2 | The field, track, thumb, and label carry their existing local targets. The field reflects `data-label-position` and `data-label-spacing` only for non-default values; the track reflects `data-size`, `data-checked`, and `data-disabled`; the thumb reflects `data-size` and `data-checked`. | `architecture:component-theming-surface`; current source and tests | -| FR3 | A busy switch renders Spinner inside the thumb; a status message renders through detached FieldStatus. | Current source, docs, and focused tests | -| FR4 | The input is named by its label (or a caller `aria-labelledby`) and described by any caller description, then the description, status message, and disabled reason that exist; the description is not part of the name. A click on the label or description toggles the switch. | `spec:AST-021`; current tests | -| FR5 | A change calls `onChange(checked, event)`; unless that call prevents default, `changeAction(checked, event)` runs in a transition and the switch shows the proposed state while the Action is pending, and `value` once it settles. | Current tests | -| FR6 | While `isLoading`, or while the shown state differs from `value`, the input sets `aria-busy`, ignores pointer input, refuses changes, keeps focus, and the switch announces its busy start once per busy period through the shared channel. | `spec:AST-086` FR8; current tests | -| FR7 | Disabled: `aria-disabled="true"` on a focusable input, no `name`, detached from its form (so it neither submits nor blocks submission when required), no change, no caller `onClick` on the field, no hover or press paint, and a half-opacity track. With `disabledMessage`, a reason tooltip opens on row hover and keyboard focus and describes the input. Implementation gap: caller DOM handlers other than `onClick`, such as `onKeyDown`, still run on the field root while disabled (`spec:AST-085` FR2). | `spec:AST-085` FR1–FR3, FR5; current tests | -| FR8 | Enabled, the input submits `on` under `htmlName` when on and nothing when off; `isRequired` sets native `required`, while `aria-required` follows the effective required state; `aria-invalid` follows `status` (`false` on a required switch without an error), so the browser's own invalid state never shows before an error. | Current tests | -| FR9 | Keyboard focus paints the shared focus outline on the track. Hovering the enabled row tints the track; pressing paints the pressed overlay on both, applying `design:user-states` DR1 and DR2; a touch press fades out from the row. Track color transitions are instant under reduced motion; under a finger the colour fades with the thumb's cross-fade (FR15). | `architecture:interaction-modality` INV1; `design:user-states` DR1, DR2 for the pressed overlay; `design:reduced-motion`; current tests | -| FR10 | The thumb travels toward the inline end when on, mirrored under RTL, and moves without a transition under reduced motion (`design:reduced-motion` DR1); under a finger it cross-fades to its rest instead (FR15). Where the primary pointer is coarse, the switch's hit area grows around the track toward 44 by 44 CSS pixels, each side stopping at half the gap to a neighbouring target so a tap goes to the nearer one (`spec:AST-094` FR42, `utils/hitArea.ts`), with nothing visible added. | WCAG 2.5.8; current tests; `design:reduced-motion` DR1; `spec:AST-094` FR42 | -| FR11 | Under forced colors, on and off stay distinguishable (Highlight track when on), and the hover tint does not hide the thumb. | WCAG 1.4.11; current tests | -| FR12 | `ref`, `onFocus`, and `onBlur` reach the input, and a caller `aria-labelledby` and `aria-describedby` name and describe it. Other base props (`id`, `data-*`, other ARIA, `onClick`, and other handlers), `className`, `style`, `xstyle`, and `width` reach the field root. Caller ARIA attributes name and describe the switch, so they MUST reach the input (`architecture:public-component-api` INV5). Implementation gap: ARIA other than `aria-labelledby` and `aria-describedby`, such as `aria-label`, lands on the field root. | Current tests; `architecture:public-component-api` INV5 | -| FR13 | The thumb is one circle under any pointer, at each state's size: a touch presentation moves and lifts it and never reshapes it, and its lift and rubber band derive from the box it renders. | Owner direction, 2026-10-26 | -| FR14 | Under a finger the thumb, lifted or banded past either end, paints inside the track, in either writing direction. | Owner direction, 2026-10-25 | -| FR15 | For every pointer (`spec:AST-094` FR43) a tap, a hold of any length or a short drag toggles the switch at the release (a tap through the browser's click; where the browser sends no click for a finger's release, as for a hold, the switch clicks its input itself), and a drag commits the side it flipped the switch to, which it reaches once the pointer is the track width less the thumb width from where it pressed, toward the other end, and leaves again once the pointer comes back past where it pressed (measured in `motion/uikit/switch.ts`: `travel`, `flipDistance`). A finger's drag commits wherever the finger lifts; a mouse's or a pen's commits when it is released inside the switch's tracking bounds, its box grown by UIControl's 70 px tracking expansion. Under a finger a press slid off and lifted outside without a flip toggles it too, as `spec:AST-094` FR11 allows for a toggle under `spec:AST-020` FR9's slide-off exception (DEC-5); a mouse or a pen released outside those bounds leaves it alone, flipped or not, as the native checkbox does. Under a finger the thumb follows the drag as UISwitch's does (iOS 26.2); under a fine primary pointer the switch keeps its own look, and the value moves its thumb (`spec:AST-094` FR13). The switch's own click, for a hold and for a flipping drag, is the exception `spec:AST-094` FR11 states for Switch. Under reduced motion nothing lifts or springs: a tap, a release or a change cross-fades the thumb to its rest and the track colour with it, while a dragging finger still moves the thumb. A finger's slide-off toggle (`spec:AST-020` FR9, DEC-5; `spec:AST-094` FR11) meets WCAG 2.5.2 Pointer Cancellation through its Abort or Undo option: sliding back, or toggling again, undoes it. A mouse or pen released outside the tracking bounds cancels the press. | `spec:AST-094` FR11, FR13, FR43; `spec:AST-020` FR9/DEC-5; `design:reduced-motion`; WCAG 2.5.2 | +| ID | Invariant | Basis | +| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | The current render places a thumb inside the switch track and arranges that control with its label: a visually hidden native `` sits over a decorative track, with the label after it by default or before it with `labelPosition="start"`. | `spec:AST-021`; current source, docs, and focused tests | +| FR2 | The field, track, thumb, and label carry their existing local targets. The field reflects `data-label-position` and `data-label-spacing` only for non-default values; the track reflects `data-size`, `data-checked`, and `data-disabled`; the thumb reflects `data-size` and `data-checked`. | `architecture:component-theming-surface`; current source and tests | +| FR3 | A busy switch renders Spinner inside the thumb; a status message renders through detached FieldStatus. | Current source, docs, and focused tests | +| FR4 | The input is named by its label (or a caller `aria-labelledby`) and described by any caller description, then the description, status message, and disabled reason that exist; the description is not part of the name. A click on the label or description toggles the switch. | `spec:AST-021`; current tests | +| FR5 | A change calls `onChange(checked, event)`; unless that call prevents default, `changeAction(checked, event)` runs in a transition and the switch shows the proposed state while the Action is pending, and `value` once it settles. | Current tests | +| FR6 | While `isLoading`, or while the shown state differs from `value`, the input sets `aria-busy`, ignores pointer input, refuses changes, keeps focus, and the switch announces its busy start once per busy period through the shared channel. | `spec:AST-086` FR8; current tests | +| FR7 | Disabled: `aria-disabled="true"` on a focusable input, no `name`, detached from its form (so it neither submits nor blocks submission when required), no change, no caller `onClick` on the field, no hover or press paint, and a half-opacity track. With `disabledMessage`, a reason tooltip opens on row hover and keyboard focus and describes the input. Implementation gap: caller DOM handlers other than `onClick`, such as `onKeyDown`, still run on the field root while disabled (`spec:AST-085` FR2). | `spec:AST-085` FR1–FR3, FR5; current tests | +| FR8 | Enabled, the input submits `on` under `htmlName` when on and nothing when off; `isRequired` sets native `required`, while `aria-required` follows the effective required state; `aria-invalid` follows `status` (`false` on a required switch without an error), so the browser's own invalid state never shows before an error. | Current tests | +| FR9 | Keyboard focus paints the shared focus outline on the track. Hovering the enabled row tints the track; pressing paints the pressed overlay on both, applying `design:user-states` DR1 and DR2; a touch press fades out from the row. Track color transitions are instant under reduced motion; under a finger the colour fades with the thumb's cross-fade (FR15). | `architecture:interaction-modality` INV1; `design:user-states` DR1, DR2 for the pressed overlay; `design:reduced-motion`; current tests | +| FR10 | The thumb travels toward the inline end when on, mirrored under RTL, and moves without a transition under reduced motion (`design:reduced-motion` DR1); under a finger it cross-fades to its rest instead (FR15). Where the primary pointer is coarse, the switch's hit area grows around the track toward 44 by 44 CSS pixels, each side stopping at half the gap to a neighbouring target so a tap goes to the nearer one (`spec:AST-094` FR42, `utils/hitArea.ts`), with nothing visible added. | WCAG 2.5.8; current tests; `design:reduced-motion` DR1; `spec:AST-094` FR42 | +| FR11 | Under forced colors, on and off stay distinguishable (Highlight track when on), and the hover tint does not hide the thumb. | WCAG 1.4.11; current tests | +| FR12 | `ref`, `onFocus`, and `onBlur` reach the input, and a caller `aria-labelledby` and `aria-describedby` name and describe it. Other base props (`id`, `data-*`, other ARIA, `onClick`, and other handlers), `className`, `style`, `xstyle`, and `width` reach the field root. Caller ARIA attributes name and describe the switch, so they MUST reach the input (`architecture:public-component-api` INV5). Implementation gap: ARIA other than `aria-labelledby` and `aria-describedby`, such as `aria-label`, lands on the field root. | Current tests; `architecture:public-component-api` INV5 | +| FR13 | The thumb is one circle under any pointer, at each state's size: a drag moves it under every pointer, a touch presentation lifts it, nothing reshapes it, and its lift and rubber band derive from the box it renders. | Owner direction, 2026-10-26 | +| FR14 | The thumb, lifted under a finger or banded past either end under any pointer, paints inside the track, in either writing direction. | Owner direction, 2026-10-25 | +| FR15 | For every pointer (`spec:AST-094` FR43) a tap, a hold of any length or a short drag toggles the switch at the release (a tap through the browser's click; where the browser sends no click for a finger's release, as for a hold, the switch clicks its input itself), and a drag commits the side it flipped the switch to, which it reaches once the pointer is the track width less the thumb width from where it pressed, toward the other end, and leaves again once the pointer comes back past where it pressed (measured in `motion/uikit/switch.ts`: `travel`, `flipDistance`). A finger's drag commits wherever the finger lifts; a mouse's or a pen's commits when it is released inside the switch's tracking bounds, its box grown by UIControl's 70 px tracking expansion. Under a finger a press slid off and lifted outside without a flip toggles it too, as `spec:AST-094` FR11 allows for a toggle under `spec:AST-020` FR9's slide-off exception (DEC-5); a mouse or a pen released outside those bounds leaves it alone, flipped or not, as the native checkbox does. Under every pointer the thumb follows the drag on `switchControl.tracking`, banded past either end, and the track's colour changes where the drag flips the switch; at the release the thumb settles to the committed end on `switchControl.travel`, starting at the release velocity, with the theme's `character` and `tempo` shaping the spring (`spec:AST-094` FR43, FR44). A tap, a hold without a drag, a key or a change of `value` moves the thumb and the track's colour on the theme's duration and easing tokens under every pointer, a finger included (`spec:AST-094` FR44). Under a touch presentation the thumb also lifts while a finger presses, on the same tokens; under a fine primary pointer it does not lift (`spec:AST-094` FR13). At a release the track's colour changes over the settle spring's computed time (`spec:AST-108` FR4a). The switch's own click, for a hold and for a flipping drag, is the exception `spec:AST-094` FR11 states for Switch. Under reduced motion nothing lifts or springs: a tap, a release or a change cross-fades the thumb to its rest and the track colour with it, while a dragging pointer still moves the thumb. A finger's slide-off toggle (`spec:AST-020` FR9, DEC-5; `spec:AST-094` FR11) meets WCAG 2.5.2 Pointer Cancellation through its Abort or Undo option: sliding back, or toggling again, undoes it. A mouse or pen released outside the tracking bounds cancels the press. | `spec:AST-094` FR11, FR13, FR43, FR44; `spec:AST-020` FR9/DEC-5; `design:reduced-motion`; WCAG 2.5.2 | ### Motion roles -Under a finger the switch plays these motion roles, which this record names -and the generated motion role table carries (`spec:AST-094` FR4, DEC-5). They -are internal, so no theme overrides them yet (DEC-11); each value is -measured on UISwitch, iOS 26.2 (`motion/uikit/switch.ts`). +This record names these motion roles, and the generated motion role table +carries them (`spec:AST-094` FR4, DEC-5). Each value is measured on UISwitch, +iOS 26.2 (`motion/uikit/switch.ts`), and a theme reaches them only through +its motion knobs (`spec:AST-094` DEC-15). The switch plays +`switchControl.tracking` and `switchControl.travel` for a drag and its +release under every pointer (FR15). The other rows are measured for motion +that a tap or a press starts, which plays the theme's duration and easing +tokens (`spec:AST-094` FR44), so the switch plays none of them. +Implementation gap: under a coarse primary pointer the switch still plays +`switchControl.lift`, `switchControl.settle`, `switchControl.minimumLiftMs`, +`switchControl.travel`, `switchControl.wellOn` and `switchControl.wellOff` +for a tap, and under a fine primary pointer the thumb does not follow a drag +and a release settles on the theme's duration. | Role | Default | Use | | ----------------------------- | --------------------------------------- | ------------------------------------------------------------ | | `switchControl.lift` | response 0.23 s, damping ratio 0.55 | The thumb lifting while a finger presses | | `switchControl.settle` | response 0.5 s, damping ratio 0.7 | The lifted thumb settling after the lift | | `switchControl.minimumLiftMs` | 235 ms | The shortest lift before the settle, so a quick tap shows it | -| `switchControl.travel` | response 0.3 s, damping ratio 1 | The thumb travelling to the committed end | -| `switchControl.tracking` | response 0.36 s, damping ratio 1 | The thumb chasing a dragging finger | +| `switchControl.travel` | response 0.3 s, damping ratio 1 | The thumb settling to the committed end after a release | +| `switchControl.tracking` | response 0.36 s, damping ratio 1 | The thumb chasing a dragging pointer | | `switchControl.wellOn` | mass 1, stiffness 85.38, damping 18.48 | The track colour turning on | | `switchControl.wellOff` | mass 1, stiffness 246.74, damping 31.42 | The track colour turning off | @@ -289,9 +301,10 @@ making the absence intentional. - `spec:AST-085` owns disabled focusability; Switch meets it. - `spec:AST-086` FR8 owns the busy announcement. - `design:user-states` owns the pressed treatment. -- `spec:AST-094` owns touch presentations and FR43's gestures for every - pointer: Switch follows its FR11 press profiles, its FR42 hit area and FR43 - (FR10, FR13–FR15). +- `spec:AST-094` owns touch presentations, FR43's gestures for every + pointer, and FR44's split between a gesture's spring and the theme's + tokens: Switch follows its FR11 press profiles, its FR42 hit area, FR43 and + FR44 (FR10, FR13–FR15). - `architecture:component-theming-surface` owns anatomy qualification, factual `none` dispositions, and composition-preserving target ownership. - FieldStatus and Spinner retain their existing public target contracts when @@ -299,21 +312,21 @@ making the absence intentional. ## Verification map -| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | -| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -| FR1, FR4 | `Switch.test.tsx` structure, label, description, and interaction suites | On/off, size, label position, hidden label | Removing stable parts, or folding the description into the name, fails; the caller description first is asserted. Evidence gap: no test asserts the label order (the position cases count children only) or the disabled reason last. | `audit:Switch/anatomy` | -| FR2 | `Switch.test.tsx` label-target, labelSpacing, and pressed-state cases; `themingTargets.test.ts` | Current local targets | A missing label, field, track, or thumb target, or a missing `data-label-spacing`, fails; a rendered class absent from the documented targets fails. Evidence gap: no test asserts `data-label-position`, or the track and thumb `data-size`, `data-checked`, and `data-disabled`. | `audit:Switch/theming` | -| FR3, FR5, FR6 | `Switch.test.tsx` loading, announcement, and status suites | Busy, pending Action, StrictMode, re-busy, detached status | Removing composed feedback, or announcing more than once per busy period, fails. Evidence gap: no test asserts the Spinner inside the thumb, `changeAction` after `onChange` with `preventDefault`, the optimistic state, or that pointer input is ignored while busy. | `audit:Switch/anatomy` | -| FR7, FR8 | `Switch.test.tsx` disabled, disabledMessage, and form cases; `PressedState.a11y.chromium.spec.ts` disabled press case | Disabled with and without reason; required; on and off | A change while disabled, a submitted disabled value, a blocked submit, or press paint on a disabled switch fails. Evidence gap: no test asserts the absent hover tint, the half-opacity track, or that caller `onKeyDown` still runs while disabled (the FR7 gap). No test asserts `aria-required` following a form-level required default. | `audit:Switch/behavior` | -| FR9–FR11 | `Switch.test.tsx` pressed-state, RTL, coarse-pointer, and forced-colors cases; `Switch.focusRing.source-build.test.mjs` | Mouse and touch press; RTL; coarse pointer; forced colors | Missing press paint, unmirrored travel, or lost forced-colors state fails. Evidence gap: no test asserts the hover tint, instant transitions under reduced motion with a mouse, or the track focus outline; the source-build check covers only how the outline finds the input. | `audit:Switch/visual` | -| FR12 | `Switch.test.tsx` rest-forwarding and ref cases | Root attributes; caller labelledby and describedby | A dropped ref, or a caller `aria-labelledby` or `aria-describedby` on the root, fails; the rest-forwarding case asserts `aria-label` on the root, which is the FR12 gap, so fixing the gap changes that test. Evidence gap: no test distinguishes `onFocus` and `onBlur` on the input from the root, or asserts a caller `onClick`, other handlers, `className`, `style`, `xstyle`, or `width` on the root. | `audit:Switch/api` | -| FR10 (coarse hit area), FR13–FR15 | `__tests__/Switch.interaction.browser.spec.ts` (Chromium and WebKit, real touch input), `__tests__/SwitchPointer.interaction.browser.spec.ts` (Chromium and WebKit, a real mouse; Chromium, a real pen), `Switch.test.tsx` hit-area cases and listener counts, and `switchTouch.test.ts` | The circle at each state's size; the 44 × 44 px hit area, and its stop halfway to a link beside it; a tap, a hold, a short drag, a flipping drag and a press slid off and lifted outside; right to left; Reduce Motion; a mouse or a pen drag past the flip, past it and back, and slid off; a click and a right click; a fine pointer's listeners at rest | A reshaped thumb, a hit area under 44 × 44 px with nothing near, a hit area past the midpoint to a neighbour, a thumb painted outside the track, a slid-off press that leaves the switch alone under a finger, a flip at any other distance, a mouse or pen drag that does not flip at the flip distance, a mouse or pen slid off that toggles, a lift or animation under a fine pointer, or more than one listener at rest under a fine pointer fails. | `audit:Switch/behavior` | -| Switch pattern — semantics | `__tests__/Switch.a11y.test.tsx` (DOM layer, higher layers reported unrun) and `__tests__/Switch.a11y.chromium.spec.ts` (accessibility tree) | Off, on, on after a controlled update, described, hidden label, disabled, focusable-disabled, required, invalid, loading | Losing the switch role, the accessible name, the visible label's words within that name, the exposed on/off state, an attached description, or disabled, required, and invalid exposure fails the shared contract. | `audit:Switch/anatomy` | -| Switch pattern — interaction | `__tests__/Switch.a11y.chromium.spec.ts` (real browser) | Every state, each checked for what applies to it: round trips, pointer cancellation, and focus-on-change cover the operable states (off, on, on after a controlled update, described, hidden label, required, invalid); Tab reach-and-escape covers every focusable state, adding focusable-disabled and loading; inoperability covers the states the user cannot change (disabled, focusable-disabled, loading). | Losing either direction of a pointer or Space round trip, letting a mouse press slid off the control still change it (WCAG 2.5.2; under a finger FR15's reversible-toggle exception applies), moving focus when the state changes (WCAG 3.2.2), failing to reach or leave the control by Tab, or changing a state the user is not meant to be able to change fails the shared contract. | `audit:Switch/anatomy` | -| Switch pattern — label stability | `__tests__/Switch.a11y.chromium.spec.ts`, reported not gated | Every operable state: off, on, on after a controlled update, described, hidden label, required, invalid | The APG requires the label not to change with the state. No current record adopts it, so the expectation is advisory: a name that flips across a round trip is reported, and does not fail the build. | `audit:Switch/anatomy` | -| Switch pattern — state inventory | `__tests__/Switch.a11y.chromium.spec.ts` inventory assertion | Every bound state | The labels `__tests__/Switch.a11y.states.ts` claims each state renders are compared against the rendered page, so a stale inventory is reported as one rather than as a standards failure. | `audit:Switch/anatomy` | -| ORD1, ORD2, PR1 | ORD1 is covered by the FR5 and FR7 rows, and the caller-description-first part of ORD2 by the FR1 row. Evidence gap: no test asserts the rest of the ORD2 order or listener bounds | — | — | `audit:Switch/behavior` | -| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and all current targets | Canonical-key drift, invalid dispositions or target spelling, or an unclaimed current local target fails repository validation. | `audit:Switch/theming` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| FR1, FR4 | `Switch.test.tsx` structure, label, description, and interaction suites | On/off, size, label position, hidden label | Removing stable parts, or folding the description into the name, fails; the caller description first is asserted. Evidence gap: no test asserts the label order (the position cases count children only) or the disabled reason last. | `audit:Switch/anatomy` | +| FR2 | `Switch.test.tsx` label-target, labelSpacing, and pressed-state cases; `themingTargets.test.ts` | Current local targets | A missing label, field, track, or thumb target, or a missing `data-label-spacing`, fails; a rendered class absent from the documented targets fails. Evidence gap: no test asserts `data-label-position`, or the track and thumb `data-size`, `data-checked`, and `data-disabled`. | `audit:Switch/theming` | +| FR3, FR5, FR6 | `Switch.test.tsx` loading, announcement, and status suites | Busy, pending Action, StrictMode, re-busy, detached status | Removing composed feedback, or announcing more than once per busy period, fails. Evidence gap: no test asserts the Spinner inside the thumb, `changeAction` after `onChange` with `preventDefault`, the optimistic state, or that pointer input is ignored while busy. | `audit:Switch/anatomy` | +| FR7, FR8 | `Switch.test.tsx` disabled, disabledMessage, and form cases; `PressedState.a11y.chromium.spec.ts` disabled press case | Disabled with and without reason; required; on and off | A change while disabled, a submitted disabled value, a blocked submit, or press paint on a disabled switch fails. Evidence gap: no test asserts the absent hover tint, the half-opacity track, or that caller `onKeyDown` still runs while disabled (the FR7 gap). No test asserts `aria-required` following a form-level required default. | `audit:Switch/behavior` | +| FR9–FR11 | `Switch.test.tsx` pressed-state, RTL, coarse-pointer, and forced-colors cases; `Switch.focusRing.source-build.test.mjs` | Mouse and touch press; RTL; coarse pointer; forced colors | Missing press paint, unmirrored travel, or lost forced-colors state fails. Evidence gap: no test asserts the hover tint, instant transitions under reduced motion with a mouse, or the track focus outline; the source-build check covers only how the outline finds the input. | `audit:Switch/visual` | +| FR12 | `Switch.test.tsx` rest-forwarding and ref cases | Root attributes; caller labelledby and describedby | A dropped ref, or a caller `aria-labelledby` or `aria-describedby` on the root, fails; the rest-forwarding case asserts `aria-label` on the root, which is the FR12 gap, so fixing the gap changes that test. Evidence gap: no test distinguishes `onFocus` and `onBlur` on the input from the root, or asserts a caller `onClick`, other handlers, `className`, `style`, `xstyle`, or `width` on the root. | `audit:Switch/api` | +| FR10 (coarse hit area), FR13–FR15 | `__tests__/Switch.interaction.browser.spec.ts` (Chromium and WebKit, real touch input), `__tests__/SwitchPointer.interaction.browser.spec.ts` (Chromium and WebKit, a real mouse; Chromium, a real pen), `Switch.test.tsx` hit-area cases and listener counts, and `switchTouch.test.ts` | The circle at each state's size; the 44 × 44 px hit area, and its stop halfway to a link beside it; a tap, a hold, a short drag, a flipping drag and a press slid off and lifted outside; right to left; Reduce Motion; a mouse or a pen drag past the flip, past it and back, and slid off; a click and a right click; a fine pointer's listeners at rest | A reshaped thumb, a hit area under 44 × 44 px with nothing near, a hit area past the midpoint to a neighbour, a thumb painted outside the track, a slid-off press that leaves the switch alone under a finger, a flip at any other distance, a mouse or pen drag that does not flip at the flip distance, a mouse or pen slid off that toggles, a lift under a fine pointer, a thumb that does not follow a mouse or pen drag, a release that settles on a theme curve or from rest, a tap under a finger that plays a measured role instead of the theme's tokens, or more than one listener at rest under a fine pointer fails. | `audit:Switch/behavior` | +| Switch pattern — semantics | `__tests__/Switch.a11y.test.tsx` (DOM layer, higher layers reported unrun) and `__tests__/Switch.a11y.chromium.spec.ts` (accessibility tree) | Off, on, on after a controlled update, described, hidden label, disabled, focusable-disabled, required, invalid, loading | Losing the switch role, the accessible name, the visible label's words within that name, the exposed on/off state, an attached description, or disabled, required, and invalid exposure fails the shared contract. | `audit:Switch/anatomy` | +| Switch pattern — interaction | `__tests__/Switch.a11y.chromium.spec.ts` (real browser) | Every state, each checked for what applies to it: round trips, pointer cancellation, and focus-on-change cover the operable states (off, on, on after a controlled update, described, hidden label, required, invalid); Tab reach-and-escape covers every focusable state, adding focusable-disabled and loading; inoperability covers the states the user cannot change (disabled, focusable-disabled, loading). | Losing either direction of a pointer or Space round trip, letting a mouse press slid off the control still change it (WCAG 2.5.2; under a finger FR15's reversible-toggle exception applies), moving focus when the state changes (WCAG 3.2.2), failing to reach or leave the control by Tab, or changing a state the user is not meant to be able to change fails the shared contract. | `audit:Switch/anatomy` | +| Switch pattern — label stability | `__tests__/Switch.a11y.chromium.spec.ts`, reported not gated | Every operable state: off, on, on after a controlled update, described, hidden label, required, invalid | The APG requires the label not to change with the state. No current record adopts it, so the expectation is advisory: a name that flips across a round trip is reported, and does not fail the build. | `audit:Switch/anatomy` | +| Switch pattern — state inventory | `__tests__/Switch.a11y.chromium.spec.ts` inventory assertion | Every bound state | The labels `__tests__/Switch.a11y.states.ts` claims each state renders are compared against the rendered page, so a stale inventory is reported as one rather than as a standards failure. | `audit:Switch/anatomy` | +| ORD1, ORD2, PR1 | ORD1 is covered by the FR5 and FR7 rows, and the caller-description-first part of ORD2 by the FR1 row. Evidence gap: no test asserts the rest of the ORD2 order or listener bounds | — | — | `audit:Switch/behavior` | +| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and all current targets | Canonical-key drift, invalid dispositions or target spelling, or an unclaimed current local target fails repository validation. | `audit:Switch/theming` | No repository check resolves `delegatesTo` owner and target pairs; the pairs above match the current Spinner and FieldStatus targets. diff --git a/docs/specs/components/TreeList/TreeList.spec.md b/docs/specs/components/TreeList/TreeList.spec.md index fbf992c17c92b..b15e7d52da2d7 100644 --- a/docs/specs/components/TreeList/TreeList.spec.md +++ b/docs/specs/components/TreeList/TreeList.spec.md @@ -102,17 +102,17 @@ Supporting one does not justify omitting another from the durable contract. ## Behavioral and layout contract -| ID | Invariant | Basis | Contract state | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- | -| FR1 | The current render contains one Tree list, one Item and Item label per rendered node, optional Header, Chevron, Item description, Start content, End content, and Guide parts, and delegated Chevron glyphs. | Current source, docs, and tests | Current behavior | -| FR2 | Tree list, Item, Chevron, Item label, and Guide carry the five current `tree-list*` targets documented below. | Current source and target docs | Current behavior | -| FR3 | Chevron glyph delegates to Icon's `icon` target. Header, Start content, and End content are consumer-owned and have no TreeList target. | Current source and component docs | Current ownership | -| FR4 | Item description is a stable library-rendered part below Item label and currently has no public target. It does not inherit through `tree-list-item-label`; both spans are siblings inside the row content. | Current source and target docs | Current reachability gap; no new target is accepted | -| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged | -| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `
  • ` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent | -| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. A press inside an open Tooltip or HoverCard surface performs no row action (`component:Tooltip` FR11). | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged | -| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only (`spec:AST-094` FR4–FR5); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2). | UIKit outline list (`UICollectionView`), iOS 26.2; `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval | -| FR9 | `TreeList` MUST accept `animatedLayout?: AnimatedLayout`. With a handle, expanding and collapsing a row report the row's size change through it, so the content after the row glides inside the handle's container, and the shown rows and the row's Chevron move on the same motion (`spec:AST-105` FR1–FR4; `spec:AST-108` FR4a). Without one, expanding and collapsing follow FR8. The prop MUST NOT change tree semantics or keyboard behavior. | `spec:AST-105` FR1, FR2; `spec:AST-108` FR4a | Current behavior | +| ID | Invariant | Basis | Contract state | +| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- | +| FR1 | The current render contains one Tree list, one Item and Item label per rendered node, optional Header, Chevron, Item description, Start content, End content, and Guide parts, and delegated Chevron glyphs. | Current source, docs, and tests | Current behavior | +| FR2 | Tree list, Item, Chevron, Item label, and Guide carry the five current `tree-list*` targets documented below. | Current source and target docs | Current behavior | +| FR3 | Chevron glyph delegates to Icon's `icon` target. Header, Start content, and End content are consumer-owned and have no TreeList target. | Current source and component docs | Current ownership | +| FR4 | Item description is a stable library-rendered part below Item label and currently has no public target. It does not inherit through `tree-list-item-label`; both spans are siblings inside the row content. | Current source and target docs | Current reachability gap; no new target is accepted | +| FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged | +| FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `
  • ` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent | +| FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. A press inside an open Tooltip or HoverCard surface performs no row action (`component:Tooltip` FR11). | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged | +| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only, on the theme's duration and easing tokens (`spec:AST-094` FR5, FR44); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2). | UIKit outline list (`UICollectionView`), iOS 26.2; `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval | +| FR9 | `TreeList` MUST accept `animatedLayout?: AnimatedLayout`. With a handle, expanding and collapsing a row report the row's size change through it, so the content after the row glides inside the handle's container, and the shown rows and the row's Chevron move on the same motion (`spec:AST-105` FR1–FR4; `spec:AST-108` FR4a). Without one, expanding and collapsing follow FR8. The prop MUST NOT change tree semantics or keyboard behavior. | `spec:AST-105` FR1, FR2; `spec:AST-108` FR4a | Current behavior | ### Allowed variation From 25af4a10f308c322a07bca236487b2014962e71e Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:49:45 -0800 Subject: [PATCH 02/11] spec(AST-094): list the motion FR44 moves, by component --- .../specs/AST-094-touch-presentations/spec.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 75a91e20cd5d0..1b318a3ce2659 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -817,6 +817,27 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and component documents its own, including the `presentation` value that keeps the released surface (FR25). +Motion that FR44 moves, by component (each record carries its own line): + +| Component | Motion | Today | Under FR44 | +| --------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | +| Switch | Thumb travel and track colour on a tap, a hold or a key, under a finger | Measured switch springs | Theme duration and easing tokens | +| Switch | Thumb lift while a finger presses, with a minimum lift time | Measured lift and settle springs | The same lift on the theme's tokens | +| Switch | A mouse's or a pen's drag | Thumb still until the release, then the theme's duration | Thumb follows; the release springs from its velocity | +| Slider | Thumb lift and drop under a finger | Measured lift and drop springs | Theme tokens | +| Slider | A mouse's or a pen's flick | Ends at the release | Coasts and settles as a finger's does | +| DropdownMenu, Popover | Grow from the trigger and shrink back under a finger | `springs.menuOpen`, `springs.menuClose` | Theme entrance and exit tokens | +| DropdownMenu | Sub-menu drill-in on a compact touch screen | `springs.subMenuOpen`, `springs.subMenuBack` | Theme move tokens | +| ContextMenu | Trigger lift, its return, and the menu's grow after a long press | The three `springs.contextMenu*` roles | Theme tokens | +| AlertDialog | Scale-in present and fade-out dismiss under a finger | `springs.alertPresent`, `springs.alertDismiss` | Theme entrance tokens; `spec:AST-090` exit tokens | +| Lightbox | Controls fade on a tap, a double tap's zoom, grow from its source, touch exit | Named measured roles | Theme tokens; `spec:AST-090` exit tokens | +| TreeList | Group slide under a coarse pointer | Measured motion | Theme tokens | +| ProgressBar | Determinate fill on a value change under a coarse pointer | Linear, `progress.fillTrackMs` times the share moved | `--duration-medium` and the theme's easing, every pointer | +| DateInput | Sheet wheel turning onto a clamped row | Unstated | Theme move tokens | +| DateInput | Sheet wheel under a trackpad | Coarse primary pointer only | Wherever the sheet shows | +| Carousel | Paging under a trackpad swipe | Coarse primary pointer only | Every pointer | +| Every overlay | A touch exit that a tap, a key or an outside press starts | The native counterpart's timing | `spec:AST-090` exit tokens, off by default | + ## Verification | Contract | Verification | Representative states | Mutation or failure expectation | From 748cb56165936fc20ab6e7749bb39acf4a70ad5a Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:00:03 -0800 Subject: [PATCH 03/11] spec(AST-094): number the gesture motion rule FR45, since FR44 is taken --- .../specs/AST-094-touch-presentations/spec.md | 44 +++++++++---------- .../AlertDialog/AlertDialog.spec.md | 2 +- .../ContextMenu/ContextMenu.spec.md | 4 +- .../components/DateInput/DateInput.spec.md | 4 +- .../DropdownMenu/DropdownMenu.spec.md | 2 +- .../components/Lightbox/Lightbox.spec.md | 6 +-- docs/specs/components/Popover/Popover.spec.md | 2 +- .../ProgressBar/ProgressBar.spec.md | 2 +- docs/specs/components/Slider/Slider.spec.md | 2 +- docs/specs/components/Switch/Switch.spec.md | 10 ++--- .../components/TreeList/TreeList.spec.md | 2 +- 11 files changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 1b318a3ce2659..0fcc074c96986 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -38,7 +38,7 @@ menus, lists and navigation with native-feeling touch behavior: measured physics and gesture thresholds instead of desktop motion stretched to a finger. What a person drags follows their hand, and what they throw settles on a spring from the speed of the throw, under every pointer. Motion that a tap, -a click, a key or the program starts plays the theme's default motion (FR44). +a click, a key or the program starts plays the theme's default motion (FR45). 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. @@ -57,8 +57,8 @@ a measured, sourced difference exists. a click, a hold, a key or the program starts, such as an entrance, an exit or a change of value, plays the theme's duration and easing tokens (`architecture:theme-tokens`) under every pointer, a finger included - (FR44). Gestures, press tracking, a part that follows a pointer, and the - spring that settles a gesture apply to every pointer (FR43, FR44). + (FR45). Gestures, press tracking, a part that follows a pointer, and the + spring that settles a gesture apply to every pointer (FR43, FR45). - Platform-matched look and feel: glass versus material surfaces, glyphs and chrome. This record governs behavior, and touch sizing only as FR42 states. - WebKit embedded in a native app and the native Swift package. This record @@ -129,7 +129,7 @@ a measured, sourced difference exists. 310.7–329.9 pt/s; releases at rest stay at 45–48.5 % and advance at 55–57.5 % (DEC-25). - **FR4 — Touch motion uses named roles.** Every spring, duration and curve a - touch presentation plays for a gesture (FR44) MUST be a motion role named for its use + touch presentation plays for a gesture (FR45) MUST be a motion role named for its use (`architecture:theme-tokens` INV3). The roles this record names are listed below with their defaults and sources; further roles a component's own record measures join through the generated motion role table (DEC-5). With tempo 1 and character @@ -181,7 +181,7 @@ a measured, sourced difference exists. The three `springs.contextMenu*` rows are measured for ContextMenu's lift, and `component:ContextMenu` owns them (DEC-5). - A role plays only for motion a gesture drives (FR44): a release's settle, a + A role plays only for motion a gesture drives (FR45): a release's settle, a flick, a pan's or a pinch's settle, a rubber band's return, and a follow. The theme's `character` normalizes a gesture's spring and its `tempo` scales the spring's duration (`spec:AST-108` FR3); the follow itself and @@ -191,7 +191,7 @@ a measured, sourced difference exists. `springs.subMenuBack`, the three `springs.contextMenu*` rows, `timing.stateChangeMs` and the four `timing.curves.*` rows) stay as the measured native reference, and no motion plays them: that motion plays the - theme's duration and easing tokens (FR44). A component's own record states + theme's duration and easing tokens (FR45). A component's own record states which of its roles a gesture drives. Implementation gap: the pager's measured paging motion (its settle @@ -223,11 +223,11 @@ a measured, sourced difference exists. exit MUST keep the look of its UIKit counterpart's exit, on every touch platform: a menu shrinks into its trigger, and an alert fades. An exit that a tap, a key, an outside press or the program starts plays on the theme's - exit duration and easing tokens (`spec:AST-090`, FR44); an exit that a drag + exit duration and easing tokens (`spec:AST-090`, FR45); an exit that a drag commits springs from the release velocity (FR38). FR8 governs their reduced-motion form. Each overlay's own record states its exit, since members own their animation (`family:overlay-dismissal` AV6). -- **FR44 — A gesture's motion springs; other motion plays the theme's +- **FR45 — A gesture's motion springs; other motion plays the theme's default motion.** Motion that a gesture drives MUST play a spring that starts at the gesture's velocity, under every pointer (FR43). A gesture is a continuous pointer movement whose velocity carries into the motion: a @@ -316,8 +316,8 @@ a measured, sourced difference exists. this condition: a lift, a sheet in place of a popover, a grow from the trigger, hit-area growth (FR42), touch press timing (FR11) and haptics (FR23). It never decides whether a part follows a pointer or whether a - gesture's settle springs (FR43, FR44), and its other motion plays the - theme's duration and easing tokens (FR44). + gesture's settle springs (FR43, FR45), and its other motion plays the + theme's duration and easing tokens (FR45). - **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 @@ -480,7 +480,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and - **FR31 — Menus and popovers grow from their trigger and shrink back.** Under a finger, on every touch device, an anchored menu or a Popover MUST open by growing out of its trigger and close by shrinking back into it, on - the theme's entrance and exit duration and easing tokens (FR44). The shrink plays in full after the surface has + the theme's entrance and exit duration and easing tokens (FR45). The shrink plays in full after the surface has 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. @@ -502,7 +502,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and show the menu beside it, with no row lit. A tap outside closes the menu and reaches nothing beneath it, and closing returns the trigger to its place. The lift and the return play on the theme's duration and easing tokens - (FR44). + (FR45). Under reduced motion FR8 applies. An inline trigger keeps today's menu, a menu presented as a bottom sheet follows the sheet, and `presentation="popover"` keeps the released menu without the lift (`architecture:public-component-api` @@ -512,7 +512,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and sub-menu's rows and a leading "Back to " row replace the menu's rows in the same surface, no flyout opens, and Back returns to the row that opened it (`component:DropdownMenu` FR11 and FR20). The change between - levels plays on the theme's move duration and easing tokens (FR44); under + levels plays on the theme's move duration and easing tokens (FR45); under reduced motion FR8 applies. - **FR35 — A presentation takes no input while it enters.** Under a finger, a tap or a drag that lands while a touch presentation is still entering, a @@ -543,7 +543,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and - **FR38 — The settle continues the pointer.** The commit or the spring-back MUST start at the release velocity (FR6), under every pointer (FR43), on a spring that the theme's `character` normalizes and its `tempo` scales - (FR44). Under reduced motion it follows `design:reduced-motion` DR2, and a + (FR45). Under reduced motion it follows `design:reduced-motion` DR2, and a release from a mouse, a trackpad or a pen follows FR43. - **FR39 — The action never needs a drag.** Every action a drag commits MUST also be reachable without a drag, by keyboard and by mouse @@ -632,7 +632,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and follows a pointer's drag, such as a switch's or a slider's thumb, a sheet, a pager, a picker wheel or a row, follows a mouse, a pen, a trackpad and a finger alike, with the same rubber band past its ends, wherever its record - lets that pointer drag it, and a release from any of them settles on FR44's + lets that pointer drag it, and a release from any of them settles on FR45's spring from the release velocity. These stay off: row swipe actions under a mouse drag, which would take the browser's text selection and native drag, while a trackpad's horizontal scroll swipes a @@ -787,7 +787,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and - BottomSheet, Toast, Lightbox, NavigationStack and keyboard dismissal: a drag that commits follows FR36–FR40. - `spec:AST-100` owns release velocity, which FR17 reads. -- Under FR44, motion that a tap, a hold, a key or the program starts plays +- Under FR45, motion that a tap, a hold, a key or the program starts plays the theme's duration and easing tokens under a finger as under a mouse. The records that state such motion carry it: `component:Switch` FR9, FR10 and FR15, `component:Slider` FR7, `component:ContextMenu` FR5, @@ -798,11 +798,11 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and default, so it closes at once unless the theme sets an exit duration (FR7). Implementation gap: Switch, Slider and ProgressBar still play their measured roles for a tap or a value change under a coarse pointer. -- Under FR44, `spec:AST-105` FR4's glide, which `component:SideNav` FR6 and +- Under FR45, `spec:AST-105` FR4's glide, which `component:SideNav` FR6 and `component:AppShell` FR17 ride, and `spec:AST-107`'s `settleTo` and an `animate` call without a velocity play a spring for motion that no gesture - drives. Their records keep their springs until each carries FR44. -- Under FR43 and FR44, a part that follows a pointer follows every pointer + drives. Their records keep their springs until each carries FR45. +- Under FR43 and FR45, a part that follows a pointer follows every pointer and its release springs from the release velocity under every pointer: `component:Switch` FR15's thumb under a mouse or a pen, `component:Slider` FR7's coast after a mouse or a pen release, and `component:DateInput` @@ -817,9 +817,9 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and component documents its own, including the `presentation` value that keeps the released surface (FR25). -Motion that FR44 moves, by component (each record carries its own line): +Motion that FR45 moves, by component (each record carries its own line): -| Component | Motion | Today | Under FR44 | +| Component | Motion | Today | Under FR45 | | --------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | | Switch | Thumb travel and track colour on a tap, a hold or a key, under a finger | Measured switch springs | Theme duration and easing tokens | | Switch | Thumb lift while a finger presses, with a minimum lift time | Measured lift and settle springs | The same lift on the theme's tokens | @@ -850,7 +850,7 @@ Motion that FR44 moves, by component (each record carries its own line): | FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring; a retarget before the browser reports the call's start | a layout read, a jump, or a lost velocity fails | | FR6 pager | Unit and browser tests of a pager's settle start | releases just under and over 225 pt/s; a catch mid-settle; a programmatic page change mid-settle | a release under the floor whose settle starts moving, a release over it whose settle starts from rest, a catch that loses the on-screen velocity, or a programmatic page change mid-settle that starts from rest fails; a page change during a drag starts from rest | | FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | -| FR44 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | +| FR45 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | | FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | | FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | | FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | diff --git a/docs/specs/components/AlertDialog/AlertDialog.spec.md b/docs/specs/components/AlertDialog/AlertDialog.spec.md index 9de55e41450e7..2347bedb26054 100644 --- a/docs/specs/components/AlertDialog/AlertDialog.spec.md +++ b/docs/specs/components/AlertDialog/AlertDialog.spec.md @@ -127,7 +127,7 @@ Consumer migration instructions belong in consumer docs and release notes. | FR3 | The anatomy is title, description, then the buttons: Cancel then action on wider screens, the action stacked above Cancel on small screens, Cancel keeping initial focus either way; the action may show a loading state while `isActionLoading`. | shipped source and focused tests | | FR4 | `isInline` renders the same anatomy without modality and without the alert-dialog role. | shipped source and public docs | | FR5 | `useImperativeAlertDialog` returns `show`, `hide`, and the element to render, and owns `isOpen`/`onOpenChange` for its caller. | shipped source and focused tests | -| FR6 | Under a touch presentation (`spec:AST-094` FR13), a modal AlertDialog presents by scaling from 1.2 to 1 with its opacity and its backdrop's dimming rising from 0 to 1 on the theme's entrance duration and easing tokens, started at the browser's commit, with the larger start kept inside the viewport; it dismisses by a fade alone on the theme's exit tokens (`spec:AST-090`) and closes at the fade's end; a close during the presentation, or a reopen during the dismissal, turns back from where it is (`spec:AST-094` FR7, FR44). `springs.alertPresent` and `springs.alertDismiss` stay as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4). Under Reduce Motion both are the shared cross-fade (`design:reduced-motion` DR1–DR3). A fine primary pointer keeps today's motion (`spec:AST-094` FR13 and its Non-goals). Implementation gap: not shipped yet (Current-state impact). | proposed from measured iOS 26.2 behavior | +| FR6 | Under a touch presentation (`spec:AST-094` FR13), a modal AlertDialog presents by scaling from 1.2 to 1 with its opacity and its backdrop's dimming rising from 0 to 1 on the theme's entrance duration and easing tokens, started at the browser's commit, with the larger start kept inside the viewport; it dismisses by a fade alone on the theme's exit tokens (`spec:AST-090`) and closes at the fade's end; a close during the presentation, or a reopen during the dismissal, turns back from where it is (`spec:AST-094` FR7, FR45). `springs.alertPresent` and `springs.alertDismiss` stay as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4). Under Reduce Motion both are the shared cross-fade (`design:reduced-motion` DR1–DR3). A fine primary pointer keeps today's motion (`spec:AST-094` FR13 and its Non-goals). Implementation gap: not shipped yet (Current-state impact). | proposed from measured iOS 26.2 behavior | | FR7 | Under a touch presentation, pointer input to the alert is held while it enters (`spec:AST-094` FR35); keyboard and assistive-technology input are never held, and the entrance never makes the content inert. While the alert leaves, its content is inert for the dismissal fade. The hold's timing stays internal (`spec:AST-002` FR3). Implementation gap: not shipped yet (Current-state impact). | `spec:AST-094` FR35; owner decision (DEC-1) | | FR8 | AlertDialog's actions are an action group (`family:action-groups`): a press that slides across them runs the action it lifts on, and a lift outside them runs nothing, under every pointer; keyboard and assistive-technology behavior is unchanged. Implementation gap: not shipped yet (Current-state impact). | `family:action-groups`; `spec:AST-094` DEC-17; DEC-3 | | FR9 | On a phone, a compact window (`spec:AST-012` FR9: narrower than `md` or shorter than 500 px) under a coarse primary pointer, the alert is 320 px wide in every theme, as iOS's is; no theme role sets it. A caller's explicit `width` still wins there (`architecture:public-component-api` INV5), and Dialog's viewport clamp still applies on a screen narrower than the alert. Elsewhere it keeps today's width. Implementation gap: not shipped yet (Current-state impact). | owner decision (DEC-2) | diff --git a/docs/specs/components/ContextMenu/ContextMenu.spec.md b/docs/specs/components/ContextMenu/ContextMenu.spec.md index 1673ff841a5a2..31b2a3556aa70 100644 --- a/docs/specs/components/ContextMenu/ContextMenu.spec.md +++ b/docs/specs/components/ContextMenu/ContextMenu.spec.md @@ -103,7 +103,7 @@ Consumer props, content modes, and presentation policy remain documented in | FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source | | FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | | FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | -| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The lift and the grow apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The trigger's lift and return and the menu's grow play on the theme's duration and easing tokens (`spec:AST-094` FR44); the three `springs.contextMenu*` roles stay as the measured reference (`spec:AST-094` FR4). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 | +| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The lift and the grow apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The trigger's lift and return and the menu's grow play on the theme's duration and easing tokens (`spec:AST-094` FR45); the three `springs.contextMenu*` roles stay as the measured reference (`spec:AST-094` FR4). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 | | FR6 | With `triggerAs="span"` the Trigger area is an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | DEC-1 | | FR7 | Sub-menu rows follow `component:DropdownMenu` FR11 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. A sub-menu with a header or footer shows its rows alone here (`component:DropdownMenu` FR20). | `component:DropdownMenu/DEC-4` | | FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. Arrow keys and typeahead skip disabled rows, shipped behavior that is not a requirement until `component:DropdownMenu` OQ1 is ruled. | Current source and tests | @@ -230,7 +230,7 @@ anchor is positioning infrastructure rather than consumer anatomy. - `spec:AST-094` FR4 lists the three motion roles this component owns, measured for its lift: `springs.contextMenuLift`, `springs.contextMenuDismiss` and `springs.contextMenuMorph`. They stay as the measured reference: a long press - is not a gesture, so the lift plays the theme's tokens (`spec:AST-094` FR44). + is not a gesture, so the lift plays the theme's tokens (`spec:AST-094` FR45). - `family:overlay-dismissal` owns shared Escape and platform-close ordering. ContextMenu's pointer presentation is a shared-stack adopter; the touch presentation delegates dismissal to BottomSheet, which is also a shared-stack diff --git a/docs/specs/components/DateInput/DateInput.spec.md b/docs/specs/components/DateInput/DateInput.spec.md index 5a8aa86084728..8dca89d69b43b 100644 --- a/docs/specs/components/DateInput/DateInput.spec.md +++ b/docs/specs/components/DateInput/DateInput.spec.md @@ -141,8 +141,8 @@ Consumer migration instructions belong in consumer docs and release notes. | FR11 | Clear MUST emit `undefined` through the ordinary change pipeline. The pointer and bottom-sheet surfaces restore field focus using their shipped timing; the native surface MUST NOT reclaim focus because doing so reopens the OS picker. Implementation gap: when the field becomes disabled or busy while its clear control holds focus, the control unmounts and focus falls to the document body; `spec:AST-085/FR4` requires focus to stay or move to a destination this record names. | Current source and focused clear tests; `spec:AST-085/FR4` | | FR12 | No surface renders native `disabled`. A disabled field MUST stay focusable with `aria-disabled`, block typing, picking, clearing, and opening, describe its disabled reason when one is given, and take disabled paint. While busy (`isLoading` or a pending Action) the pointer and bottom-sheet surfaces expose `aria-disabled` and `aria-busy`, stay focusable, and block new commits; the native surface does the same for `isLoading`, and during a pending Action stays editable so the OS picker remains attached but rejects additional commits until the Action settles. A busy field that is not disabled keeps its ordinary paint and Spinner, shows no hover ring while commits are blocked, and keeps its keyboard focus ring. Caller handlers other than focus and blur MUST NOT run while the field is disabled (`spec:AST-085` FR2). Implementation gap: only a caller `onClick` is skipped while disabled; caller key, paste, and pointer handlers (such as `onKeyDown`, `onKeyUp`, `onPaste`, and `onPointerDown`) still run on every surface, the native field included. | `spec:AST-085/FR1–FR6`; `family:input-fields/FR4–FR6`; `family:buttons/FR4`; current source and focused tests | | FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source | -| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR44). | UIDatePicker on the iOS 26.2 simulator; current source | -| FR15 | Wherever the sheet shows, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13, settling on a spring from that speed (`spec:AST-094` FR44). A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source | +| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR45). | UIDatePicker on the iOS 26.2 simulator; current source | +| FR15 | Wherever the sheet shows, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13, settling on a spring from that speed (`spec:AST-094` FR45). A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source | ### Allowed variation diff --git a/docs/specs/components/DropdownMenu/DropdownMenu.spec.md b/docs/specs/components/DropdownMenu/DropdownMenu.spec.md index b166582bc4bb0..e86b6e4a83d84 100644 --- a/docs/specs/components/DropdownMenu/DropdownMenu.spec.md +++ b/docs/specs/components/DropdownMenu/DropdownMenu.spec.md @@ -139,7 +139,7 @@ component-local concepts; each keeps its released default. | FR16 | Checkbox rows follow `component:DropdownMenuCheckboxItem`. | Current source | | FR17 | Arrow keys and typeahead skip disabled rows, including checkbox and radio rows. Shipped behavior, not a requirement until OQ1 is ruled. | Current source and tests | | FR20 | With `header` or `footer` on a sub-menu (`DropdownMenuSubMenu`), its surface is a modal dialog named by its row (AR7), holding the header, the sub-menu's `role="menu"` (rows only), then the footer. It sits in a host `DropdownMenu` renders beside its menu, never inside a menu; under a root with no such host (`ContextMenu`) the sub-menu shows its rows alone. On a fine pointer the dialog is the flyout; on a compact touch display, the drilled-in view, which replaces the root's menu and regions until popped. A region keeps its own keys and presses. The rows are one Tab stop: Tab and Shift+Tab walk the header's controls, the rows, then the footer's, and wrap; from a deeper flyout, that flyout closes and the walk goes on from the rows. An open lands as for any sub-menu (FR11, FR12), not on a region. Escape in the dialog, outside an IME composition and unless a control there handled it, returns focus to the row and leaves the root open. A pointer leaving the flyout keeps it open until focus leaves, and it closes with the flyout holding its row. Without either prop nothing changes. | Current behavior; FR15; AR5 | -| FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button and collapse back into it on the theme's duration and easing tokens (`spec:AST-094` FR31, FR44), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own. | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6 | +| FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button and collapse back into it on the theme's duration and easing tokens (`spec:AST-094` FR31, FR45), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own. | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6 | ### Allowed variation diff --git a/docs/specs/components/Lightbox/Lightbox.spec.md b/docs/specs/components/Lightbox/Lightbox.spec.md index e80f579f2f9e5..57a96c7e4cc6c 100644 --- a/docs/specs/components/Lightbox/Lightbox.spec.md +++ b/docs/specs/components/Lightbox/Lightbox.spec.md @@ -137,18 +137,18 @@ FR21). Consumer props and usage remain documented in `Lightbox.doc.mjs`. | FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled | | FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled | | FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap | -| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the zoom settle, the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR44). Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap | +| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the zoom settle, the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR45). Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap | | FR6 | With `hasZoom`, on an image, a pinch zooms about the fingers and pans while it zooms; past the minimum or maximum the photo resists and eases back without overshoot. A double tap zooms to the larger of filling the viewer and 2×, about the tapped point; a second double tap returns to the fitted size. A zoomed photo pans with momentum and rubber-band edges. A pinch on the controls or the caption still zooms the page. | `spec:AST-094` FR3, FR10 | settled; implementation gap | | FR7 | In a gallery, a horizontal drag moves the row of items, which sit apart by the measured gap. The release lands on at most one neighbouring item by `spec:AST-094` FR3's paging rule (DEC-25). Paging applies over a video item's frame as over a photo; a drag that starts on the video's own controls, such as its scrubber or volume, stays the video's. A zoomed photo pans to its edge before a new drag pages. A pinch never pages. Previous, Next, and the arrow keys keep paging. A controlled `index` the caller refuses springs back. `onIndexChange` reports a swipe. | `spec:AST-094` FR3, FR14, FR36–FR39, DEC-25 | settled; implementation gap | | FR8 | On a fitted image, a downward drag moves the photo with the finger, shrinks it, and fades the backdrop. The release decides once: a projected travel past the commit point closes the viewer and reports `onOpenChange(false)` at the lift; a release moving back springs back. Video is unchanged. | `spec:AST-094` FR36–FR40 | settled; implementation gap | | FR9 | With `hasZoom`, a pinch in from the fitted size is a release decision: below the measured fraction of the fitted size the release closes the viewer; opening the fingers again by the measured amount before the lift is moving back and cancels. A pinch that starts zoomed in never closes the viewer. | `spec:AST-094` FR36, FR37 | settled; implementation gap | | FR10 | With `hasZoom`, under a coarse pointer only, a tap on the photo fades the controls and caption and darkens the backdrop. While they are hidden, the next tap on the photo, a tap beside the photo, or a cancelled drag brings them back, and that tap beside the photo does not close the viewer (ORD3). AR5 governs keyboard and assistive-technology access while they are hidden. | `architecture:public-component-api` INV16 | settled; implementation gap | | FR11 | A mouse, a pen, and a trackpad also use FR7's drag paging, FR8's drag to close, a ctrl-wheel or trackpad pinch over the photo for FR6's zoom and FR9's close, and a wheel to pan a zoomed photo; over the photo these replace the browser's page zoom and scroll. A double-click zooms 2× about the clicked point. A click on the photo keeps the controls, and the viewer opens and closes at once. | `spec:AST-094` FR43; `spec:AST-017` FR59 | settled; implementation gap | -| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens and returns into it when it closes, on the theme's duration and easing tokens (`spec:AST-094` FR44): the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap | +| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens and returns into it when it closes, on the theme's duration and easing tokens (`spec:AST-094` FR45): the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap | | FR13 | `hasZoom={false}` turns off FR6, FR9, and FR10. FR7 and FR8 add to the buttons, keys, and Escape and remove none of them, so Lightbox takes no `presentation` prop. | `architecture:public-component-api` INV16 | settled | | FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap | | FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled | -| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on the theme's exit tokens (`spec:AST-090`; `spec:AST-094` FR7, FR44). `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap | +| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on the theme's exit tokens (`spec:AST-090`; `spec:AST-094` FR7, FR45). `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap | ### Allowed variation diff --git a/docs/specs/components/Popover/Popover.spec.md b/docs/specs/components/Popover/Popover.spec.md index 7fe0476bded0e..492c3cd3be09a 100644 --- a/docs/specs/components/Popover/Popover.spec.md +++ b/docs/specs/components/Popover/Popover.spec.md @@ -187,7 +187,7 @@ table owns their semantic effect. | 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 on the theme's duration and easing tokens, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR35 and FR44 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it | +| FR9 | Under a finger, Popover grows out of its trigger and shrinks back into it on the theme's duration and easing tokens, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR35 and FR45 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it | ### Allowed variation diff --git a/docs/specs/components/ProgressBar/ProgressBar.spec.md b/docs/specs/components/ProgressBar/ProgressBar.spec.md index 7e827b0313e3c..8c75898b053df 100644 --- a/docs/specs/components/ProgressBar/ProgressBar.spec.md +++ b/docs/specs/components/ProgressBar/ProgressBar.spec.md @@ -164,7 +164,7 @@ Consumer migration instructions belong in consumer docs and release notes. | FR1 | Every determinate presentation ProgressBar provides is a sufficient standalone visual for distinguishing completed from remaining progress; external visible text may supplement it but is never required, and supplementary text or graphics never substitute for that distinction. ProgressBar does not make visual correctness depend on caller-declared external content it cannot verify or associate. | `component:ProgressBar/DEC-1`; `spec:AST-002` | | FR2 | New public API that adjusts the visual distinction is considered only after the component has a correct base behavior and a stable caller-owned distinction still remains. | `component:ProgressBar/DEC-1`; `spec:AST-002` | | FR3 | The root holds a label row and a track. The label names the track (`role="progressbar"`) through `aria-labelledby`; `isLabelHidden` hides it visually only. When determinate, the track exposes `aria-valuenow` (the clamped value), `aria-valuemin` 0, `aria-valuemax` (`max`), and `aria-valuetext` from `formatValueLabel`, which defaults to the rounded percentage; `hasValueLabel` shows the same text beside the label. A zero or non-finite `max` gives 0% and never renders `NaN`. Astryx-owned value text comes from one catalog message (`architecture:internationalization` INV1a). Implementation gap: the default text is assembled in code (Current-state impact). | Current source and tests | -| FR4 | The track is 8px high, fully rounded, in `--color-background-muted`, and clips the fill. The determinate fill spans the track and slides along it so its leading edge sits at the clamped percentage, mirrored by the bar's own writing direction, moving from where it is drawn over `--duration-medium` with the theme's easing when the value changes, under every pointer, a finger included (`spec:AST-094` FR44). Under a coarse primary pointer (`spec:AST-094` FR13), above zero it draws at least 16px long, a mark that length covers counts as on the fill (FR7), and at zero it paints nothing. `progress.fillTrackMs` (1000ms, UIKit's progress view for a whole-track change) and its linear curve, `progress.curves.fill`, stay in the generated motion role table as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4, DEC-5). Implementation gap: under a coarse primary pointer the fill still moves linearly for `progress.fillTrackMs` times the share of the track the value moved. Under reduced motion a new value lands at once on every pointer (`design:reduced-motion` DR1). The fill paints `--color-accent`, `--color-success`, `--color-warning`, or `--color-error`, or `--color-text-disabled` for `neutral` and disabled bars; a theme-added variant has a safe theme-independent fill (`architecture:component-theming-surface` INV14). Implementation gap: a theme-added variant paints no fill and no on-fill mark color until a theme styles it (Current-state impact). In forced colors the fill is sized to the value instead of sliding and keeps a 1px `CanvasText` outline so its shape stays visible (`architecture:theme-tokens` INV11). | Current source and tests; UIKit measurement (iOS 26.2 simulator); `component:ProgressBar/DEC-2` | +| FR4 | The track is 8px high, fully rounded, in `--color-background-muted`, and clips the fill. The determinate fill spans the track and slides along it so its leading edge sits at the clamped percentage, mirrored by the bar's own writing direction, moving from where it is drawn over `--duration-medium` with the theme's easing when the value changes, under every pointer, a finger included (`spec:AST-094` FR45). Under a coarse primary pointer (`spec:AST-094` FR13), above zero it draws at least 16px long, a mark that length covers counts as on the fill (FR7), and at zero it paints nothing. `progress.fillTrackMs` (1000ms, UIKit's progress view for a whole-track change) and its linear curve, `progress.curves.fill`, stay in the generated motion role table as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4, DEC-5). Implementation gap: under a coarse primary pointer the fill still moves linearly for `progress.fillTrackMs` times the share of the track the value moved. Under reduced motion a new value lands at once on every pointer (`design:reduced-motion` DR1). The fill paints `--color-accent`, `--color-success`, `--color-warning`, or `--color-error`, or `--color-text-disabled` for `neutral` and disabled bars; a theme-added variant has a safe theme-independent fill (`architecture:component-theming-surface` INV14). Implementation gap: a theme-added variant paints no fill and no on-fill mark color until a theme styles it (Current-state impact). In forced colors the fill is sized to the value instead of sliding and keeps a 1px `CanvasText` outline so its shape stays visible (`architecture:theme-tokens` INV11). | Current source and tests; UIKit measurement (iOS 26.2 simulator); `component:ProgressBar/DEC-2` | | FR5 | `isDisabled` mutes the fill, label, value text, and marks and adds no ARIA state. | Current source and tests | | FR6 | `isIndeterminate` removes every value attribute and the value text. Without reduced motion, on every pointer, a 40% fill slides across the clipped track, eased, 1.5s a pass, mirrored by the bar's own rendered direction (`architecture:internationalization` INV11). Under reduced motion, on every pointer, the fill spans the whole track as one solid bar, with no segment and no travel, and only its opacity pulses: from 1 down to 0.84 and back, each way taking the `progress.reducedMotionPulseMs` role (2000ms) on the `timing.curves.move` curve; the track is unchanged. The pulse is a busy loop, so tempo does not scale it (`spec:AST-094` FR4, `spec:AST-108` FR4). It applies `design:reduced-motion` DR1's no-travel rule, replacing the travel with an opacity change, to a loop that record leaves to its component. At the 0.84 floor the fill keeps at least 3:1 against the track in both themes for every tone that reaches 3:1 at full opacity (AR4). Implementation gap: the slide, and the centering of marks, mirror under any right-to-left ancestor rather than the bar's own direction, so in a left-to-right region of a right-to-left page they run against the label (Current-state impact). | Current source and tests; `component:ProgressBar/DEC-2` | | FR7 | Each finite mark renders, outside the `progressbar` element, a tick at its clamped position whose tooltip shows its label on hover and focus; marks inside the filled part take the fill's contrasting color, and marks do not render while indeterminate. A focusable mark exposes its label as its accessible name (WCAG 2.2 SC 4.1.2). Implementation gap: marks are focusable `span`s with no role or name, labelled only as a description once the tooltip loads (Current-state impact). | Current source and tests | diff --git a/docs/specs/components/Slider/Slider.spec.md b/docs/specs/components/Slider/Slider.spec.md index da52f29b39e55..e9f044e150614 100644 --- a/docs/specs/components/Slider/Slider.spec.md +++ b/docs/specs/components/Slider/Slider.spec.md @@ -115,7 +115,7 @@ modes, states, and usage remain documented in `Slider.doc.mjs`. | FR4 | Label and status presentation continue to use Field and FieldStatus; value tooltips continue to use Tooltip. | Current source and focused tests | Verified composition boundary | | FR5 | `onChangeEnd` fires once when an interaction ends — a pointer drag or track press is released or cancelled (after a release, once its coast settles; FR7), or a keyboard step (arrow, Page, Home, End) completes — and carries the value the owner renders after its synchronous response to that interaction's `onChange` calls, including any clamp, snap, or rejection; it never carries a value Slider emitted that the owner did not render. A range reports both values the same way. | Maintainer decision (DEC-3) | Approved | | FR6 | When both thumbs of a range share a value, a pointer press takes the thumb that can travel toward the pointer: a press on the track beside them takes the thumb on its side, and a press on the thumbs or on their shared value, such as its mark label, moves neither until the pointer moves, when the direction of that first movement picks the thumb, which takes the drag's focus, pressed state and value tooltip. Until then the upper thumb, drawn over the lower one, holds the press, its focus and its pressed state, so a press that never moves changes no value and leaves focus on the upper thumb. A drag that began on the shared thumbs and comes back past their value moves the other thumb: the dragged thumb stops against it and the other follows the pointer, so with both at 80 a drag to 95 and back to 50 ends at [50, 80]. | Current source and focused tests | Accepted contract | -| FR7 | Every pointer — a finger, a pen, or a mouse — drags a thumb by its travel. A press on a thumb changes no value. Once the pointer has moved `spec:AST-094/FR19`'s 10 px from where it pressed — a distance the pointer travels, not a zone around the thumb — the value moves by the pointer's further travel along the track over the track's length, from wherever the thumb was grabbed, so the thumb never jumps to the pointer; each move is written to the thumb with no component render (`spec:AST-094/FR9`). Between coincident range thumbs, FR6's first movement is the one that crosses the 10 px. A finger or a pen claims the drag only along the track, so a swipe across it scrolls the page; a mouse claims in any direction. Under a coarse primary pointer, after hydration (`spec:AST-094/FR13`), each thumb takes a finger or a pen through a 48 px touch target that grows toward a range's other thumb only to half the gap (`spec:AST-094/FR42`), and the held thumb lifts and drops back on the theme's duration and easing tokens (`spec:AST-094/FR44`). A finger's or a pen's drag that brings the value to either end plays UISlider's end haptic where the browser can vibrate, and a mouse's drag plays none; iOS Safari has no haptic for that moment (`spec:AST-094/FR23`). After any pointer's release, a finger's, a pen's or a mouse's, the thumb coasts toward 0.4 of the release's momentum projection and settles on the `springs.sliderRelease` motion role, about 0.6 s, with `onChange` firing through the coast (`spec:AST-094/FR44`); the role is internal, with UISlider's measured value at `tempo` 1 and `'standard'`, and follows the theme's motion knobs (`spec:AST-094/FR4`). `springs.sliderLift` and `springs.sliderDrop` stay as UISlider's measured reference, and no motion plays them. Implementation gap: under a coarse primary pointer the thumb still lifts on `springs.sliderLift` and drops on `springs.sliderDrop`, and a mouse's or a pen's drag ends at its release, with no coast. A tap or a click on the track still sets the value: a finger's tap when it lifts, and a mouse press at once, where a primary click lands; a secondary click and a wheel or trackpad scroll change nothing (`spec:AST-094/FR43`). Under reduced motion the thumb does not lift, a tap's value applies at once, and the coast continues (`design:reduced-motion/DR2`). | UIKit measurements and maintainer decision (DEC-4) | Approved | +| FR7 | Every pointer — a finger, a pen, or a mouse — drags a thumb by its travel. A press on a thumb changes no value. Once the pointer has moved `spec:AST-094/FR19`'s 10 px from where it pressed — a distance the pointer travels, not a zone around the thumb — the value moves by the pointer's further travel along the track over the track's length, from wherever the thumb was grabbed, so the thumb never jumps to the pointer; each move is written to the thumb with no component render (`spec:AST-094/FR9`). Between coincident range thumbs, FR6's first movement is the one that crosses the 10 px. A finger or a pen claims the drag only along the track, so a swipe across it scrolls the page; a mouse claims in any direction. Under a coarse primary pointer, after hydration (`spec:AST-094/FR13`), each thumb takes a finger or a pen through a 48 px touch target that grows toward a range's other thumb only to half the gap (`spec:AST-094/FR42`), and the held thumb lifts and drops back on the theme's duration and easing tokens (`spec:AST-094/FR45`). A finger's or a pen's drag that brings the value to either end plays UISlider's end haptic where the browser can vibrate, and a mouse's drag plays none; iOS Safari has no haptic for that moment (`spec:AST-094/FR23`). After any pointer's release, a finger's, a pen's or a mouse's, the thumb coasts toward 0.4 of the release's momentum projection and settles on the `springs.sliderRelease` motion role, about 0.6 s, with `onChange` firing through the coast (`spec:AST-094/FR45`); the role is internal, with UISlider's measured value at `tempo` 1 and `'standard'`, and follows the theme's motion knobs (`spec:AST-094/FR4`). `springs.sliderLift` and `springs.sliderDrop` stay as UISlider's measured reference, and no motion plays them. Implementation gap: under a coarse primary pointer the thumb still lifts on `springs.sliderLift` and drops on `springs.sliderDrop`, and a mouse's or a pen's drag ends at its release, with no coast. A tap or a click on the track still sets the value: a finger's tap when it lifts, and a mouse press at once, where a primary click lands; a secondary click and a wheel or trackpad scroll change nothing (`spec:AST-094/FR43`). Under reduced motion the thumb does not lift, a tap's value applies at once, and the coast continues (`design:reduced-motion/DR2`). | UIKit measurements and maintainer decision (DEC-4) | Approved | ### Observed current target asymmetry diff --git a/docs/specs/components/Switch/Switch.spec.md b/docs/specs/components/Switch/Switch.spec.md index 22c05a7ddadb8..46108ecd3307c 100644 --- a/docs/specs/components/Switch/Switch.spec.md +++ b/docs/specs/components/Switch/Switch.spec.md @@ -58,7 +58,7 @@ system_specs: spec:AST-094/FR6, spec:AST-094/FR18, spec:AST-094/FR24, - spec:AST-094/FR44, + spec:AST-094/FR45, ] --- @@ -165,7 +165,7 @@ Consumer migration instructions belong in consumer docs and release notes. | FR12 | `ref`, `onFocus`, and `onBlur` reach the input, and a caller `aria-labelledby` and `aria-describedby` name and describe it. Other base props (`id`, `data-*`, other ARIA, `onClick`, and other handlers), `className`, `style`, `xstyle`, and `width` reach the field root. Caller ARIA attributes name and describe the switch, so they MUST reach the input (`architecture:public-component-api` INV5). Implementation gap: ARIA other than `aria-labelledby` and `aria-describedby`, such as `aria-label`, lands on the field root. | Current tests; `architecture:public-component-api` INV5 | | FR13 | The thumb is one circle under any pointer, at each state's size: a drag moves it under every pointer, a touch presentation lifts it, nothing reshapes it, and its lift and rubber band derive from the box it renders. | Owner direction, 2026-10-26 | | FR14 | The thumb, lifted under a finger or banded past either end under any pointer, paints inside the track, in either writing direction. | Owner direction, 2026-10-25 | -| FR15 | For every pointer (`spec:AST-094` FR43) a tap, a hold of any length or a short drag toggles the switch at the release (a tap through the browser's click; where the browser sends no click for a finger's release, as for a hold, the switch clicks its input itself), and a drag commits the side it flipped the switch to, which it reaches once the pointer is the track width less the thumb width from where it pressed, toward the other end, and leaves again once the pointer comes back past where it pressed (measured in `motion/uikit/switch.ts`: `travel`, `flipDistance`). A finger's drag commits wherever the finger lifts; a mouse's or a pen's commits when it is released inside the switch's tracking bounds, its box grown by UIControl's 70 px tracking expansion. Under a finger a press slid off and lifted outside without a flip toggles it too, as `spec:AST-094` FR11 allows for a toggle under `spec:AST-020` FR9's slide-off exception (DEC-5); a mouse or a pen released outside those bounds leaves it alone, flipped or not, as the native checkbox does. Under every pointer the thumb follows the drag on `switchControl.tracking`, banded past either end, and the track's colour changes where the drag flips the switch; at the release the thumb settles to the committed end on `switchControl.travel`, starting at the release velocity, with the theme's `character` and `tempo` shaping the spring (`spec:AST-094` FR43, FR44). A tap, a hold without a drag, a key or a change of `value` moves the thumb and the track's colour on the theme's duration and easing tokens under every pointer, a finger included (`spec:AST-094` FR44). Under a touch presentation the thumb also lifts while a finger presses, on the same tokens; under a fine primary pointer it does not lift (`spec:AST-094` FR13). At a release the track's colour changes over the settle spring's computed time (`spec:AST-108` FR4a). The switch's own click, for a hold and for a flipping drag, is the exception `spec:AST-094` FR11 states for Switch. Under reduced motion nothing lifts or springs: a tap, a release or a change cross-fades the thumb to its rest and the track colour with it, while a dragging pointer still moves the thumb. A finger's slide-off toggle (`spec:AST-020` FR9, DEC-5; `spec:AST-094` FR11) meets WCAG 2.5.2 Pointer Cancellation through its Abort or Undo option: sliding back, or toggling again, undoes it. A mouse or pen released outside the tracking bounds cancels the press. | `spec:AST-094` FR11, FR13, FR43, FR44; `spec:AST-020` FR9/DEC-5; `design:reduced-motion`; WCAG 2.5.2 | +| FR15 | For every pointer (`spec:AST-094` FR43) a tap, a hold of any length or a short drag toggles the switch at the release (a tap through the browser's click; where the browser sends no click for a finger's release, as for a hold, the switch clicks its input itself), and a drag commits the side it flipped the switch to, which it reaches once the pointer is the track width less the thumb width from where it pressed, toward the other end, and leaves again once the pointer comes back past where it pressed (measured in `motion/uikit/switch.ts`: `travel`, `flipDistance`). A finger's drag commits wherever the finger lifts; a mouse's or a pen's commits when it is released inside the switch's tracking bounds, its box grown by UIControl's 70 px tracking expansion. Under a finger a press slid off and lifted outside without a flip toggles it too, as `spec:AST-094` FR11 allows for a toggle under `spec:AST-020` FR9's slide-off exception (DEC-5); a mouse or a pen released outside those bounds leaves it alone, flipped or not, as the native checkbox does. Under every pointer the thumb follows the drag on `switchControl.tracking`, banded past either end, and the track's colour changes where the drag flips the switch; at the release the thumb settles to the committed end on `switchControl.travel`, starting at the release velocity, with the theme's `character` and `tempo` shaping the spring (`spec:AST-094` FR43, FR45). A tap, a hold without a drag, a key or a change of `value` moves the thumb and the track's colour on the theme's duration and easing tokens under every pointer, a finger included (`spec:AST-094` FR45). Under a touch presentation the thumb also lifts while a finger presses, on the same tokens; under a fine primary pointer it does not lift (`spec:AST-094` FR13). At a release the track's colour changes over the settle spring's computed time (`spec:AST-108` FR4a). The switch's own click, for a hold and for a flipping drag, is the exception `spec:AST-094` FR11 states for Switch. Under reduced motion nothing lifts or springs: a tap, a release or a change cross-fades the thumb to its rest and the track colour with it, while a dragging pointer still moves the thumb. A finger's slide-off toggle (`spec:AST-020` FR9, DEC-5; `spec:AST-094` FR11) meets WCAG 2.5.2 Pointer Cancellation through its Abort or Undo option: sliding back, or toggling again, undoes it. A mouse or pen released outside the tracking bounds cancels the press. | `spec:AST-094` FR11, FR13, FR43, FR45; `spec:AST-020` FR9/DEC-5; `design:reduced-motion`; WCAG 2.5.2 | ### Motion roles @@ -176,7 +176,7 @@ its motion knobs (`spec:AST-094` DEC-15). The switch plays `switchControl.tracking` and `switchControl.travel` for a drag and its release under every pointer (FR15). The other rows are measured for motion that a tap or a press starts, which plays the theme's duration and easing -tokens (`spec:AST-094` FR44), so the switch plays none of them. +tokens (`spec:AST-094` FR45), so the switch plays none of them. Implementation gap: under a coarse primary pointer the switch still plays `switchControl.lift`, `switchControl.settle`, `switchControl.minimumLiftMs`, `switchControl.travel`, `switchControl.wellOn` and `switchControl.wellOff` @@ -302,9 +302,9 @@ making the absence intentional. - `spec:AST-086` FR8 owns the busy announcement. - `design:user-states` owns the pressed treatment. - `spec:AST-094` owns touch presentations, FR43's gestures for every - pointer, and FR44's split between a gesture's spring and the theme's + pointer, and FR45's split between a gesture's spring and the theme's tokens: Switch follows its FR11 press profiles, its FR42 hit area, FR43 and - FR44 (FR10, FR13–FR15). + FR45 (FR10, FR13–FR15). - `architecture:component-theming-surface` owns anatomy qualification, factual `none` dispositions, and composition-preserving target ownership. - FieldStatus and Spinner retain their existing public target contracts when diff --git a/docs/specs/components/TreeList/TreeList.spec.md b/docs/specs/components/TreeList/TreeList.spec.md index b15e7d52da2d7..0de4b600fe2e5 100644 --- a/docs/specs/components/TreeList/TreeList.spec.md +++ b/docs/specs/components/TreeList/TreeList.spec.md @@ -111,7 +111,7 @@ Supporting one does not justify omitting another from the durable contract. | FR5 | `TreeListItemData` MUST admit `className`, `style`, and `xstyle` as one per-item styling seam. Every supplied value MUST compose on the same Item row box; an implementation MUST NOT treat current support for only one mechanism as authority to exclude the other two. | Owner decision; `architecture:public-component-api` INV5–INV6; `spec:AST-002/DEC-1` | Settled intent; implementation may be staged | | FR6 | The Item row box is the logical element that paints the row, owns its hover state, and contains Start content, Item label and description, and End content. The styling seam MUST NOT move to the semantic `
  • ` or another element without a compatibility decision. | Owner decision and the caller-owned per-row interaction boundary | Settled intent | | FR7 | A click on an Item row outside its Item label performs the row's action: an Item with `onClick` runs it; otherwise an Item with children toggles; otherwise an Item whose only action is its link follows that link as Enter does, with modified and middle clicks following the shared enlarged-surface rule in `family:navigation-destinations`. The Chevron and controls in Start content and End content keep their own activation, and a disabled Item does nothing. A press inside an open Tooltip or HoverCard surface performs no row action (`component:Tooltip` FR11). | Owner decision; `family:navigation-destinations` | Settled intent; implementation may be staged | -| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only, on the theme's duration and easing tokens (`spec:AST-094` FR5, FR44); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2). | UIKit outline list (`UICollectionView`), iOS 26.2; `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval | +| FR8 | Expanding or collapsing an Item with children MUST play its group's own motion: under a coarse primary pointer its rows slide out from under the parent as one block, clipped to the group's box, and back on collapse, a collapsing group's rows leaving the accessibility tree as the slide starts, while the Chevron turns, by transform only, on the theme's duration and easing tokens (`spec:AST-094` FR5, FR45); under a fine pointer the group appears and leaves at once. With an `animatedLayout` handle, the group and the rows after it follow `spec:AST-105`'s shared motion under every pointer (FR4, FR5); without one, the group's own slide stays touch-only and the rows after the group take their places at once (`spec:AST-105` FR2). | UIKit outline list (`UICollectionView`), iOS 26.2; `spec:AST-105` FR2 | Proposed; awaiting an engineering owner's approval | | FR9 | `TreeList` MUST accept `animatedLayout?: AnimatedLayout`. With a handle, expanding and collapsing a row report the row's size change through it, so the content after the row glides inside the handle's container, and the shown rows and the row's Chevron move on the same motion (`spec:AST-105` FR1–FR4; `spec:AST-108` FR4a). Without one, expanding and collapsing follow FR8. The prop MUST NOT change tree semantics or keyboard behavior. | `spec:AST-105` FR1, FR2; `spec:AST-108` FR4a | Current behavior | ### Allowed variation From 4a8943117dfb8a6fc4313362cda1ad18da1c4400 Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:41:27 -0800 Subject: [PATCH 04/11] spec(AST-094): name the two approved springs FR45 keeps, use the theme spec's character wording, and align evidence rows and gaps --- .../motion-roles.md | 2 +- .../specs/AST-094-touch-presentations/spec.md | 114 ++++++++++-------- .../AlertDialog/AlertDialog.spec.md | 2 +- .../components/DateInput/DateInput.spec.md | 2 +- .../ProgressBar/ProgressBar.spec.md | 5 + .../SegmentedControl/SegmentedControl.spec.md | 2 +- scripts/motion-roles-table.mjs | 2 +- 7 files changed, 72 insertions(+), 57 deletions(-) diff --git a/docs/specs/AST-094-touch-presentations/motion-roles.md b/docs/specs/AST-094-touch-presentations/motion-roles.md index 9a4613e3b0030..da64b09ce2a2d 100644 --- a/docs/specs/AST-094-touch-presentations/motion-roles.md +++ b/docs/specs/AST-094-touch-presentations/motion-roles.md @@ -2,7 +2,7 @@ # Motion roles -Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives and for a touch presentation's exit (`spec:AST-094` FR45, FR7). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and a role measured for that motion stays as the native reference. Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. +Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives and for a touch presentation's exit (`spec:AST-094` FR45, FR7). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. | Role | Kind | Default | Named by | | ------------------------------- | -------- | --------------------------------------------------------------- | ----------------------- | diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 097243f663cb9..5c8d30ef9b38f 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -195,8 +195,9 @@ a measured, sourced difference exists. A role plays only for motion a gesture drives (FR45): a release's settle, a flick, a pan's or a pinch's settle, a rubber band's return, and a follow. - The theme's `character` normalizes a gesture's spring and its `tempo` - scales the spring's duration (`spec:AST-108` FR3); the follow itself and + The theme's `tempo` scales a gesture's spring's duration and its + `character` offsets the role's measured bounce by the role's fixed weight + (`spec:AST-108` FR3); the follow itself and the rubber band stay fixed (`spec:AST-108` FR4). A role measured for a touch presentation's exit, such as `springs.menuClose` and `springs.contextMenuDismiss`, keeps timing that exit (FR7, `spec:AST-090` @@ -206,7 +207,7 @@ a measured, sourced difference exists. `springs.contextMenuMorph`, `timing.stateChangeMs`, `timing.curves.entrance` and `timing.curves.move`) stay as the measured native reference, and no motion plays them: that motion plays the theme's - duration and easing tokens (FR45). A component's own record states which of + duration and easing tokens (FR45). Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). A component's own record states which of its roles a gesture drives. Implementation gap: the pager's measured paging motion (its settle @@ -245,14 +246,15 @@ a measured, sourced difference exists. starts at the gesture's velocity, under every pointer (FR43). A gesture is a continuous pointer movement whose velocity carries into the motion: a drag, a flick, a swipe, a pan, a pinch, and the settle and rubber-band - return after any of them. The theme's `character` normalizes the spring and - its `tempo` scales the spring's duration (`spec:AST-108` FR3); the follow + return after any of them. The theme's `tempo` scales the spring's duration + and its `character` offsets the role's measured bounce by the role's fixed + weight (`spec:AST-108` FR3); the follow and the rubber band themselves stay fixed (`spec:AST-108` FR4). A part that follows a pointer follows it under every pointer (FR43). Every other motion, which a tap, a click, a hold, a key or the program starts, MUST play the theme's motion tokens, its duration and easing tokens scaled by `tempo` (`architecture:theme-tokens`, `spec:AST-108` FR2), under every - pointer, a finger included. A tap has no velocity, so it is not a gesture. + pointer, a finger included. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). A tap has no velocity, so it is not a gesture. A touch presentation keeps its own look for that motion (FR13), such as a menu that grows from its trigger or a thumb that lifts, and plays it on those tokens. A press that starts as a tap or a hold and continues as a @@ -566,8 +568,8 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and toward the start MUST cancel, whatever the travel so far. - **FR38 — The settle continues the pointer.** The commit or the spring-back MUST start at the release velocity (FR6), under every pointer (FR43), on a - spring that the theme's `character` normalizes and its `tempo` scales - (FR45). Under reduced motion it follows `design:reduced-motion` DR2, and a + spring whose duration the theme's `tempo` scales and whose measured bounce + its `character` offsets by the role's fixed weight (FR45). Under reduced motion it follows `design:reduced-motion` DR2, and a release from a mouse, a trackpad or a pen follows FR43. - **FR39 — The action never needs a drag.** Every action a drag commits MUST also be reachable without a drag, by keyboard and by mouse @@ -820,12 +822,16 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and and FR15, `component:Slider` FR7, `component:ContextMenu` FR5, `component:DropdownMenu` FR21, `component:Popover` FR9, `component:AlertDialog` FR6, `component:Lightbox` FR5 and FR12, - `component:TreeList` FR8, `component:ProgressBar` FR4 and - `component:SegmentedControl` FR9 and FR10. A touch presentation's exit + `component:TreeList` FR8, `component:ProgressBar` FR4, + `component:SegmentedControl` FR9 and FR10 and `component:DateInput` FR14. A touch presentation's exit keeps the timing FR7 gives it, and no exit switch removes it - (`spec:AST-090` FR14). Implementation gap: Switch, Slider, SegmentedControl - and ProgressBar still play their measured roles for a tap, a key or a value - change under a coarse pointer. + (`spec:AST-090` FR14). Implementation gap: Switch, Slider and + SegmentedControl still play their measured roles for a tap or a key under a + coarse pointer; ProgressBar's fill still moves linearly for + `progress.fillTrackMs` times the share of the track moved under a coarse + pointer (`component:ProgressBar` FR4); and DateInput's sheet wheel turns + onto a clamped row with the browser's smooth scroll, not the theme's move + tokens (`component:DateInput` FR14). - Under FR45, `spec:AST-105` FR4's glide, which `component:SideNav` FR6 and `component:AppShell` FR17 ride, and `spec:AST-107`'s `settleTo` and an `animate` call without a velocity play a spring for motion that no gesture @@ -833,10 +839,12 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and - Under FR43 and FR45, a part that follows a pointer follows every pointer and its release springs from the release velocity under every pointer: `component:Switch` FR15's thumb under a mouse or a pen, `component:Slider` - FR7's coast after a mouse or a pen release, and `component:DateInput` + FR7's coast after a mouse or a pen release, `component:SegmentedControl` + FR10's settle after a mouse or a pen release, and `component:DateInput` FR15's wheel under a trackpad wherever the sheet shows. Implementation gap: - Switch's thumb does not follow a mouse or a pen, and Slider's mouse and pen - releases do not coast. + Switch's thumb does not follow a mouse or a pen, Slider's mouse and pen + releases do not coast, and SegmentedControl's mouse and pen releases slide + on the theme's duration. - `design:user-states` DR1 and DR3, `spec:AST-025` FR14 and `spec:AST-013` FR3 each carry an amendment for touch presentations (FR11, FR14, Platform support); `design:motion` DR3 and DR5 (draft) are departed from or met as @@ -865,42 +873,43 @@ Motion that FR45 moves, by component (each record carries its own line): | DateInput | Sheet wheel under a trackpad | Coarse primary pointer only | Wherever the sheet shows | | Carousel | Paging under a trackpad swipe | Coarse primary pointer only | Every pointer | | SegmentedControl | Selection slide on a tap or a key under a finger | `segmented.selectionSpring` | Theme duration and easing tokens | +| SegmentedControl | A mouse's or a pen's drag release | Slides on the theme's duration | Settles on a spring from the release velocity | ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1, FR2 | Value check over every measured value and its uses | every value; a guess and its measurement task; a platform-specific value; two components playing the same motion | an unsourced value, an unknown source kind, a guess without a listed measurement, an unsourced platform split, or two values for one use fails | -| FR3 | Unit tests of the spring, decay, rubber band and projection math against an independent integration and values read from UIKit | under-, critically and overdamped springs; release velocities; both rates; overscroll depths | changing a formula or a constant breaks the agreement with UIKit's values | -| FR3 pager | Unit tests of the pager's page choice against the recorded UIKit releases | releases at 292.7–298.6 pt/s that stay and 310.7–329.9 pt/s that advance, at 10, 25 and 40 % of a page; releases at rest at 45–48.5 % and 55–57.5 %; a 1.5-page drag released at 0, 600 and 3000 pt/s; a release moving back toward the start page | a page chosen by projection, a release at or past 300 pt/s that stays, or a release that travels more than one page from its start fails | -| FR4 | Role unit tests over every touch motion | each role at its table value; a theme that writes `motion` | a literal played in place of a role, a value that differs from the table at `tempo` 1 and `'standard'`, or a theme that changes a role other than through its motion knobs fails | -| FR5 | Browser proof: an animation keeps advancing frame by frame through a 1 s main-thread block, with no layout or paint per frame | Chromium phone emulation; WebKit | a script-driven or layout-animating implementation freezes or lays out | -| FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring; a retarget before the browser reports the call's start | a layout read, a jump, or a lost velocity fails | -| FR6 pager | Unit and browser tests of a pager's settle start | releases just under and over 225 pt/s; a catch mid-settle; a programmatic page change mid-settle | a release under the floor whose settle starts moving, a release over it whose settle starts from rest, a catch that loses the on-screen velocity, or a programmatic page change mid-settle that starts from rest fails; a page change during a drag starts from rest | -| FR6 starved | Switch frame proofs on a starved main thread (4 workers on 2 cores); a unit test of a retarget on a settle tail | a second tap mid-travel, with frames read late or its lift handled 100 ms late; a first frame committed late; a retarget once the thumb is still | a jump out of a thumb that is still fails; a mid-travel step within the browser's commit delay is the known limit | -| FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | -| FR45 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, other than an exit FR7 keeps, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | -| FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | -| FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | -| FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | -| FR14 | Browser tests of a sheet over a scrolled list | scrolled and at its top | a midway handoff fails | -| FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | -| FR18–FR22 | Browser tests with real touch input (`spec:AST-013` FR10) | per-axis pans beside native scrolling, page zoom, cancelled touches, clicks after taps, keyboard activation, a modal scroll lock, a control that keeps its press through a drag; a still finger with sub-pixel jitter over a scroll view; a finger moved 9 px and 10 px straight, and (7, 7) and (8, 8) diagonally; a recognizer on an element whose author `touch-action` is narrower, including one without `pinch-zoom`; a screen-edge pan landing at 40 px and at 41 px from the edge, at 14 px and 15 px of travel, and a horizontal scroll that starts outside the band; a screen-edge pan on an element within the band, and on one that reaches beyond it; a sheet's pull from a list at its top, and from a list scrolled away from its top; a long press dragged past its tolerance after it began, which never scrolls the page | a ghost click, a scroll or zoom blocked other than by the author's `touch-action`, or a cancelled unowned touchmove outside FR19's four exceptions, a tap cancelled by sub-pixel movement or by under 10 px of travel, a scroll that does not take the touch at 10 px, or a derived `touch-action` wider than the author's, a screen-edge pan that claims outside its band or before 15 px on an element within the band, or has not claimed at 15 px, or an author scroll outside the band that is blocked, a band claim of `pan-x` on an element that reaches beyond the band, or a 15 px start there, or a scroll-edge pan that cancels a move a scroller could scroll, or begins before 10 pt, fails | -| FR20 | Browser tests with real touch input in Chromium phone emulation (`spec:AST-013` FR10) | a horizontal drag under `pan-y`; a vertical drag under `pan-y` over a scroller with room; a vertical drag where no scroller has room | a horizontal drag under `pan-y` counted as the browser taking the touch, a vertical one over a scroller with room not counted, or a later uncancelable move counted, fails | -| FR23 | Haptics tests on the recorded moments | tap, drag, lift | a haptic outside a native moment fails | -| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a gesture listener in a component that uses no gesture, an idle document listener outside a press other than the one shared press controller and the one stray-click swallower, a second press controller, a per-instance `document` or `window` listener, or a leak fails | -| FR25 | Component tests of each moved default and its `presentation` value | default; `presentation` set to the released surface | a moved default that `presentation` does not restore, or a public threshold, fails | -| FR26–FR28, FR30 | Menu press tests and each member's pointer-stream tests, as `family:action-groups`' verification map lists them and a real-Chromium touch test | a slide from one row to another; a lift outside; a cancel; a second finger; edge autoscroll | an act on the press-start row, two acts for one gesture, a lift outside that closes, or a lit row after a cancel fails | -| FR29 | Trigger tests with explicit timestamps | a press in a fixed bar; a press in scrolling content held 149 and 150 ms; a quick tap in scrolling content; a scroll taking the touch before 150 ms; a drift of 12 and 14 pt; moves before and after the handoff; `presentation="popover"` | a fixed-bar press that waits, a held finger's scroll-view open before 150 ms or after a scroll, a quick tap that does not open at its lift, an open past the drift allowance, a lift that acts before the handoff, or a `popover` trigger that opens on press fails | -| FR31–FR33 | Browser tests and storyboards under a finger | a menu, a Popover and a ContextMenu lift: open, close, a pan outside, a tap outside, a pinch over the surface; a mouse | a surface that does not grow from and shrink into its trigger, a pan outside that leaves a menu open, a Popover whose dismissal changed, a pinch blocked other than by the author's `touch-action`, a tap outside that reaches the page, or a changed mouse path fails | -| FR34, FR35 | Browser tests and storyboards under a finger | a sub-menu drilling in and its Back row returning; a tap and a drag during a popover's and a menu's growth; a pinch during growth; a mouse; a tap and a drag during an alert's, a sheet's, and a dialog's entrance; a key and a screen reader during an entrance; a pinch during an alert's and a sheet's entrance; a tap and a scroll on the page beside an entering non-modal sheet, which pass | a drill-in that leaves the parent's rows in place, a change between levels that does not play on its roles, a tap during growth that closes, scrolls or moves focus, a pinch blocked other than by the author's `touch-action`, or a changed mouse path, a held key or assistive-technology activation, a hidden content tree, or a tap or drag during an entrance that closes the presentation, scrolls a held page, moves focus or reaches anything beneath, fails | -| FR36–FR40 | Release tests on synthetic touch streams with explicit timestamps, per use | below and past each commit point; a flick that projects past it; a slow drag past it; a release moving back; Reduce Motion; keyboard and mouse paths | a decision that changes mid-settle, a release moving back that commits, a settle that does not start at the release velocity (a paging release starts its settle per FR6's paging paragraph, DEC-25), or an action with no keyboard or mouse path fails | -| FR41 | Browser tests with real touch input (`spec:AST-013` FR10) | a tap; a long press on a ContextMenu trigger, a DropdownMenu trigger the caller renders, a row with a context menu, and a slider thumb; a long press on a checkbox row's label and on plain text; a cancelled press; mouse and keyboard selection; a pen long press; a mouse long press, which makes no context request; a mouse drag a recognizer claims, with and without a page selection; a mouse drag that starts on selectable text | a tap that selects, a hold with an action that starts a selection or callout, a label or plain text whose long press does not select natively, a suppression that outlives the press, a change to native selection elsewhere, to keyboard or assistive-technology selection, or to mouse selection outside a drag a recognizer claims, a claimed drag that takes a new selection or after which an existing one does not survive, or a drag starting on text that does not select text fails | -| FR42 | Default-theme geometry tests under a coarse and a fine pointer, and hit-testing between adjacent targets | every component at phone width with a finger and with a mouse; a dense toolbar; a chip and its remove button; a Switch beside a label link | a default-theme box that grows under touch or at phone width other than with its text, or, where no component or module record sets the hit area, a touch target under 44 px where neighbors allow it or a hit area that overlaps a neighbor's target, fails | -| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a slider's thumb under a wheel and a trackpad scroll; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse-drag swipe action, a pen pinch, an edge pan under a mouse, a trackpad, or a pen, a press delay under a fine pointer, a wheel or trackpad scroll that changes a thumb's value or does not reach the scroll under it, an idle listener in a component with no gesture, or added stylesheet bytes, fails | -| FR43 trackpad | Unit tests on timed wheel streams; WebKit browser tests with trackpad streams (Chromium's driver wheels read as a mouse wheel) | a form and a required sheet pulled 30 to 240 px by a finger and by a trackpad; mouse lines, notches, ticks and accelerated deltas over a sheet, a pager and a row; ten swipes each starting mid-momentum, ten whose momentum ends, and a slow drag with 150 ms pauses; a trackpad flick and a slow stopped drag on an info sheet; a fling, a slow 60 px swipe, a 42 px spike and back-to-back swipes on a pager; a row opened and swiped back within one gesture, and a long fast swipe; a pinch ending; a trackpad pan through a window blur and through a hidden page | a trackpad stretch that differs from a finger's for the same travel (120 px gives 54 px on the reference sheet), a mouse wheel that moves a sheet, a pager or a row, splitter counts other than ten, ten and one, momentum that moves a surface, a flick released with no velocity, a pager that travels past one page or pages on a spike, an info sheet a flick leaves open or a slow stopped drag closes, an open row that reveals its other side within one gesture, a trackpad pan a window blur or a hidden page leaves mid-gesture, or a pinch that ends after 100 ms fails | -| FR44 | Unit tests of a pen press a window blur ends, on a Switch, a Slider and the host, under a coarse and a fine pointer | a pen press on a Switch and on a Slider, blur mid-press: nothing toggles or commits; a pen whose contact ends in `pointercancel` | with the host keeping no note of a blur-cancelled pen, the Switch toggles at the pen's click | -| all | iOS simulator: the UIKit reference capture and the same gesture in Safari; real iOS Safari and Android Chrome devices | per component | an unmatched curve or timing is a parity defect | +| Contract | Verification | Representative states | Mutation or failure expectation | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1, FR2 | Value check over every measured value and its uses | every value; a guess and its measurement task; a platform-specific value; two components playing the same motion | an unsourced value, an unknown source kind, a guess without a listed measurement, an unsourced platform split, or two values for one use fails | +| FR3 | Unit tests of the spring, decay, rubber band and projection math against an independent integration and values read from UIKit | under-, critically and overdamped springs; release velocities; both rates; overscroll depths | changing a formula or a constant breaks the agreement with UIKit's values | +| FR3 pager | Unit tests of the pager's page choice against the recorded UIKit releases | releases at 292.7–298.6 pt/s that stay and 310.7–329.9 pt/s that advance, at 10, 25 and 40 % of a page; releases at rest at 45–48.5 % and 55–57.5 %; a 1.5-page drag released at 0, 600 and 3000 pt/s; a release moving back toward the start page | a page chosen by projection, a release at or past 300 pt/s that stays, or a release that travels more than one page from its start fails | +| FR4 | Role unit tests over every touch motion | each role at its table value; a theme that writes `motion` | a literal played in place of a role, a value that differs from the table at `tempo` 1 and `'standard'`, or a theme that changes a role other than through its motion knobs fails | +| FR5 | Browser proof: an animation keeps advancing frame by frame through a 1 s main-thread block, with no layout or paint per frame | Chromium phone emulation; WebKit | a script-driven or layout-animating implementation freezes or lays out | +| FR6 | Unit and browser tests catching an animation mid-flight and retargeting it | catch at peak velocity; retarget with a new spring; a retarget before the browser reports the call's start | a layout read, a jump, or a lost velocity fails | +| FR6 pager | Unit and browser tests of a pager's settle start | releases just under and over 225 pt/s; a catch mid-settle; a programmatic page change mid-settle | a release under the floor whose settle starts moving, a release over it whose settle starts from rest, a catch that loses the on-screen velocity, or a programmatic page change mid-settle that starts from rest fails; a page change during a drag starts from rest | +| FR6 starved | Switch frame proofs on a starved main thread (4 workers on 2 cores); a unit test of a retarget on a settle tail | a second tap mid-travel, with frames read late or its lift handled 100 ms late; a first frame committed late; a retarget once the thumb is still | a jump out of a thumb that is still fails; a mid-travel step within the browser's commit delay is the known limit | +| FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | +| FR45 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, other than an exit FR7 keeps, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | +| FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | +| FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | +| FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | +| FR14 | Browser tests of a sheet over a scrolled list | scrolled and at its top | a midway handoff fails | +| FR15–FR17 | Recognizer unit tests on synthetic streams with explicit timestamps | exclusive, simultaneous, require-to-fail, nested; a watcher beside a claiming winner; a blocked main thread; a pan at 10 pt per axis | a wrong winner, a late deadline, or a translation that keeps the hysteresis, or a watcher failed or cancelled by a claiming winner fails | +| FR18–FR22 | Browser tests with real touch input (`spec:AST-013` FR10) | per-axis pans beside native scrolling, page zoom, cancelled touches, clicks after taps, keyboard activation, a modal scroll lock, a control that keeps its press through a drag; a still finger with sub-pixel jitter over a scroll view; a finger moved 9 px and 10 px straight, and (7, 7) and (8, 8) diagonally; a recognizer on an element whose author `touch-action` is narrower, including one without `pinch-zoom`; a screen-edge pan landing at 40 px and at 41 px from the edge, at 14 px and 15 px of travel, and a horizontal scroll that starts outside the band; a screen-edge pan on an element within the band, and on one that reaches beyond it; a sheet's pull from a list at its top, and from a list scrolled away from its top; a long press dragged past its tolerance after it began, which never scrolls the page | a ghost click, a scroll or zoom blocked other than by the author's `touch-action`, or a cancelled unowned touchmove outside FR19's four exceptions, a tap cancelled by sub-pixel movement or by under 10 px of travel, a scroll that does not take the touch at 10 px, or a derived `touch-action` wider than the author's, a screen-edge pan that claims outside its band or before 15 px on an element within the band, or has not claimed at 15 px, or an author scroll outside the band that is blocked, a band claim of `pan-x` on an element that reaches beyond the band, or a 15 px start there, or a scroll-edge pan that cancels a move a scroller could scroll, or begins before 10 pt, fails | +| FR20 | Browser tests with real touch input in Chromium phone emulation (`spec:AST-013` FR10) | a horizontal drag under `pan-y`; a vertical drag under `pan-y` over a scroller with room; a vertical drag where no scroller has room | a horizontal drag under `pan-y` counted as the browser taking the touch, a vertical one over a scroller with room not counted, or a later uncancelable move counted, fails | +| FR23 | Haptics tests on the recorded moments | tap, drag, lift | a haptic outside a native moment fails | +| FR24 | Resource tests counting listeners, observers and timers | fine primary pointer; coarse pointer enabled and disabled; many instances; unmount | a gesture listener in a component that uses no gesture, an idle document listener outside a press other than the one shared press controller and the one stray-click swallower, a second press controller, a per-instance `document` or `window` listener, or a leak fails | +| FR25 | Component tests of each moved default and its `presentation` value | default; `presentation` set to the released surface | a moved default that `presentation` does not restore, or a public threshold, fails | +| FR26–FR28, FR30 | Menu press tests and each member's pointer-stream tests, as `family:action-groups`' verification map lists them and a real-Chromium touch test | a slide from one row to another; a lift outside; a cancel; a second finger; edge autoscroll | an act on the press-start row, two acts for one gesture, a lift outside that closes, or a lit row after a cancel fails | +| FR29 | Trigger tests with explicit timestamps | a press in a fixed bar; a press in scrolling content held 149 and 150 ms; a quick tap in scrolling content; a scroll taking the touch before 150 ms; a drift of 12 and 14 pt; moves before and after the handoff; `presentation="popover"` | a fixed-bar press that waits, a held finger's scroll-view open before 150 ms or after a scroll, a quick tap that does not open at its lift, an open past the drift allowance, a lift that acts before the handoff, or a `popover` trigger that opens on press fails | +| FR31–FR33 | Browser tests and storyboards under a finger | a menu, a Popover and a ContextMenu lift: open, close, a pan outside, a tap outside, a pinch over the surface; a mouse | a surface that does not grow from and shrink into its trigger, a pan outside that leaves a menu open, a Popover whose dismissal changed, a pinch blocked other than by the author's `touch-action`, a tap outside that reaches the page, or a changed mouse path fails | +| FR34, FR35 | Browser tests and storyboards under a finger | a sub-menu drilling in and its Back row returning; a tap and a drag during a popover's and a menu's growth; a pinch during growth; a mouse; a tap and a drag during an alert's, a sheet's, and a dialog's entrance; a key and a screen reader during an entrance; a pinch during an alert's and a sheet's entrance; a tap and a scroll on the page beside an entering non-modal sheet, which pass | a drill-in that leaves the parent's rows in place, a change between levels that does not play on the theme's move duration and easing tokens, a tap during growth that closes, scrolls or moves focus, a pinch blocked other than by the author's `touch-action`, or a changed mouse path, a held key or assistive-technology activation, a hidden content tree, or a tap or drag during an entrance that closes the presentation, scrolls a held page, moves focus or reaches anything beneath, fails | +| FR36–FR40 | Release tests on synthetic touch streams with explicit timestamps, per use | below and past each commit point; a flick that projects past it; a slow drag past it; a release moving back; Reduce Motion; keyboard and mouse paths | a decision that changes mid-settle, a release moving back that commits, a settle that does not start at the release velocity (a paging release starts its settle per FR6's paging paragraph, DEC-25), or an action with no keyboard or mouse path fails | +| FR41 | Browser tests with real touch input (`spec:AST-013` FR10) | a tap; a long press on a ContextMenu trigger, a DropdownMenu trigger the caller renders, a row with a context menu, and a slider thumb; a long press on a checkbox row's label and on plain text; a cancelled press; mouse and keyboard selection; a pen long press; a mouse long press, which makes no context request; a mouse drag a recognizer claims, with and without a page selection; a mouse drag that starts on selectable text | a tap that selects, a hold with an action that starts a selection or callout, a label or plain text whose long press does not select natively, a suppression that outlives the press, a change to native selection elsewhere, to keyboard or assistive-technology selection, or to mouse selection outside a drag a recognizer claims, a claimed drag that takes a new selection or after which an existing one does not survive, or a drag starting on text that does not select text fails | +| FR42 | Default-theme geometry tests under a coarse and a fine pointer, and hit-testing between adjacent targets | every component at phone width with a finger and with a mouse; a dense toolbar; a chip and its remove button; a Switch beside a label link | a default-theme box that grows under touch or at phone width other than with its text, or, where no component or module record sets the hit area, a touch target under 44 px where neighbors allow it or a hit area that overlaps a neighbor's target, fails | +| FR43 | Browser tests with a mouse, a trackpad, a pen, a finger, and the keyboard, and resource counts | a pan by drag and by wheel; a slider's thumb under a wheel and a trackpad scroll; a ctrl-wheel pinch; a long press, a right click, and Shift+F10; a mouse press; a handle drag; a row swipe with a mouse and a trackpad; a component with no gesture | a gesture missing under a mouse or pen, a mouse-drag swipe action, a pen pinch, an edge pan under a mouse, a trackpad, or a pen, a press delay under a fine pointer, a wheel or trackpad scroll that changes a thumb's value or does not reach the scroll under it, an idle listener in a component with no gesture, or added stylesheet bytes, fails | +| FR43 trackpad | Unit tests on timed wheel streams; WebKit browser tests with trackpad streams (Chromium's driver wheels read as a mouse wheel) | a form and a required sheet pulled 30 to 240 px by a finger and by a trackpad; mouse lines, notches, ticks and accelerated deltas over a sheet, a pager and a row; ten swipes each starting mid-momentum, ten whose momentum ends, and a slow drag with 150 ms pauses; a trackpad flick and a slow stopped drag on an info sheet; a fling, a slow 60 px swipe, a 42 px spike and back-to-back swipes on a pager; a row opened and swiped back within one gesture, and a long fast swipe; a pinch ending; a trackpad pan through a window blur and through a hidden page | a trackpad stretch that differs from a finger's for the same travel (120 px gives 54 px on the reference sheet), a mouse wheel that moves a sheet, a pager or a row, splitter counts other than ten, ten and one, momentum that moves a surface, a flick released with no velocity, a pager that travels past one page or pages on a spike, an info sheet a flick leaves open or a slow stopped drag closes, an open row that reveals its other side within one gesture, a trackpad pan a window blur or a hidden page leaves mid-gesture, or a pinch that ends after 100 ms fails | +| FR44 | Unit tests of a pen press a window blur ends, on a Switch, a Slider and the host, under a coarse and a fine pointer | a pen press on a Switch and on a Slider, blur mid-press: nothing toggles or commits; a pen whose contact ends in `pointercancel` | with the host keeping no note of a blur-cancelled pen, the Switch toggles at the pen's click | +| all | iOS simulator: the UIKit reference capture and the same gesture in Safari; real iOS Safari devices; Android Chrome in the Android emulator (DEC-27) | per component | an unmatched curve or timing is a parity defect | ## Decision log @@ -1223,13 +1232,14 @@ hold, a key or the program starts has no speed to carry, so it plays the theme's motion tokens, its duration and easing scaled by `tempo`, under every pointer, and a theme's timing reaches it as it reaches every other timed motion. A part that follows a pointer is not timed motion, so it follows every -pointer. The theme's `character` normalizes a gesture's spring and its -`tempo` scales it, so one theme feels the same for both kinds of motion. +pointer. The theme's `tempo` scales a gesture's spring and its `character` +offsets the role's measured bounce by the role's fixed weight, so one theme +feels the same for both kinds of motion. The owner decided this on 2026-11-02. Motion stays with the theme's duration, but an element that follows an interaction is outside those timings. A mouse release settles on a spring from the release speed, so a flick keeps its -throw. The theme's character normalizes those springs and its tempo scales +throw. The theme's character shapes those springs and its tempo scales them. Any interaction that involves a gesture uses a spring. A tap has no velocity, so an interaction without a gesture uses the default motion, which applies the theme's motion tokens. diff --git a/docs/specs/components/AlertDialog/AlertDialog.spec.md b/docs/specs/components/AlertDialog/AlertDialog.spec.md index b5b794c7d6b1a..5cee466afc86c 100644 --- a/docs/specs/components/AlertDialog/AlertDialog.spec.md +++ b/docs/specs/components/AlertDialog/AlertDialog.spec.md @@ -227,7 +227,7 @@ The dialog surface (the Dialog element AlertDialog renders) carries `alert-dialo | Contract | Verification | Representative states | Mutation or failure expectation | Audit section | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | FR1–FR5 | `AlertDialog.test.tsx` ("calls onOpenChange(false) on Escape", "does not call onOpenChange when action is clicked", "does not call onAction on Escape", "does not disable the cancel button while the action is loading", "exposes a named group instead, with no aria-modal") and `useImperativeAlertDialog.test.tsx` ("opens on show()", "closes on hide()") | open, loading, inline, imperative | Escape not cancelling, the action closing by itself, or the inline path claiming the role fails. | `audit:AlertDialog/behavior` | -| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and a spring-curve oracle for the dismissal, a busy-main-thread case, and real-device clips on iOS Safari and Android Chrome | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | +| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and a spring-curve oracle for the dismissal, a busy-main-thread case, real-device clips on iOS Safari, and Android Chrome clips in the Android emulator (`spec:AST-094` DEC-27) | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | | FR7 | Browser tests under a coarse pointer, with keyboard and assistive-technology input | a tap during the presentation, Enter and Escape during it, a screen reader's activation, a tap during the dismissal; a mouse click during the presentation under a fine pointer | a held key or assistive-technology action, content made inert during the entrance, a tap that reaches a leaving alert, or a mouse click held under a fine pointer fails. No test exists yet; FR7 is not shipped. | `audit:AlertDialog/behavior` | | FR8 | `AlertDialog.test.tsx` ("AlertDialog action tracking"), `useActionGroupTracking.test.tsx` and `AlertDialog.interaction.browser.spec.ts` | a press on Cancel slid onto the action and lifted, and a press lifted outside the actions, each with a mouse, a pen, and a finger; Enter and Space on each action | the press-start action running after a slide, any action running on a lift outside, or a changed keyboard or assistive-technology activation fails. | `audit:AlertDialog/behavior` | | FR9 | Browser tests at a phone width and at a desktop width, in the default and an alternate theme | a phone width and a desktop width, in the default and an alternate theme; a caller `width` at a phone width; a screen narrower than 320 px; a narrow window under a fine pointer; a coarse screen that is not compact | a phone-width alert other than 320 px in any theme without a caller `width`, a caller `width` overridden, a desktop width changed, an alert wider than the viewport, or a 320 px alert in a narrow fine-pointer window or on a non-compact coarse screen fails. No test exists yet; FR9 is not shipped. | `audit:AlertDialog/behavior` | diff --git a/docs/specs/components/DateInput/DateInput.spec.md b/docs/specs/components/DateInput/DateInput.spec.md index a57f65f7dc260..316b5631aa912 100644 --- a/docs/specs/components/DateInput/DateInput.spec.md +++ b/docs/specs/components/DateInput/DateInput.spec.md @@ -141,7 +141,7 @@ Consumer migration instructions belong in consumer docs and release notes. | FR11 | Clear MUST emit `undefined` through the ordinary change pipeline. The pointer and bottom-sheet surfaces restore field focus using their shipped timing; the native surface MUST NOT reclaim focus because doing so reopens the OS picker. Implementation gap: when the field becomes disabled or busy while its clear control holds focus, the control unmounts and focus falls to the document body; `spec:AST-085/FR4` requires focus to stay or move to a destination this record names. | Current source and focused clear tests; `spec:AST-085/FR4` | | FR12 | No surface renders native `disabled`. A disabled field MUST stay focusable with `aria-disabled`, block typing, picking, clearing, and opening, describe its disabled reason when one is given, and take disabled paint. While busy (`isLoading` or a pending Action) the pointer and bottom-sheet surfaces expose `aria-disabled` and `aria-busy`, stay focusable, and block new commits; the native surface does the same for `isLoading`, and during a pending Action stays editable so the OS picker remains attached but rejects additional commits until the Action settles. A busy field that is not disabled keeps its ordinary paint and Spinner, shows no hover ring while commits are blocked, and keeps its keyboard focus ring. Caller handlers other than focus and blur MUST NOT run while the field is disabled (`spec:AST-085` FR2). Implementation gap: only a caller `onClick` is skipped while disabled; caller key, paste, and pointer handlers (such as `onKeyDown`, `onKeyUp`, `onPaste`, and `onPointerDown`) still run on every surface, the native field included. | `spec:AST-085/FR1–FR6`; `family:input-fields/FR4–FR6`; `family:buttons/FR4`; current source and focused tests | | FR13 | A bottom-sheet picker wheel that comes to rest on a disabled row (outside `min`/`max`) MUST continue to the nearest enabled row, a tie going toward the committed row, and select it there. A wheel resting on its committed row MUST NOT select or commit, even when that row is outside the bounds, so opening a bounded sheet never changes an untouched value: a committed value outside the bounds stays, and an empty field stays empty. This holds for every wheel in the DateInput, TimeInput, and DateTimeInput sheets. | DEC-1; UIDatePicker on the iOS 26.2 simulator; current source | -| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR45). | UIDatePicker on the iOS 26.2 simulator; current source | +| FR14 | A shown bottom-sheet picker wheel whose value changes from outside, as when a bound clamps the time after another wheel turned, MUST turn onto the new row rather than jump to it, on the theme's move duration and easing tokens (`spec:AST-094` FR45). Implementation gap: the wheel's turn plays the browser's smooth scroll, not the theme's move tokens. | UIDatePicker on the iOS 26.2 simulator; current source | | FR15 | Wherever the sheet shows, a sheet wheel's pan takes a trackpad's two-finger scroll, as `spec:AST-094` FR43 allows a record to grant: the rows follow the fingers, coast on at the lift with the fingers' last speed, and rest on a row by FR13, settling on a spring from that speed (`spec:AST-094` FR45). A mouse-wheel notch turns the wheel one row, as an arrow key does, with no coast. A ctrl-scroll stays the page's zoom (`spec:AST-094` FR43). Implementation gap: the wheel is the browser's scroll-snap column for both inputs, so a trackpad's momentum runs at the browser's rate and a mouse-wheel notch moves the column by the browser's scroll step, several rows. | `spec:AST-094` FR43; current source | | FR16 | Enter in the pointer field's text input commits typed text that has not been committed and is then held back, so a form never submits the value from before the commit. With no typed text pending, Enter is not prevented, so a form submits as it does from any text field, except on a disabled or busy field, where Enter does nothing (`spec:AST-085` FR2). | diff --git a/docs/specs/components/ProgressBar/ProgressBar.spec.md b/docs/specs/components/ProgressBar/ProgressBar.spec.md index 8c75898b053df..a54ac99f518a2 100644 --- a/docs/specs/components/ProgressBar/ProgressBar.spec.md +++ b/docs/specs/components/ProgressBar/ProgressBar.spec.md @@ -111,6 +111,11 @@ Consumer migration instructions belong in consumer docs and release notes. exported for themes to extend, but an added variant matches no built-in fill or on-fill mark color, so the fill paints nothing until a theme styles it, where `architecture:component-theming-surface` INV14 asks for a safe baseline. +- **Touch fill still plays the measured timing (FR4).** Under a coarse + primary pointer a value change still moves the fill linearly for + `progress.fillTrackMs` times the share of the track moved, where + `spec:AST-094` FR45 has it move on `--duration-medium` and the theme's + easing under every pointer. - **Indeterminate slide follows any right-to-left ancestor (FR6).** The indeterminate slide and the centering of marks mirror under any right-to-left ancestor rather than the bar's own direction, so in a diff --git a/docs/specs/components/SegmentedControl/SegmentedControl.spec.md b/docs/specs/components/SegmentedControl/SegmentedControl.spec.md index 3f547206c76ec..43b6683c6284f 100644 --- a/docs/specs/components/SegmentedControl/SegmentedControl.spec.md +++ b/docs/specs/components/SegmentedControl/SegmentedControl.spec.md @@ -139,7 +139,7 @@ Consumer migration instructions belong in consumer docs and release notes. | FR7 | `isDisabled` sets `aria-disabled` on the radiogroup, disables every segment, dims the control to half opacity, and blocks selection. The group MUST keep a Tab stop while disabled, placed as in FR2, and pointer events stay on (`spec:AST-085` FR1, FR7). With `disabledMessage`, a reason tooltip opens on hover and keyboard focus; the reason MUST describe the focusable segment (`spec:AST-085` FR5, FR7). Implementation gap: caller handlers on the radiogroup, such as `onKeyDown`, still run while disabled (`spec:AST-085` FR2). | `spec:AST-085`; current tests | | FR8 | The radiogroup owns `role`, `aria-label` (from `label`), `aria-disabled`, and `data-size`; a caller value for any of them is replaced. Other base props reach the radiogroup, and `ref` reaches it. Implementation gap: a caller `aria-describedby` is replaced, by the disabled-reason id or by nothing, against `architecture:public-component-api` INV5. | `architecture:public-component-api` INV5; current source and tests | | FR9 | Under every pointer, a change of selection slides the selected surface from the old segment to the new one as one surface, the radiogroup's `::before`, animating transform and opacity only. A change that a tap, a click, a key or the program starts slides on the theme's duration and easing tokens, a finger included (`spec:AST-094` FR45); a drag's release settles on a spring from the release velocity (FR10, FR13). | Owner direction; `spec:AST-094` FR13; current source and tests | -| FR10 | A drag's release settles on the internal `segmented.*` motion roles with UIKit's measured values, starting at the release velocity under every pointer, with the theme's `character` and `tempo` shaping the spring (`spec:AST-094` FR4, FR45, DEC-15; thresholds internal under `spec:AST-002` FR3). Implementation gap: under a finger a tap's, a click's or a key's slide still plays `segmented.selectionSpring`, and under a mouse or a pen a drag's release slides on the theme's duration; under Reduce Motion the old surface fades out in place as the new one fades in (`design:reduced-motion` DR1–DR3). With a fine pointer under Reduce Motion, the theme's transition shows the change. | UIKit measurements (iOS 26.2); `design:reduced-motion` DR1–DR3; current tests | +| FR10 | A drag's release settles on the internal `segmented.*` motion roles with UIKit's measured values, starting at the release velocity under every pointer, with the theme's `character` and `tempo` shaping the spring (`spec:AST-094` FR4, FR45, DEC-15; thresholds internal under `spec:AST-002` FR3). Under Reduce Motion the old surface fades out in place as the new one fades in (`design:reduced-motion` DR1–DR3). With a fine pointer under Reduce Motion, the theme's transition shows the change. Implementation gap: under a finger a tap's, a click's or a key's slide still plays `segmented.selectionSpring`, and under a mouse or a pen a drag's release slides on the theme's duration. | UIKit measurements (iOS 26.2); `design:reduced-motion` DR1–DR3; current tests | | FR11 | Under a finger, a held unselected segment paints the pressed overlay on its part, as under a mouse (`design:user-states` DR1); no theme role changes or dims it. | `design:user-states` DR1; current tests | | FR12 | During a drag of the selected surface, the selected label's colour follows the surface, with an internal hysteresis around each boundary; the weights change at the lift. A tap selects through the browser's click (`spec:AST-094` FR11). | Owner decision (weights at the lift); current tests | | FR13 | Under a finger, a drag from the selected segment carries the selected surface with the finger, within an internal overshoot past the end segments, and the lift commits the segment nearest the finger through that segment's click; a drag that starts on another segment leaves the surface in place and commits where it lifts, as a menu's press-tracking slide commits where it lifts (`spec:AST-094` FR26–FR28). A mouse and a pen drag the same way (`spec:AST-094` FR43). Under Reduce Motion the surface still follows the pointer, as direct movement (`design:reduced-motion`), and a finger's release cross-fades; under a fine pointer a mouse's or a pen's release lands at once (DR3). A lift outside the control commits nothing (FR3). | `spec:AST-094` FR26–FR28, FR43; `design:reduced-motion`; current tests | diff --git a/scripts/motion-roles-table.mjs b/scripts/motion-roles-table.mjs index 959a1a036e0ce..d3e4a325b3370 100644 --- a/scripts/motion-roles-table.mjs +++ b/scripts/motion-roles-table.mjs @@ -109,7 +109,7 @@ export function renderMotionRoleTable(roles, {fr4, records}) { '', '# Motion roles', '', - "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives and for a touch presentation's exit (`spec:AST-094` FR45, FR7). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and a role measured for that motion stays as the native reference. Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", + "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives and for a touch presentation's exit (`spec:AST-094` FR45, FR7). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", '', '| Role | Kind | Default | Named by |', '| --- | --- | --- | --- |', From 22e080d844b47c8f40124d9b39779b743d4af99f Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:55:02 -0800 Subject: [PATCH 05/11] spec(AST-090, AST-094): a touch exit a tap, key or program starts plays the theme's exit duration and easing --- docs/specs/AST-090-exit-motion/spec.md | 63 ++++++----- .../motion-roles.md | 2 +- .../specs/AST-094-touch-presentations/spec.md | 104 +++++++++--------- .../AlertDialog/AlertDialog.spec.md | 42 +++---- .../ContextMenu/ContextMenu.spec.md | 32 +++--- .../DropdownMenu/DropdownMenu.spec.md | 2 +- .../components/Lightbox/Lightbox.spec.md | 36 +++--- docs/specs/components/Popover/Popover.spec.md | 22 ++-- scripts/motion-roles-table.mjs | 2 +- 9 files changed, 159 insertions(+), 146 deletions(-) diff --git a/docs/specs/AST-090-exit-motion/spec.md b/docs/specs/AST-090-exit-motion/spec.md index 9524f83894a1c..81f1db00b7c86 100644 --- a/docs/specs/AST-090-exit-motion/spec.md +++ b/docs/specs/AST-090-exit-motion/spec.md @@ -72,9 +72,10 @@ components read through two shared primitives is a system fact. ## Non-goals - Exits of touch presentations, which each component's record gives under - `spec:AST-094` FR7 and FR8 (FR14). The exit tokens here do not reach touch - motion: its roles stay internal, and a theme reaches it only through - `spec:AST-108`'s knobs (`spec:AST-094` FR4, DEC-15). + `spec:AST-094` FR7 and FR8 (FR14). The exit switches here do not reach + touch exits; a touch exit that a tap, a key or the program starts plays + `--duration-exit` and `--ease-exit`, and a gesture-driven exit keeps its + velocity spring (FR14). - Surfaces that already animate out (`Drawer`, `BottomSheet`, `MobileNav`, `Toast`, and the `TopNavMegaMenu` panel) keep their exits, durations, and curves. `BottomSheet`'s close in a pointer presentation reads `--ease-exit` @@ -177,9 +178,12 @@ governs them in a touch presentation. (`spec:AST-003` FR18, FR19). - **FR14 — Touch presentations keep their native exit.** A dialog or layer surface in a touch presentation plays the exit its component's record gives - it under `spec:AST-094` FR7 and FR8. The exit switches, `--ease-exit`, and - `--duration-exit` do not change that exit: a theme that leaves a kind off - does not remove it, and a theme that turns a kind on does not replace it. + it under `spec:AST-094` FR7 and FR8. A touch presentation's exit that a + tap, key or program starts plays the theme's exit duration and easing, + `--duration-exit` and `--ease-exit` (`spec:AST-094` FR7); a gesture-driven + exit keeps its velocity spring. The exit switches do not change that exit: + a theme that leaves a kind off does not remove it, and a theme that turns a + kind on does not replace it. ### Platform support @@ -222,10 +226,10 @@ governs them in a touch presentation. shields an owner (FR4, DEC-1), and a closing pinned `Tooltip` gives up Escape when its exit starts (FR4, DEC-6). A leaving dialog is still the current native modal, so it keeps its outlet and toasts stay operable above it (FR16, FR18). -- `spec:AST-094`: no claim changes. A touch presentation keeps the exit FR7 and - FR8 give it (FR14), and its motion roles stay internal (FR4, DEC-15). The exit - tokens here time pointer presentations, which its non-goals leave to the - theme's duration and easing tokens. +- `spec:AST-094`: a touch presentation keeps the exit FR7 and FR8 give it + (FR14). One that a tap, a key or the program starts plays `--duration-exit` + and `--ease-exit`, and one a gesture drives keeps its velocity spring + (`spec:AST-094` FR7, FR45, DEC-26). - `spec:AST-108`: no claim changes. Its FR2 admits `--ease-exit` at the same curve at every `character`; a spring-driven exit plays its own exit role and a CSS part beside it takes the spring's settle time (FR3, FR4a); and an exit @@ -253,23 +257,23 @@ governs them in a touch presentation. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | Component unit suites run unchanged with default tokens | Every dialog and layer member in a pointer presentation, opened and closed | A surface stays rendered after its close with default tokens | -| FR2 | Real-browser test under a theme that sets one kind, fine primary pointer | Dialogs on with layers off; layers on with dialogs off; a nested `Theme` | The unset kind animates, or the set kind closes at once | -| FR3 | Real-browser frame sampling during an exit | A dialog with and without an opener, a fullscreen dialog, a layer in each placement, right-to-left | The surface leaves its entrance path, or keeps the entrance curve | -| FR4 | Real-browser test | A press and an Enter key on a control inside a leaving surface | The control activates | -| FR5 | Dismissal invariant tests | Escape during a leaving Dialog or Popover with an owner beneath; Escape during a leaving unpinned Tooltip over a Dialog; Escape during the exit of a pinned Tooltip over a Dialog | The owner beneath a leaving owner closes, a leaving unpinned Tooltip absorbs the press, or a closing pinned Tooltip takes an Escape after its exit starts | -| FR6 | Unit and real-browser focus and accessibility-tree tests | Dialog and layer closes, by keyboard and by pointer; the accessibility tree during a layer exit | Dialog focus returns before the dialog is hidden or lands on the body; layer focus waits for the exit; a leaving layer's exit is in the accessibility tree | -| FR7 | Unit tests | Controlled and uncontrolled closes | A callback or `isOpen` waits for the exit | -| FR8 | Unit and real-browser tests | Reopen during an exit, unmount during an exit, `Activity` hidden during an exit | The surface disappears and reappears, or stays after unmount | -| FR9 | Unit test with a suppressed end event; real-browser test with animations and transitions disabled page-wide | Open and close in one update; page-wide `transition: none`; no end event | The surface is present more than 100ms after its exit duration | -| FR10 | Real-browser test with reduced motion emulated, fine primary pointer | Each kind turned on | An exit plays | -| FR11 | Real-browser frame sampling during an exit | A hover card, a menu with an open sub-menu, a selector with a search query | The leaving surface shows other content, or none, before it is hidden | -| FR12 | Token default tests, theme template inventory test, token documentation check | Defaults; the `BottomSheet` closing curve | A default drifts, or `BottomSheet` restates the curve | -| FR13 | Real-browser test | A toast action pressed, and a toast dismissed, during a dialog exit; the same toast after the dialog is hidden | The toast ignores input or fades with the dialog during the exit, or loses its entries or timers when the dialog is hidden | -| FR14 | Real-browser test with a coarse primary pointer, after hydration | A Dialog, a Popover, and a menu opened and closed by a finger, with each kind switched off and on; the same surfaces under reduced motion | A switch changes the touch exit, or the surface closes without the exit `spec:AST-094` FR7 and FR8 give it | -| Platform | Real-browser tests in Chromium and WebKit | A dialog exit and a layer exit with each kind on; a layer close where the Popover API is missing | A layer exit paints outside the top layer or does not play, or a close without the Popover API is delayed | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | Component unit suites run unchanged with default tokens | Every dialog and layer member in a pointer presentation, opened and closed | A surface stays rendered after its close with default tokens | +| FR2 | Real-browser test under a theme that sets one kind, fine primary pointer | Dialogs on with layers off; layers on with dialogs off; a nested `Theme` | The unset kind animates, or the set kind closes at once | +| FR3 | Real-browser frame sampling during an exit | A dialog with and without an opener, a fullscreen dialog, a layer in each placement, right-to-left | The surface leaves its entrance path, or keeps the entrance curve | +| FR4 | Real-browser test | A press and an Enter key on a control inside a leaving surface | The control activates | +| FR5 | Dismissal invariant tests | Escape during a leaving Dialog or Popover with an owner beneath; Escape during a leaving unpinned Tooltip over a Dialog; Escape during the exit of a pinned Tooltip over a Dialog | The owner beneath a leaving owner closes, a leaving unpinned Tooltip absorbs the press, or a closing pinned Tooltip takes an Escape after its exit starts | +| FR6 | Unit and real-browser focus and accessibility-tree tests | Dialog and layer closes, by keyboard and by pointer; the accessibility tree during a layer exit | Dialog focus returns before the dialog is hidden or lands on the body; layer focus waits for the exit; a leaving layer's exit is in the accessibility tree | +| FR7 | Unit tests | Controlled and uncontrolled closes | A callback or `isOpen` waits for the exit | +| FR8 | Unit and real-browser tests | Reopen during an exit, unmount during an exit, `Activity` hidden during an exit | The surface disappears and reappears, or stays after unmount | +| FR9 | Unit test with a suppressed end event; real-browser test with animations and transitions disabled page-wide | Open and close in one update; page-wide `transition: none`; no end event | The surface is present more than 100ms after its exit duration | +| FR10 | Real-browser test with reduced motion emulated, fine primary pointer | Each kind turned on | An exit plays | +| FR11 | Real-browser frame sampling during an exit | A hover card, a menu with an open sub-menu, a selector with a search query | The leaving surface shows other content, or none, before it is hidden | +| FR12 | Token default tests, theme template inventory test, token documentation check | Defaults; the `BottomSheet` closing curve | A default drifts, or `BottomSheet` restates the curve | +| FR13 | Real-browser test | A toast action pressed, and a toast dismissed, during a dialog exit; the same toast after the dialog is hidden | The toast ignores input or fades with the dialog during the exit, or loses its entries or timers when the dialog is hidden | +| FR14 | Real-browser test with a coarse primary pointer, after hydration | A Dialog, a Popover, and a menu opened and closed by a finger, with each kind switched off and on; the same surfaces under reduced motion | A switch changes the touch exit, a touch exit that a tap starts plays off `--duration-exit` and `--ease-exit`, or the surface closes without the exit `spec:AST-094` FR7 and FR8 give it | +| Platform | Real-browser tests in Chromium and WebKit | A dialog exit and a layer exit with each kind on; a layer close where the Popover API is missing | A layer exit paints outside the top layer or does not play, or a close without the Popover API is delayed | ## Decision log @@ -338,7 +342,10 @@ On a touch device a surface moves the way its native counterpart does, by default and with no opt-in (`spec:AST-094`), and that record leaves pointer presentations' timing and easing to the theme's tokens. The switches here therefore govern pointer presentations only, and a touch presentation keeps -its native exit whatever they resolve to. +its native exit whatever they resolve to. A touch exit that a tap, a key or +the program starts plays the theme's exit duration and easing, +`--duration-exit` and `--ease-exit`, and a gesture-driven exit keeps its +velocity spring (`spec:AST-094` DEC-26). Rejected: letting the exit switches turn touch exits off, which removes the exit `spec:AST-094` FR7 requires. diff --git a/docs/specs/AST-094-touch-presentations/motion-roles.md b/docs/specs/AST-094-touch-presentations/motion-roles.md index da64b09ce2a2d..28e7722922290 100644 --- a/docs/specs/AST-094-touch-presentations/motion-roles.md +++ b/docs/specs/AST-094-touch-presentations/motion-roles.md @@ -2,7 +2,7 @@ # Motion roles -Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives and for a touch presentation's exit (`spec:AST-094` FR45, FR7). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. +Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. | Role | Kind | Default | Named by | | ------------------------------- | -------- | --------------------------------------------------------------- | ----------------------- | diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 5c8d30ef9b38f..72e7127d16d56 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -61,8 +61,9 @@ a measured, sourced difference exists. a click, a hold, a key or the program starts, such as an entrance, a move or a change of value, plays the theme's duration and easing tokens (`architecture:theme-tokens`) under every pointer, a finger included - (FR45). A touch presentation's exit stays as FR7 gives it - (`spec:AST-090` FR14). Gestures, press tracking, a part that follows a + (FR45). A touch presentation's exit that no gesture drives keeps its look + and plays the theme's exit duration and easing, and no exit switch removes + it (FR7, `spec:AST-090` FR14). Gestures, press tracking, a part that follows a pointer, and the spring that settles a gesture apply to every pointer (FR43, FR45). - Platform-matched look and feel: glass versus material surfaces, glyphs and @@ -82,8 +83,7 @@ a measured, sourced difference exists. ### Physics - **FR1 — Measured values, the same everywhere.** Every spring, duration - and deceleration rate that a gesture's motion plays (FR45), every exit that - FR7 keeps, and every rubber-band coefficient, gesture threshold, input + and deceleration rate that a gesture's motion plays (FR45), and every rubber-band coefficient, gesture threshold, input timing 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 reaches the @@ -138,8 +138,7 @@ a measured, sourced difference exists. 310.7–329.9 pt/s; releases at rest stay at 45–48.5 % and advance at 55–57.5 % (DEC-25). - **FR4 — Touch motion uses named roles.** Every spring, duration and curve a - touch presentation plays for a gesture (FR45), or for an exit FR7 keeps, - MUST be a motion role named for its use + touch presentation plays for a gesture (FR45) MUST be a motion role named for its use (`architecture:theme-tokens` INV3). The roles this record names are listed below with their defaults and sources; further roles a component's own record measures join through the generated motion role table (DEC-5). With tempo 1 and character @@ -198,16 +197,14 @@ a measured, sourced difference exists. The theme's `tempo` scales a gesture's spring's duration and its `character` offsets the role's measured bounce by the role's fixed weight (`spec:AST-108` FR3); the follow itself and - the rubber band stay fixed (`spec:AST-108` FR4). A role measured for a - touch presentation's exit, such as `springs.menuClose` and - `springs.contextMenuDismiss`, keeps timing that exit (FR7, `spec:AST-090` - FR14). The other rows measured for motion that a tap, a hold, a key or the - program starts (`springs.menuOpen`, `springs.subMenuOpen`, - `springs.subMenuBack`, `springs.contextMenuLift`, - `springs.contextMenuMorph`, `timing.stateChangeMs`, + the rubber band stay fixed (`spec:AST-108` FR4). The rows measured for + motion that a tap, a hold, a key or the program starts, an exit included + (`springs.menuOpen`, `springs.menuClose`, `springs.subMenuOpen`, + `springs.subMenuBack`, the three `springs.contextMenu*` rows, `timing.stateChangeMs`, `timing.curves.entrance` and `timing.curves.move`) stay as the measured native reference, and no motion plays them: that motion plays the theme's - duration and easing tokens (FR45). Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). A component's own record states which of + duration and easing tokens (FR45), and an exit plays the theme's exit + duration and easing (FR7). Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). A component's own record states which of its roles a gesture drives. Implementation gap: the pager's measured paging motion (its settle @@ -237,10 +234,16 @@ a measured, sourced difference exists. change during a drag starts from rest (DEC-25). On a starved page, a retarget may skip ahead by up to the browser's commit delay (start-at-call). -- **FR7 — Exits animate as the native counterpart's do.** A touch - presentation MUST play the exit its UIKit counterpart plays, on every touch - platform. FR8 governs their reduced-motion form. Each overlay's own record states its - exit, since members own their animation (`family:overlay-dismissal` AV6). +- **FR7 — Exits keep the native counterpart's look.** A touch presentation + MUST play an exit with the look of its UIKit counterpart's exit, on every + touch platform: a menu shrinks into its trigger, and an alert fades. An + exit that a tap, a key, an outside press or the program starts plays the + theme's exit duration and easing, `--duration-exit` and `--ease-exit` + (`spec:AST-090` FR14, FR45); an exit that a gesture drives keeps its spring + from the release velocity (FR38, FR45). The theme's exit switches never + remove a touch exit (`spec:AST-090` FR14). FR8 governs their reduced-motion + form. Each overlay's own record states its exit, since members own their + animation (`family:overlay-dismissal` AV6). - **FR45 — A gesture's motion springs; other motion plays the theme's motion tokens.** Motion that a gesture drives MUST play a spring that starts at the gesture's velocity, under every pointer (FR43). A gesture is @@ -258,8 +261,9 @@ a measured, sourced difference exists. A touch presentation keeps its own look for that motion (FR13), such as a menu that grows from its trigger or a thumb that lifts, and plays it on those tokens. A press that starts as a tap or a hold and continues as a - drag springs from the drag's release. A touch presentation's exit stays as - FR7 gives it, and no exit switch removes it (`spec:AST-090` FR14). Gesture + drag springs from the drag's release. A touch presentation's exit that + no gesture drives plays the theme's exit duration and easing, and no exit + switch removes it (FR7, `spec:AST-090` FR14). Gesture thresholds, the timings that decide who gets a touch, hold durations, flick speeds, the press model's clocks and haptics are measured input values, not motion roles, and stay internal as FR4 states. Under reduced motion, `design:reduced-motion` @@ -335,7 +339,7 @@ a measured, sourced difference exists. trigger, hit-area growth (FR42), touch press timing (FR11) and haptics (FR23). It never decides whether a part follows a pointer or whether a gesture's settle springs (FR43, FR45). Its other motion plays the theme's - duration and easing tokens (FR45), and its exit stays as FR7 gives it. + duration and easing tokens (FR45), and its exit plays as FR7 states. - **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 @@ -505,7 +509,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and Under a finger, on every touch device, an anchored menu or a Popover MUST open by growing out of its trigger and close by shrinking back into it. The grow plays on the theme's entrance duration and easing tokens (FR45); the - shrink plays on `springs.menuClose` (FR4). The shrink plays in full after the surface has + shrink plays on the theme's exit duration and easing (FR7). The shrink plays in full after the surface has 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. @@ -527,8 +531,8 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and show the menu beside it, with no row lit. A tap outside closes the menu and reaches nothing beneath it, and closing returns the trigger to its place. The lift plays on the theme's duration and easing tokens (FR45); the - return is part of the menu's exit and plays on `springs.contextMenuDismiss` - (FR4, FR7). + return is part of the menu's exit and plays on the theme's exit duration + and easing (FR7). Under reduced motion FR8 applies. An inline trigger keeps today's menu, a menu presented as a bottom sheet follows the sheet, and `presentation="popover"` keeps the released menu without the lift (`architecture:public-component-api` @@ -821,11 +825,12 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and The records that state such motion carry it: `component:Switch` FR9, FR10 and FR15, `component:Slider` FR7, `component:ContextMenu` FR5, `component:DropdownMenu` FR21, `component:Popover` FR9, - `component:AlertDialog` FR6, `component:Lightbox` FR5 and FR12, + `component:AlertDialog` FR6, `component:Lightbox` FR5, FR12 and FR16, `component:TreeList` FR8, `component:ProgressBar` FR4, `component:SegmentedControl` FR9 and FR10 and `component:DateInput` FR14. A touch presentation's exit - keeps the timing FR7 gives it, and no exit switch removes it - (`spec:AST-090` FR14). Implementation gap: Switch, Slider and + that a tap, a key or the program starts plays `spec:AST-090`'s + `--duration-exit` and `--ease-exit`, and no exit switch removes it (FR7; + `spec:AST-090` FR14 and DEC-5 carry it). Implementation gap: Switch, Slider and SegmentedControl still play their measured roles for a tap or a key under a coarse pointer; ProgressBar's fill still moves linearly for `progress.fillTrackMs` times the share of the track moved under a coarse @@ -855,25 +860,26 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and Motion that FR45 moves, by component (each record carries its own line): -| Component | Motion | Today | Under FR45 | -| --------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | -| Switch | Thumb travel and track colour on a tap, a hold or a key, under a finger | Measured switch springs | Theme duration and easing tokens | -| Switch | Thumb lift while a finger presses, with a minimum lift time | Measured lift and settle springs | The same lift on the theme's tokens | -| Switch | A mouse's or a pen's drag | Thumb still until the release, then the theme's duration | Thumb follows; the release springs from its velocity | -| Slider | Thumb lift and drop under a finger | Measured lift and drop springs | Theme tokens | -| Slider | A mouse's or a pen's flick | Ends at the release | Coasts and settles as a finger's does | -| DropdownMenu, Popover | Grow from the trigger under a finger | `springs.menuOpen` | Theme entrance tokens | -| DropdownMenu | Sub-menu drill-in on a compact touch screen | `springs.subMenuOpen`, `springs.subMenuBack` | Theme move tokens | -| ContextMenu | Trigger lift and the menu's grow after a long press | `springs.contextMenuLift`, `springs.contextMenuMorph` | Theme tokens | -| AlertDialog | Scale-in present under a finger | `springs.alertPresent` | Theme entrance tokens | -| Lightbox | Controls fade on a tap, a double tap's zoom, the grow from its source | Named measured roles | Theme tokens | -| TreeList | Group slide under a coarse pointer | Measured motion | Theme tokens | -| ProgressBar | Determinate fill on a value change under a coarse pointer | Linear, `progress.fillTrackMs` times the share moved | `--duration-medium` and the theme's easing, every pointer | -| DateInput | Sheet wheel turning onto a clamped row | Unstated | Theme move tokens | -| DateInput | Sheet wheel under a trackpad | Coarse primary pointer only | Wherever the sheet shows | -| Carousel | Paging under a trackpad swipe | Coarse primary pointer only | Every pointer | -| SegmentedControl | Selection slide on a tap or a key under a finger | `segmented.selectionSpring` | Theme duration and easing tokens | -| SegmentedControl | A mouse's or a pen's drag release | Slides on the theme's duration | Settles on a spring from the release velocity | +| Component | Motion | Today | Under FR45 | +| --------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- | +| Switch | Thumb travel and track colour on a tap, a hold or a key, under a finger | Measured switch springs | Theme duration and easing tokens | +| Switch | Thumb lift while a finger presses, with a minimum lift time | Measured lift and settle springs | The same lift on the theme's tokens | +| Switch | A mouse's or a pen's drag | Thumb still until the release, then the theme's duration | Thumb follows; the release springs from its velocity | +| Slider | Thumb lift and drop under a finger | Measured lift and drop springs | Theme tokens | +| Slider | A mouse's or a pen's flick | Ends at the release | Coasts and settles as a finger's does | +| DropdownMenu, Popover | Grow from the trigger and shrink back under a finger | `springs.menuOpen`, `springs.menuClose` | Theme entrance tokens; theme exit duration and easing | +| DropdownMenu | Sub-menu drill-in on a compact touch screen | `springs.subMenuOpen`, `springs.subMenuBack` | Theme move tokens | +| ContextMenu | Trigger lift and the menu's grow after a long press | `springs.contextMenuLift`, `springs.contextMenuMorph` | Theme tokens | +| AlertDialog | Scale-in present under a finger | `springs.alertPresent` | Theme entrance tokens | +| Lightbox | Controls fade on a tap, a double tap's zoom, the grow from its source | Named measured roles | Theme tokens | +| TreeList | Group slide under a coarse pointer | Measured motion | Theme tokens | +| ProgressBar | Determinate fill on a value change under a coarse pointer | Linear, `progress.fillTrackMs` times the share moved | `--duration-medium` and the theme's easing, every pointer | +| DateInput | Sheet wheel turning onto a clamped row | Unstated | Theme move tokens | +| DateInput | Sheet wheel under a trackpad | Coarse primary pointer only | Wherever the sheet shows | +| Carousel | Paging under a trackpad swipe | Coarse primary pointer only | Every pointer | +| Every overlay | A touch exit that a tap, a key or an outside press starts | The native counterpart's timing | `--duration-exit` and `--ease-exit`; the exit switches never remove it | +| SegmentedControl | Selection slide on a tap or a key under a finger | `segmented.selectionSpring` | Theme duration and easing tokens | +| SegmentedControl | A mouse's or a pen's drag release | Slides on the theme's duration | Settles on a spring from the release velocity | ## Verification @@ -888,7 +894,7 @@ Motion that FR45 moves, by component (each record carries its own line): | FR6 pager | Unit and browser tests of a pager's settle start | releases just under and over 225 pt/s; a catch mid-settle; a programmatic page change mid-settle | a release under the floor whose settle starts moving, a release over it whose settle starts from rest, a catch that loses the on-screen velocity, or a programmatic page change mid-settle that starts from rest fails; a page change during a drag starts from rest | | FR6 starved | Switch frame proofs on a starved main thread (4 workers on 2 cores); a unit test of a retarget on a settle tail | a second tap mid-travel, with frames read late or its lift handled 100 ms late; a first frame committed late; a retarget once the thumb is still | a jump out of a thumb that is still fails; a mid-travel step within the browser's commit delay is the known limit | | FR7, FR8 | Component browser tests and storyboards with and without Reduce Motion | entrance, exit, move; a drag and its lift under Reduce Motion | a missing exit, any travel, scale or spring under Reduce Motion other than a coast after a throw and its settle at an edge (`design:reduced-motion` DR2), or an opacity fade over 200 ms fails | -| FR45 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, other than an exit FR7 keeps, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | +| FR45 | Browser tests with a mouse, a pen, a trackpad and a finger at each `tempo` bound and each `character`, and with Reduce Motion | a tap, a click, a key and a programmatic change on a Switch, a menu, an alert, a sheet and a ProgressBar under a fine and a coarse pointer; a drag release, a flick and a pinch on a Switch, a Slider, a sheet, a pager and a Lightbox photo under each pointer; a tap that continues as a drag | a tap, click, key or programmatic change under a finger that plays a measured role instead of the theme's duration and easing tokens, a gesture's settle that starts from rest or plays a theme curve, a gesture's spring that ignores `character` or `tempo`, or a part that follows one pointer and not another, fails | | FR9, FR10 | Recognizer unit tests on synthetic streams and browser tests with real touch input | drag, second finger, pinch, page zoom, main-thread block | a second finger that breaks a drag, or a page pinch blocked other than by the author's `touch-action`, fails | | FR11, FR12 | Press and long-press tests on synthetic streams with explicit timestamps | inside and outside a scroll view; a drag the browser takes; a lift inside; a menu row under a finger; a press on a row of a menu whose rows fit and of one that scrolls; a slide-off lift on a Switch and on a Radio | the wrong profile's timing, a press that survives the browser taking the touch, or a control outside an action group activated other than by the browser's click, outside `spec:AST-020` FR9's slide-off exception, a row of a menu whose rows fit that waits 150 ms, or one of a scrolling menu that does not, or a slide-off lift that completes a control outside `spec:AST-020` FR9's exception, fails | | FR13 | Resolver unit tests and a server-render test | coarse and fine pointer; narrow, wide and short; server render; hydrating render | a component that answers the conditions differently, or a touch presentation in the server or hydrating render, fails | @@ -1248,8 +1254,8 @@ This narrows the measured motion roles of DEC-1, DEC-3 and DEC-15 to motion a gesture drives, and extends the Non-goals' theme timing to every pointer. Measured input timings, such as who gets a touch, hold durations and flick speeds, stay internal values and are not motion roles. A touch presentation's -exit stays as FR7 and `spec:AST-090` DEC-5 give it; this decision does not -reach it. +exit that no gesture drives plays the theme's exit duration and easing; the +theme's exit switches still never remove it (`spec:AST-090` DEC-5). Rejected: keeping the measured springs for motion a tap starts under a finger. Rejected: a theme curve for a mouse release, which stops a flick dead. diff --git a/docs/specs/components/AlertDialog/AlertDialog.spec.md b/docs/specs/components/AlertDialog/AlertDialog.spec.md index 5cee466afc86c..645a93ceb019b 100644 --- a/docs/specs/components/AlertDialog/AlertDialog.spec.md +++ b/docs/specs/components/AlertDialog/AlertDialog.spec.md @@ -46,15 +46,15 @@ system_specs: ## Contract at a glance -| Area | Contract | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Public contract | `AlertDialog`, `AlertDialogProps`, and `useImperativeAlertDialog` are exported from `@astryxdesign/core/AlertDialog`; `title`, `description`, `actionLabel`, `onAction`, and `onOpenChange` are required with `isOpen`, plus cancel label, action variant, loading, width, and an inline preview mode. | -| Behavior | A modal confirmation that interrupts: it opens through the native dialog, cannot be dismissed by a click outside, closes on Escape or Cancel through `onOpenChange(false)`, and runs `onAction` on the action button. | -| End-user impact | A keyboard or screen-reader user is told a decision is needed (the title and description are announced), lands on Cancel, and cannot lose the dialog by clicking past it. On a touch screen the alert scales in with iOS's look on the theme's motion tokens, fades out as iOS's does, and ignores taps while it enters (FR6, FR7), and on a phone it is 320 px wide (FR9); under every pointer, mouse included, a press that slides from Cancel to the action runs the action it lifts on (FR8). | -| Builder impact | None. | -| Compatibility/readiness | FR1–FR5, FR8 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6, FR7 and FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. | -| Review checks | Reject a modal AlertDialog without `role="alertdialog"`, a title or description not linked by id, initial focus on the destructive action, or a backdrop click that dismisses it. | -| Governing rules | `architecture:public-component-api/INV1,INV5–INV8`; `architecture:component-theming-surface/INV3–INV7`; `architecture:layer-runtime`; `component:Dialog/AR1–AR3`; `spec:AST-020/FR15`; `spec:AST-094` FR4, FR13, FR35, DEC-15, DEC-17; `family:action-groups`; `design:reduced-motion`; `spec:AST-017`. | +| Area | Contract | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Public contract | `AlertDialog`, `AlertDialogProps`, and `useImperativeAlertDialog` are exported from `@astryxdesign/core/AlertDialog`; `title`, `description`, `actionLabel`, `onAction`, and `onOpenChange` are required with `isOpen`, plus cancel label, action variant, loading, width, and an inline preview mode. | +| Behavior | A modal confirmation that interrupts: it opens through the native dialog, cannot be dismissed by a click outside, closes on Escape or Cancel through `onOpenChange(false)`, and runs `onAction` on the action button. | +| End-user impact | A keyboard or screen-reader user is told a decision is needed (the title and description are announced), lands on Cancel, and cannot lose the dialog by clicking past it. On a touch screen the alert scales in and fades out with iOS's look, on the theme's motion tokens, and ignores taps while it enters (FR6, FR7), and on a phone it is 320 px wide (FR9); under every pointer, mouse included, a press that slides from Cancel to the action runs the action it lifts on (FR8). | +| Builder impact | None. | +| Compatibility/readiness | FR1–FR5, FR8 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6, FR7 and FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. | +| Review checks | Reject a modal AlertDialog without `role="alertdialog"`, a title or description not linked by id, initial focus on the destructive action, or a backdrop click that dismisses it. | +| Governing rules | `architecture:public-component-api/INV1,INV5–INV8`; `architecture:component-theming-surface/INV3–INV7`; `architecture:layer-runtime`; `component:Dialog/AR1–AR3`; `spec:AST-020/FR15`; `spec:AST-094` FR4, FR13, FR35, DEC-15, DEC-17; `family:action-groups`; `design:reduced-motion`; `spec:AST-017`. | This table is a review projection; the body below is authoritative. @@ -121,17 +121,17 @@ Consumer migration instructions belong in consumer docs and release notes. ## Behavioral and layout contract -| ID | Invariant | Basis | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| FR1 | A modal AlertDialog opens through Dialog's native modal path and a click on the backdrop does not dismiss it. | shipped source (`purpose="form"`); Alert Dialog accessibility contract (pending) | -| FR2 | Escape and the Cancel button both call `onOpenChange(false)`; the action button calls `onAction` and leaves closing to the caller. | shipped source and focused tests | -| FR3 | The anatomy is title, description, then the buttons: Cancel then action on wider screens, the action stacked above Cancel on small screens, Cancel keeping initial focus either way; the action may show a loading state while `isActionLoading`. | shipped source and focused tests | -| FR4 | `isInline` renders the same anatomy without modality and without the alert-dialog role. | shipped source and public docs | -| FR5 | `useImperativeAlertDialog` returns `show`, `hide`, and the element to render, and owns `isOpen`/`onOpenChange` for its caller. | shipped source and focused tests | -| FR6 | Under a touch presentation (`spec:AST-094` FR13), a modal AlertDialog presents by scaling from 1.2 to 1 with its opacity and its backdrop's dimming rising from 0 to 1 on the theme's entrance duration and easing tokens (`spec:AST-094` FR45), started at the browser's commit, with the larger start kept inside the viewport; it dismisses by a fade alone on the motion role `springs.alertDismiss`, its exit (`spec:AST-094` FR7), and closes at the fade's end; a close during the presentation, or a reopen during the dismissal, turns back from where it is. `springs.alertPresent` stays as UIKit's measured reference, and no motion plays it; `springs.alertDismiss` is internal, with UIKit's measured value at `tempo` 1 and `'standard'`, and follows the theme's motion knobs (`spec:AST-094` FR4, DEC-15). Under Reduce Motion both are the shared cross-fade (`design:reduced-motion` DR1–DR3). A fine primary pointer keeps today's motion (`spec:AST-094` FR13 and its Non-goals). Implementation gap: not shipped yet (Current-state impact). | proposed from measured iOS 26.2 behavior | -| FR7 | Under a touch presentation, pointer input to the alert is held while it enters (`spec:AST-094` FR35); keyboard and assistive-technology input are never held, and the entrance never makes the content inert. While the alert leaves, its content is inert for the dismissal fade. The hold's timing stays internal (`spec:AST-002` FR3). Implementation gap: not shipped yet (Current-state impact). | `spec:AST-094` FR35; owner decision (DEC-1) | -| FR8 | AlertDialog's actions are an action group (`family:action-groups`): a press that slides across them runs the action it lifts on, and a lift outside them runs nothing, under every pointer; keyboard and assistive-technology behavior is unchanged. | `family:action-groups`; `spec:AST-094` DEC-17; DEC-3 | -| FR9 | On a phone, a compact window (`spec:AST-012` FR9: narrower than `md` or shorter than 500 px) under a coarse primary pointer, the alert is 320 px wide in every theme, as iOS's is; no theme role sets it. A caller's explicit `width` still wins there (`architecture:public-component-api` INV5), and Dialog's viewport clamp still applies on a screen narrower than the alert. Elsewhere it keeps today's width. Implementation gap: not shipped yet (Current-state impact). | owner decision (DEC-2) | +| ID | Invariant | Basis | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | +| FR1 | A modal AlertDialog opens through Dialog's native modal path and a click on the backdrop does not dismiss it. | shipped source (`purpose="form"`); Alert Dialog accessibility contract (pending) | +| FR2 | Escape and the Cancel button both call `onOpenChange(false)`; the action button calls `onAction` and leaves closing to the caller. | shipped source and focused tests | +| FR3 | The anatomy is title, description, then the buttons: Cancel then action on wider screens, the action stacked above Cancel on small screens, Cancel keeping initial focus either way; the action may show a loading state while `isActionLoading`. | shipped source and focused tests | +| FR4 | `isInline` renders the same anatomy without modality and without the alert-dialog role. | shipped source and public docs | +| FR5 | `useImperativeAlertDialog` returns `show`, `hide`, and the element to render, and owns `isOpen`/`onOpenChange` for its caller. | shipped source and focused tests | +| FR6 | Under a touch presentation (`spec:AST-094` FR13), a modal AlertDialog presents by scaling from 1.2 to 1 with its opacity and its backdrop's dimming rising from 0 to 1 on the theme's entrance duration and easing tokens (`spec:AST-094` FR45), started at the browser's commit, with the larger start kept inside the viewport; it dismisses by a fade alone on the theme's exit duration and easing, `--duration-exit` and `--ease-exit` (`spec:AST-094` FR7; `spec:AST-090` FR14), and closes at the fade's end; a close during the presentation, or a reopen during the dismissal, turns back from where it is. `springs.alertPresent` and `springs.alertDismiss` stay as UIKit's measured reference, and no motion plays them (`spec:AST-094` FR4). Under Reduce Motion both are the shared cross-fade (`design:reduced-motion` DR1–DR3). A fine primary pointer keeps today's motion (`spec:AST-094` FR13 and its Non-goals). Implementation gap: not shipped yet (Current-state impact). | proposed from measured iOS 26.2 behavior | +| FR7 | Under a touch presentation, pointer input to the alert is held while it enters (`spec:AST-094` FR35); keyboard and assistive-technology input are never held, and the entrance never makes the content inert. While the alert leaves, its content is inert for the dismissal fade. The hold's timing stays internal (`spec:AST-002` FR3). Implementation gap: not shipped yet (Current-state impact). | `spec:AST-094` FR35; owner decision (DEC-1) | +| FR8 | AlertDialog's actions are an action group (`family:action-groups`): a press that slides across them runs the action it lifts on, and a lift outside them runs nothing, under every pointer; keyboard and assistive-technology behavior is unchanged. | `family:action-groups`; `spec:AST-094` DEC-17; DEC-3 | +| FR9 | On a phone, a compact window (`spec:AST-012` FR9: narrower than `md` or shorter than 500 px) under a coarse primary pointer, the alert is 320 px wide in every theme, as iOS's is; no theme role sets it. A caller's explicit `width` still wins there (`architecture:public-component-api` INV5), and Dialog's viewport clamp still applies on a screen narrower than the alert. Elsewhere it keeps today's width. Implementation gap: not shipped yet (Current-state impact). | owner decision (DEC-2) | ### Allowed variation @@ -227,7 +227,7 @@ The dialog surface (the Dialog element AlertDialog renders) carries `alert-dialo | Contract | Verification | Representative states | Mutation or failure expectation | Audit section | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | FR1–FR5 | `AlertDialog.test.tsx` ("calls onOpenChange(false) on Escape", "does not call onOpenChange when action is clicked", "does not call onAction on Escape", "does not disable the cancel button while the action is loading", "exposes a named group instead, with no aria-modal") and `useImperativeAlertDialog.test.tsx` ("opens on show()", "closes on hide()") | open, loading, inline, imperative | Escape not cancelling, the action closing by itself, or the inline path claiming the role fails. | `audit:AlertDialog/behavior` | -| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and a spring-curve oracle for the dismissal, a busy-main-thread case, real-device clips on iOS Safari, and Android Chrome clips in the Android emulator (`spec:AST-094` DEC-27) | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | +| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and the dismissal, a busy-main-thread case, real-device clips on iOS Safari, and Android Chrome clips in the Android emulator (`spec:AST-094` DEC-27) | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | | FR7 | Browser tests under a coarse pointer, with keyboard and assistive-technology input | a tap during the presentation, Enter and Escape during it, a screen reader's activation, a tap during the dismissal; a mouse click during the presentation under a fine pointer | a held key or assistive-technology action, content made inert during the entrance, a tap that reaches a leaving alert, or a mouse click held under a fine pointer fails. No test exists yet; FR7 is not shipped. | `audit:AlertDialog/behavior` | | FR8 | `AlertDialog.test.tsx` ("AlertDialog action tracking"), `useActionGroupTracking.test.tsx` and `AlertDialog.interaction.browser.spec.ts` | a press on Cancel slid onto the action and lifted, and a press lifted outside the actions, each with a mouse, a pen, and a finger; Enter and Space on each action | the press-start action running after a slide, any action running on a lift outside, or a changed keyboard or assistive-technology activation fails. | `audit:AlertDialog/behavior` | | FR9 | Browser tests at a phone width and at a desktop width, in the default and an alternate theme | a phone width and a desktop width, in the default and an alternate theme; a caller `width` at a phone width; a screen narrower than 320 px; a narrow window under a fine pointer; a coarse screen that is not compact | a phone-width alert other than 320 px in any theme without a caller `width`, a caller `width` overridden, a desktop width changed, an alert wider than the viewport, or a 320 px alert in a narrow fine-pointer window or on a non-compact coarse screen fails. No test exists yet; FR9 is not shipped. | `audit:AlertDialog/behavior` | diff --git a/docs/specs/components/ContextMenu/ContextMenu.spec.md b/docs/specs/components/ContextMenu/ContextMenu.spec.md index 4a8b0e888bdc4..81f352f066914 100644 --- a/docs/specs/components/ContextMenu/ContextMenu.spec.md +++ b/docs/specs/components/ContextMenu/ContextMenu.spec.md @@ -97,18 +97,18 @@ Consumer props, content modes, and presentation policy remain documented in ## Behavioral and layout contract -| ID | Invariant | Basis | -| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| FR1 | Every render contains a caller-provided Trigger area; with `isDisabled` no invocation opens a menu and the native context menu is left alone. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | -| FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source | -| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | -| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | -| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The lift and the grow apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The trigger's lift and the menu's grow play on the theme's duration and easing tokens (`spec:AST-094` FR45), and `springs.contextMenuLift` and `springs.contextMenuMorph` stay as the measured reference (`spec:AST-094` FR4); the trigger's return is part of the menu's exit and plays on `springs.contextMenuDismiss` (`spec:AST-094` FR4, FR7). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 | -| FR6 | With `triggerAs="span"` the Trigger area is an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | DEC-1 | -| FR7 | Sub-menu rows follow `component:DropdownMenu` FR11 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. A sub-menu with a header or footer shows its rows alone here (`component:DropdownMenu` FR20). | `component:DropdownMenu/DEC-4` | -| FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. Arrow keys and typeahead skip disabled rows, shipped behavior that is not a requirement until `component:DropdownMenu` OQ1 is ruled. | Current source and tests | -| FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | -| FR10 | ContextMenu MUST be built on the public headless context menu (`spec:AST-107` FR8), with its invocation, open state, and cleanup the factory's and no behavior of its own beside the factory's. Implementation gap: no headless context menu ships; ContextMenu owns its invocation, open state, outside-press listener, and cleanup itself. | `spec:AST-107` FR8 | +| ID | Invariant | Basis | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| FR1 | Every render contains a caller-provided Trigger area; with `isDisabled` no invocation opens a menu and the native context menu is left alone. Pointer presentation opens a Pointer menu surface at the current local cursor anchor; touch presentation opens a Touch sheet frame containing a Touch menu surface and, for data mode, a Touch action list and Touch action rows. | Current source, docs, and tests | +| FR2 | The `context-menu` target stays on the painted Pointer menu surface and alternative Touch menu surface, not on the Trigger area or cursor anchor. | `architecture:component-theming-surface`; current source | +| FR3 | Pointer action rows retain DropdownMenu ownership. The touch frame retains BottomSheet ownership, while data-driven touch lists and rows retain List ownership. | Current source and owner docs | +| FR4 | Compound `menuContent` remains a caller-supplied pointer-menu interior and may also render inside the touch frame; the data-driven touch path instead converts the same item data to List and ListItem presentation. | Current source and tests | +| FR5 | The Pointer menu surface carries `data-astryx-menu-press` and follows `module:DropdownMenu/useMenuPress` FR1–FR7: the row under the release acts, the highlight follows a held pointer, a mouse released outside closes and a finger leaves the menu open, the stray click never acts, and `touch-action` follows overflow. The long press that opened the menu does not close it as it lifts; a tap outside closes it, and a mouse press outside closes it at the press. Under a finger the menu MUST close as a pan that starts outside it begins (`spec:AST-094` FR32), MUST hand the held finger that opened it to the menu as the opening press, and MUST treat a pen's press outside as FR32 does (FR33). The lift and the grow apply only to a menu opened by touch (a touch-initiated open on a coarse pointer); a menu opened by mouse or keyboard keeps its released appearance and motion, with no lift, no tint and no grow. A touch-opened menu MUST grow out of the finger's point and shrink back into it (`spec:AST-094` FR31) as its own surface, by transform and opacity only (`spec:AST-094` FR5), with no second element such as a glass surface growing behind it, and on a block trigger MUST lift the trigger above the page with the menu beside it (FR33; DD1, DEC-2). The trigger's lift and the menu's grow play on the theme's duration and easing tokens (`spec:AST-094` FR45), and the trigger's return, part of the menu's exit, plays on the theme's exit duration and easing (`spec:AST-094` FR7); the three `springs.contextMenu*` roles stay as the measured reference (`spec:AST-094` FR4). Implementation gap: it keeps the layer's own entrance and exit, a scroll that starts outside leaves it open, the opening finger is not handed to the menu so it cannot slide to a row, no trigger lift or page tint exists, and a pen press outside closes it at the press. Right-click, Shift+F10, long-press invocation, and keyboard navigation are as FR1 and FR8 state. | `module:DropdownMenu/useMenuPress`; `spec:AST-094` FR26–FR33 | +| FR6 | With `triggerAs="span"` the Trigger area is an inline element that owns a box, so the cursor anchor measures from the trigger's own rect on every invocation path. Right-click, Shift+F10, long-press, and `context-menu` target placement all use that box. | DEC-1 | +| FR7 | Sub-menu rows follow `component:DropdownMenu` FR11 inside the pointer menu: ContextMenu mounts the same drill-in view stack, named after its `label`, so a sub-menu drills in on a phone here too. A sub-menu with a header or footer shows its rows alone here (`component:DropdownMenu` FR20). | `component:DropdownMenu/DEC-4` | +| FR8 | In the pointer menu ArrowDown on the last enabled row wraps to the first and ArrowUp on the first to the last; PageDown and PageUp page as in `component:DropdownMenu` FR12; a held key's auto-repeat does not activate; typeahead matches the row's label element alone. Arrow keys and typeahead skip disabled rows, shipped behavior that is not a requirement until `component:DropdownMenu` OQ1 is ruled. | Current source and tests | +| FR9 | While the Pointer menu surface is open, it participates in `family:overlay-dismissal` through the shared stack and provides descendant depth. One Escape press affects only the top-most present nested surface; ContextMenu closes only when it is that surface. | `family:overlay-dismissal` FR1–FR6; `ContextMenu.test.tsx` nested-layer case | +| FR10 | ContextMenu MUST be built on the public headless context menu (`spec:AST-107` FR8), with its invocation, open state, and cleanup the factory's and no behavior of its own beside the factory's. Implementation gap: no headless context menu ships; ContextMenu owns its invocation, open state, outside-press listener, and cleanup itself. | `spec:AST-107` FR8 | ### Allowed variation @@ -229,10 +229,10 @@ anchor is positioning infrastructure rather than consumer anatomy. - `spec:AST-094` owns the touch press, lift, grow, and dismissal rules (FR5). - `spec:AST-094` FR4 lists the three motion roles this component owns, measured for its lift: `springs.contextMenuLift`, `springs.contextMenuDismiss` and - `springs.contextMenuMorph`. A long press is not a gesture, so the lift and - the grow play the theme's tokens and their two roles stay as the measured - reference (`spec:AST-094` FR45); `springs.contextMenuDismiss` times the - return, which is part of the exit (`spec:AST-094` FR7). + `springs.contextMenuMorph`. They stay as the measured reference: a long + press is not a gesture, so the lift and the grow play the theme's tokens + (`spec:AST-094` FR45), and the return, part of the exit, plays the theme's + exit duration and easing (`spec:AST-094` FR7). - `family:overlay-dismissal` owns shared Escape and platform-close ordering. ContextMenu's pointer presentation is a shared-stack adopter; the touch presentation delegates dismissal to BottomSheet, which is also a shared-stack diff --git a/docs/specs/components/DropdownMenu/DropdownMenu.spec.md b/docs/specs/components/DropdownMenu/DropdownMenu.spec.md index 8a198873dedfb..af6e74684eccf 100644 --- a/docs/specs/components/DropdownMenu/DropdownMenu.spec.md +++ b/docs/specs/components/DropdownMenu/DropdownMenu.spec.md @@ -139,7 +139,7 @@ component-local concepts; each keeps its released default. | FR16 | Checkbox rows follow `component:DropdownMenuCheckboxItem`. | Current source | | FR17 | Arrow keys and typeahead skip disabled rows, including checkbox and radio rows. Shipped behavior, not a requirement until OQ1 is ruled. | Current source and tests | | FR20 | With `header` or `footer` on a sub-menu (`DropdownMenuSubMenu`), its surface is a modal dialog named by its row (AR7), holding the header, the sub-menu's `role="menu"` (rows only), then the footer. It sits in a host `DropdownMenu` renders beside its menu, never inside a menu; under a root with no such host (`ContextMenu`) the sub-menu shows its rows alone. On a fine pointer the dialog is the flyout; on a compact touch display, the drilled-in view, which replaces the root's menu and regions until popped. A region keeps its own keys and presses. The rows are one Tab stop: Tab and Shift+Tab walk the header's controls, the rows, then the footer's, and wrap; from a deeper flyout, that flyout closes and the walk goes on from the rows. An open lands as for any sub-menu (FR11, FR12), not on a region. Escape in the dialog, outside an IME composition and unless a control there handled it, returns focus to the row and leaves the root open. A pointer leaving the flyout keeps it open until focus leaves, and it closes with the flyout holding its row. Without either prop nothing changes. | Current behavior; FR15; AR5 | -| FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button on the theme's entrance duration and easing tokens and collapse back into it on `springs.menuClose` (`spec:AST-094` FR31, FR45), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own. | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6 | +| FR21 | Under a finger, in the touch modes (`presentation` not pinned to `'popover'`), the pointer menu MUST grow out of the Trigger button on the theme's entrance duration and easing tokens and collapse back into it on the theme's exit duration and easing (`spec:AST-094` FR7, FR31, FR45), and MUST close as a pan that starts outside it begins (FR32); a pinned `'popover'` keeps the layer's own entrance. A pan dismissal is the menu's own close: focus that was in the menu returns to the Trigger button, whose `aria-expanded` turns false. A mouse keeps the layer's own entrance and exit. Implementation gap: the menu keeps the layer's own entrance and exit under a finger too, and has no pan-outside dismissal of its own. | `spec:AST-094` FR31, FR32; `family:overlay-dismissal` AV6 | ### Allowed variation diff --git a/docs/specs/components/Lightbox/Lightbox.spec.md b/docs/specs/components/Lightbox/Lightbox.spec.md index 445bf9c8f25a2..2241ad74b31ed 100644 --- a/docs/specs/components/Lightbox/Lightbox.spec.md +++ b/docs/specs/components/Lightbox/Lightbox.spec.md @@ -131,24 +131,24 @@ FR21). Consumer props and usage remain documented in `Lightbox.doc.mjs`. ## Behavioral and layout contract -| ID | Candidate invariant | Basis | Draft review state | -| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------- | -| FR1 | The viewer contains one active Media item and a Close button; galleries additionally contain Previous and Next buttons plus a Counter, and Caption renders only when supplied. | Current source, docs, and tests | settled | -| FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled | -| FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled | -| FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap | -| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the zoom settle, the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR45). Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap | -| FR6 | With `hasZoom`, on an image, a pinch zooms about the fingers and pans while it zooms; past the minimum or maximum the photo resists and eases back without overshoot. A double tap zooms to the larger of filling the viewer and 2×, about the tapped point; a second double tap returns to the fitted size. A zoomed photo pans with momentum and rubber-band edges. A pinch on the controls or the caption still zooms the page. | `spec:AST-094` FR3, FR10 | settled; implementation gap | -| FR7 | In a gallery, a horizontal drag moves the row of items, which sit apart by the measured gap. The release lands on at most one neighbouring item by `spec:AST-094` FR3's paging rule (DEC-25). Paging applies over a video item's frame as over a photo; a drag that starts on the video's own controls, such as its scrubber or volume, stays the video's. A zoomed photo pans to its edge before a new drag pages. A pinch never pages. Previous, Next, and the arrow keys keep paging. A controlled `index` the caller refuses springs back. `onIndexChange` reports a swipe. | `spec:AST-094` FR3, FR14, FR36–FR39, DEC-25 | settled; implementation gap | -| FR8 | On a fitted image, a downward drag moves the photo with the finger, shrinks it, and fades the backdrop. The release decides once: a projected travel past the commit point closes the viewer and reports `onOpenChange(false)` at the lift; a release moving back springs back. Video is unchanged. | `spec:AST-094` FR36–FR40 | settled; implementation gap | -| FR9 | With `hasZoom`, a pinch in from the fitted size is a release decision: below the measured fraction of the fitted size the release closes the viewer; opening the fingers again by the measured amount before the lift is moving back and cancels. A pinch that starts zoomed in never closes the viewer. | `spec:AST-094` FR36, FR37 | settled; implementation gap | -| FR10 | With `hasZoom`, under a coarse pointer only, a tap on the photo fades the controls and caption and darkens the backdrop. While they are hidden, the next tap on the photo, a tap beside the photo, or a cancelled drag brings them back, and that tap beside the photo does not close the viewer (ORD3). AR5 governs keyboard and assistive-technology access while they are hidden. | `architecture:public-component-api` INV16 | settled; implementation gap | -| FR11 | A mouse, a pen, and a trackpad also use FR7's drag paging, FR8's drag to close, a ctrl-wheel or trackpad pinch over the photo for FR6's zoom and FR9's close, and a wheel to pan a zoomed photo; over the photo these replace the browser's page zoom and scroll. A double-click zooms 2× about the clicked point. A click on the photo keeps the controls, and the viewer opens and closes at once. | `spec:AST-094` FR43; `spec:AST-017` FR59 | settled; implementation gap | -| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens, on the theme's duration and easing tokens (`spec:AST-094` FR45), and returns into it when it closes: the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap | -| FR13 | `hasZoom={false}` turns off FR6, FR9, and FR10. FR7 and FR8 add to the buttons, keys, and Escape and remove none of them, so Lightbox takes no `presentation` prop. | `architecture:public-component-api` INV16 | settled | -| FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap | -| FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled | -| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on its named motion role. `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap | +| ID | Candidate invariant | Basis | Draft review state | +| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------------------- | +| FR1 | The viewer contains one active Media item and a Close button; galleries additionally contain Previous and Next buttons plus a Counter, and Caption renders only when supplied. | Current source, docs, and tests | settled | +| FR2 | Viewer overlay carries the current `lightbox` target. Media, Caption, and Counter carry no Lightbox public target. | Current source and public docs | settled | +| FR3 | Close, Previous, and Next button surfaces use Button's `button` target through IconButton rather than Lightbox-owned button targets. | Current source and target docs | settled | +| FR4 | The open viewer is a focus region through Core's shared focus-region module (`component:Dialog/DEC-2`): Tab from its last control moves focus to its first and Shift+Tab from its first moves focus to its last, so focus never leaves the viewer for the browser's own interface. | `component:Dialog/DEC-2` | settled; implementation gap | +| FR5 | Under a coarse primary pointer, after hydration, Lightbox presents FR6–FR10 with no opt-in. Every spring a gesture drives in them (the zoom settle, the page settle, the dismiss and the pinch close) is a named motion role read through the motion role table, never a literal, and starts at the gesture's velocity; motion that a tap, a double tap, a key or the program starts (the controls fade and a double tap's zoom transition) plays the theme's duration and easing tokens (`spec:AST-094` FR45). Under reduced motion they follow AR6. | `spec:AST-094` FR4, FR13 | settled; implementation gap | +| FR6 | With `hasZoom`, on an image, a pinch zooms about the fingers and pans while it zooms; past the minimum or maximum the photo resists and eases back without overshoot. A double tap zooms to the larger of filling the viewer and 2×, about the tapped point; a second double tap returns to the fitted size. A zoomed photo pans with momentum and rubber-band edges. A pinch on the controls or the caption still zooms the page. | `spec:AST-094` FR3, FR10 | settled; implementation gap | +| FR7 | In a gallery, a horizontal drag moves the row of items, which sit apart by the measured gap. The release lands on at most one neighbouring item by `spec:AST-094` FR3's paging rule (DEC-25). Paging applies over a video item's frame as over a photo; a drag that starts on the video's own controls, such as its scrubber or volume, stays the video's. A zoomed photo pans to its edge before a new drag pages. A pinch never pages. Previous, Next, and the arrow keys keep paging. A controlled `index` the caller refuses springs back. `onIndexChange` reports a swipe. | `spec:AST-094` FR3, FR14, FR36–FR39, DEC-25 | settled; implementation gap | +| FR8 | On a fitted image, a downward drag moves the photo with the finger, shrinks it, and fades the backdrop. The release decides once: a projected travel past the commit point closes the viewer and reports `onOpenChange(false)` at the lift; a release moving back springs back. Video is unchanged. | `spec:AST-094` FR36–FR40 | settled; implementation gap | +| FR9 | With `hasZoom`, a pinch in from the fitted size is a release decision: below the measured fraction of the fitted size the release closes the viewer; opening the fingers again by the measured amount before the lift is moving back and cancels. A pinch that starts zoomed in never closes the viewer. | `spec:AST-094` FR36, FR37 | settled; implementation gap | +| FR10 | With `hasZoom`, under a coarse pointer only, a tap on the photo fades the controls and caption and darkens the backdrop. While they are hidden, the next tap on the photo, a tap beside the photo, or a cancelled drag brings them back, and that tap beside the photo does not close the viewer (ORD3). AR5 governs keyboard and assistive-technology access while they are hidden. | `architecture:public-component-api` INV16 | settled; implementation gap | +| FR11 | A mouse, a pen, and a trackpad also use FR7's drag paging, FR8's drag to close, a ctrl-wheel or trackpad pinch over the photo for FR6's zoom and FR9's close, and a wheel to pan a zoomed photo; over the photo these replace the browser's page zoom and scroll. A double-click zooms 2× about the clicked point. A click on the photo keeps the controls, and the viewer opens and closes at once. | `spec:AST-094` FR43; `spec:AST-017` FR59 | settled; implementation gap | +| FR12 | Inside `experimental` only, under a coarse pointer, the photo grows out of its source when the viewer opens, on the theme's duration and easing tokens (`spec:AST-094` FR45), and returns into it when it closes, on the theme's exit duration and easing (`spec:AST-094` FR7): the element `experimental.sourceFor(index)` returns, else the opener when it shows a picture. A close reports `onOpenChange(false)` at the request; the viewer stays only until the photo lands. Pointer input during the entrance is held; keys and assistive technology stay live. Without `experimental`, opening is unchanged and closing plays FR16. | `spec:AST-017` FR21; `spec:AST-094` FR35 | settled; implementation gap | +| FR13 | `hasZoom={false}` turns off FR6, FR9, and FR10. FR7 and FR8 add to the buttons, keys, and Escape and remove none of them, so Lightbox takes no `presentation` prop. | `architecture:public-component-api` INV16 | settled | +| FR14 | Gestures, paging, motion, and reduced motion run through Core's one shared set of internal touch modules; Lightbox bundles no copy of a module's primitive and adds no gesture listener outside them. | `architecture:react-component-runtime` INV13 | settled; implementation gap | +| FR15 | Lightbox supports a Picture-in-Picture window as its only second document; there it ranks Escape and platform close among that window's members. | `family:overlay-dismissal` FR8 | settled | +| FR16 | Under a coarse primary pointer, a close that is not a drag down (FR8), a pinch in (FR9), or a return into a source (FR12) plays the touch exit: Close, Escape, a platform close, and a tap beside the photo while the controls are shown (ORD3). The photo shrinks a little and fades out in place with the backdrop and controls, as the native viewer's zoom transition does without a source, on the theme's exit duration and easing, `--duration-exit` and `--ease-exit` (`spec:AST-090` FR14; `spec:AST-094` FR7, FR45). `onOpenChange(false)` reports at the request, and the viewer takes no input while the exit plays. Under a fine pointer the viewer closes at once (FR11). | `spec:AST-094` FR7 | settled; implementation gap | ### Allowed variation diff --git a/docs/specs/components/Popover/Popover.spec.md b/docs/specs/components/Popover/Popover.spec.md index 493644a8ca5c0..a4cc71dcbb627 100644 --- a/docs/specs/components/Popover/Popover.spec.md +++ b/docs/specs/components/Popover/Popover.spec.md @@ -177,17 +177,17 @@ table owns their semantic effect. ## Behavioral and layout contract -| ID | Invariant | Basis | Acceptance and implementation state | -| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| FR1 | Popover and public `usePopover` expose one visibility lifecycle with canonical show, hide, and toggle operations. Every opening path inherits same-gesture reopen protection without changing public signatures. | Current public package surface, `architecture:layer-runtime/INV7`, and owner approval on 2026-09-07 | Accepted; implemented and covered by focused Popover-family tests | -| FR2 | Popover derives semantic focus entry identically across activation modalities: first genuine caller content, then labeled dialog-surface fallback. The injected close control is excluded. Shared interaction modality controls focus indication, not destination. | PR #5373, `architecture:interaction-modality`, and owner approval on 2026-09-07 | Accepted; implemented and covered by Popover focus tests | -| FR3 | Retired. Viewport and safe-area capping is the layer runtime's (`spec:AST-059` FR2, FR3); Popover clamps its preferred size with the runtime's cap and caps its painted surface with the same definition. | `spec:AST-059`, owner direction 2026-10-03 | Retired; see `architecture:layer-runtime` INV11 and the `Core/Layer` viewport-inset stories | -| FR4 | Popover enables internal scrolling only after measured overflow exceeds the current tolerance. Fitting content does not become a scroll container. | PR #5373 and current tests | Verified current behavior | -| FR5 | Overflow signals while open coalesce into at most one measurement per animation frame, and Popover owns no measurement observers while closed. | PR #5373 and current tests | Verified current resource behavior | -| 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 on the theme's entrance duration and easing tokens and shrinks back into it on `springs.menuClose`, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR35 and FR45 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it | +| ID | Invariant | Basis | Acceptance and implementation state | +| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| FR1 | Popover and public `usePopover` expose one visibility lifecycle with canonical show, hide, and toggle operations. Every opening path inherits same-gesture reopen protection without changing public signatures. | Current public package surface, `architecture:layer-runtime/INV7`, and owner approval on 2026-09-07 | Accepted; implemented and covered by focused Popover-family tests | +| FR2 | Popover derives semantic focus entry identically across activation modalities: first genuine caller content, then labeled dialog-surface fallback. The injected close control is excluded. Shared interaction modality controls focus indication, not destination. | PR #5373, `architecture:interaction-modality`, and owner approval on 2026-09-07 | Accepted; implemented and covered by Popover focus tests | +| FR3 | Retired. Viewport and safe-area capping is the layer runtime's (`spec:AST-059` FR2, FR3); Popover clamps its preferred size with the runtime's cap and caps its painted surface with the same definition. | `spec:AST-059`, owner direction 2026-10-03 | Retired; see `architecture:layer-runtime` INV11 and the `Core/Layer` viewport-inset stories | +| FR4 | Popover enables internal scrolling only after measured overflow exceeds the current tolerance. Fitting content does not become a scroll container. | PR #5373 and current tests | Verified current behavior | +| FR5 | Overflow signals while open coalesce into at most one measurement per animation frame, and Popover owns no measurement observers while closed. | PR #5373 and current tests | Verified current resource behavior | +| 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 on the theme's entrance duration and easing tokens and shrinks back into it on the theme's exit duration and easing, and a touch during its growth is ignored, as `spec:AST-094` FR31, FR35 and FR45 state. Its dismissal is unchanged, and a mouse keeps today's entrance. | `spec:AST-094` FR31, FR35 | Pending implementation; tests land with it | ### Allowed variation diff --git a/scripts/motion-roles-table.mjs b/scripts/motion-roles-table.mjs index d3e4a325b3370..fb2a88d4051e9 100644 --- a/scripts/motion-roles-table.mjs +++ b/scripts/motion-roles-table.mjs @@ -109,7 +109,7 @@ export function renderMotionRoleTable(roles, {fr4, records}) { '', '# Motion roles', '', - "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives and for a touch presentation's exit (`spec:AST-094` FR45, FR7). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", + "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", '', '| Role | Kind | Default | Named by |', '| --- | --- | --- | --- |', From 5aa0e0f68d61e31c55620ea379d202885b1dc6aa Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:33:42 -0800 Subject: [PATCH 06/11] spec(AST-094): name the animator's animate among the kept springs, and align failure conditions and decisions with the new split --- docs/specs/AST-090-exit-motion/spec.md | 11 +++---- .../motion-roles.md | 2 +- .../specs/AST-094-touch-presentations/spec.md | 4 +-- docs/specs/AST-099-mobile-keyboard/spec.md | 12 ++++---- .../AlertDialog/AlertDialog.spec.md | 2 +- .../SegmentedControl/SegmentedControl.spec.md | 30 +++++++++---------- scripts/motion-roles-table.mjs | 2 +- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/docs/specs/AST-090-exit-motion/spec.md b/docs/specs/AST-090-exit-motion/spec.md index 81f1db00b7c86..7b9e787e6ae9d 100644 --- a/docs/specs/AST-090-exit-motion/spec.md +++ b/docs/specs/AST-090-exit-motion/spec.md @@ -338,11 +338,12 @@ Rejected: holding the live layer in the top layer with an `overlay` transition, **Reference:** `spec:AST-090/DEC-5` **Decider:** `cixzhang`, `2026-10-28` -On a touch device a surface moves the way its native counterpart does, by -default and with no opt-in (`spec:AST-094`), and that record leaves pointer -presentations' timing and easing to the theme's tokens. The switches here -therefore govern pointer presentations only, and a touch presentation keeps -its native exit whatever they resolve to. A touch exit that a tap, a key or +On a touch device a surface keeps the look of its native counterpart, by +default and with no opt-in (`spec:AST-094`). That record springs only motion +a gesture drives and gives every other motion, under every pointer, the +theme's tokens (`spec:AST-094` FR45). The switches here govern whether a +pointer presentation's exit plays, and a touch presentation keeps its exit +whatever they resolve to. A touch exit that a tap, a key or the program starts plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit`, and a gesture-driven exit keeps its velocity spring (`spec:AST-094` DEC-26). diff --git a/docs/specs/AST-094-touch-presentations/motion-roles.md b/docs/specs/AST-094-touch-presentations/motion-roles.md index 28e7722922290..1b55140d7f45a 100644 --- a/docs/specs/AST-094-touch-presentations/motion-roles.md +++ b/docs/specs/AST-094-touch-presentations/motion-roles.md @@ -2,7 +2,7 @@ # Motion roles -Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. +Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. | Role | Kind | Default | Named by | | ------------------------------- | -------- | --------------------------------------------------------------- | ----------------------- | diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 72e7127d16d56..6247d4ee8f7fa 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -204,7 +204,7 @@ a measured, sourced difference exists. `timing.curves.entrance` and `timing.curves.move`) stay as the measured native reference, and no motion plays them: that motion plays the theme's duration and easing tokens (FR45), and an exit plays the theme's exit - duration and easing (FR7). Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). A component's own record states which of + duration and easing (FR7). Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). A component's own record states which of its roles a gesture drives. Implementation gap: the pager's measured paging motion (its settle @@ -257,7 +257,7 @@ a measured, sourced difference exists. motion, which a tap, a click, a hold, a key or the program starts, MUST play the theme's motion tokens, its duration and easing tokens scaled by `tempo` (`architecture:theme-tokens`, `spec:AST-108` FR2), under every - pointer, a finger included. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). A tap has no velocity, so it is not a gesture. + pointer, a finger included. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). A tap has no velocity, so it is not a gesture. A touch presentation keeps its own look for that motion (FR13), such as a menu that grows from its trigger or a thumb that lifts, and plays it on those tokens. A press that starts as a tap or a hold and continues as a diff --git a/docs/specs/AST-099-mobile-keyboard/spec.md b/docs/specs/AST-099-mobile-keyboard/spec.md index d73d00e0a75ff..84b283f61f0a1 100644 --- a/docs/specs/AST-099-mobile-keyboard/spec.md +++ b/docs/specs/AST-099-mobile-keyboard/spec.md @@ -234,18 +234,18 @@ implementation; this record states only the motion and its limits. Rejected: a one-step relayout that jumps after the keyboard settles; and overriding the app's viewport setting. -### DEC-3 — Simulators gate merge; physical phones gate release +### DEC-3 — Simulators gate merge; a physical iPhone gates release **Reference:** `spec:AST-099/DEC-3` **Decider:** `cixzhang`, `2026-10-24` Simulator and emulator evidence in a top-level document, with Gboard on the Android emulator, is enough to merge keyboard work, so it is not blocked on -hardware, while a pass on a physical iPhone and a physical Android phone, -including Samsung Keyboard and the phone's real refresh timing, is required -before the keyboard module ships in a release, because keyboard timing and viewport behavior differ on -real devices. `spec:AST-094` DEC-27 replaces the physical Android phone with -emulator evidence: Android claims need no human device check. +hardware, while a pass on a physical iPhone, including the phone's real +refresh timing, is required before the keyboard module ships in a release, +because keyboard timing and viewport behavior differ on real devices. Android +claims rest on emulator evidence and need no human device check +(`spec:AST-094` DEC-27). Rejected: physical devices as a merge gate, which blocks work on hardware; and physical devices as optional follow-up, which can ship untested keyboard behavior. diff --git a/docs/specs/components/AlertDialog/AlertDialog.spec.md b/docs/specs/components/AlertDialog/AlertDialog.spec.md index 645a93ceb019b..54c2d5ae920f5 100644 --- a/docs/specs/components/AlertDialog/AlertDialog.spec.md +++ b/docs/specs/components/AlertDialog/AlertDialog.spec.md @@ -227,7 +227,7 @@ The dialog surface (the Dialog element AlertDialog renders) carries `alert-dialo | Contract | Verification | Representative states | Mutation or failure expectation | Audit section | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | | FR1–FR5 | `AlertDialog.test.tsx` ("calls onOpenChange(false) on Escape", "does not call onOpenChange when action is clicked", "does not call onAction on Escape", "does not disable the cancel button while the action is loading", "exposes a named group instead, with no aria-modal") and `useImperativeAlertDialog.test.tsx` ("opens on show()", "closes on hide()") | open, loading, inline, imperative | Escape not cancelling, the action closing by itself, or the inline path claiming the role fails. | `audit:AlertDialog/behavior` | -| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and the dismissal, a busy-main-thread case, real-device clips on iOS Safari, and Android Chrome clips in the Android emulator (`spec:AST-094` DEC-27) | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a curve off its role, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | +| FR6, PR2 | Browser tests under a coarse pointer in Chromium and WebKit, a theme-token curve oracle for the present and the dismissal, a busy-main-thread case, real-device clips on iOS Safari, and Android Chrome clips in the Android emulator (`spec:AST-094` DEC-27) | present, dismiss, close during present, reopen during dismiss, Reduce Motion, a fine pointer, a narrow viewport | a present or a tap-, key- or program-started dismissal that plays a measured role instead of the theme's entrance or exit duration and easing, a start outside the viewport, a jump on interruption, a scale under Reduce Motion, changed motion under a fine pointer, or an added element fails. No test exists yet; FR6 is not shipped. | `audit:AlertDialog/behavior` | | FR7 | Browser tests under a coarse pointer, with keyboard and assistive-technology input | a tap during the presentation, Enter and Escape during it, a screen reader's activation, a tap during the dismissal; a mouse click during the presentation under a fine pointer | a held key or assistive-technology action, content made inert during the entrance, a tap that reaches a leaving alert, or a mouse click held under a fine pointer fails. No test exists yet; FR7 is not shipped. | `audit:AlertDialog/behavior` | | FR8 | `AlertDialog.test.tsx` ("AlertDialog action tracking"), `useActionGroupTracking.test.tsx` and `AlertDialog.interaction.browser.spec.ts` | a press on Cancel slid onto the action and lifted, and a press lifted outside the actions, each with a mouse, a pen, and a finger; Enter and Space on each action | the press-start action running after a slide, any action running on a lift outside, or a changed keyboard or assistive-technology activation fails. | `audit:AlertDialog/behavior` | | FR9 | Browser tests at a phone width and at a desktop width, in the default and an alternate theme | a phone width and a desktop width, in the default and an alternate theme; a caller `width` at a phone width; a screen narrower than 320 px; a narrow window under a fine pointer; a coarse screen that is not compact | a phone-width alert other than 320 px in any theme without a caller `width`, a caller `width` overridden, a desktop width changed, an alert wider than the viewport, or a 320 px alert in a narrow fine-pointer window or on a non-compact coarse screen fails. No test exists yet; FR9 is not shipped. | `audit:AlertDialog/behavior` | diff --git a/docs/specs/components/SegmentedControl/SegmentedControl.spec.md b/docs/specs/components/SegmentedControl/SegmentedControl.spec.md index 43b6683c6284f..76e01fb0a6466 100644 --- a/docs/specs/components/SegmentedControl/SegmentedControl.spec.md +++ b/docs/specs/components/SegmentedControl/SegmentedControl.spec.md @@ -230,21 +230,21 @@ segment parts as well. ## Verification map -| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -| FR1, AR1 | `SegmentedControl.test.tsx` rendering, size, and data-testid cases; `RadioGroup.a11y.test.tsx` and `RadioGroup.a11y.chromium.spec.ts` (in `RadioList/__tests__`) `segmented-group-*` states | Control with several segments; sizes | Lost group semantics or name fails. Evidence gap: no test asserts the `segmented-control` target or `data-size`. | `audit:SegmentedControl/anatomy` | -| FR2, FR3, ORD2, AR2 | `SegmentedControl.test.tsx` keyboard-navigation, unmatched-value and Tab-stop cases; `RadioGroup.a11y.test.tsx` and `RadioGroup.a11y.chromium.spec.ts` (in `RadioList/__tests__`) `segmented-group-*` states; the radio-group pattern's aborted-press check on `segmented-option-unselected`; the `segmented-group-selected-rtl` state's arrow round trip under `dir="rtl"`; `SegmentedControl.interaction.browser.spec.ts` slide-off cases for a finger and a mouse, with focus in the group or not | Arrows, wrap, Home, End; Tab-in with an unmatched or disabled selection | A value change on Tab entry, no wrap, or landing on a disabled segment fails. A press that slides off with focus outside the group and still selects fails. A press with focus already in the group that selects before its lift fails. | `audit:SegmentedControl/accessibility` | -| FR4, ORD1 | `SegmentedControl.test.tsx` container handler forwarding cases | Caller `onKeyDown`, with and without `preventDefault` | A caller key handler that cannot cancel navigation fails. Evidence gap: no test asserts the caller `onFocus` order or the keyboard hint. | `audit:SegmentedControl/behavior` | -| FR5 | `SegmentedControl.test.tsx` layout cases | Hug, fill, narrow row, icon-only | A control that overflows, or squeezes icon-only segments, fails. | `audit:SegmentedControl/layout` | -| FR6 | Evidence gap: no test asserts size precedence or its propagation | — | — | `audit:SegmentedControl/layout` | -| FR7, AR3 | `SegmentedControl.test.tsx` disabled and disabledMessage cases; `RadioGroup.a11y.test.tsx` and `RadioGroup.a11y.chromium.spec.ts` (in `RadioList/__tests__`) `segmented-group-disabled` and `segmented-group-disabled-with-message` states | Group and item disabled; with reason | A selection while disabled, or a missing reason tooltip, fails. Evidence gap: no test asserts the half opacity, or caller handlers while disabled. The `segmented-group-disabled` state declares a Tab stop. | `audit:SegmentedControl/accessibility` | -| FR8 | `SegmentedControl.test.tsx` role-override and data-testid cases | Forwarded role | A forwarded prop that overrides the role fails. Evidence gap: no test asserts `ref` or a caller `aria-describedby`. | `audit:SegmentedControl/api` | -| FR9, FR10, PR2 | `useSegmentedSelectionMotion.test.ts` slide, retarget, keyboard, Reduce Motion and fine-pointer cases; `SegmentedControl.interaction.browser.spec.ts` slide, measured-spring, Reduce Motion, busy-main-thread and mouse-click proofs in Chromium and WebKit | a tap, a retarget, keyboard selection, Reduce Motion, a mouse | a second surface or added element, a Layout or Paint per frame, a curve off its role, travel under Reduce Motion, or a mouse slide off the theme's timing fails | `audit:SegmentedControl/theming` | -| FR11 | `SegmentedControl.interaction.browser.spec.ts` pressed-overlay proofs; `PressedState.a11y.chromium.spec.ts` segmented-control cases | a held unselected segment, a held selected segment | an unselected segment without the pressed overlay, or a dim under the default theme fails | `audit:SegmentedControl/theming` | -| FR12, FR13 | `useSegmentedSelectionMotion.test.ts` and `useSegmentedTouchTracking.test.ts` drag, face and lift cases; `SegmentedControl.interaction.browser.spec.ts` drag, overshoot, hysteresis, tap-click, slide-off, Reduce Motion drag, mouse and pen proofs | a hold, a drag, a release between segments, the end overshoot, a slow dither, a tap, a drag from another segment, a lift outside, Reduce Motion | a weight change mid-drag, a label that flips twice in a small dither, a commit anywhere but the segment nearest the finger, a commit after a lift outside, or a surface that stays put under Reduce Motion fails | `audit:SegmentedControl/anatomy` | -| PR1 | Evidence gap: no test asserts the absence of global listeners | — | — | `audit:SegmentedControl/behavior` | -| Target inventory | `packages/core/src/theme/themingTargets.test.ts` | Control and item targets | A rendered class absent from the documented targets fails. | `audit:SegmentedControl/theming` | -| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and both current targets | Missing, extra, prefixed, or stale mappings fail repository validation. | `audit:SegmentedControl/theming` | +| Contract | Verification | Representative states | Mutation or failure expectation | Audit section | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | +| FR1, AR1 | `SegmentedControl.test.tsx` rendering, size, and data-testid cases; `RadioGroup.a11y.test.tsx` and `RadioGroup.a11y.chromium.spec.ts` (in `RadioList/__tests__`) `segmented-group-*` states | Control with several segments; sizes | Lost group semantics or name fails. Evidence gap: no test asserts the `segmented-control` target or `data-size`. | `audit:SegmentedControl/anatomy` | +| FR2, FR3, ORD2, AR2 | `SegmentedControl.test.tsx` keyboard-navigation, unmatched-value and Tab-stop cases; `RadioGroup.a11y.test.tsx` and `RadioGroup.a11y.chromium.spec.ts` (in `RadioList/__tests__`) `segmented-group-*` states; the radio-group pattern's aborted-press check on `segmented-option-unselected`; the `segmented-group-selected-rtl` state's arrow round trip under `dir="rtl"`; `SegmentedControl.interaction.browser.spec.ts` slide-off cases for a finger and a mouse, with focus in the group or not | Arrows, wrap, Home, End; Tab-in with an unmatched or disabled selection | A value change on Tab entry, no wrap, or landing on a disabled segment fails. A press that slides off with focus outside the group and still selects fails. A press with focus already in the group that selects before its lift fails. | `audit:SegmentedControl/accessibility` | +| FR4, ORD1 | `SegmentedControl.test.tsx` container handler forwarding cases | Caller `onKeyDown`, with and without `preventDefault` | A caller key handler that cannot cancel navigation fails. Evidence gap: no test asserts the caller `onFocus` order or the keyboard hint. | `audit:SegmentedControl/behavior` | +| FR5 | `SegmentedControl.test.tsx` layout cases | Hug, fill, narrow row, icon-only | A control that overflows, or squeezes icon-only segments, fails. | `audit:SegmentedControl/layout` | +| FR6 | Evidence gap: no test asserts size precedence or its propagation | — | — | `audit:SegmentedControl/layout` | +| FR7, AR3 | `SegmentedControl.test.tsx` disabled and disabledMessage cases; `RadioGroup.a11y.test.tsx` and `RadioGroup.a11y.chromium.spec.ts` (in `RadioList/__tests__`) `segmented-group-disabled` and `segmented-group-disabled-with-message` states | Group and item disabled; with reason | A selection while disabled, or a missing reason tooltip, fails. Evidence gap: no test asserts the half opacity, or caller handlers while disabled. The `segmented-group-disabled` state declares a Tab stop. | `audit:SegmentedControl/accessibility` | +| FR8 | `SegmentedControl.test.tsx` role-override and data-testid cases | Forwarded role | A forwarded prop that overrides the role fails. Evidence gap: no test asserts `ref` or a caller `aria-describedby`. | `audit:SegmentedControl/api` | +| FR9, FR10, PR2 | `useSegmentedSelectionMotion.test.ts` slide, retarget, keyboard, Reduce Motion and fine-pointer cases; `SegmentedControl.interaction.browser.spec.ts` slide, measured-spring, Reduce Motion, busy-main-thread and mouse-click proofs in Chromium and WebKit | a tap, a retarget, keyboard selection, Reduce Motion, a mouse | a second surface or added element, a Layout or Paint per frame, a tap-, click- or key-started slide that plays a measured role instead of the theme's duration and easing tokens, a drag release under any pointer that settles from rest or on a theme curve instead of a spring from its velocity, or travel under Reduce Motion fails | `audit:SegmentedControl/theming` | +| FR11 | `SegmentedControl.interaction.browser.spec.ts` pressed-overlay proofs; `PressedState.a11y.chromium.spec.ts` segmented-control cases | a held unselected segment, a held selected segment | an unselected segment without the pressed overlay, or a dim under the default theme fails | `audit:SegmentedControl/theming` | +| FR12, FR13 | `useSegmentedSelectionMotion.test.ts` and `useSegmentedTouchTracking.test.ts` drag, face and lift cases; `SegmentedControl.interaction.browser.spec.ts` drag, overshoot, hysteresis, tap-click, slide-off, Reduce Motion drag, mouse and pen proofs | a hold, a drag, a release between segments, the end overshoot, a slow dither, a tap, a drag from another segment, a lift outside, Reduce Motion | a weight change mid-drag, a label that flips twice in a small dither, a commit anywhere but the segment nearest the finger, a commit after a lift outside, or a surface that stays put under Reduce Motion fails | `audit:SegmentedControl/anatomy` | +| PR1 | Evidence gap: no test asserts the absence of global listeners | — | — | `audit:SegmentedControl/behavior` | +| Target inventory | `packages/core/src/theme/themingTargets.test.ts` | Control and item targets | A rendered class absent from the documented targets fails. | `audit:SegmentedControl/theming` | +| Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and both current targets | Missing, extra, prefixed, or stale mappings fail repository validation. | `audit:SegmentedControl/theming` | ## Current-state impact diff --git a/scripts/motion-roles-table.mjs b/scripts/motion-roles-table.mjs index fb2a88d4051e9..9be156a36c5a4 100644 --- a/scripts/motion-roles-table.mjs +++ b/scripts/motion-roles-table.mjs @@ -109,7 +109,7 @@ export function renderMotionRoleTable(roles, {fr4, records}) { '', '# Motion roles', '', - "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Two approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4) and the animator's `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", + "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", '', '| Role | Kind | Default | Named by |', '| --- | --- | --- | --- |', From c3c695bf133c3567db6e84d6e5b491a62b17af7f Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:09:39 -0800 Subject: [PATCH 07/11] spec(AST-094, AST-090): exempt Reduce Motion substitutions and busy loops, coasts keep their deceleration, and retitle the touch exit rule --- docs/specs/AST-090-exit-motion/spec.md | 8 ++++---- .../motion-roles.md | 2 +- .../specs/AST-094-touch-presentations/spec.md | 19 +++++++++++-------- docs/specs/components/Slider/Slider.spec.md | 2 +- scripts/motion-roles-table.mjs | 2 +- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/specs/AST-090-exit-motion/spec.md b/docs/specs/AST-090-exit-motion/spec.md index 7b9e787e6ae9d..1782161067978 100644 --- a/docs/specs/AST-090-exit-motion/spec.md +++ b/docs/specs/AST-090-exit-motion/spec.md @@ -176,7 +176,7 @@ governs them in a touch presentation. above it for the whole exit (`spec:AST-003` FR16). When it is hidden, they move to the latest remaining outlet with their entries and timers intact (`spec:AST-003` FR18, FR19). -- **FR14 — Touch presentations keep their native exit.** A dialog or layer +- **FR14 — Touch exits are never removed; tap-started exits play the theme's exit timing.** A dialog or layer surface in a touch presentation plays the exit its component's record gives it under `spec:AST-094` FR7 and FR8. A touch presentation's exit that a tap, key or program starts plays the theme's exit duration and easing, @@ -250,8 +250,8 @@ governs them in a touch presentation. `--ease-exit`. Its touch close keeps its own motion, which the exit tokens do not reach (`spec:AST-094` FR7, DEC-15). - Consumer docs: `motion` describes both curves, that exits are off by default - in a pointer presentation, how a theme turns each kind on, that touch - presentations keep their native exits, and that callers keep content rendered + in a pointer presentation, how a theme turns each kind on, that a touch + exit is never removed and a tap-started one plays the theme's exit timing, and that callers keep content rendered while a surface closes. The token tables and the theme template list the new tokens, and each member component's docs describe its exit. @@ -333,7 +333,7 @@ is also the default. Rejected: holding the live layer in the top layer with an `overlay` transition, which plays only in browsers that support that transition. -### DEC-5 — Touch presentations keep their native exit +### DEC-5 — Touch exits are never removed; tap-started exits play the theme's exit timing **Reference:** `spec:AST-090/DEC-5` **Decider:** `cixzhang`, `2026-10-28` diff --git a/docs/specs/AST-094-touch-presentations/motion-roles.md b/docs/specs/AST-094-touch-presentations/motion-roles.md index 1b55140d7f45a..0af94edd054aa 100644 --- a/docs/specs/AST-094-touch-presentations/motion-roles.md +++ b/docs/specs/AST-094-touch-presentations/motion-roles.md @@ -2,7 +2,7 @@ # Motion roles -Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. +Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Reduce Motion substitutions and continuous busy loops (such as ProgressBar's indeterminate pulse, `component:ProgressBar` FR6) keep their named roles; they are neither gesture- nor interaction-started motion. Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles. | Role | Kind | Default | Named by | | ------------------------------- | -------- | --------------------------------------------------------------- | ----------------------- | diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md index 6247d4ee8f7fa..a04f84ba05340 100644 --- a/docs/specs/AST-094-touch-presentations/spec.md +++ b/docs/specs/AST-094-touch-presentations/spec.md @@ -204,12 +204,14 @@ a measured, sourced difference exists. `timing.curves.entrance` and `timing.curves.move`) stay as the measured native reference, and no motion plays them: that motion plays the theme's duration and easing tokens (FR45), and an exit plays the theme's exit - duration and easing (FR7). Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). A component's own record states which of + duration and easing (FR7). Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Reduce Motion substitutions and continuous busy loops (such as ProgressBar's indeterminate pulse, `component:ProgressBar` FR6) keep their named roles; they are neither gesture- nor interaction-started motion. A component's own record states which of its roles a gesture drives. - Implementation gap: the pager's measured paging motion (its settle - integrator, its 300 ms animated scroll and that scroll's sinusoidal curve, - DEC-25) plays from internal constants and is not yet a named role. + Implementation gap: the pager's measured settle after a release (DEC-25) + plays from internal constants and is not yet a named role. Its animated + scroll to a page that the program asks for is a programmatic move, so it + plays the theme's duration and easing tokens (FR45); today it plays a + 300 ms scroll on a sinusoidal curve. ### Motion @@ -245,8 +247,9 @@ a measured, sourced difference exists. form. Each overlay's own record states its exit, since members own their animation (`family:overlay-dismissal` AV6). - **FR45 — A gesture's motion springs; other motion plays the theme's - motion tokens.** Motion that a gesture drives MUST play a spring that - starts at the gesture's velocity, under every pointer (FR43). A gesture is + motion tokens.** Motion that a gesture drives MUST play a spring, or for + a coast its measured deceleration rate, that starts at the gesture's + velocity, under every pointer (FR43). A gesture is a continuous pointer movement whose velocity carries into the motion: a drag, a flick, a swipe, a pan, a pinch, and the settle and rubber-band return after any of them. The theme's `tempo` scales the spring's duration @@ -257,7 +260,7 @@ a measured, sourced difference exists. motion, which a tap, a click, a hold, a key or the program starts, MUST play the theme's motion tokens, its duration and easing tokens scaled by `tempo` (`architecture:theme-tokens`, `spec:AST-108` FR2), under every - pointer, a finger included. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). A tap has no velocity, so it is not a gesture. + pointer, a finger included. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Reduce Motion substitutions and continuous busy loops (such as ProgressBar's indeterminate pulse, `component:ProgressBar` FR6) keep their named roles; they are neither gesture- nor interaction-started motion. A tap has no velocity, so it is not a gesture. A touch presentation keeps its own look for that motion (FR13), such as a menu that grows from its trigger or a thumb that lifts, and plays it on those tokens. A press that starts as a tap or a hold and continues as a @@ -858,7 +861,7 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and component documents its own, including the `presentation` value that keeps the released surface (FR25). -Motion that FR45 moves, by component (each record carries its own line): +Motion that FR45 moves, by component. Each component's record carries its own line, except Carousel, whose trackpad paging this record states through the shared pager (FR43): | Component | Motion | Today | Under FR45 | | --------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------- | diff --git a/docs/specs/components/Slider/Slider.spec.md b/docs/specs/components/Slider/Slider.spec.md index 2468762e7b042..3bfe5f088884a 100644 --- a/docs/specs/components/Slider/Slider.spec.md +++ b/docs/specs/components/Slider/Slider.spec.md @@ -267,7 +267,7 @@ separately listed value tooltip retains Tooltip's target. | FR4 | Source inspection; focused tests cover label, status, and disabled-reason Tooltip only | Label, status, value tooltip, disabled reason | Shared composition or its accessible association disappears; value-tooltip composition still lacks focused coverage. | `audit:Slider/anatomy` | | FR5 | Pending: `Slider.test.tsx` change-end cases driven by an owner that renders, clamps, or rejects the emitted value (not yet in the suite) | Single and range; pointer release before and after the owner re-renders; each keyboard step | `onChangeEnd` reports a value the owner did not render, fires more than once per interaction, or skips a completed keyboard step | `audit:Slider/accessibility` | | FR6 | `Slider.test.tsx` coincident range thumbs cases | Both thumbs at the minimum, the maximum and mid-range; a track press on each side; a press on the thumbs moved either way; a press that never moves, on the thumbs and on the mark label of their value; a drag that passes back over the shared value; right to left | A press or drag on coincident thumbs moves neither thumb, moves the thumb that cannot travel toward the pointer, keeps its focus, pressed state or tooltip on a thumb the drag has left, or leaves a press that never moves on the lower thumb | `audit:Slider/anatomy` | -| FR7 | `Slider.test.tsx`, `useSliderTouch.test.tsx`, and the touch and fine-pointer browser tests (`SliderTouch.interaction.browser.spec.ts`, `SliderFinePointer.interaction.browser.spec.ts`) in Chromium and WebKit | Drag, threshold, grab offset, coast, slow release, track tap and swipe, mouse click, secondary click, wheel, close range thumbs, vertical, right to left, reduced motion | The thumb jumps to the pointer, a press moves the value, a swipe across is trapped, a drag waits on the owner's render, a flick's release from any pointer that does not coast, or `onChangeEnd` fires before a finger's coast stops | `audit:Slider/anatomy` | +| FR7 | `Slider.test.tsx`, `useSliderTouch.test.tsx`, and the touch and fine-pointer browser tests (`SliderTouch.interaction.browser.spec.ts`, `SliderFinePointer.interaction.browser.spec.ts`) in Chromium and WebKit | Drag, threshold, grab offset, coast, slow release, track tap and swipe, mouse click, secondary click, wheel, close range thumbs, vertical, right to left, reduced motion | The thumb jumps to the pointer, a press moves the value, a swipe across is trapped, a drag waits on the owner's render, a flick's release from any pointer that does not coast, or `onChangeEnd` fires before any pointer's coast ends | `audit:Slider/anatomy` | | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical anatomy and four current local targets | Missing, extra, prefixed, stale, or unclaimed mappings fail repository validation. | `audit:Slider/theming` | The focused Slider suite asserts the `slider-control` target's orientation and diff --git a/scripts/motion-roles-table.mjs b/scripts/motion-roles-table.mjs index 9be156a36c5a4..0afaf2a180fac 100644 --- a/scripts/motion-roles-table.mjs +++ b/scripts/motion-roles-table.mjs @@ -109,7 +109,7 @@ export function renderMotionRoleTable(roles, {fr4, records}) { '', '# Motion roles', '', - "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", + "Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind, its default as the table measures it, and the record that names it. A role FR4 lists is named by FR4; any other role is one a component's own record measures, named by the current component records that carry its id. A role plays only for motion a gesture drives (`spec:AST-094` FR45). Motion that a tap, a click, a hold, a key or the program starts plays the theme's duration and easing tokens, and an exit it starts plays the theme's exit duration and easing (`spec:AST-094` FR7), and a role measured for that motion stays as the native reference. Three approved motions keep their springs until their own records change: the layout glide (`spec:AST-105` FR4), the animator's `animate` (`spec:AST-107` FR3), and `useDragToSnapPoints`'s `settleTo` (`spec:AST-107`). Reduce Motion substitutions and continuous busy loops (such as ProgressBar's indeterminate pulse, `component:ProgressBar` FR6) keep their named roles; they are neither gesture- nor interaction-started motion. Measured input timings, such as gesture thresholds, hold durations and flick speeds, are not motion roles.", '', '| Role | Kind | Default | Named by |', '| --- | --- | --- | --- |', From e540dba7232f2533d1c2e6269deb3bd2ca31516b Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:17:52 -0800 Subject: [PATCH 08/11] spec: the animator's value mode follows its channels' timing; fix ProgressBar's change count and Slider's coast class --- docs/architecture/react-component-runtime.md | 3 +-- docs/specs/components/ProgressBar/ProgressBar.spec.md | 6 +++--- docs/specs/components/Slider/Slider.spec.md | 9 +++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/architecture/react-component-runtime.md b/docs/architecture/react-component-runtime.md index 56838fde56f98..cda5e32c6585d 100644 --- a/docs/architecture/react-component-runtime.md +++ b/docs/architecture/react-component-runtime.md @@ -294,8 +294,7 @@ those contracts. lockstep with Core, and limited by lint to Core and Lab (`spec:AST-017` FR21). - **INV15 — The animator animates a number.** The animator has a value mode: - it plays a number, such as a count or a chart value, on the same springs and - motion roles as its property channels, and hands the value to an `onUpdate` + it plays a number, such as a count or a chart value, on the same timing as its property channels: theme motion tokens for a program-started change, and measured springs only where `spec:AST-094` FR45 keeps them, and hands the value to an `onUpdate` callback each frame for the component to format and write. The component owns the change size past which it swaps to the new value instead of counting, so a jump from twelve to over a million does not spin. Under reduced motion the diff --git a/docs/specs/components/ProgressBar/ProgressBar.spec.md b/docs/specs/components/ProgressBar/ProgressBar.spec.md index a54ac99f518a2..4c0d416a17d64 100644 --- a/docs/specs/components/ProgressBar/ProgressBar.spec.md +++ b/docs/specs/components/ProgressBar/ProgressBar.spec.md @@ -80,10 +80,10 @@ Consumer syntax and examples belong in `ProgressBar.doc.mjs`. - Released default preserved: `yes` — determinate, `value` 0, `max` 100, `accent`, visible label, no value text, no marks. -- Compatibility class: records shipped behavior plus one motion change with +- Compatibility class: records shipped behavior plus two motion changes with no API, default, or value change: the indeterminate fill under reduced - motion (FR6); a determinate fill moves on the theme's eased duration under - every pointer (FR4); + motion (FR6), and a determinate fill that moves on the theme's eased + duration under every pointer (FR4); the four canonical `progress-bar*` targets are the only target classes. - Controlled/uncontrolled behavior: not applicable; `value` is caller-owned. - Migration decision: none diff --git a/docs/specs/components/Slider/Slider.spec.md b/docs/specs/components/Slider/Slider.spec.md index 3bfe5f088884a..fdb7b90327694 100644 --- a/docs/specs/components/Slider/Slider.spec.md +++ b/docs/specs/components/Slider/Slider.spec.md @@ -69,13 +69,14 @@ an additive target for the interactive control surface. moves is when a touch flick's `onChangeEnd` fires, about 0.6 s after the lift, once the coast settles; an owner that acts at the release keeps doing so through `onChangeEnd` and receives it after the coast. -- A mouse's or a pen's flick (FR7): it coasts as a finger's does, so its - `onChangeEnd` fires about 0.6 s after the release, once the coast settles; a - slow drag still ends at the release (`spec:AST-094/DEC-26`). +- A mouse's or a pen's flick (FR7): ships as a patch `[fix]`, the same class + as the finger's coast. It coasts as a finger's does, so its `onChangeEnd` + fires about 0.6 s after the release, once the coast settles; a slow drag + still ends at the release (`spec:AST-094/DEC-26`). - Controlled/uncontrolled behavior: unchanged; Slider remains controlled - Migration decision: an owner that moves `value` only from `onChangeEnd` moves it from `onChange` instead (FR5); an owner that needs the value at a - finger's lift reads the last `onChange` instead of waiting for + release (finger, mouse or pen) reads the last `onChange` instead of waiting for `onChangeEnd` (FR7) Consumer migration instructions belong in consumer docs and release notes. From 11f209ebcb4c9d29f3627c7802c7a482139f6cbb Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:44:57 -0800 Subject: [PATCH 09/11] spec: INV15's check follows its channels' timing; AlertDialog and Switch cite FR7 and FR45 --- docs/architecture/react-component-runtime.md | 2 +- docs/specs/components/AlertDialog/AlertDialog.spec.md | 2 +- docs/specs/components/Switch/Switch.spec.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/react-component-runtime.md b/docs/architecture/react-component-runtime.md index cda5e32c6585d..875cf3d657d68 100644 --- a/docs/architecture/react-component-runtime.md +++ b/docs/architecture/react-component-runtime.md @@ -483,6 +483,6 @@ component suite is not blanket conformance with every invariant. | INV11 | Source boundary check plus hook tests across StrictMode replay and unmount | A second keyboard listener or measurement in Core; a consumer reading a CSS custom property on the root or a shared ancestor for the height; an export of the hook; a component outside Core measuring by a rule other than AST-099 FR2, or riding the keyboard; a listener left after unmount; a Core rider driven by JavaScript per frame or by its own transition; or more than one layout read per keyboard change | | INV12 | Source boundary check plus real-browser tests on a throttled phone profile over a 10-level by 50-sibling tree and two changes in one frame | A per-frame height or layout, a walk past the area or into a subtree other than `display: contents`, an element moved twice for one frame, an added element other than a swap's one transient copy, that copy left after the swap ends, or an exported reporting function fails | | INV13 | Source boundary check plus bundle analysis of a page with several touch components, and real-browser tests on a throttled phone profile, under a fine pointer, and on a hybrid device | A second copy of a touch module on one page, a component bundling its own copy of a module's primitive, an `import()` of a module in a component other than `Item`'s released loader for its deprecated swipe props, a swipe module (the gesture core, the animator, the panels' rendering, the row's gesture code) imported by `Item` or `ListItem`, swipe code in `Item` other than its deprecated path (that loader, the released row style, the role gate, the `commit` warning, the bridge, the click guard, the event forwarders, and the coexistence diagnostic), or any of it outside the deprecated path, a public export or doc entry for the modules, an install call or installed check in the public API, a gesture listener in a component that uses no gesture, or an idle document listener outside a press other than the one shared press controller and the one stray-click swallower, an import of `@astryxdesign/core/internal/*` outside Core and Lab, a class outside the recognizer family, an `instanceof` check on a recognizer, an internal factory with a public factory's name, or a direct module import for a job the public layer covers fails | -| INV15 | Unit tests of the animator's value mode and real-browser proofs of its adopters | A value off its motion role, a count past the component's swap size, a count or cross-fade under reduced motion with a coarse or a fine pointer, an intermediate value announced, or a public export fails | +| INV15 | Unit tests of the animator's value mode and real-browser proofs of its adopters | A value off its channels' timing (the theme's tokens for a program-started change, a spring only where `spec:AST-094` FR45 keeps one), a count past the component's swap size, a count or cross-fade under reduced motion with a coarse or a fine pointer, an intermediate value announced, or a public export fails | | INV16 | Review of each Core component that handles a concern Core owns in a shared hook or utility | A Core component that keeps its own effect, listener, or tracker for that concern fails, whether or not it also calls the hook or utility | | INV16 (slot presence) | `no-nullish-jsx-guard` strict lint over Core and RichText, with no baseline, and `no-truthy-jsx-guard` strict lint over Core, with no baseline; both catch inline guards only, so review also checks for a presence test hoisted into a variable, converted (`!!value`, `Boolean(value)`), made by an early return or a `??` default, or passed through a prop spread, which the lints pass; empty-slot tests that pass `''`, `false`, `true`, and `null` and compare with an omitted slot, plus `0` as content, in Item, MetadataList, ChatSystemMessage, TreeList, TopNavMegaMenu, RichTextEditor, and RichTextEditorToolbar | A slot given `''`, `true`, or `false` draws a wrapper, lane, border, or separator that an omitted slot does not, an element loses the name it has without the slot, `0` does not render, or a new bare nullish or truthiness presence test, inline or hoisted, a truthiness guard past the baseline, a baseline entry added, or a hand-written copy of the check lands | diff --git a/docs/specs/components/AlertDialog/AlertDialog.spec.md b/docs/specs/components/AlertDialog/AlertDialog.spec.md index a2a264ac3ef4f..e683fdcded155 100644 --- a/docs/specs/components/AlertDialog/AlertDialog.spec.md +++ b/docs/specs/components/AlertDialog/AlertDialog.spec.md @@ -54,7 +54,7 @@ system_specs: | Builder impact | None. | | Compatibility/readiness | FR1–FR5, FR8 and AR1 record shipped behavior, apart from AR1's Tab wrap. FR6, FR7 and FR9 are requirements not shipped yet. Each gap is listed under Current-state impact. | | Review checks | Reject a modal AlertDialog without `role="alertdialog"`, a title or description not linked by id, initial focus on the destructive action, or a backdrop click that dismisses it. | -| Governing rules | `architecture:public-component-api/INV1,INV5–INV8`; `architecture:component-theming-surface/INV3–INV7`; `architecture:layer-runtime`; `component:Dialog/AR1–AR3`; `spec:AST-020/FR15`; `spec:AST-094` FR4, FR13, FR35, DEC-15, DEC-17; `family:action-groups`; `design:reduced-motion`; `spec:AST-017`. | +| Governing rules | `architecture:public-component-api/INV1,INV5–INV8`; `architecture:component-theming-surface/INV3–INV7`; `architecture:layer-runtime`; `component:Dialog/AR1–AR3`; `spec:AST-020/FR15`; `spec:AST-094` FR4, FR7, FR45, FR13, FR35, DEC-15, DEC-17; `family:action-groups`; `design:reduced-motion`; `spec:AST-017`. | This table is a review projection; the body below is authoritative. diff --git a/docs/specs/components/Switch/Switch.spec.md b/docs/specs/components/Switch/Switch.spec.md index 8f72d326cd9f4..7e07a155776ee 100644 --- a/docs/specs/components/Switch/Switch.spec.md +++ b/docs/specs/components/Switch/Switch.spec.md @@ -82,7 +82,7 @@ system_specs: | Builder impact | None. | | Compatibility/readiness | Under a finger a hold and a press slid off now toggle the switch, and a tap moves the thumb on the theme's motion tokens; under a mouse or a pen the thumb now follows a drag, and a drag past the flip distance flips it (FR15); a coarse pointer's hit area grows toward 44 px (FR10); no API, default, or DOM change. Caller handlers other than `onClick` run while disabled, and caller ARIA lands on the field root; both are listed under Current-state impact. | | Review checks | Reject a lost switch role or name, a change while disabled or busy, focus moving on change, a ring painted anywhere but the track, a thumb that travels the wrong way under RTL, or a disabled switch that submits. | -| Governing rules | `spec:AST-021` switch pattern; `spec:AST-085` FR1–FR3, FR5, FR6; `spec:AST-086` FR8; `design:user-states` DR1, DR2 (pressed overlay); `design:reduced-motion` DR1; WCAG 2.5.2, 3.2.2, 1.4.11; `spec:AST-094` FR4, FR11, FR13, FR24, FR42, FR43; `spec:AST-020` FR9 (DEC-5). | +| Governing rules | `spec:AST-021` switch pattern; `spec:AST-085` FR1–FR3, FR5, FR6; `spec:AST-086` FR8; `design:user-states` DR1, DR2 (pressed overlay); `design:reduced-motion` DR1; WCAG 2.5.2, 3.2.2, 1.4.11; `spec:AST-094` FR4, FR7, FR45, FR11, FR13, FR24, FR42, FR43; `spec:AST-020` FR9 (DEC-5). | This table is a review projection; the body below is authoritative. From ebb5dbf37952e904c308fe3a03d9c091a8583856 Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:57:41 -0800 Subject: [PATCH 10/11] spec(AST-057, AST-090): a swipe row's tap or Escape close plays the theme's exit tokens; already-animating surfaces keep pointer and gesture timing --- docs/specs/AST-057-row-actions/spec.md | 18 ++++++++++++------ docs/specs/AST-090-exit-motion/spec.md | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/specs/AST-057-row-actions/spec.md b/docs/specs/AST-057-row-actions/spec.md index 7564a73dc0887..a9a99f8e39294 100644 --- a/docs/specs/AST-057-row-actions/spec.md +++ b/docs/specs/AST-057-row-actions/spec.md @@ -142,10 +142,12 @@ and its panels stay put, so no element is added to any row. - **A render prop for a custom panel.** Deferred, not foreclosed: a `render*`-shaped addition can land when a caller needs something the vocabulary cannot say (DEC-1). -- **Gesture tuning.** Axis-lock distance, commit ratio, fling velocity, slide - and spring durations, 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). +- **Gesture tuning.** Axis-lock distance, commit ratio, fling velocity, + resistance past the panel, and the slides and springs a swipe drives: + behavior constants in source, not public API and not design tokens (the + same ruling the touch press model received for its clocks). A close that a + tap or Escape starts plays the theme's exit tokens (FR7, `spec:AST-094` + FR45). - **Vertical swipes**, and a swipe whose only meaning is dismissal (Toast already has that in `useToastGesture`). - **Public out-of-flow slots on `Item`.** Deliberately not admitted: the @@ -250,9 +252,9 @@ constants. | FR2 | `Item` MUST render each side's panel from its entries into an out-of-flow position on its existing root and MUST add no element to the row for it. The gesture MUST drive the row's travel by writing a custom property on the root; the root's transform MUST read it and each panel's transform its negation. | DEC-5; `useToastGesture` | Proposed; real-Chromium evidence required | | FR3 | `swipeBehavior` MUST be one closed union, `SwipeBehavior`, with the values `reveal` and `commit`, default `reveal`, applying to both sides. `commit` with more than one entry on a side MUST warn in development, because the other entries have no touch path. | DEC-2 | Proposed | | FR4 | A sideways drag by a finger or a pen, or a horizontal wheel gesture, MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; while a full swipe is confirmed (FR5) the outermost entry alone MUST cover the uncovered space; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required | -| FR5 | Under `reveal`, a release goes by its speed first: at the release speed or faster it opens the row while the row is opening and closes it while it is closing; a slower one opens the row past half the panel's width and closes it short of that. Speed alone MUST NOT fire an entry. Dragged past the confirm point, the row MUST run ahead toward its far edge with the outermost entry covering its place while the finger stays down; a lift still past the confirm point MUST fire the **outermost** entry and no other, at any speed; a swipe that confirmed and came back short of the confirm point MUST close the row and fire nothing; a disabled outermost entry MUST NOT confirm. Under `commit`, only a lift past the confirm point MUST slide the row out and fire the outermost entry; every other release closes; nothing MUST rest. After any entry fires, under either model, the row MUST spring back unless the entry has `hasRemoval`, in which case it MUST hold out at the edge it travelled toward. That bit is the only thing an entry declares about the row; the component MUST NOT wait on, observe, or act on what the caller does next, and MUST NOT warn if a held-out row is never removed. A release short of the threshold MUST spring back under both models. Under reduced motion the slide out, the spring back, and the hold-out follow `design:reduced-motion`: the row reaches its resting place with no travel or spring and cross-fades (DR2) when a finger started the swipe, or, when a pen or a trackpad did, cross-fades on a coarse primary pointer (DR2) and arrives at once otherwise (DR3). `onActivate` MUST fire once the row's content has left its box, and may return a Promise. | DEC-1, DEC-2; UIKit `performsFirstActionWithFullSwipe` | Proposed; real-device evidence required | +| FR5 | Under `reveal`, a release goes by its speed first: at the release speed or faster it opens the row while the row is opening and closes it while it is closing; a slower one opens the row past half the panel's width and closes it short of that. Speed alone MUST NOT fire an entry. Dragged past the confirm point, the row MUST run ahead toward its far edge with the outermost entry covering its place while the finger stays down; a lift still past the confirm point MUST fire the **outermost** entry and no other, at any speed; a swipe that confirmed and came back short of the confirm point MUST close the row and fire nothing; a disabled outermost entry MUST NOT confirm. Under `commit`, only a lift past the confirm point MUST slide the row out and fire the outermost entry; every other release closes; nothing MUST rest. After any entry fires, under either model, the row MUST return unless the entry has `hasRemoval`, in which case it MUST hold out at the edge it travelled toward. When a swipe fired the entry, the return is gesture-driven and springs from the release velocity; when a tap on the entry fired it, the return is tap-started and plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit` (`spec:AST-094` FR45). That bit is the only thing an entry declares about the row; the component MUST NOT wait on, observe, or act on what the caller does next, and MUST NOT warn if a held-out row is never removed. A release short of the threshold MUST spring back under both models. Under reduced motion the slide out, the spring back, and the hold-out follow `design:reduced-motion`: the row reaches its resting place with no travel or spring and cross-fades (DR2) when a finger started the swipe, or, when a pen or a trackpad did, cross-fades on a coarse primary pointer (DR2) and arrives at once otherwise (DR3). `onActivate` MUST fire once the row's content has left its box, and may return a Promise. | DEC-1, DEC-2; UIKit `performsFirstActionWithFullSwipe` | Proposed; real-device evidence required | | FR6 | The drag MUST decide its axis once, early, and a mostly vertical drag MUST stay the scroller's. A mouse MUST NOT start the drag. The click the browser synthesizes after a drag MUST NOT fire the row's primary action. | Press-model ORD1 | Proposed; real-device evidence required | -| FR7 | An open row (resting open, or settling open from the release that opened it) MUST close on a pointer landing anywhere outside it, on a touch tap on its own content, on a scroll of anything that holds it while focus is outside its open panel, on Escape with focus inside it, on firing an entry, and on unmount. That tap MUST activate nothing and paint no press, an exception to the cell press profile of `spec:AST-094` FR11. Because a drag on a neighbour is a pointer outside it, at most one row is open at a time without any shared state. | DEC-3; SwiftUI `swipeActionsContainer()` for the outcome | Proposed | +| FR7 | An open row (resting open, or settling open from the release that opened it) MUST close on a pointer landing anywhere outside it, on a touch tap on its own content, on a scroll of anything that holds it while focus is outside its open panel, on Escape with focus inside it, on firing an entry, and on unmount. That tap MUST activate nothing and paint no press, an exception to the cell press profile of `spec:AST-094` FR11. Because a drag on a neighbour is a pointer outside it, at most one row is open at a time without any shared state. A close that a tap, a pointer outside, a scroll, Escape, a fired entry's tap or the program starts plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit`; a close a swipe drives keeps its measured spring from the release velocity (`spec:AST-094` FR45). Implementation gap: the shipped row settles every close on one 200 ms ease-out today. | DEC-3; SwiftUI `swipeActionsContainer()` for the outcome | Proposed | | FR8 | Directions MUST be logical: `trailing` is uncovered by a drag toward the inline start and sits at the inline end; `leading` the reverse; so the finger, the uncovered edge, and the panel agree under RTL. | `architecture:public-component-api` INV2 (logical direction) | Proposed | | FR9 | The element containing swipe-capable rows MUST clip in the inline axis with `overflow-inline: clip`, never `hidden`. `List` MUST set it; `Item`'s consumer documentation MUST state the obligation for any other host. | DEC-5; `List` root styles | Proposed | | FR10 | The `createSwipe` module MUST import every module the swipe runs on — the shared gesture core, the animator, the panels' rendering, and the row's gesture code — statically. `Item` and `ListItem` MUST import none of these swipe modules and MUST NOT load them with `import()`; the touch modules `Item` uses for other jobs, such as press feedback, are outside this rule. DEP-0017 makes one exception, `Item`'s deprecated path, which serves the deprecated props only and leaves with CLN-0025 in 0.8.0: the released loader, which loads the swipe code with `import()` the first time a row has `swipeActions`; the released row style `Item` applies from that row's first render; the released role gate and `commit` warning; the bridge the loaded layer fills, the click guard that reads it, and the event forwarders from the row's root into it; and the development-only coexistence diagnostic when `swipe` and a deprecated prop are both given. An app that never imports `createSwipe` therefore loads no swipe code unless a row uses the deprecated props, and a row given `swipe` is swipeable at its first paint. Importing or calling `createSwipe` MUST do no DOM work. | DEC-8; `architecture:public-component-api` INV19; `architecture:react-component-runtime` INV13 | Proposed | @@ -300,6 +302,10 @@ constants. ## Current-state impact +- Implementation gap (FR5, FR7): the shipped row settles every close on one + 200 ms ease-out today, where `spec:AST-094` FR45 has a close that a tap or + Escape starts play `--duration-exit` and `--ease-exit` and a close a swipe + drives keep its measured spring. - Implementation gap: `swipe`, `createSwipe`, and the types `SwipeAction`, `SwipeActionVariant`, `SwipeBehavior`, `CreateSwipeOptions`, and `Swipe` are not built. `Item` diff --git a/docs/specs/AST-090-exit-motion/spec.md b/docs/specs/AST-090-exit-motion/spec.md index 2b2f0531d7264..0ee7f0322888f 100644 --- a/docs/specs/AST-090-exit-motion/spec.md +++ b/docs/specs/AST-090-exit-motion/spec.md @@ -77,9 +77,9 @@ components read through two shared primitives is a system fact. `--duration-exit` and `--ease-exit`, and a gesture-driven exit keeps its velocity spring (FR14). - Surfaces that already animate out (`Drawer`, `BottomSheet`, `MobileNav`, - `Toast`, and the `TopNavMegaMenu` panel) keep their durations and curves - only for an exit a gesture drives, a swipe or a drag with velocity, which - keeps its measured spring. In a touch presentation, an exit that a tap, a + `Toast`, and the `TopNavMegaMenu` panel) keep their durations and curves in + a pointer presentation, and for an exit a gesture drives, a swipe or a drag + with velocity, which keeps its measured spring. In a touch presentation, an exit that a tap, a key or the program starts plays `--duration-exit` and `--ease-exit` (FR14), `BottomSheet`'s close included. `BottomSheet`'s close in a pointer presentation reads `--ease-exit` at the curve it uses (FR12); moving the From 7ae648a7688818c887958b274c3c88a6023bd30b Mon Sep 17 00:00:00 2001 From: Cindy Zhang <2781099+cixzhang@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:59:21 -0800 Subject: [PATCH 11/11] spec(AST-057): format --- docs/specs/AST-057-row-actions/spec.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/specs/AST-057-row-actions/spec.md b/docs/specs/AST-057-row-actions/spec.md index a9a99f8e39294..34341870b1a0c 100644 --- a/docs/specs/AST-057-row-actions/spec.md +++ b/docs/specs/AST-057-row-actions/spec.md @@ -246,19 +246,19 @@ constants. ### Behavioral contract -| ID | Invariant | Basis | Verification state | -| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------- | -| FR1 | `Item` MUST take its swipe actions as one prop, `swipe`, whose value is the `Swipe` that `createSwipe(options)` returns, where `options` is `{leading?: readonly SwipeAction[]; trailing?: readonly SwipeAction[]; swipeBehavior?: SwipeBehavior}` and `SwipeAction` is `{id?, label: string, icon?: ReactNode, onActivate, isDisabled?, variant?: 'neutral' \| 'accent' \| 'destructive', hasRemoval?: boolean}` with `variant` defaulting to `accent`. An entry MUST NOT carry a node the component renders as the control. `Swipe` MUST be opaque, made only by `createSwipe`. `ListItem` MUST pass `swipe` through unchanged. The deprecated `swipeActions` and `swipeBehavior` props follow DEP-0017. | DEC-1, DEC-8; `DropdownMenu.items` data mode | Proposed | -| FR2 | `Item` MUST render each side's panel from its entries into an out-of-flow position on its existing root and MUST add no element to the row for it. The gesture MUST drive the row's travel by writing a custom property on the root; the root's transform MUST read it and each panel's transform its negation. | DEC-5; `useToastGesture` | Proposed; real-Chromium evidence required | -| FR3 | `swipeBehavior` MUST be one closed union, `SwipeBehavior`, with the values `reveal` and `commit`, default `reveal`, applying to both sides. `commit` with more than one entry on a side MUST warn in development, because the other entries have no touch path. | DEC-2 | Proposed | -| FR4 | A sideways drag by a finger or a pen, or a horizontal wheel gesture, MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; while a full swipe is confirmed (FR5) the outermost entry alone MUST cover the uncovered space; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required | +| ID | Invariant | Basis | Verification state | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------- | +| FR1 | `Item` MUST take its swipe actions as one prop, `swipe`, whose value is the `Swipe` that `createSwipe(options)` returns, where `options` is `{leading?: readonly SwipeAction[]; trailing?: readonly SwipeAction[]; swipeBehavior?: SwipeBehavior}` and `SwipeAction` is `{id?, label: string, icon?: ReactNode, onActivate, isDisabled?, variant?: 'neutral' \| 'accent' \| 'destructive', hasRemoval?: boolean}` with `variant` defaulting to `accent`. An entry MUST NOT carry a node the component renders as the control. `Swipe` MUST be opaque, made only by `createSwipe`. `ListItem` MUST pass `swipe` through unchanged. The deprecated `swipeActions` and `swipeBehavior` props follow DEP-0017. | DEC-1, DEC-8; `DropdownMenu.items` data mode | Proposed | +| FR2 | `Item` MUST render each side's panel from its entries into an out-of-flow position on its existing root and MUST add no element to the row for it. The gesture MUST drive the row's travel by writing a custom property on the root; the root's transform MUST read it and each panel's transform its negation. | DEC-5; `useToastGesture` | Proposed; real-Chromium evidence required | +| FR3 | `swipeBehavior` MUST be one closed union, `SwipeBehavior`, with the values `reveal` and `commit`, default `reveal`, applying to both sides. `commit` with more than one entry on a side MUST warn in development, because the other entries have no touch path. | DEC-2 | Proposed | +| FR4 | A sideways drag by a finger or a pen, or a horizontal wheel gesture, MUST move the row and uncover the side's panel, showing each entry's `label` and `icon` as a filled surface in its `variant`'s colour; while a full swipe is confirmed (FR5) the outermost entry alone MUST cover the uncovered space; the panel MUST NOT be visible at rest, by any pixel. | Real-browser finding: a zero-width panel still paints its padding | Proposed; real-Chromium evidence required | | FR5 | Under `reveal`, a release goes by its speed first: at the release speed or faster it opens the row while the row is opening and closes it while it is closing; a slower one opens the row past half the panel's width and closes it short of that. Speed alone MUST NOT fire an entry. Dragged past the confirm point, the row MUST run ahead toward its far edge with the outermost entry covering its place while the finger stays down; a lift still past the confirm point MUST fire the **outermost** entry and no other, at any speed; a swipe that confirmed and came back short of the confirm point MUST close the row and fire nothing; a disabled outermost entry MUST NOT confirm. Under `commit`, only a lift past the confirm point MUST slide the row out and fire the outermost entry; every other release closes; nothing MUST rest. After any entry fires, under either model, the row MUST return unless the entry has `hasRemoval`, in which case it MUST hold out at the edge it travelled toward. When a swipe fired the entry, the return is gesture-driven and springs from the release velocity; when a tap on the entry fired it, the return is tap-started and plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit` (`spec:AST-094` FR45). That bit is the only thing an entry declares about the row; the component MUST NOT wait on, observe, or act on what the caller does next, and MUST NOT warn if a held-out row is never removed. A release short of the threshold MUST spring back under both models. Under reduced motion the slide out, the spring back, and the hold-out follow `design:reduced-motion`: the row reaches its resting place with no travel or spring and cross-fades (DR2) when a finger started the swipe, or, when a pen or a trackpad did, cross-fades on a coarse primary pointer (DR2) and arrives at once otherwise (DR3). `onActivate` MUST fire once the row's content has left its box, and may return a Promise. | DEC-1, DEC-2; UIKit `performsFirstActionWithFullSwipe` | Proposed; real-device evidence required | -| FR6 | The drag MUST decide its axis once, early, and a mostly vertical drag MUST stay the scroller's. A mouse MUST NOT start the drag. The click the browser synthesizes after a drag MUST NOT fire the row's primary action. | Press-model ORD1 | Proposed; real-device evidence required | -| FR7 | An open row (resting open, or settling open from the release that opened it) MUST close on a pointer landing anywhere outside it, on a touch tap on its own content, on a scroll of anything that holds it while focus is outside its open panel, on Escape with focus inside it, on firing an entry, and on unmount. That tap MUST activate nothing and paint no press, an exception to the cell press profile of `spec:AST-094` FR11. Because a drag on a neighbour is a pointer outside it, at most one row is open at a time without any shared state. A close that a tap, a pointer outside, a scroll, Escape, a fired entry's tap or the program starts plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit`; a close a swipe drives keeps its measured spring from the release velocity (`spec:AST-094` FR45). Implementation gap: the shipped row settles every close on one 200 ms ease-out today. | DEC-3; SwiftUI `swipeActionsContainer()` for the outcome | Proposed | -| FR8 | Directions MUST be logical: `trailing` is uncovered by a drag toward the inline start and sits at the inline end; `leading` the reverse; so the finger, the uncovered edge, and the panel agree under RTL. | `architecture:public-component-api` INV2 (logical direction) | Proposed | -| FR9 | The element containing swipe-capable rows MUST clip in the inline axis with `overflow-inline: clip`, never `hidden`. `List` MUST set it; `Item`'s consumer documentation MUST state the obligation for any other host. | DEC-5; `List` root styles | Proposed | -| FR10 | The `createSwipe` module MUST import every module the swipe runs on — the shared gesture core, the animator, the panels' rendering, and the row's gesture code — statically. `Item` and `ListItem` MUST import none of these swipe modules and MUST NOT load them with `import()`; the touch modules `Item` uses for other jobs, such as press feedback, are outside this rule. DEP-0017 makes one exception, `Item`'s deprecated path, which serves the deprecated props only and leaves with CLN-0025 in 0.8.0: the released loader, which loads the swipe code with `import()` the first time a row has `swipeActions`; the released row style `Item` applies from that row's first render; the released role gate and `commit` warning; the bridge the loaded layer fills, the click guard that reads it, and the event forwarders from the row's root into it; and the development-only coexistence diagnostic when `swipe` and a deprecated prop are both given. An app that never imports `createSwipe` therefore loads no swipe code unless a row uses the deprecated props, and a row given `swipe` is swipeable at its first paint. Importing or calling `createSwipe` MUST do no DOM work. | DEC-8; `architecture:public-component-api` INV19; `architecture:react-component-runtime` INV13 | Proposed | -| FR11 | A caller MAY call `createSwipe` inline in a render. A new `Swipe` value MUST NOT close an open row, restart a settle, or attach the gesture again; the row reads the latest value's entries and handlers, so a handler sees the caller's latest state, and a changed entry list repaints the panel. | DEC-8 | Proposed | +| FR6 | The drag MUST decide its axis once, early, and a mostly vertical drag MUST stay the scroller's. A mouse MUST NOT start the drag. The click the browser synthesizes after a drag MUST NOT fire the row's primary action. | Press-model ORD1 | Proposed; real-device evidence required | +| FR7 | An open row (resting open, or settling open from the release that opened it) MUST close on a pointer landing anywhere outside it, on a touch tap on its own content, on a scroll of anything that holds it while focus is outside its open panel, on Escape with focus inside it, on firing an entry, and on unmount. That tap MUST activate nothing and paint no press, an exception to the cell press profile of `spec:AST-094` FR11. Because a drag on a neighbour is a pointer outside it, at most one row is open at a time without any shared state. A close that a tap, a pointer outside, a scroll, Escape, a fired entry's tap or the program starts plays the theme's exit duration and easing, `--duration-exit` and `--ease-exit`; a close a swipe drives keeps its measured spring from the release velocity (`spec:AST-094` FR45). Implementation gap: the shipped row settles every close on one 200 ms ease-out today. | DEC-3; SwiftUI `swipeActionsContainer()` for the outcome | Proposed | +| FR8 | Directions MUST be logical: `trailing` is uncovered by a drag toward the inline start and sits at the inline end; `leading` the reverse; so the finger, the uncovered edge, and the panel agree under RTL. | `architecture:public-component-api` INV2 (logical direction) | Proposed | +| FR9 | The element containing swipe-capable rows MUST clip in the inline axis with `overflow-inline: clip`, never `hidden`. `List` MUST set it; `Item`'s consumer documentation MUST state the obligation for any other host. | DEC-5; `List` root styles | Proposed | +| FR10 | The `createSwipe` module MUST import every module the swipe runs on — the shared gesture core, the animator, the panels' rendering, and the row's gesture code — statically. `Item` and `ListItem` MUST import none of these swipe modules and MUST NOT load them with `import()`; the touch modules `Item` uses for other jobs, such as press feedback, are outside this rule. DEP-0017 makes one exception, `Item`'s deprecated path, which serves the deprecated props only and leaves with CLN-0025 in 0.8.0: the released loader, which loads the swipe code with `import()` the first time a row has `swipeActions`; the released row style `Item` applies from that row's first render; the released role gate and `commit` warning; the bridge the loaded layer fills, the click guard that reads it, and the event forwarders from the row's root into it; and the development-only coexistence diagnostic when `swipe` and a deprecated prop are both given. An app that never imports `createSwipe` therefore loads no swipe code unless a row uses the deprecated props, and a row given `swipe` is swipeable at its first paint. Importing or calling `createSwipe` MUST do no DOM work. | DEC-8; `architecture:public-component-api` INV19; `architecture:react-component-runtime` INV13 | Proposed | +| FR11 | A caller MAY call `createSwipe` inline in a render. A new `Swipe` value MUST NOT close an open row, restart a settle, or attach the gesture again; the row reads the latest value's entries and handlers, so a handler sees the caller's latest state, and a changed entry list repaints the panel. | DEC-8 | Proposed | ### Accessibility contract