feat: add ConnectedButtonGroup component - #5028
Conversation
…group Introduces ConnectedButtonGroup, the MD3 successor to SegmentedButtons. Selected buttons morph to a fully-rounded shape, the connected inner corners expand on press, and the group supports single- and multi-select across the extra-small to extra-large size scale. Component-specific tokens are extracted into tokens.ts; size, shape and color resolution live in utils.ts. SegmentedButtons is marked as deprecated in favour of the new component.
…ty overlay Pre-blending the disabled container color with color().alpha() breaks on Android dynamic themes where onSurface is a PlatformColor object rather than a string — the fallback rendered a fully opaque near-black container. Apply the MD3 12% disabled opacity as a style on an absolute-fill overlay instead, which works with any ColorValue. Found during on-device Android verification.
- tokens: pressed inner corner now morphs sharper than rest (M3 ConnectedButtonGroup*Tokens: Inner=Small, PressedInner=ExtraSmall), and large/extra-large rest inner corners corrected to large(16)/largeIncreased(20). - press morph: selected buttons also morph on press (pressed precedence over checked, matching Compose ToggleButton); pressOut returns to the rest radius. - accessibility: buttons now use the radio (single-select) / checkbox (multi-select) role instead of button+aria-checked, valid on web and better announced natively. - reduce-motion: shape morph honours the reduce-motion setting (ReduceMotion), matching Switch/FAB. - docs: register ConnectedButtonGroup in component-docs.config so the deprecation link and generated page resolve. - tests: cover shape tokens and role semantics (22 total).
- resolve corner shapes through the shared ShapeToken/resolveCornerRadius from theme/utils/shape instead of a local re-implementation. - drop the per-button rippleColor prop and runtime ripple color derivation; TouchableRipple's stateLayerPressed default applies, consistent with the removal of customRippleColor across components. - expose background and hitSlop passthroughs per button, mirroring SegmentedButtons, and remove the ineffective automatic touch-target expansion. - drive the corner morph with the theme's fast spatial spring (toRawSpring), matching Switch and FAB, instead of timing curves. - add radiogroup semantics to the single-select row and collision-proof list keys. - honour intrinsic button widths in content-sized parents (flexBasis auto). - export ConnectedButtonGroupSize, register a docs screenshot, keep the docs config alphabetical, and extend tests with render snapshots (23).
The docs build requires a committed page under 5.x/docs/components for every entry in component-docs.config; add the ConnectedButtonGroup page (usage, props, theme colors) mirroring SegmentedButtons, regenerate the 6.x content, and point the SegmentedButtons deprecation notice at the correct relative path so the link resolves.
| /** | ||
| * @deprecated Segmented buttons are deprecated in the Material Design 3 spec | ||
| * and replaced by the Connected Button Group. Use | ||
| * [`ConnectedButtonGroup`](../ConnectedButtonGroup/ConnectedButtonGroup) instead — it exposes the |
There was a problem hiding this comment.
- Relative markdown link ../ConnectedButtonGroup/... won’t resolve in IDE hovers — use bare
ConnectedButtonGroup. - API isn’t fully the same: density has no equivalent; please mention migrate to size (default "small" ≈ 40dp).
| }, [animateStart, animateEnd, outerRadius]); | ||
|
|
||
| const showCheck = checked && showSelectedCheck; | ||
| const showIcon = Boolean(icon) && !showCheck; |
There was a problem hiding this comment.
SegmentedButtonItem scales check in / icon out (and keeps icon for icon-only). Here the check replaces the icon with no transition.
Intentional? If yes, document; if not, restore swap behavior for migration parity.
| count: number | ||
| ): ConnectedButtonPosition => { | ||
| if (count <= 1) { | ||
| return 'single'; |
There was a problem hiding this comment.
what do you think to make a const with all this strings and apply them here and in other files - like
Position.single, Position.first and etc
| const showIcon = Boolean(icon) && !showCheck; | ||
| const iconGap = label ? { marginEnd: sizeStyle.iconLabelGap } : null; | ||
|
|
||
| const getTestID = (suffix: string) => |
There was a problem hiding this comment.
this one could be moves out of component - it's just a static function - just pass testID as well
const getTestID = (testID, suffix: string) => ...
| import type { IconSource } from '../Icon'; | ||
| import type { Props as TouchableRippleProps } from '../TouchableRipple/TouchableRipple'; | ||
|
|
||
| type ConditionalValue<T extends string = string> = |
There was a problem hiding this comment.
in case your component already has folder and you use tokens.ts and utils.ts - maybe it makes sense to create types.ts file and store there all your types - it would make components with less code and much more readable. Up to you
Motivation
The Material Design 3 spec deprecates segmented buttons in favor of the connected button group. This PR adds a
ConnectedButtonGroupcomponent implementing that spec as part of the ongoing modernization effort:buttons/value/onValueChangeAPI shape asSegmentedButtons, easing migration).extra-small→extra-large: 32/40/56/96/136dp) with per-size component tokens extracted intotokens.ts.secondaryContainer/onSecondaryContainer, unselectedsurfaceContainer/onSurfaceVariant, disabledonSurfaceat 12% container / 38% content opacity (applied as styles soPlatformColorthemes work).radio(single-select, inside aradiogrouprow) orcheckbox(multi-select) role with checked/disabled state; per-buttonaria-label, optional selected check icon,hitSlop/backgroundpassthroughs.resolveCornerRadius/ShapeToken,toRawSpring,TouchableRipple'sstateLayerPressedripple default).SegmentedButtonsis marked@deprecatedpointing to this component (non-breaking).Related issue
Fixes #4979
Test plan
yarn typecheck,yarn lint— clean.yarn test ConnectedButtonGroup— 23 tests + 4 render snapshots covering selection (single/multi), sizing, MD3 color roles, disabled behavior, selection check, custom colors, accessibility roles, and shape tokens.