Skip to content

feat(remix): add segmented control component - #108

Merged
tilucasoli merged 11 commits into
mainfrom
feat/segmented-control
Aug 6, 2026
Merged

feat(remix): add segmented control component#108
tilucasoli merged 11 commits into
mainfrom
feat/segmented-control

Conversation

@leoafarias

@leoafarias leoafarias commented Aug 4, 2026

Copy link
Copy Markdown
Member

Description

Adds RemixSegmentedControl<T extends Object>: a controlled, non-clearable single-select control with equal segment sizing, a persistent track, roving keyboard focus, RTL-aware navigation, and horizontal or vertical orientation.

This is stacked on Remix publish foundation #102.

Component API

  • RemixSegmentedControl<T> accepts items, nullable selectedValue, non-null-emitting onChanged, enabled, orientation, loop, semanticLabel, excludeSemantics, style, and authoritative styleSpec.
  • RemixSegmentedControlItem<T> accepts a stable non-null value, optional label/icon, required accessible name for icon-only items, per-item enabled/focus/autofocus state, and a per-item style.
  • Activating the selected segment never clears it or emits another change.
  • Horizontal keyboard order follows LTR/RTL; vertical controls use Up/Down. Home/End, Space/Enter, disabled-item skipping, wrapping/clamping, and one-tab-stop behavior are covered.
  • Equal sizing works in bounded, unbounded, narrow, vertical, RTL, intrinsic-width, and 200% text-scale layouts.

Reuse and implementation

  • Composes Naked's existing NakedToggleGroup and NakedToggleOption primitives for selection, focus, keyboard, pointer, and semantics behavior.
  • Reuses Remix's RemixStyleSpecBuilder, StyleSpecBuilder, RemixBoxWithEffects, StyledText, and StyledIcon.
  • It does not wrap RemixToggleGroup: segmented control has a persistent track, equal-segment geometry, and effects-aware item surfaces that are a different public anatomy.
  • The equal-segment render object saturates finite arithmetic, rejects negative/NaN/infinite spacing, preserves positive child extents when gaps exceed available space, and keeps semantics/pointer bounds inside the track.
  • Minimum and maximum main-axis intrinsics are computed separately from each child's corresponding intrinsic extent, so intrinsic-column parents can shrink and wrap labels without changing natural maximum sizing.

Mix/code-generation surface

  • Both track and item anatomy use @MixableSpec.
  • Track container and item container use generated forwarded BoxStyler APIs.
  • mainAxisSize, track spacing, nested item, label, icon, and item spacing are generated fields.
  • The only handwritten generated-surface override is SegmentedControlItemSpec.lerp, required so Remix box effects interpolate instead of snap-lerping an otherwise unrecognized custom spec type.
  • No handwritten generated-style extension methods were added.
  • Clean generation reproduced all 26 committed artifacts byte-for-byte.

No FortalSegmentedControl or fortalSegmentedControlStyle preset ships in v1. The docs now make explicit styling required, the icon-only example accepts a required SegmentedControlStyler, and the playground records why it hand-rolls its visual recipe. A Radix/Fortal preset remains a deliberate visual-design follow-up rather than an invented default in this correctness PR.

Visual Evidence

Captured and visually verified locally; the PNGs are ignored and are not part of the package diff:

  • .context/screenshots/segmented-control-light.png — 388×344, SHA-256 b396d71c614c392e0930ff3a11d747fc47d77f97a3aab547b55178fb8407725b
  • .context/screenshots/segmented-control-dark.png — 388×344, SHA-256 12f83a6f3aee012738506749c7b4b3ec30e680e23a167c01b59bae8db4c23aa9

Hosted GitHub attachment upload remains pending because the connector and CLI do not expose the issue-attachment endpoint.

Validation

  • fvm flutter test packages/remix/test/components/segmented_control packages/remix/test/components/toggle_group packages/remix/test/public_api_test.dart packages/remix/test/public_api_compatibility_test.dart — 159/159 passed on hosted naked_ui 1.0.0-beta.9.
  • The selected and unselected option contracts now assert isInMutuallyExclusiveGroup: true, matching Naked's segmented-control semantics.
  • fvm dart run melos run generate:check — 26 generated artifacts reproduced byte-for-byte.
  • fvm flutter analyze --fatal-infos — clean.
  • Fortal parity verified the hosted naked_ui 1.0.0-beta.9 resolution while retaining the compatible ^1.0.0-beta.8 package constraint.
  • GitHub test / Run tests for all packages on final head 3cd570db3 — passed.
  • Formatting, conflict-marker scan, and git diff --check — clean.

naked_ui 1.0.0-beta.9 contains the mutually-exclusive ToggleOption semantics fix from conceptadev/naked_ui#84, and this branch's lockfile now resolves that hosted release.

Related Issues


Draft gates

  • Public API, Naked/Remix reuse, Mix code-generation, and adversarial review complete.
  • Equal-layout numeric and intrinsic hardening covered by regressions.
  • Final focused and full repository CI pass on hosted naked_ui 1.0.0-beta.9.
  • Light/dark screenshots captured and visually inspected.
  • Missing Fortal preset documented as an explicit v1 visual-scope deferral.
  • Naked UI refactor(remix): generate Fortal wrappers #84 is merged and released in a compatible prerelease.
  • Mutually-exclusive semantics expectations are true and final CI passes on that dependency.
  • Hosted screenshots are attached to this PR.
  • Manual VoiceOver/TalkBack, web accessibility-tree, keyboard, RTL, narrow-width, and 200% text-scale checks are recorded.

Breaking Change

Does this PR require users of the package to manually update their code?

  • Yes, this is a breaking change.
  • No, this is not a breaking change.

Base automatically changed from chore/publish-foundation to main August 4, 2026 21:08
Brings in the data list component (#103) and the publish-foundation
change (#102).

Both conflicts were additions at the same insertion point in the public
API tests, so both sides are kept: the segmented control cases and the
data list cases now sit side by side. The barrel export, docs nav, and
both playground lists auto-merged, keeping Data List alongside the
alphabetical segmented control placement.
# Conflicts:
#	packages/remix/test/public_api_compatibility_test.dart
#	packages/remix/test/public_api_test.dart
@leoafarias
leoafarias marked this pull request as ready for review August 5, 2026 19:09
leoafarias and others added 5 commits August 5, 2026 17:08
The four cross-axis intrinsic overrides derived each segment's share from
the raw `spacing`, while `_mainAxisLayout` clamps it so gaps can never
starve every segment. When `spacing` exceeded the queried extent the
overrides floored the per-child budget to zero, so an `IntrinsicWidth`
parent sized the track to zero even though real layout gave each segment
a positive share.

Both paths now go through `_effectiveSpacingFor`, which also removes the
duplicated budget expression from all four overrides.

Also adds `segmented_control_style.dart` with `call<T extends Object>()`,
matching the hand-written pattern generic components use — codegen's
`@MixableSpec(target:)` cannot express a generic widget, so ToggleGroup
and Radio carry their own. Without it `RemixSegmentedControl.styleFrom`
could not build the widget in one step, unlike every other component.
Brings in the labeled checkbox contracts (#105) and the compound menu
items (#101).

The only conflict was CHANGELOG.md, where both sides inserted at the top
of `## Unreleased`. Both sides are kept: the segmented control entry now
sits above the checkbox label and menu item entries. Everything else
auto-merged — the playground registry took the segmented control and menu
entries side by side, and the public API compatibility test kept both
sets of cases.
The merge with main dropped the closing `);` of the
`segmentedControl` declaration, causing 9 analyzer errors in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tilucasoli
tilucasoli merged commit 9e40196 into main Aug 6, 2026
2 checks passed
@tilucasoli
tilucasoli deleted the feat/segmented-control branch August 6, 2026 00:03
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.

2 participants