Skip to content

refactor(core): SegmentedControl's press dim plays through one internal press-dim function - #8563

Closed
cixzhang wants to merge 904 commits into
mainfrom
refactor/press-dim
Closed

cixzhang wants to merge 904 commits into
mainfrom
refactor/press-dim

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8127 (SegmentedControl's touch press), and lands after it. #8127's branch is on a fork, so this PR's base has to be main, and GitHub's file count includes #8127's stack. This PR's own change is 4 files, +370/−141: compare against #8127's head.

User impact

None. A finger on an unselected segment dims its title and brings it back exactly as before: UIKit's 470 ms fade and 180 ms lift on CSS ease, the 200 ms Reduce Motion fade (or an instant change on a fine pointer), and the hand-back to the segment's own style at full opacity.

What changes

SegmentedControl's pressed-title dim moves into one internal function with one job, utils/pressDim.ts, which sits beside the press machine. The press machine and SegmentedControl decide when a title dims; pressDim plays the fade:

  • fadePressDim(element, to, {durationMs, curve}, {touchRelease}) plays one opacity curve, from the opacity on screen to to, through the element's shared animator (animatorFor(element)). The fade runs on the compositor, shares the element's one animator, and takes the animator's Reduce Motion policy (design:reduced-motion DR2–DR4).
  • stopPressDim(element) hands the element back to its own style at once.
  • isPressDimReleased(element) says whether the dim has let go entirely: the animator shows 1 and holds no inline opacity or running opacity animation on the element.

It takes plain inputs and names no component. It isn't exported from any entry point and isn't listed in core's exports (architecture:public-component-api INV12). This is the press-feedback move of the shared touch-module plan. No other component duplicates this dim today.

Size (gzipped, minified esbuild bundle of SegmentedControl from packages/core/dist, sideEffects honoured)

main Base (#8127) This PR
Desktop 24.71 KB 57.29 KB 57.34 KB
Phone 24.71 KB 57.29 KB 57.34 KB

| Package stylesheet (astryx.css, gzip) | 33,181 B | 34,333 B | 34,333 B (byte-identical) |

main → #8127 is #8127's stack (SegmentedControl's touch press, its motion and the shared touch runtime it imports). This PR's own change is +0.05 KB of JavaScript and no CSS (the stylesheet is byte-identical to #8127's): the dim moves into pressDim, which adds the released check below. The touch-only CSS behind main → #8127 is #8127's and is justified there.

Touch cost (architecture:component-style-authoring INV6): SegmentedControl's touch code is imported statically, one copy per page shared by every touch component, under the shared-module rule proposed in #8464 (AST-080 DEC-5), so it isn't loaded lazily. This PR moves code into that shared runtime rather than adding to it: +0.05 KB.

Desktop and phone are the same because SegmentedControl's touch code is imported statically. The dim moves rather than disappears, so the saving comes when the next owner reuses fadePressDim instead of carrying its own copy.

Testing

  • pressDim.test.ts: one opacity curve through the animator, from the opacity on screen to the target on the clock and curve; the touch release is passed on; stopPressDim hands back at once.
  • SegmentedControl's title tests now read both the fades asked for and the opacity the animator shows. They check the 470 ms dim (mid-fade at 400 ms, done by 471 ms), the 180 ms return, the 200 ms Reduce Motion fade (mid-fade at 100 ms, done by 201 ms), the instant change on a fine pointer, and a finger sliding back onto a segment during its return dimming it to the pressed state's 0.2 again (the state's opacity is kept until the dim has let go, since the animator's opacity is on screen until then), a press after a finished return reading the pressed style again, and a press between a return's end and the animator letting go still dimming to the state. 104 tests pass across the four files.
  • Real browser: SegmentedControl's interaction spec, which plays this dim, runs in CI's interaction lane.
  • tsc, eslint, check:sync: clean.

vjeux and others added 30 commits October 25, 2026 22:38
…e roles yet) into the role-reading animator

The motion README's introduction keeps the animator and adds #8257's
sentence that no shipped component reads the roles yet.
…t, with no theme surface

Touch motion stays internal (the maintainer's ruling; the base keeps only
spec:AST-094 FR4's roles, internal too). segmentedControl.selectionSpring,
UISegmentedControl's measured spring, is a plain constant: no
registerSegmentedControlTable, no role, no theme variable.
- The record's FR6, the changeset and the component doc say so; the doc
  loses "a theme can retune the slide".
- The browser proof that a theme retunes the slide becomes one that the
  measured spring plays whatever a theme sets (the old role variable, a slow
  duration scale); the Reduce Motion DR4 proof slows the theme's durations
  instead of a role variable.
- The table test holds the spring to no role and to its measured value under
  the old variable.
…ty back on attach

Under Reduce Motion on a touch screen, a dip over the stylesheet's opacity
that ended while the animator was detached wrote the borrowed value inline
on the next attach, so the element stopped following its stylesheet. attach
now hands a finished or interrupted borrow back instead.
…, a traced waitForRest that looks ahead) into the role-reading animator
…xport reverted; overlap and borrow fixes)

Conflicts in animator.ts and the motion README: #8056's Reduce Motion plan
wins, which already carries a waiting move to the next call's invisible
moment and keeps a running dip's borrow. #8055's new tests run against the
plan, the re-timing one advancing the clock to the landing and the
preference change written through the media stub.
…hey hold

- dispose() settles a move still waiting to land, and the calls it
  carries, as interrupted, where it left their finished pending for good.
- stop() and a node replacement honour an exit by role's borrowed restore:
  the stylesheet's opacity goes back to the stylesheet instead of staying
  inline.
- releaseIdentity's doc comment sits on releaseIdentity again.
… Reduce Motion teardown fixes) as a dependency
…es only the springs it plays

The springs table was one object, so a component that played one sheet
spring bundled every spring (architecture:component-style-authoring INV6).
Each row is now its own export (SHEET_PRESENT_SPRING, SETTLE_SPRING, ...),
a literal of its own so a bundler can drop the rows nothing imports, and
springs composes them by row name for the existing readers; a row export is
the same object as its row. The registry takes only the springs spec:AST-094
FR4 names, so registering the roles ships no other spring. The table test
reads a row's source from the JSDoc above its export, and checks that rows
measured on one spring stay equal now that they no longer spread a shared
base. A bundle test imports one row, and registers the roles, and finds no
other spring; both fail on the previous code.
Under the plan a detach lands the dip's move at once, handing the
stylesheet's opacity back in the model, but the fade's end value written
inline stayed on the element, so re-attaching it showed opacity 0. The
detach now removes it with the borrow (#8055's late-attach test).
…ng animator

The table test keeps both rules: a row's source sits in the JSDoc above its
export, and a value bound from an exported group is checked where the group
declares it.
…s Reduce Motion teardown fixes, main a17ee0a) into the role-reading animator
…dip's opacity back

Under Reduce Motion on a touch screen, attach(null), attaching another
element or dispose() during a dip over the stylesheet's opacity left that
opacity inline on the outgoing element. All three now end the borrow on the
element it ran on, which replaces the late-attach hand-back (a borrow no
longer outlives its element).
…y pointer is coarse and a recognizer is enabled

From CodeBunny on #8073 (spec:AST-094 FR19), fixed once here: the host
wrote its managed touch-action on every device. It now writes it only
while the primary pointer is coarse and an attached recognizer is
enabled, and restores the element's own value when either stops being
true; with the default coarse-pointer read it follows the media query's
change too. The test observes no touch-action write under a fine pointer,
the write once coarse, its removal while the recognizer is disabled and
again when the pointer turns fine.
…ated press fold into utils/pressFeedback.ts

The touch press controller is the one writer of data-astryx-press:
paintPress and readPressPaint move from gestures/pressPaint.ts into
utils/pressFeedback.ts, and the controller writes through the same function,
so a fade's clock and the attribute never drift apart and a press never
overwrites a held row. gestures/delegatedPress.ts, a second press machine no
component installed, is removed with its tests and its exports from the
internal gestures index, so no surface changes. paintPress drops its
attribute option, which only the delegated press used.
…ttach and dispose)

Conflicts in animator.ts: handBackBorrow joins releaseIdentity's comment
above it, and dispose() hands the borrow back and demotes.
# Conflicts:
#	packages/cli/assets/docs/motion.doc.mjs
#	packages/cli/test/__golden__/no-icon-set/docs-topics.json
#	packages/core/src/motion/roleKinds.generated.ts
…ener; #8379's press convergence) into #8076

The press tracker test's Button helper takes both sides' options: this
branch's cancelTravel and isBraking, and #8073's isDisabled.
…ixes, #8056's Reduce Motion teardown fixes, main a17ee0a) into #8076

The animator changeset takes #8258's wording (momentum is the only curve
decay plays; curves have their own changeset), without "no component uses
it yet", which an aggregated release would make false.
…stants, with no theme surface

Touch motion stays internal (the maintainer's ruling; the base registers
only spec:AST-094 FR4's roles, internally). The table's values (the
selection, lift and tracking springs, the lift's growth, the tap's hang,
the drag's reach and the pressed title's clocks, curves and opacity) are
plain constants: no registerSegmentedControlTable, and the touch module
reads them directly instead of resolving them as roles.
- The component doc loses "a theme can retune the slide and the lift".
- The browser proof that a theme retunes the slide becomes one that the
  measured spring plays whatever a theme sets (the old role variable, a slow
  duration scale); the Reduce Motion DR4 proof slows the theme's durations
  instead of a role variable.
- The table test holds every value to no role, and the springs to their
  measured values under the old role variables.
The look stays the theme's: the lift's size follows its spacing, the shadow
is --shadow-high, and the dim is the item target's pressed-state opacity.
#8073 moved paintPress into utils/pressFeedback.ts and removed the
delegated press, which nothing installed; the README still described
installPressAttribute and usePressAttribute and listed their test. The
rows section now names the document press controller, and paintPress its
module.
# Conflicts:
#	packages/core/src/SegmentedControl/SegmentedControl.doc.mjs
#	packages/core/src/SegmentedControl/SegmentedControl.spec.md
#	packages/core/src/SegmentedControl/__tests__/SegmentedControl.interaction.browser.spec.ts
#	packages/core/src/SegmentedControl/useSegmentedSelectionMotion.ts
#	packages/core/src/motion/uikit/segmented.test.ts
#	packages/core/src/motion/uikit/segmented.ts
…ites its record

- Pinch, rotation, screen-edge pan and swipe import their per-group table
  exports (UIKIT_PINCH, UIKIT_ROTATION, UIKIT_SCREEN_EDGE_PAN, UIKIT_SWIPE)
  instead of the UIKIT_GESTURES union, so a bundle that uses one recognizer
  carries only its group (INV6).
- HoverGestureRecognizer's header names its governing record:
  architecture:interaction-modality (hover only enhances).

The one-edge ScreenEdgePan touch-action stays interim per current FR18;
D13a is pending.
… gesture host uses off iOS

The gesture line's selection lock (`spec:AST-094` FR41, #8270) now refuses
a held finger's selection by cancelling `selectstart` wherever iOS WebKit
does not run, and writes `user-select` only where it does (iOS WebKit fires
no `selectstart` for a long press). The proof read the control's computed
`user-select`, so it failed in Chromium (`auto`) and WebKit (`text`) on the
new line although the lock held.

It now asks whether a selection starting on the held segment is refused:
a cancelable `selectstart` dispatched there is cancelled while the finger is
down and not after the lift, and `user-select` stays as at rest throughout
(no style write, nothing repaints). With the pan's `selectionLock` removed
the proof fails in both engines; iOS WebKit's style path stays covered by
the simulator recordings.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold until an engineering owner approves or rejects the draft SegmentedControl record's FR5–FR10 (the touch slide, the pressed dim, the lift, the label colour follow and drag-to-select), including whether design:user-states DR1/DR3 and spec:AST-094 FR11, which still require the pressed overlay and an unchanged held surface under a finger, are amended. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do if they are an engineering owner. The re-entry dim is fixed at this head; whatever the decision, these still need changes:

  • UISegmentedControl's spring, lift and dim clocks are played as constants (motion/uikit/segmented.ts), although AST-094 FR4 requires them to be motion roles in the generated role table.
  • The pressed state is set outside themeProps() and is missing from the item target's states in SegmentedControl.doc.mjs, against the theming-surface architecture's INV6.
  • Each segment keeps its first pressed-opacity reading until the control unmounts, so after a theme changes the pressed opacity a segment pressed before still dims to the old value; forgetting the reading once its return finishes would fix it.
  • The compare against main is about 29,000 changed lines, with SegmentedControl's touch layer imported statically and its size unmeasured against main, because #8127's stack is in it; landing #8127 first leaves this change at its four files.

[Automated review]

*/
private pressedOpacity(segment: HTMLElement): number {
const kept = this.pressedOpacities.get(segment);
if (this.dimmedTitles.has(segment) && kept !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This returns the kept reading whenever the segment is in dimmedTitles, and dimmedTitles is only cleared by stop(), which runs on unmount. So once a segment has been pressed, a later change to the pressed state's opacity (a theme switch) never reaches it: with the pressed opacity changed to 0.5, a segment pressed for the first time dims to 0.5 while one pressed before still dims to 0.2. Forgetting the reading when the segment's return to 1 finishes would keep the re-entry fix and pick up theme changes; a test that changes the state's opacity between two presses would cover it.

*
* Every leaf carries one or more `@source`; uikit.test.ts enforces it.
* Durations are in ms; curves are cubic-bézier control points
* [x1, y1, x2, y2]. The values are internal constants: `spec:AST-094` FR4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AST-094 FR4 says every spring, duration and curve a touch presentation plays MUST be a motion role, with a component's measured roles joining the generated motion role table (DEC-5); DEC-11 keeps roles internal but does not exempt them. Registering these values as internal component roles (as the navigation and scroll tables do), or an owner amending FR4, would resolve it.

clock: 'fade' | 'lift',
): void {
if (isPressed) {
segment.setAttribute(PRESSED_STATE, 'pressed');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pressed state is reflected here with setAttribute, outside themeProps(), and SegmentedControl.doc.mjs still lists only ['selected', 'disabled'] as the item target's states, while the docs tell theme authors to set the dim on pressed. The theming-surface architecture's INV6 has interaction state reflected as a target capability; declaring pressed in the target's states, and either reflecting it through themeProps() or recording this imperative reflection as an exception, would resolve it.

…e pressed state again

The kept pressed-state reading lasted until unmount, so a theme change to the
pressed opacity never reached a segment pressed before. The reading is now
kept only while the segment's dim is still moving (pressDim's new
shownPressDim reads what it shows); once the return finishes, the next press
reads the state again. A new test retunes the state between two presses and
fails on the old rule.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold until an engineering owner approves or rejects the draft SegmentedControl record's FR5–FR10 (the touch slide, the pressed dim, the lift, the label colour follow and drag-to-select), including whether design:user-states DR1/DR3 and spec:AST-094 FR11, which still require the pressed overlay and an unchanged held surface under a finger, are amended. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do if they are an engineering owner. The theme-change reading is fixed at this head; whatever the decision, these still need changes:

  • UISegmentedControl's spring, lift and dim clocks are played as constants (motion/uikit/segmented.ts), although AST-094 FR4 requires them to be motion roles in the generated role table.
  • The pressed state is set outside themeProps() and is missing from the item target's states in SegmentedControl.doc.mjs, against the theming-surface architecture's INV6.
  • pressedOpacity re-reads computed style as soon as the animator's model reports 1, but the return's inline opacity: 1 and its held animation stay until the animator's retire timer runs, so a press in that gap dims to 1 instead of 0.2.
  • The compare against main is about 29,000 changed lines, with SegmentedControl's touch layer imported statically and its size unreported against main, because #8127's stack is in it; landing #8127 first leaves this change at its four files.

[Automated review]

*/
private pressedOpacity(segment: HTMLElement): number {
const kept = this.pressedOpacities.get(segment);
if (kept !== undefined && shownPressDim(segment) < 1) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shownPressDim is the animator's model value, which reads exactly 1 as soon as the return curve's time has passed, but the return's inline opacity: 1 (animator.ts rebuild) and its fill: 'both' hold stay until the retire setTimeout runs. A press in that gap re-reads getComputedStyle(segment).opacity and gets 1 instead of the pressed state's 0.2, so that press shows no dim and keeps 1. Treating the dim as at rest only once the animator has released the opacity (no inline value, nothing running on the channel), and a test that presses between the curve's end and the retire timer, would cover it.

vjeux and others added 4 commits October 26, 2026 13:55
…face before writing it

paintLayer copied the selected segment's computed surface onto the
radiogroup's layer variables one property at a time: read a value from
the segment's computed style, write it to the track, read the next. A
write to the track's style makes the next read from a computed style
recompute it, so every paint restyled the control once per surface
property. A Chromium trace of an Android lift (Reduce Motion, where a
change paints both layers) put about 60 ms of the touchend handler there:
63 forced restyles of 11 elements, `getPropertyValue` taking most of its
samples.

It now reads every value first, then writes them. A unit proof records the
style traffic of a change (each computed declaration taken, each value read
from one, each write to the track) and asks that no read follow a write
made after its declaration was taken: 50 such reads before this change,
none after.
…or has let go of the segment

A return's curve can end before the animator retires its inline opacity and
fill hold; a press in that gap re-read getComputedStyle and got 1. pressDim's
isPressDimReleased (replacing shownPressDim) now says the dim has let go only
when the animator shows 1 and holds no inline opacity and no running opacity
animation on the element; until then the segment keeps its reading. A new
test presses in that gap and fails on the old check.
# Conflicts:
#	packages/core/src/SegmentedControl/useSegmentedSelectionMotion.test.ts
…hey write them

The style-traffic proof #8094 adds for a change also runs through the
touch path, the one a Chromium trace of an Android lift measured: a press,
a drag across two segments and the lift's commit, with and without Reduce
Motion. No read from a computed style follows a write to the track made
after it was taken; on the interleaved paint this path made 50 such reads.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold until an engineering owner approves or rejects the draft SegmentedControl record's FR5–FR10 (the touch slide, the pressed dim, the lift, the label colour follow and drag-to-select), including whether design:user-states DR1/DR3 and spec:AST-094 FR11, which still require the pressed overlay and an unchanged held surface under a finger, are amended. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do if they are an engineering owner. The dim after a return now waits for the animator to let go, which fixes the earlier no-dim press; whatever the decision, these still need changes:

  • UISegmentedControl's spring, lift and dim clocks are played as constants (motion/uikit/segmented.ts), although AST-094 FR4 requires motion roles in the generated role table.
  • The pressed state is set outside themeProps() and is missing from the item target's states in SegmentedControl.doc.mjs, against the theming-surface architecture's INV6.
  • A screen-edge pan given two horizontal edges sets touch-action: pan-y pinch-zoom on the whole element, which AST-094 FR18 (as amended by DEC-13) rules out outside the edge bands.
  • The size report against main needs SegmentedControl's CSS delta and a justification for the statically imported touch cost, and the compare is about 29,000 changed lines because #8127's stack is in it; landing #8127 first leaves this change at its four files.

[Automated review]

Comment on lines +34 to +35
* [x1, y1, x2, y2]. The values are internal constants: `spec:AST-094` FR4
* names no segmented-control motion role, so no theme retunes them. The lens

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AST-094 FR4 makes every spring, duration and curve a touch presentation plays a motion role, with a component's measured roles joining the generated motion role table (DEC-5), and says a component "MUST NOT play a literal". DEC-11 keeps the roles internal; it does not exempt these values from being roles. Registering selectionSpring, liftSpring and the pressedTitle clocks there (still internal) and reading them through the table would conform, as the navigation and scroll domains already do.

clock: 'fade' | 'lift',
): void {
if (isPressed) {
segment.setAttribute(PRESSED_STATE, 'pressed');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pressed state is reflected here with setAttribute, outside themeProps(), and SegmentedControl.doc.mjs still declares the item target's states as ['selected', 'disabled'], while its description tells theme authors to set the dim on the pressed state. The component-theming-surface architecture's INV6 asks for interaction state to be reflected as a target capability through themeProps; declaring pressed in the target's states, and reflecting it there or recording the exception, would close that.

Comment on lines +63 to +79
override get nativeTouchBehavior(): NativeTouchBehavior {
if (this.edges.length === 1) {
return ALL_NATIVE_TOUCH;
}
const horizontal = this.edges.some(
edge => edge !== 'top' && edge !== 'bottom',
);
const vertical = this.edges.some(
edge => edge === 'top' || edge === 'bottom',
);
return {
panX: !horizontal,
panY: !vertical,
pinchZoom: true,
doubleTapZoom: false,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With two horizontal edges this reports panX: false, so the host writes touch-action: pan-y pinch-zoom on the whole element and the author's sideways scrolling stops everywhere, not only in the 40.2 px bands. AST-094 FR18, as amended on main by DEC-13, says a screen-edge pan claims pan-x only for a touch that lands in the edge band and leaves the author's scrolling untouched outside it. Keeping every native behaviour for several edges too, and claiming per touch inside each band, would match the one-edge path.

cixzhang and others added 2 commits October 26, 2026 14:56
… through internal segmented motion roles

spec:AST-094 FR4 has every spring, duration and curve a touch presentation
plays be a motion role; SegmentedControl played its table's values as
constants. registerSegmentedTable registers selectionSpring, liftSpring,
tapLiftHangMs and the pressed title's clocks and curves as internal roles
(DEC-11: no theme surface), FR4's role table names them with their measured
values, and the motion registers the table and reads the durations and
curves through resolveMotion. A new test checks each role.

Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>
The press dim reads the pressed title's clocks and curves through their
segmented roles (resolveMotion), as #8127's fadeTitle now does; fadeTitle
itself stays removed (the dim is pressDim's).
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold until an engineering owner approves or rejects the draft SegmentedControl record's FR5–FR10 (the touch slide, the pressed dim, the lift, the label colour follow and drag-to-select) and the seven segmented.* motion roles this head adds to spec:AST-094 FR4, including whether design:user-states DR1/DR3 and AST-094 FR11, which still require the pressed overlay and an unchanged held surface under a finger, are amended. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do if they are an engineering owner. Whatever the decision, these still need changes:

  • The pressed state is set outside themeProps() and is missing from the item target's states in SegmentedControl.doc.mjs, against the theming-surface architecture's INV6.
  • A screen-edge pan given two horizontal edges sets touch-action: pan-y pinch-zoom on the whole element, which AST-094 FR18 (as amended by DEC-13) rules out outside the edge bands.
  • The stylesheet growth the size report measures (+1,152 B gzip against main), almost all of it touch-only CSS, is over the style-authoring architecture's 1 KB flag (INV6) and needs a justification.
  • The compare is about 29,000 changed lines because #8127's stack is in it; landing #8127 first leaves this change at its four files.

[Automated review]

Comment on lines +138 to +144
| `segmented.selectionSpring` | response 0.4 s, damping ratio 0.85 | measured: UISegmentedControl's selection slide, iOS 26.2 simulator, 2026-10-23 |
| `segmented.liftSpring` | response 0.25 s, damping ratio 1 | measured: UISegmentedControl's lens lift, iOS 26.2 simulator, 2026-10-23 |
| `segmented.tapLiftHangMs` | 230 ms | measured: UISegmentedControl's lens lift held through a tap's slide, iOS 26.2 simulator, 2026-10-23 |
| `segmented.pressedTitle.fadeMs` | 470 ms | measured: UISegmentedControl's pressed title dimming, iOS 26.2 simulator, 2026-10-24 |
| `segmented.pressedTitle.fadeCurve` | cubic-bezier(0.25, 0.1, 0.25, 1) | measured: UISegmentedControl's pressed title dimming, iOS 26.2 simulator, 2026-10-24 |
| `segmented.pressedTitle.liftMs` | 180 ms | measured: UISegmentedControl's pressed title returning at the lift, iOS 26.2 simulator, 2026-10-24 |
| `segmented.pressedTitle.liftCurve` | cubic-bezier(0.25, 0.1, 0.25, 1) | measured: UISegmentedControl's pressed title returning at the lift, iOS 26.2 simulator, 2026-10-24 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These seven segmented.* rows add roles to a current record's FR4 table, so they need an engineering owner's decision (/approve-spec <full head SHA>), together with the draft SegmentedControl FR5–FR10 they time. FR4 already lets a component's own record add measured roles through the generated motion role table (DEC-5), which registerSegmentedTable does in code.

clock: 'fade' | 'lift',
): void {
if (isPressed) {
segment.setAttribute(PRESSED_STATE, 'pressed');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pressed state is reflected here with setAttribute, outside themeProps(), and SegmentedControl.doc.mjs:89 still declares the item target's states as ['selected', 'disabled'], while its description tells theme authors to set the dim on the pressed state. The component-theming-surface architecture's INV6 asks for interaction state to be reflected as a target capability through themeProps; declaring pressed in the target's states, and reflecting it there or recording the exception, would close that.

edge => edge === 'top' || edge === 'bottom',
);
return {
panX: !horizontal,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With two horizontal edges this reports panX: false, so the host writes touch-action: pan-y pinch-zoom on the whole element and sideways scrolling stops everywhere, not only in the 40.2 px bands. AST-094 FR18, as amended on main by DEC-13, says a screen-edge pan claims pan-x only for a touch that lands in the edge band and leaves the author's scrolling untouched outside it; keeping every native behaviour for several edges too, and claiming per touch inside each band, would match the one-edge path.

cixzhang and others added 4 commits October 26, 2026 15:43
…passes the late-click guard; the pressed dim is internal, not a theming state

- The late-click guard lets a click with detail 0 through (a screen
  reader's or switch access's activation), as AST-094 FR21 and the shared
  tracker's swallower do; isLateClick holds the rule, with tests.
- The pressed dim stays UIKit's internal 0.2: the item comment and the usage
  doc no longer tell themes to retune it on a pressed state the theming
  catalog does not declare (component-theming-surface INV6, INV7).
- segmented.test.ts asserts the registered segmented roles, and the motion
  header says the values play through those internal roles.

Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>
…etune the internal dim

Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request is on hold until an engineering owner approves or rejects the draft SegmentedControl record's FR5–FR10 (the touch slide, the pressed dim, the lift, the label colour follow and drag-to-select) and the seven segmented.* motion roles this head adds to spec:AST-094 FR4, including whether design:user-states DR1/DR3 and AST-094 FR11, which still require the pressed overlay and an unchanged held surface under a finger, are amended. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do if they are an engineering owner. Whatever the decision, these still need changes:

  • The press dim is now described as internal, but it is still written as a data-pressed state that a theme's pressed key reaches, outside themeProps() and undeclared, while FR7 and the changeset still promise a themed 0.2 default; one contract has to win (theming-surface architecture INV6).
  • A screen-edge pan given two horizontal edges sets touch-action: pan-y pinch-zoom on the whole element, which AST-094 FR18 (DEC-13) rules out outside the edge bands.
  • The touch-only stylesheet growth against main (+1,152 B gzip by the description's figures) needs a justification under the style-authoring architecture's INV6; #8127's description covers only +56 B.
  • The compare is about 29,500 changed lines because #8127's stack is in it; landing #8127 first leaves this change at its four files.

[Automated review]

clock: 'fade' | 'lift',
): void {
if (isPressed) {
segment.setAttribute(PRESSED_STATE, 'pressed');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The dim is written here as data-pressed="pressed", outside themeProps(). The item's style now calls it internal, but a theme's pressed key still compiles to exactly [data-pressed="pressed"] (the theme compiler emits a rule for every state key), and pressedOpacityOf reads the result back from computed style, so a theme can still retune the dim while the target's states don't declare it. If it is internal, playing the dim to segmentedControl.pressedTitle.opacity with an attribute no theme key compiles to would make it so; if it is theme-facing, declaring pressed and reflecting it through themeProps() would match the theming-surface architecture's INV6.

| FR4 | Selected and disabled remain reflected states on `segmented-control-item`, not standalone anatomy or targets. | Current source, docs, and tests | Verified current behavior; no target change |
| FR5 | Under a finger (`spec:AST-094` FR13), 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; under a mouse the theme's colour transition applies. | `spec:AST-094` FR13; current source and tests | Proposed; pending owner review |
| FR6 | The slide runs on UISegmentedControl's measured spring (response 0.40 s, damping ratio 0.85), an internal constant no theme retunes; under Reduced Motion the old surface fades out in place as the new one fades in (`design:reduced-motion`). | `spec:AST-094` FR4, DEC-11; UISegmentedControl, iOS 26.2 simulator | Proposed; pending owner review |
| FR7 | Under a finger, `segmented-control-item` reflects a `pressed` state on the unselected segment a finger holds; its `opacity` is that segment's dim (0.2 by default), faded on the compositor. | Owner direction, 2026-10-25 | Proposed; pending owner review |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FR7 (with the Intent above and the changeset) still says segmented-control-item reflects a pressed state whose opacity is the dim, "0.2 by default", while SegmentedControlItem.tsx now calls the dim "internal, not a theming state". Rewording FR7 to the contract the code keeps would let an owner approve what actually ships.

Comment on lines +64 to +79
if (this.edges.length === 1) {
return ALL_NATIVE_TOUCH;
}
const horizontal = this.edges.some(
edge => edge !== 'top' && edge !== 'bottom',
);
const vertical = this.edges.some(
edge => edge === 'top' || edge === 'bottom',
);
return {
panX: !horizontal,
panY: !vertical,
pinchZoom: true,
doubleTapZoom: false,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With two horizontal edges this reports panX: false, so the host writes touch-action: pan-y pinch-zoom on the whole element and sideways scrolling stops everywhere, not only in the 40.2 px bands. AST-094 FR18 on main (DEC-13) has a screen-edge pan claim pan-x only for a touch that lands in the edge band and leave the author's scrolling untouched outside it; keeping every native touch behaviour here and claiming per touch inside each band, as the one-edge path does, would match it (and the test at ScreenEdgePanGestureRecognizer.test.ts:61-68 would change with it).

@cixzhang cixzhang closed this Oct 26, 2026
@cixzhang
cixzhang deleted the refactor/press-dim branch October 26, 2026 23:40

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants