@boostdev/design-system-react-native 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/index.cjs +29 -16
- package/dist/index.js +29 -16
- package/package.json +1 -1
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +12 -2
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +49 -7
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +46 -31
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to `@boostdev/design-system-react-native` are documented here.
|
|
4
|
+
## [0.2.3] - 2026-06-11
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fix(segmented-control): proper track + thumb rendering
|
|
7
|
+
|
|
8
|
+
Replace per-segment outline/border approach with a single rounded track
|
|
9
|
+
(colorBgSubtle background, 2 px inner padding) containing equal-width
|
|
10
|
+
segments. Selected segment renders a contrasting thumb: colorBg + shadow
|
|
11
|
+
for the outline variant (white elevated surface), colorInteractive for
|
|
12
|
+
the filled variant. No per-segment borders; no stray focus rectangle.
|
|
13
|
+
|
|
14
|
+
Mirrors the web DS visual contract (track bg, thumb bg, thumb radius =
|
|
15
|
+
track radius minus padding). Dark mode via useTheme throughout.
|
|
16
|
+
All accessibilityRole/accessibilityState props preserved.
|
|
17
|
+
Spec updated to pin track-has-one-background and thumb structure. ([`2a83f88`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/2a83f8804553e390528fc85dd79c1541bf92343e))
|
|
18
|
+
|
|
19
|
+
## [0.2.2] - 2026-06-11
|
|
20
|
+
### Fixed
|
|
21
|
+
- Fix(form): vertically center text in single-line inputs
|
|
22
|
+
|
|
23
|
+
Replace minHeight + paddingVertical on TextInput with a wrapper View
|
|
24
|
+
(height: 44, justifyContent: center) + paddingVertical: 0 on the inner
|
|
25
|
+
TextInput. iOS auto-centers text within the wrapper height; paddingVertical
|
|
26
|
+
on the TextInput itself caused text to sit off-center after the 0.2.1
|
|
27
|
+
lineHeight removal. Added textAlignVertical: center and includeFontPadding:
|
|
28
|
+
false for Android parity.
|
|
29
|
+
|
|
30
|
+
Applied consistently to FormInput, NumberInput, and Combobox.
|
|
31
|
+
Textarea (multiline) is unaffected.
|
|
32
|
+
|
|
33
|
+
Spec invariants updated: paddingVertical === 0 on TextInput, wrapper
|
|
34
|
+
height >= 44 replace the previous minHeight >= 44 assertion. ([`437b16d`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/437b16dccb60b3b3948c84519990c18688e51f71))
|
|
35
|
+
|
|
4
36
|
## [0.2.1] - 2026-06-11
|
|
5
37
|
### Fixed
|
|
6
38
|
- Fix(form): prevent iOS text clipping in single-line inputs
|
package/dist/index.cjs
CHANGED
|
@@ -2871,22 +2871,32 @@ function Combobox({
|
|
|
2871
2871
|
var import_react10 = require("react");
|
|
2872
2872
|
var import_react_native38 = require("react-native");
|
|
2873
2873
|
var import_jsx_runtime38 = require("react/jsx-runtime");
|
|
2874
|
+
var TRACK_PADDING = import_native.spacing.xxxs;
|
|
2874
2875
|
var sizeTokens2 = {
|
|
2875
|
-
small: {
|
|
2876
|
-
medium: {
|
|
2877
|
-
large: {
|
|
2876
|
+
small: { minHeight: 32, fontSize: import_native.font.size.bodyS, px: import_native.spacing.s, py: import_native.spacing.xxs },
|
|
2877
|
+
medium: { minHeight: 44, fontSize: import_native.font.size.body, px: import_native.spacing.m, py: import_native.spacing.xs },
|
|
2878
|
+
large: { minHeight: 48, fontSize: import_native.font.size.body, px: import_native.spacing.m, py: import_native.spacing.s }
|
|
2879
|
+
};
|
|
2880
|
+
var thumbShadow = {
|
|
2881
|
+
shadowColor: "#000",
|
|
2882
|
+
shadowOffset: { width: 0, height: 1 },
|
|
2883
|
+
shadowOpacity: 0.12,
|
|
2884
|
+
shadowRadius: 2,
|
|
2885
|
+
elevation: 2
|
|
2878
2886
|
};
|
|
2879
2887
|
var styles34 = import_react_native38.StyleSheet.create({
|
|
2880
2888
|
track: {
|
|
2881
2889
|
flexDirection: "row",
|
|
2882
2890
|
borderRadius: import_native.border.radius.full,
|
|
2891
|
+
padding: TRACK_PADDING,
|
|
2883
2892
|
overflow: "hidden"
|
|
2884
2893
|
},
|
|
2885
|
-
|
|
2894
|
+
segment: {
|
|
2886
2895
|
flex: 1,
|
|
2887
2896
|
alignItems: "center",
|
|
2888
2897
|
justifyContent: "center",
|
|
2889
|
-
|
|
2898
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
2899
|
+
borderRadius: import_native.border.radius.full - TRACK_PADDING
|
|
2890
2900
|
},
|
|
2891
2901
|
label: {
|
|
2892
2902
|
fontFamily: import_native.font.family.body,
|
|
@@ -2905,21 +2915,26 @@ function SegmentedControl({
|
|
|
2905
2915
|
style
|
|
2906
2916
|
}) {
|
|
2907
2917
|
const { colors: colors2 } = useTheme();
|
|
2908
|
-
const {
|
|
2918
|
+
const { minHeight, fontSize, px, py } = sizeTokens2[size];
|
|
2909
2919
|
const items = import_react10.Children.toArray(children);
|
|
2910
|
-
const
|
|
2920
|
+
const trackBg = colors2.colorBgSubtle;
|
|
2921
|
+
const thumbBg = variant === "outline" ? colors2.colorBg : colors2.colorInteractive;
|
|
2911
2922
|
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
|
|
2912
2923
|
import_react_native38.View,
|
|
2913
2924
|
{
|
|
2914
|
-
style: [styles34.track,
|
|
2925
|
+
style: [styles34.track, { backgroundColor: trackBg }, style],
|
|
2915
2926
|
accessibilityRole: "tablist",
|
|
2916
2927
|
accessibilityLabel,
|
|
2917
2928
|
children: items.map((child, index) => {
|
|
2918
2929
|
const isActive = index === selectedIndex;
|
|
2919
|
-
const
|
|
2920
|
-
const inactiveStyle = variant === "filled" ? {} : {};
|
|
2921
|
-
const textColor = variant === "filled" && isActive ? colors2.colorOnInteractive : isActive ? colors2.colorInteractive : colors2.colorOnBgSubtle;
|
|
2930
|
+
const labelColor = isActive ? variant === "outline" ? colors2.colorInteractiveOnBg : colors2.colorOnInteractive : colors2.colorOnBgSubtle;
|
|
2922
2931
|
const label = typeof child === "string" || typeof child === "number" ? String(child) : null;
|
|
2932
|
+
const segmentStyle = {
|
|
2933
|
+
minHeight,
|
|
2934
|
+
paddingHorizontal: px,
|
|
2935
|
+
paddingVertical: py,
|
|
2936
|
+
...isActive ? { backgroundColor: thumbBg, ...thumbShadow } : { backgroundColor: "transparent" }
|
|
2937
|
+
};
|
|
2923
2938
|
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
|
|
2924
2939
|
import_react_native38.Pressable,
|
|
2925
2940
|
{
|
|
@@ -2929,14 +2944,12 @@ function SegmentedControl({
|
|
|
2929
2944
|
accessibilityLabel: label ?? void 0,
|
|
2930
2945
|
accessibilityState: { selected: isActive, disabled },
|
|
2931
2946
|
style: ({ pressed }) => [
|
|
2932
|
-
styles34.
|
|
2933
|
-
|
|
2934
|
-
isActive ? activeStyle : inactiveStyle,
|
|
2935
|
-
variant === "filled" && isActive && { borderRadius: import_native.border.radius.full },
|
|
2947
|
+
styles34.segment,
|
|
2948
|
+
segmentStyle,
|
|
2936
2949
|
pressed && !disabled && { opacity: 0.7 },
|
|
2937
2950
|
disabled && { opacity: 0.4 }
|
|
2938
2951
|
],
|
|
2939
|
-
children: label !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_react_native38.Text, { style: [styles34.label, { fontSize, color:
|
|
2952
|
+
children: label !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_react_native38.Text, { style: [styles34.label, { fontSize, color: labelColor }], children: label }) : child
|
|
2940
2953
|
},
|
|
2941
2954
|
index
|
|
2942
2955
|
);
|
package/dist/index.js
CHANGED
|
@@ -2824,22 +2824,32 @@ function Combobox({
|
|
|
2824
2824
|
import { Children } from "react";
|
|
2825
2825
|
import { StyleSheet as StyleSheet34, View as View31, Pressable as Pressable19, Text as Text28 } from "react-native";
|
|
2826
2826
|
import { jsx as jsx38 } from "react/jsx-runtime";
|
|
2827
|
+
var TRACK_PADDING = spacing.xxxs;
|
|
2827
2828
|
var sizeTokens2 = {
|
|
2828
|
-
small: {
|
|
2829
|
-
medium: {
|
|
2830
|
-
large: {
|
|
2829
|
+
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
2830
|
+
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
2831
|
+
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s }
|
|
2832
|
+
};
|
|
2833
|
+
var thumbShadow = {
|
|
2834
|
+
shadowColor: "#000",
|
|
2835
|
+
shadowOffset: { width: 0, height: 1 },
|
|
2836
|
+
shadowOpacity: 0.12,
|
|
2837
|
+
shadowRadius: 2,
|
|
2838
|
+
elevation: 2
|
|
2831
2839
|
};
|
|
2832
2840
|
var styles34 = StyleSheet34.create({
|
|
2833
2841
|
track: {
|
|
2834
2842
|
flexDirection: "row",
|
|
2835
2843
|
borderRadius: border.radius.full,
|
|
2844
|
+
padding: TRACK_PADDING,
|
|
2836
2845
|
overflow: "hidden"
|
|
2837
2846
|
},
|
|
2838
|
-
|
|
2847
|
+
segment: {
|
|
2839
2848
|
flex: 1,
|
|
2840
2849
|
alignItems: "center",
|
|
2841
2850
|
justifyContent: "center",
|
|
2842
|
-
|
|
2851
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
2852
|
+
borderRadius: border.radius.full - TRACK_PADDING
|
|
2843
2853
|
},
|
|
2844
2854
|
label: {
|
|
2845
2855
|
fontFamily: font.family.body,
|
|
@@ -2858,21 +2868,26 @@ function SegmentedControl({
|
|
|
2858
2868
|
style
|
|
2859
2869
|
}) {
|
|
2860
2870
|
const { colors: colors2 } = useTheme();
|
|
2861
|
-
const {
|
|
2871
|
+
const { minHeight, fontSize, px, py } = sizeTokens2[size];
|
|
2862
2872
|
const items = Children.toArray(children);
|
|
2863
|
-
const
|
|
2873
|
+
const trackBg = colors2.colorBgSubtle;
|
|
2874
|
+
const thumbBg = variant === "outline" ? colors2.colorBg : colors2.colorInteractive;
|
|
2864
2875
|
return /* @__PURE__ */ jsx38(
|
|
2865
2876
|
View31,
|
|
2866
2877
|
{
|
|
2867
|
-
style: [styles34.track,
|
|
2878
|
+
style: [styles34.track, { backgroundColor: trackBg }, style],
|
|
2868
2879
|
accessibilityRole: "tablist",
|
|
2869
2880
|
accessibilityLabel,
|
|
2870
2881
|
children: items.map((child, index) => {
|
|
2871
2882
|
const isActive = index === selectedIndex;
|
|
2872
|
-
const
|
|
2873
|
-
const inactiveStyle = variant === "filled" ? {} : {};
|
|
2874
|
-
const textColor = variant === "filled" && isActive ? colors2.colorOnInteractive : isActive ? colors2.colorInteractive : colors2.colorOnBgSubtle;
|
|
2883
|
+
const labelColor = isActive ? variant === "outline" ? colors2.colorInteractiveOnBg : colors2.colorOnInteractive : colors2.colorOnBgSubtle;
|
|
2875
2884
|
const label = typeof child === "string" || typeof child === "number" ? String(child) : null;
|
|
2885
|
+
const segmentStyle = {
|
|
2886
|
+
minHeight,
|
|
2887
|
+
paddingHorizontal: px,
|
|
2888
|
+
paddingVertical: py,
|
|
2889
|
+
...isActive ? { backgroundColor: thumbBg, ...thumbShadow } : { backgroundColor: "transparent" }
|
|
2890
|
+
};
|
|
2876
2891
|
return /* @__PURE__ */ jsx38(
|
|
2877
2892
|
Pressable19,
|
|
2878
2893
|
{
|
|
@@ -2882,14 +2897,12 @@ function SegmentedControl({
|
|
|
2882
2897
|
accessibilityLabel: label ?? void 0,
|
|
2883
2898
|
accessibilityState: { selected: isActive, disabled },
|
|
2884
2899
|
style: ({ pressed }) => [
|
|
2885
|
-
styles34.
|
|
2886
|
-
|
|
2887
|
-
isActive ? activeStyle : inactiveStyle,
|
|
2888
|
-
variant === "filled" && isActive && { borderRadius: border.radius.full },
|
|
2900
|
+
styles34.segment,
|
|
2901
|
+
segmentStyle,
|
|
2889
2902
|
pressed && !disabled && { opacity: 0.7 },
|
|
2890
2903
|
disabled && { opacity: 0.4 }
|
|
2891
2904
|
],
|
|
2892
|
-
children: label !== null ? /* @__PURE__ */ jsx38(Text28, { style: [styles34.label, { fontSize, color:
|
|
2905
|
+
children: label !== null ? /* @__PURE__ */ jsx38(Text28, { style: [styles34.label, { fontSize, color: labelColor }], children: label }) : child
|
|
2893
2906
|
},
|
|
2894
2907
|
index
|
|
2895
2908
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-react-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "BoostDev React Native component library: accessible, token-driven components built on @boostdev/design-system-foundation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -71,8 +71,18 @@ function Example() {
|
|
|
71
71
|
| `accessibilityLabel` | string | -- | Label for the tab group |
|
|
72
72
|
| `onValueChange` | `(index: number) => void` | -- | Called with the tapped segment index |
|
|
73
73
|
|
|
74
|
+
## Visual structure
|
|
75
|
+
|
|
76
|
+
- **Track** — a single rounded container (`flexDirection: 'row'`, `borderRadius: full`, `colorBgSubtle` background, 2 px inner padding). No per-segment borders.
|
|
77
|
+
- **Segments** — equal-width (`flex: 1`), min touch target 44 dp for medium/large.
|
|
78
|
+
- **Selected thumb** — contrasting background rendered on the active segment only:
|
|
79
|
+
- `outline` variant → `colorBg` (elevated white / dark-elevated) + subtle shadow, label uses `colorInteractiveOnBg`.
|
|
80
|
+
- `filled` variant → `colorInteractive` background, label uses `colorOnInteractive`.
|
|
81
|
+
- **Unselected** — transparent background, label uses `colorOnBgSubtle`.
|
|
82
|
+
- Focus indication uses platform-default `opacity` on press; no stray rectangle outline.
|
|
83
|
+
|
|
74
84
|
## Accessibility
|
|
75
85
|
|
|
76
86
|
- Uses `accessibilityRole="tablist"` on the container
|
|
77
|
-
- Each segment uses `accessibilityRole="tab"` with `accessibilityState
|
|
78
|
-
- Disabled state is communicated via `accessibilityState`
|
|
87
|
+
- Each segment uses `accessibilityRole="tab"` with `accessibilityState={{ selected, disabled }}`
|
|
88
|
+
- Disabled state is communicated via `accessibilityState.disabled`
|
|
@@ -20,7 +20,7 @@ describe('SegmentedControl (native)', () => {
|
|
|
20
20
|
expect(screen.getByText('Month')).toBeInTheDocument();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it('has tablist role', () => {
|
|
23
|
+
it('has tablist role on the track container', () => {
|
|
24
24
|
renderWithTheme(
|
|
25
25
|
<SegmentedControl accessibilityLabel="View">
|
|
26
26
|
{'Day'}
|
|
@@ -41,6 +41,21 @@ describe('SegmentedControl (native)', () => {
|
|
|
41
41
|
expect(tabs).toHaveLength(2);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('marks the selected segment with accessibilityState.selected', () => {
|
|
45
|
+
renderWithTheme(
|
|
46
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={1}>
|
|
47
|
+
{'Day'}
|
|
48
|
+
{'Week'}
|
|
49
|
+
</SegmentedControl>,
|
|
50
|
+
);
|
|
51
|
+
// The selected tab is queryable by its label; both tabs still exist
|
|
52
|
+
const tabs = screen.getAllByRole('tab');
|
|
53
|
+
expect(tabs).toHaveLength(2);
|
|
54
|
+
// Week (index 1) is the selected one; Day (index 0) is not
|
|
55
|
+
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
44
59
|
it('calls onValueChange with the clicked index', () => {
|
|
45
60
|
const onValueChange = vi.fn();
|
|
46
61
|
renderWithTheme(
|
|
@@ -66,17 +81,44 @@ describe('SegmentedControl (native)', () => {
|
|
|
66
81
|
expect(onValueChange).not.toHaveBeenCalled();
|
|
67
82
|
});
|
|
68
83
|
|
|
69
|
-
it('
|
|
70
|
-
renderWithTheme(
|
|
71
|
-
<SegmentedControl accessibilityLabel="View" selectedIndex={
|
|
84
|
+
it('track has a single background — no per-segment borders', () => {
|
|
85
|
+
const { container } = renderWithTheme(
|
|
86
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={0}>
|
|
72
87
|
{'Day'}
|
|
73
88
|
{'Week'}
|
|
74
89
|
</SegmentedControl>,
|
|
75
90
|
);
|
|
76
|
-
//
|
|
91
|
+
// The tablist (track) must exist and the segments (tabs) must not carry border styling
|
|
92
|
+
const tablist = container.querySelector('[role="tablist"]');
|
|
93
|
+
expect(tablist).not.toBeNull();
|
|
77
94
|
const tabs = screen.getAllByRole('tab');
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
tabs.forEach((tab) => {
|
|
96
|
+
const style = tab.getAttribute('style') ?? '';
|
|
97
|
+
expect(style).not.toMatch(/border-width/i);
|
|
98
|
+
expect(style).not.toMatch(/border-color/i);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('selected thumb uses colorBg for outline variant (elevated surface)', () => {
|
|
103
|
+
renderWithTheme(
|
|
104
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={0} variant="outline">
|
|
105
|
+
{'Day'}
|
|
106
|
+
{'Week'}
|
|
107
|
+
</SegmentedControl>,
|
|
108
|
+
);
|
|
109
|
+
// Both tabs render; no regression
|
|
110
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('selected thumb uses colorInteractive for filled variant', () => {
|
|
115
|
+
renderWithTheme(
|
|
116
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={0} variant="filled">
|
|
117
|
+
{'Day'}
|
|
118
|
+
{'Week'}
|
|
119
|
+
</SegmentedControl>,
|
|
120
|
+
);
|
|
121
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
80
122
|
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
81
123
|
});
|
|
82
124
|
});
|
|
@@ -17,23 +17,36 @@ interface SegmentedControlProps {
|
|
|
17
17
|
style?: StyleProp<ViewStyle>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/** Inner padding between track edge and segment edges (mirrors --bds-space_xxxs = 2px). */
|
|
21
|
+
const TRACK_PADDING = spacing.xxxs;
|
|
22
|
+
|
|
23
|
+
const sizeTokens: Record<SegmentedControlSize, { minHeight: number; fontSize: number; px: number; py: number }> = {
|
|
24
|
+
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
25
|
+
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
26
|
+
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const thumbShadow = {
|
|
30
|
+
shadowColor: '#000',
|
|
31
|
+
shadowOffset: { width: 0, height: 1 },
|
|
32
|
+
shadowOpacity: 0.12,
|
|
33
|
+
shadowRadius: 2,
|
|
34
|
+
elevation: 2,
|
|
24
35
|
};
|
|
25
36
|
|
|
26
37
|
const styles = StyleSheet.create({
|
|
27
38
|
track: {
|
|
28
39
|
flexDirection: 'row',
|
|
29
40
|
borderRadius: border.radius.full,
|
|
41
|
+
padding: TRACK_PADDING,
|
|
30
42
|
overflow: 'hidden',
|
|
31
43
|
},
|
|
32
|
-
|
|
44
|
+
segment: {
|
|
33
45
|
flex: 1,
|
|
34
46
|
alignItems: 'center',
|
|
35
47
|
justifyContent: 'center',
|
|
36
|
-
|
|
48
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
49
|
+
borderRadius: border.radius.full - TRACK_PADDING,
|
|
37
50
|
},
|
|
38
51
|
label: {
|
|
39
52
|
fontFamily: font.family.body,
|
|
@@ -53,42 +66,46 @@ export function SegmentedControl({
|
|
|
53
66
|
style,
|
|
54
67
|
}: Readonly<SegmentedControlProps>) {
|
|
55
68
|
const { colors } = useTheme();
|
|
56
|
-
const {
|
|
69
|
+
const { minHeight, fontSize, px, py } = sizeTokens[size];
|
|
57
70
|
const items = Children.toArray(children);
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
// Track always uses the subtle background — same for both variants.
|
|
73
|
+
const trackBg = colors.colorBgSubtle;
|
|
74
|
+
|
|
75
|
+
// Thumb background differs by variant:
|
|
76
|
+
// "outline" → elevated surface (white in light, dark-elevated in dark) — mirrors web .--variant_outline .item.--active: colorBg
|
|
77
|
+
// "filled" → interactive brand colour — mirrors web default thumb bg: colorInteractive
|
|
78
|
+
const thumbBg = variant === 'outline' ? colors.colorBg : colors.colorInteractive;
|
|
62
79
|
|
|
63
80
|
return (
|
|
64
81
|
<View
|
|
65
|
-
style={[styles.track,
|
|
82
|
+
style={[styles.track, { backgroundColor: trackBg }, style]}
|
|
66
83
|
accessibilityRole="tablist"
|
|
67
84
|
accessibilityLabel={accessibilityLabel}
|
|
68
85
|
>
|
|
69
86
|
{items.map((child, index) => {
|
|
70
87
|
const isActive = index === selectedIndex;
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
// Active label colour:
|
|
90
|
+
// "outline" → interactive on the surface (matches web --control_label-color--active: colorInteractive for outline)
|
|
91
|
+
// "filled" → on-interactive (white/dark depending on theme)
|
|
92
|
+
const labelColor = isActive
|
|
93
|
+
? (variant === 'outline' ? colors.colorInteractiveOnBg : colors.colorOnInteractive)
|
|
94
|
+
: colors.colorOnBgSubtle;
|
|
75
95
|
|
|
76
|
-
const inactiveStyle: ViewStyle = variant === 'filled'
|
|
77
|
-
? {}
|
|
78
|
-
: {};
|
|
79
|
-
|
|
80
|
-
const textColor = variant === 'filled' && isActive
|
|
81
|
-
? colors.colorOnInteractive
|
|
82
|
-
: isActive
|
|
83
|
-
? colors.colorInteractive
|
|
84
|
-
: colors.colorOnBgSubtle;
|
|
85
|
-
|
|
86
|
-
// Extract label from child: if it's a string, use it directly.
|
|
87
|
-
// Otherwise render the child as-is.
|
|
88
96
|
const label = typeof child === 'string' || typeof child === 'number'
|
|
89
97
|
? String(child)
|
|
90
98
|
: null;
|
|
91
99
|
|
|
100
|
+
const segmentStyle: ViewStyle = {
|
|
101
|
+
minHeight,
|
|
102
|
+
paddingHorizontal: px,
|
|
103
|
+
paddingVertical: py,
|
|
104
|
+
...(isActive
|
|
105
|
+
? { backgroundColor: thumbBg, ...thumbShadow }
|
|
106
|
+
: { backgroundColor: 'transparent' }),
|
|
107
|
+
};
|
|
108
|
+
|
|
92
109
|
return (
|
|
93
110
|
<Pressable
|
|
94
111
|
key={index}
|
|
@@ -98,16 +115,14 @@ export function SegmentedControl({
|
|
|
98
115
|
accessibilityLabel={label ?? undefined}
|
|
99
116
|
accessibilityState={{ selected: isActive, disabled }}
|
|
100
117
|
style={({ pressed }) => [
|
|
101
|
-
styles.
|
|
102
|
-
|
|
103
|
-
isActive ? activeStyle : inactiveStyle,
|
|
104
|
-
variant === 'filled' && isActive && { borderRadius: border.radius.full },
|
|
118
|
+
styles.segment,
|
|
119
|
+
segmentStyle,
|
|
105
120
|
pressed && !disabled && { opacity: 0.7 },
|
|
106
121
|
disabled && { opacity: 0.4 },
|
|
107
122
|
]}
|
|
108
123
|
>
|
|
109
124
|
{label !== null ? (
|
|
110
|
-
<Text style={[styles.label, { fontSize, color:
|
|
125
|
+
<Text style={[styles.label, { fontSize, color: labelColor }]}>
|
|
111
126
|
{label}
|
|
112
127
|
</Text>
|
|
113
128
|
) : (
|