Skip to content

feat(segmented): the chosen face slides to the new choice (D-055, Android 0.9.0) - #152

Merged
jlc488 merged 2 commits into
mainfrom
feat/segmented-slide
Nov 13, 2026
Merged

jlc488 merged 2 commits into
mainfrom
feat/segmented-slide

Conversation

@jlc488

@jlc488 jlc488 commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

What

The segmented control's chosen face now slides to the new choice, on the web (React, Solid, Vue) and on Android (D-055). Before this, the old segment's face disappeared and the new one's appeared, with only the colors fading. The owner chose the sliding face.

  • One face per control, not one per segment. Choosing another segment moves it there over motion.duration.base (200ms) with the standard easing, and its width follows. This is the same move as the Android DdsTabs underline. The face looks as in D-054: bg.raised, a 1dp border.strong edge, a level-1 shadow.
  • Only a change of choice moves it. On the first layout, and when the same choice only changes place (rotation, font size, window resize), the face is put there without travel. The first frame already shows it on the chosen segment.
  • Reduced motion: 0ms under prefers-reduced-motion on the web; on Android it moves at once when animations are off on the device.
  • Web: one .dds-segmented__indicator inside the track, on the chosen item's box (--left/--top/--width/--height). One small DOM function, attachSegmentIndicator, does the measuring for React, Solid and Vue. It is the same file in each package, and a contract test keeps the three identical. It slides only when the chosen item changes, follows resizes without a slide, and turns the slide off on transitionend. Without ResizeObserver it does nothing: the indicator stays hidden and the chosen item paints its own face. On a server, in a jsdom test or in an old browser, only the slide is lost. The server and the first client render are the same hidden span, so hydration is unchanged. Once the indicator is placed, :has() clears the item's own face, so there is only ever one.
  • Android: one thumb behind the row, placed and animated in the row's own layout (the DdsTabs pattern). Segments now draw only press, hover and focus. The focus ring lies over the thumb's edge.
  • No API change on either platform.

Other options considered (D-055): Ark's SegmentGroup.Indicator (the first commit here). Zag observes the chosen item with ResizeObserver unconditionally, so CI's Solid hydration check crashed with win.ResizeObserver is not a constructor, and consumers' jsdom tests would too. Zag has no switch for it and measures only at start and on a value change, so a late mount doesn't work either. Also CSS anchor positioning (no script, but Firefox lacks it, the transition when the anchor changes differs between browsers, and several controls on one page need anchor-scope), keeping only the color fade, and 300ms (it would not match the tab underline).

Release

  • Android 0.9.0 (VERSION_NAME, changelog, guide §12, READMEs).
  • Web: a minor changeset for dds-css, dds-react, dds-solid and dds-vue (fixed group → 0.35.0).

Tests

  • tests/browser/segmented-slide.ts, used by the React/Solid component spec and the Vue spec: the indicator sits on the chosen item's box, the chosen item has no face of its own (only one face), the indicator runs a position transition on a change, and it ends on the new item.
  • tests/foundation-contracts.test.mjs: the base duration and standard easing, 0ms under reduced motion, hidden until measured, the :has() handover, and all three frameworks render the indicator.
  • SegmentedOnDeviceTest: on the first frame (clock held) the face is already on the chosen segment and 44dp tall; 100ms into a change it is between the old and new segments, in place and in width; it ends on the new segment; no choice draws no face. The existing pixel, no-shift and screen-reader tests stay.

Verified locally

  • Web: verify:source:stage1-2, verify:source:stage3-4, verify:foundation, verify:foundation:core, verify:release, the release checks for solid (passing without any stub), react, vue, table, svelte and table-vue, the test steps for table, vue, svelte and table-vue, verify:react:browser, the table-react test and browser steps, and the canary dependencies/test/build steps. Package unit tests and type checks pass for dds-react, dds-solid and dds-vue, with new tests for the no-ResizeObserver path in all three and a placement test in React.
  • Not green locally because of machine load, as approved by the owner, so GitHub CI is the judge: date/time-picker 5s timeouts in verify:react:test and verify:solid:test (main fails the same way under this load, and those files pass 64/64 on their own); npm install timing out in one verify:react:release run (it passed in an earlier run); verify:foundation:layouts and verify:canary:preview (a page or server didn't appear within 5s; neither involves this control). The final Vue browser run timed out across the whole page (page.goto over 30s at 77% CPU). The earlier Vue run, before the transitionend fix, failed only the slide check, and that failure is what this commit fixes.
  • Android: the full CI command — BUILD SUCCESSFUL; every unit suite 0 failures / 0 errors. On SM-F971N, SegmentedOnDeviceTest 24/24 (the slide tests ran, not skipped). I checked the catalog in light and dark on the phone: the face ends flush on the new segment both ways.

…roid 0.9.0)

Choosing another segment used to swap faces: the old one's went, the new
one's appeared, only the colors fading. The owner chose a sliding face.

One face per control instead of one per segment. It moves to the new choice
over motion.duration.base (200ms) with the standard easing, its width
following, the same move as DdsTabs's underline. The first layout and a new
place for the same choice (rotation, font size, resize) put it there without
travel; reduced motion moves it at once.

Web: one .dds-segmented__indicator in the track (React and Solid render Ark's
SegmentGroup.Indicator; Vue measures it itself). It stays hidden until
measured, and until then the chosen item draws its own face, so server-
rendered first paint and plain HTML lose nothing. Android: one thumb behind
the row; segments draw only press, hover and focus.

Tests: browser tests for React, Solid and Vue (face on the chosen item, one
face, travels, ends on the new item), a source contract, and device tests
(first frame already on the chosen segment, midway between, ends on it).
…t ResizeObserver

CI's Solid hydration check failed with "win.ResizeObserver is not a
constructor": Ark's SegmentGroup.Indicator makes Zag observe the chosen item
unconditionally, through the element's own window. Zag offers no switch for it,
and it measures only at start and on a value change, so mounting the indicator
later would leave it unplaced. A test-only polyfill would have hidden the same
crash in consumers' jsdom tests (the owner asked not to cover it that way).

One small DOM function, attachSegmentIndicator, now does the measuring for
React, Solid and Vue (the same file in each package; a contract test keeps them
identical). Without ResizeObserver it does nothing: the indicator stays hidden
and the chosen item paints its own face, so servers, jsdom tests and old
browsers lose only the slide. The server and the first client render are the
same hidden span, so hydration is unchanged. The fixtures keep their original
stubs, so the release checks now run the guarded path.

The slide is switched off on transitionend rather than on any resize: in Vue a
resize notice arrives right after the click and was cancelling the slide (the
local Vue browser run caught it).
@jlc488
jlc488 merged commit b77822f into main Nov 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant