@boostdev/design-system-react-native 0.2.1 → 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 +225 -151
- package/dist/index.js +225 -151
- package/package.json +1 -1
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +28 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +31 -13
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +25 -8
- package/src/components/interaction/form/FormInput/FormInput.tsx +52 -31
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +24 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +45 -27
- 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
|
@@ -12,6 +12,9 @@ import { useTheme } from '../../../../native/ThemeContext';
|
|
|
12
12
|
import { Label } from '../atoms/Label';
|
|
13
13
|
import { Message } from '../atoms/Message';
|
|
14
14
|
|
|
15
|
+
/** Fixed height for single-line control fields (satisfies 44pt touch target). */
|
|
16
|
+
const INPUT_HEIGHT = 44;
|
|
17
|
+
|
|
15
18
|
type InputType = 'text' | 'email' | 'password' | 'tel' | 'url' | 'number' | 'search';
|
|
16
19
|
|
|
17
20
|
interface FormInputProps {
|
|
@@ -44,16 +47,28 @@ const styles = StyleSheet.create({
|
|
|
44
47
|
container: {
|
|
45
48
|
gap: spacing.xxs,
|
|
46
49
|
},
|
|
50
|
+
/**
|
|
51
|
+
* The wrapper draws the pill border and provides the fixed 44pt touch-target height.
|
|
52
|
+
* justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
|
|
53
|
+
*/
|
|
54
|
+
inputWrapper: {
|
|
55
|
+
height: INPUT_HEIGHT,
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
borderWidth: 1,
|
|
58
|
+
borderRadius: border.radius.full,
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
62
|
+
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
63
|
+
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
64
|
+
*/
|
|
47
65
|
input: {
|
|
48
66
|
fontFamily: font.family.body,
|
|
49
67
|
fontSize: font.size.body,
|
|
50
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
51
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
52
68
|
paddingHorizontal: spacing.s,
|
|
53
|
-
paddingVertical:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
minHeight: 44,
|
|
69
|
+
paddingVertical: 0,
|
|
70
|
+
textAlignVertical: 'center',
|
|
71
|
+
includeFontPadding: false,
|
|
57
72
|
},
|
|
58
73
|
});
|
|
59
74
|
|
|
@@ -84,38 +99,44 @@ export function FormInput({
|
|
|
84
99
|
return (
|
|
85
100
|
<View style={[styles.container, style]}>
|
|
86
101
|
<Label label={label} />
|
|
87
|
-
<
|
|
88
|
-
accessibilityLabel={typeof label === 'string' ? label : name}
|
|
89
|
-
accessibilityState={{ disabled }}
|
|
90
|
-
accessibilityHint={hint}
|
|
91
|
-
value={value}
|
|
92
|
-
defaultValue={defaultValue}
|
|
93
|
-
placeholder={placeholder}
|
|
94
|
-
placeholderTextColor={colors.colorOnBgSubtle}
|
|
95
|
-
keyboardType={keyboardTypeMap[type]}
|
|
96
|
-
secureTextEntry={type === 'password'}
|
|
97
|
-
autoCapitalize={type === 'email' || type === 'url' ? 'none' : 'sentences'}
|
|
98
|
-
autoComplete={type === 'email' ? 'email' : type === 'tel' ? 'tel' : undefined}
|
|
99
|
-
editable={!disabled}
|
|
100
|
-
onChangeText={onChange}
|
|
101
|
-
onFocus={() => {
|
|
102
|
-
setFocused(true);
|
|
103
|
-
onFocus?.();
|
|
104
|
-
}}
|
|
105
|
-
onBlur={() => {
|
|
106
|
-
setFocused(false);
|
|
107
|
-
onBlur?.();
|
|
108
|
-
}}
|
|
102
|
+
<View
|
|
109
103
|
style={[
|
|
110
|
-
styles.
|
|
104
|
+
styles.inputWrapper,
|
|
111
105
|
{
|
|
112
106
|
borderColor,
|
|
113
|
-
color: colors.colorOnBg,
|
|
114
107
|
backgroundColor: colors.colorBg,
|
|
115
108
|
},
|
|
116
109
|
disabled && { opacity: 0.4 },
|
|
117
110
|
]}
|
|
118
|
-
|
|
111
|
+
>
|
|
112
|
+
<TextInput
|
|
113
|
+
accessibilityLabel={typeof label === 'string' ? label : name}
|
|
114
|
+
accessibilityState={{ disabled }}
|
|
115
|
+
accessibilityHint={hint}
|
|
116
|
+
value={value}
|
|
117
|
+
defaultValue={defaultValue}
|
|
118
|
+
placeholder={placeholder}
|
|
119
|
+
placeholderTextColor={colors.colorOnBgSubtle}
|
|
120
|
+
keyboardType={keyboardTypeMap[type]}
|
|
121
|
+
secureTextEntry={type === 'password'}
|
|
122
|
+
autoCapitalize={type === 'email' || type === 'url' ? 'none' : 'sentences'}
|
|
123
|
+
autoComplete={type === 'email' ? 'email' : type === 'tel' ? 'tel' : undefined}
|
|
124
|
+
editable={!disabled}
|
|
125
|
+
onChangeText={onChange}
|
|
126
|
+
onFocus={() => {
|
|
127
|
+
setFocused(true);
|
|
128
|
+
onFocus?.();
|
|
129
|
+
}}
|
|
130
|
+
onBlur={() => {
|
|
131
|
+
setFocused(false);
|
|
132
|
+
onBlur?.();
|
|
133
|
+
}}
|
|
134
|
+
style={[
|
|
135
|
+
styles.input,
|
|
136
|
+
{ color: colors.colorOnBg },
|
|
137
|
+
]}
|
|
138
|
+
/>
|
|
139
|
+
</View>
|
|
119
140
|
<Message type="error" message={error} />
|
|
120
141
|
<Message type="hint" message={hint} />
|
|
121
142
|
</View>
|
|
@@ -60,4 +60,28 @@ describe('NumberInput (native)', () => {
|
|
|
60
60
|
expect(screen.getByRole('button', { name: 'Decrease' })).toBeDisabled();
|
|
61
61
|
expect(screen.getByRole('button', { name: 'Increase' })).toBeDisabled();
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Vertical-centering invariant: the TextInput must have no vertical padding so that
|
|
66
|
+
* iOS centers text automatically within the wrapper height.
|
|
67
|
+
*/
|
|
68
|
+
it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
|
|
69
|
+
renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
|
|
70
|
+
const input = screen.getByDisplayValue('3');
|
|
71
|
+
const style = getComputedStyle(input);
|
|
72
|
+
expect(parseFloat(style.paddingTop)).toBe(0);
|
|
73
|
+
expect(parseFloat(style.paddingBottom)).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Touch-target invariant: the wrapper View around the TextInput must provide
|
|
78
|
+
* the 44pt height (stepper height), replacing minHeight on the TextInput itself.
|
|
79
|
+
*/
|
|
80
|
+
it('has height of 44pt on the TextInput wrapper (touch target)', () => {
|
|
81
|
+
renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
|
|
82
|
+
const input = screen.getByDisplayValue('3');
|
|
83
|
+
const wrapper = input.parentElement as HTMLElement;
|
|
84
|
+
const heightPx = parseFloat(getComputedStyle(wrapper).height);
|
|
85
|
+
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
86
|
+
});
|
|
63
87
|
});
|
|
@@ -60,18 +60,30 @@ const styles = StyleSheet.create({
|
|
|
60
60
|
fontWeight: font.weight.semibold,
|
|
61
61
|
lineHeight: 24,
|
|
62
62
|
},
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The inputWrapper holds the border and fixed height so justifyContent: 'center'
|
|
65
|
+
* vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
|
|
66
|
+
*/
|
|
67
|
+
inputWrapper: {
|
|
64
68
|
flex: 1,
|
|
69
|
+
height: STEPPER_SIZE,
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
borderTopWidth: 1,
|
|
72
|
+
borderBottomWidth: 1,
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
76
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
77
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
78
|
+
*/
|
|
79
|
+
input: {
|
|
65
80
|
fontFamily: font.family.body,
|
|
66
81
|
fontSize: font.size.body,
|
|
67
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
68
|
-
// lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
|
|
69
82
|
textAlign: 'center',
|
|
70
83
|
paddingHorizontal: spacing.xs,
|
|
71
|
-
paddingVertical:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
minHeight: STEPPER_SIZE,
|
|
84
|
+
paddingVertical: 0,
|
|
85
|
+
textAlignVertical: 'center',
|
|
86
|
+
includeFontPadding: false,
|
|
75
87
|
},
|
|
76
88
|
});
|
|
77
89
|
|
|
@@ -140,32 +152,38 @@ export function NumberInput({
|
|
|
140
152
|
</Text>
|
|
141
153
|
</Pressable>
|
|
142
154
|
|
|
143
|
-
<
|
|
144
|
-
accessibilityLabel={label}
|
|
145
|
-
accessibilityState={{ disabled }}
|
|
146
|
-
accessibilityValue={{
|
|
147
|
-
min,
|
|
148
|
-
max,
|
|
149
|
-
now: currentValue,
|
|
150
|
-
text: String(currentValue),
|
|
151
|
-
}}
|
|
152
|
-
keyboardType="numeric"
|
|
153
|
-
value={String(currentValue)}
|
|
154
|
-
editable={!disabled}
|
|
155
|
-
onChangeText={(text) => {
|
|
156
|
-
const parsed = parseFloat(text);
|
|
157
|
-
const safe = isNaN(parsed) ? 0 : parsed;
|
|
158
|
-
update(safe);
|
|
159
|
-
}}
|
|
155
|
+
<View
|
|
160
156
|
style={[
|
|
161
|
-
styles.
|
|
157
|
+
styles.inputWrapper,
|
|
162
158
|
{
|
|
163
159
|
borderColor,
|
|
164
|
-
color: colors.colorOnBg,
|
|
165
160
|
backgroundColor: colors.colorBg,
|
|
166
161
|
},
|
|
167
162
|
]}
|
|
168
|
-
|
|
163
|
+
>
|
|
164
|
+
<TextInput
|
|
165
|
+
accessibilityLabel={label}
|
|
166
|
+
accessibilityState={{ disabled }}
|
|
167
|
+
accessibilityValue={{
|
|
168
|
+
min,
|
|
169
|
+
max,
|
|
170
|
+
now: currentValue,
|
|
171
|
+
text: String(currentValue),
|
|
172
|
+
}}
|
|
173
|
+
keyboardType="numeric"
|
|
174
|
+
value={String(currentValue)}
|
|
175
|
+
editable={!disabled}
|
|
176
|
+
onChangeText={(text) => {
|
|
177
|
+
const parsed = parseFloat(text);
|
|
178
|
+
const safe = isNaN(parsed) ? 0 : parsed;
|
|
179
|
+
update(safe);
|
|
180
|
+
}}
|
|
181
|
+
style={[
|
|
182
|
+
styles.input,
|
|
183
|
+
{ color: colors.colorOnBg },
|
|
184
|
+
]}
|
|
185
|
+
/>
|
|
186
|
+
</View>
|
|
169
187
|
|
|
170
188
|
<Pressable
|
|
171
189
|
accessibilityRole="button"
|
|
@@ -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
|
) : (
|