@hero-design/rn 8.104.1-alpha.3 → 8.105.0

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.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/CHANGELOG.md +6 -18
  3. package/assets/fonts/hero-icons-mobile.ttf +0 -0
  4. package/es/index.js +521 -284
  5. package/lib/assets/fonts/hero-icons-mobile.ttf +0 -0
  6. package/lib/index.js +521 -283
  7. package/package.json +1 -1
  8. package/src/components/Icon/HeroIcon/glyphMap.json +1 -1
  9. package/src/components/Icon/IconList.ts +2 -0
  10. package/src/components/SegmentedControl/SegmentedItem.tsx +192 -0
  11. package/src/components/SegmentedControl/StyledSegmentedControl.tsx +62 -0
  12. package/src/components/SegmentedControl/__tests__/SegmentedItem.spec.tsx +162 -0
  13. package/src/components/SegmentedControl/__tests__/__snapshots__/SegmentedItem.spec.tsx.snap +131 -0
  14. package/src/components/SegmentedControl/__tests__/__snapshots__/index.spec.tsx.snap +359 -0
  15. package/src/components/SegmentedControl/__tests__/index.spec.tsx +247 -0
  16. package/src/components/SegmentedControl/index.tsx +61 -0
  17. package/src/components/SegmentedControl/types.ts +46 -0
  18. package/src/index.ts +2 -0
  19. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +53 -0
  20. package/src/theme/components/segmentedControl.ts +60 -0
  21. package/src/theme/getTheme.ts +3 -0
  22. package/src/types.ts +2 -0
  23. package/stats/8.105.0/rn-stats.html +4844 -0
  24. package/types/components/Icon/IconList.d.ts +1 -1
  25. package/types/components/Icon/index.d.ts +1 -1
  26. package/types/components/SegmentedControl/SegmentedItem.d.ts +18 -0
  27. package/types/components/SegmentedControl/StyledSegmentedControl.d.ts +26 -0
  28. package/types/components/SegmentedControl/index.d.ts +31 -0
  29. package/types/components/SegmentedControl/types.d.ts +43 -0
  30. package/types/components/TextInput/index.d.ts +1 -1
  31. package/types/index.d.ts +2 -1
  32. package/types/theme/components/segmentedControl.d.ts +46 -0
  33. package/types/theme/getTheme.d.ts +2 -0
  34. package/types/types.d.ts +2 -1
@@ -0,0 +1,359 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`SegmentedControl should render correctly with default props 1`] = `
4
+ <View
5
+ style={
6
+ {
7
+ "flex": 1,
8
+ }
9
+ }
10
+ >
11
+ <View
12
+ style={
13
+ [
14
+ {},
15
+ [
16
+ {
17
+ "alignItems": "center",
18
+ "alignSelf": "flex-start",
19
+ "backgroundColor": "#f6f6f7",
20
+ "borderRadius": 16,
21
+ "flexDirection": "row",
22
+ "height": 36,
23
+ "justifyContent": "center",
24
+ "paddingHorizontal": 4,
25
+ },
26
+ undefined,
27
+ ],
28
+ ]
29
+ }
30
+ themeSize="medium"
31
+ >
32
+ <View
33
+ accessibilityState={
34
+ {
35
+ "busy": undefined,
36
+ "checked": undefined,
37
+ "disabled": undefined,
38
+ "expanded": undefined,
39
+ "selected": undefined,
40
+ }
41
+ }
42
+ accessibilityValue={
43
+ {
44
+ "max": undefined,
45
+ "min": undefined,
46
+ "now": undefined,
47
+ "text": undefined,
48
+ }
49
+ }
50
+ accessible={true}
51
+ collapsable={false}
52
+ focusable={true}
53
+ onClick={[Function]}
54
+ onResponderGrant={[Function]}
55
+ onResponderMove={[Function]}
56
+ onResponderRelease={[Function]}
57
+ onResponderTerminate={[Function]}
58
+ onResponderTerminationRequest={[Function]}
59
+ onStartShouldSetResponder={[Function]}
60
+ style={
61
+ {
62
+ "alignItems": "center",
63
+ "backgroundColor": "#ffffff",
64
+ "borderRadius": 12,
65
+ "elevation": 6,
66
+ "flexDirection": "row",
67
+ "flexGrow": 1,
68
+ "height": 28,
69
+ "justifyContent": "center",
70
+ "opacity": 1,
71
+ "shadowColor": "#001f23",
72
+ "shadowOffset": {
73
+ "height": 2,
74
+ "width": 0,
75
+ },
76
+ "shadowOpacity": 0.12,
77
+ "shadowRadius": 2,
78
+ }
79
+ }
80
+ >
81
+ <View
82
+ style={
83
+ [
84
+ {},
85
+ [
86
+ {
87
+ "alignItems": "center",
88
+ "flexDirection": "row",
89
+ "gap": 8,
90
+ "justifyContent": "center",
91
+ },
92
+ undefined,
93
+ ],
94
+ ]
95
+ }
96
+ >
97
+ <View
98
+ style={
99
+ [
100
+ {},
101
+ [
102
+ {
103
+ "alignItems": "center",
104
+ "flexDirection": "row",
105
+ "gap": 4,
106
+ "justifyContent": "center",
107
+ },
108
+ undefined,
109
+ ],
110
+ ]
111
+ }
112
+ >
113
+ <Text
114
+ allowFontScaling={false}
115
+ style={
116
+ [
117
+ {
118
+ "color": "#001f23",
119
+ "fontFamily": "BeVietnamPro-SemiBold",
120
+ "fontSize": 14,
121
+ "letterSpacing": 0.24,
122
+ "lineHeight": 22,
123
+ },
124
+ [
125
+ {},
126
+ undefined,
127
+ ],
128
+ ]
129
+ }
130
+ themeIntent="body"
131
+ themeTypeface="neutral"
132
+ themeVariant="small-bold"
133
+ >
134
+ Option 1
135
+ </Text>
136
+ </View>
137
+ </View>
138
+ </View>
139
+ <View
140
+ accessibilityState={
141
+ {
142
+ "busy": undefined,
143
+ "checked": undefined,
144
+ "disabled": undefined,
145
+ "expanded": undefined,
146
+ "selected": undefined,
147
+ }
148
+ }
149
+ accessibilityValue={
150
+ {
151
+ "max": undefined,
152
+ "min": undefined,
153
+ "now": undefined,
154
+ "text": undefined,
155
+ }
156
+ }
157
+ accessible={true}
158
+ collapsable={false}
159
+ focusable={true}
160
+ onClick={[Function]}
161
+ onResponderGrant={[Function]}
162
+ onResponderMove={[Function]}
163
+ onResponderRelease={[Function]}
164
+ onResponderTerminate={[Function]}
165
+ onResponderTerminationRequest={[Function]}
166
+ onStartShouldSetResponder={[Function]}
167
+ style={
168
+ {
169
+ "alignItems": "center",
170
+ "backgroundColor": "transparent",
171
+ "borderRadius": 12,
172
+ "flexDirection": "row",
173
+ "flexGrow": 1,
174
+ "height": 28,
175
+ "justifyContent": "center",
176
+ "opacity": 1,
177
+ }
178
+ }
179
+ >
180
+ <View
181
+ style={
182
+ [
183
+ {},
184
+ [
185
+ {
186
+ "alignItems": "center",
187
+ "flexDirection": "row",
188
+ "gap": 8,
189
+ "justifyContent": "center",
190
+ },
191
+ undefined,
192
+ ],
193
+ ]
194
+ }
195
+ >
196
+ <View
197
+ style={
198
+ [
199
+ {},
200
+ [
201
+ {
202
+ "alignItems": "center",
203
+ "flexDirection": "row",
204
+ "gap": 4,
205
+ "justifyContent": "center",
206
+ },
207
+ undefined,
208
+ ],
209
+ ]
210
+ }
211
+ >
212
+ <Text
213
+ allowFontScaling={false}
214
+ style={
215
+ [
216
+ {
217
+ "color": "#808f91",
218
+ "fontFamily": "BeVietnamPro-SemiBold",
219
+ "fontSize": 14,
220
+ "letterSpacing": 0.24,
221
+ "lineHeight": 22,
222
+ },
223
+ [
224
+ {},
225
+ undefined,
226
+ ],
227
+ ]
228
+ }
229
+ themeIntent="inactive"
230
+ themeTypeface="neutral"
231
+ themeVariant="small-bold"
232
+ >
233
+ Option 2
234
+ </Text>
235
+ </View>
236
+ </View>
237
+ </View>
238
+ <View
239
+ accessibilityState={
240
+ {
241
+ "busy": undefined,
242
+ "checked": undefined,
243
+ "disabled": undefined,
244
+ "expanded": undefined,
245
+ "selected": undefined,
246
+ }
247
+ }
248
+ accessibilityValue={
249
+ {
250
+ "max": undefined,
251
+ "min": undefined,
252
+ "now": undefined,
253
+ "text": undefined,
254
+ }
255
+ }
256
+ accessible={true}
257
+ collapsable={false}
258
+ focusable={true}
259
+ onClick={[Function]}
260
+ onResponderGrant={[Function]}
261
+ onResponderMove={[Function]}
262
+ onResponderRelease={[Function]}
263
+ onResponderTerminate={[Function]}
264
+ onResponderTerminationRequest={[Function]}
265
+ onStartShouldSetResponder={[Function]}
266
+ style={
267
+ {
268
+ "alignItems": "center",
269
+ "backgroundColor": "transparent",
270
+ "borderRadius": 12,
271
+ "flexDirection": "row",
272
+ "flexGrow": 1,
273
+ "height": 28,
274
+ "justifyContent": "center",
275
+ "opacity": 1,
276
+ }
277
+ }
278
+ >
279
+ <View
280
+ style={
281
+ [
282
+ {},
283
+ [
284
+ {
285
+ "alignItems": "center",
286
+ "flexDirection": "row",
287
+ "gap": 8,
288
+ "justifyContent": "center",
289
+ },
290
+ undefined,
291
+ ],
292
+ ]
293
+ }
294
+ >
295
+ <View
296
+ style={
297
+ [
298
+ {},
299
+ [
300
+ {
301
+ "alignItems": "center",
302
+ "flexDirection": "row",
303
+ "gap": 4,
304
+ "justifyContent": "center",
305
+ },
306
+ undefined,
307
+ ],
308
+ ]
309
+ }
310
+ >
311
+ <Text
312
+ allowFontScaling={false}
313
+ style={
314
+ [
315
+ {
316
+ "color": "#808f91",
317
+ "fontFamily": "BeVietnamPro-SemiBold",
318
+ "fontSize": 14,
319
+ "letterSpacing": 0.24,
320
+ "lineHeight": 22,
321
+ },
322
+ [
323
+ {},
324
+ undefined,
325
+ ],
326
+ ]
327
+ }
328
+ themeIntent="inactive"
329
+ themeTypeface="neutral"
330
+ themeVariant="small-bold"
331
+ >
332
+ Option 3
333
+ </Text>
334
+ </View>
335
+ </View>
336
+ </View>
337
+ </View>
338
+ <View
339
+ pointerEvents="box-none"
340
+ position="bottom"
341
+ style={
342
+ [
343
+ {
344
+ "bottom": 0,
345
+ "elevation": 9999,
346
+ "flexDirection": "column-reverse",
347
+ "left": 0,
348
+ "paddingHorizontal": 24,
349
+ "paddingVertical": 16,
350
+ "position": "absolute",
351
+ "right": 0,
352
+ "top": 0,
353
+ },
354
+ undefined,
355
+ ]
356
+ }
357
+ />
358
+ </View>
359
+ `;
@@ -0,0 +1,247 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import SegmentedControl, { SegmentedControlProps } from '..';
4
+ import type { SegmentedControlItemConfig } from '../types';
5
+ import renderWithTheme from '../../../testHelpers/renderWithTheme';
6
+ import { scale } from '../../../utils/scale';
7
+
8
+ describe('SegmentedControl', () => {
9
+ const defaultItems: SegmentedControlItemConfig[] = [
10
+ { value: 'option1', label: 'Option 1' },
11
+ { value: 'option2', label: 'Option 2' },
12
+ { value: 'option3', label: 'Option 3' },
13
+ ];
14
+
15
+ const defaultProps: SegmentedControlProps = {
16
+ items: defaultItems,
17
+ value: 'option1',
18
+ onItemPress: jest.fn(),
19
+ };
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ it('should render correctly with default props', () => {
26
+ const { toJSON, getByText } = renderWithTheme(
27
+ <SegmentedControl {...defaultProps} />
28
+ );
29
+
30
+ expect(getByText('Option 1')).toBeVisible();
31
+ expect(getByText('Option 2')).toBeVisible();
32
+ expect(getByText('Option 3')).toBeVisible();
33
+ expect(toJSON()).toMatchSnapshot();
34
+ });
35
+
36
+ describe('sizing', () => {
37
+ it.each`
38
+ size | expectedHeight
39
+ ${'medium'} | ${scale(36)}
40
+ ${'large'} | ${scale(44)}
41
+ `(
42
+ 'should have correct height of $expectedHeight for $size size',
43
+ ({ size, expectedHeight }) => {
44
+ const { getByTestId } = renderWithTheme(
45
+ <SegmentedControl
46
+ {...defaultProps}
47
+ size={size}
48
+ testID="segmented-control"
49
+ />
50
+ );
51
+
52
+ const wrapper = getByTestId('segmented-control');
53
+ expect(wrapper).toHaveStyle({ height: expectedHeight });
54
+ }
55
+ );
56
+ });
57
+
58
+ it('should display icons when provided', () => {
59
+ const itemsWithIcons: SegmentedControlItemConfig[] = [
60
+ {
61
+ value: 'home',
62
+ label: 'Home',
63
+ prefix: 'home-outlined',
64
+ testID: 'home-item',
65
+ },
66
+ {
67
+ value: 'settings',
68
+ label: 'Settings',
69
+ suffix: 'cog-outlined',
70
+ testID: 'settings-item',
71
+ },
72
+ ];
73
+
74
+ const { getByText, getByTestId } = renderWithTheme(
75
+ <SegmentedControl {...defaultProps} items={itemsWithIcons} value="home" />
76
+ );
77
+
78
+ expect(getByTestId('home-item-prefix')).toBeVisible();
79
+ expect(getByTestId('settings-item-suffix')).toBeVisible();
80
+ expect(getByText('Home')).toBeVisible();
81
+ expect(getByText('Settings')).toBeVisible();
82
+ });
83
+
84
+ it('should not call onItemPress when disabled item is clicked', () => {
85
+ const onItemPress = jest.fn();
86
+ const itemsWithDisabled: SegmentedControlItemConfig[] = [
87
+ { value: 'option1', label: 'Option 1' },
88
+ { value: 'option2', label: 'Option 2', disabled: true },
89
+ ];
90
+
91
+ const { getByText } = renderWithTheme(
92
+ <SegmentedControl
93
+ {...defaultProps}
94
+ items={itemsWithDisabled}
95
+ onItemPress={onItemPress}
96
+ />
97
+ );
98
+
99
+ fireEvent.press(getByText('Option 2'));
100
+ expect(onItemPress).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it.each`
104
+ value | max | expectedText
105
+ ${10} | ${10} | ${'10'}
106
+ ${15} | ${10} | ${'10+'}
107
+ ${99} | ${undefined} | ${'99'}
108
+ `(
109
+ 'should display count badge $expectedText for $value and $max',
110
+ ({ value, max, expectedText }) => {
111
+ const itemsWithBadges: SegmentedControlItemConfig[] = [
112
+ {
113
+ value: 'counter',
114
+ label: 'Counter',
115
+ badge: { type: 'counter', value, max },
116
+ },
117
+ ];
118
+
119
+ const { getByText } = renderWithTheme(
120
+ <SegmentedControl
121
+ {...defaultProps}
122
+ items={itemsWithBadges}
123
+ value="counter"
124
+ />
125
+ );
126
+
127
+ expect(getByText('Counter')).toBeVisible();
128
+ expect(getByText(expectedText)).toBeVisible();
129
+ }
130
+ );
131
+
132
+ it('should display status badge correctly', () => {
133
+ const itemsWithStatusBadge: SegmentedControlItemConfig[] = [
134
+ {
135
+ value: 'status',
136
+ label: 'Status',
137
+ badge: { type: 'status' },
138
+ testID: 'status-item',
139
+ },
140
+ ];
141
+
142
+ const { getByText, getByTestId } = renderWithTheme(
143
+ <SegmentedControl
144
+ {...defaultProps}
145
+ items={itemsWithStatusBadge}
146
+ value="status"
147
+ />
148
+ );
149
+
150
+ expect(getByText('Status')).toBeVisible();
151
+ expect(getByTestId('status-item-badge')).toBeVisible();
152
+ });
153
+
154
+ it('should display subtext when provided', () => {
155
+ const itemsWithSubtext: SegmentedControlItemConfig[] = [
156
+ {
157
+ value: 'item1',
158
+ label: 'Main Label',
159
+ subText: 'Secondary text',
160
+ },
161
+ ];
162
+
163
+ const { getByText } = renderWithTheme(
164
+ <SegmentedControl
165
+ {...defaultProps}
166
+ items={itemsWithSubtext}
167
+ value="item1"
168
+ />
169
+ );
170
+
171
+ expect(getByText('Main Label')).toBeVisible();
172
+ expect(getByText('Secondary text')).toBeVisible();
173
+ expect(getByText('·')).toBeVisible(); // bullet separator
174
+ });
175
+
176
+ it('should render complex control with all features correctly', () => {
177
+ const complexItems: SegmentedControlItemConfig[] = [
178
+ {
179
+ value: 'complex1',
180
+ label: 'Dashboard',
181
+ subText: 'Overview',
182
+ prefix: 'home-outlined',
183
+ suffix: 'cog-outlined',
184
+ badge: { type: 'counter', value: 5, max: 3 },
185
+ testID: 'complex1',
186
+ },
187
+ {
188
+ value: 'complex2',
189
+ label: 'Profile',
190
+ badge: { type: 'status' },
191
+ disabled: true,
192
+ testID: 'complex2',
193
+ },
194
+ {
195
+ value: 'complex3',
196
+ label: 'Settings',
197
+ prefix: 'pencil-outlined',
198
+ testID: 'complex3',
199
+ },
200
+ ];
201
+
202
+ const { getByText, getByTestId } = renderWithTheme(
203
+ <SegmentedControl
204
+ {...defaultProps}
205
+ items={complexItems}
206
+ value="complex1"
207
+ size="large"
208
+ testID="complex-control"
209
+ />
210
+ );
211
+
212
+ // Control container
213
+ expect(getByTestId('complex-control')).toBeVisible();
214
+ expect(getByTestId('complex-control')).toHaveStyle({ height: scale(44) }); // large size
215
+
216
+ // First item - all features
217
+ expect(getByText('Dashboard')).toBeVisible();
218
+ expect(getByText('Overview')).toBeVisible();
219
+ expect(getByText('·')).toBeVisible(); // bullet separator
220
+ expect(getByText('3+')).toBeVisible(); // over max badge
221
+ expect(getByTestId('complex1-prefix')).toBeVisible();
222
+ expect(getByTestId('complex1-suffix')).toBeVisible();
223
+
224
+ // Second item - disabled with status badge
225
+ expect(getByText('Profile')).toBeVisible();
226
+ expect(getByTestId('complex2-badge')).toBeVisible();
227
+
228
+ // Third item - simple with prefix
229
+ expect(getByText('Settings')).toBeVisible();
230
+ expect(getByTestId('complex3-prefix')).toBeVisible();
231
+ });
232
+
233
+ it('should call onItemPress with correct item when pressed', () => {
234
+ const onItemPress = jest.fn();
235
+ const { getByText } = renderWithTheme(
236
+ <SegmentedControl {...defaultProps} onItemPress={onItemPress} />
237
+ );
238
+
239
+ fireEvent.press(getByText('Option 2'));
240
+
241
+ expect(onItemPress).toHaveBeenCalledTimes(1);
242
+ expect(onItemPress).toHaveBeenCalledWith({
243
+ value: 'option2',
244
+ label: 'Option 2',
245
+ });
246
+ });
247
+ });
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { StyleProp, ViewProps, ViewStyle } from 'react-native';
3
+ import SegmentedItem from './SegmentedItem';
4
+ import { StyledSegmentedControlWrapper } from './StyledSegmentedControl';
5
+ import type { SegmentedControlItemConfig } from './types';
6
+
7
+ export interface SegmentedControlProps extends ViewProps {
8
+ /**
9
+ * The size of the segmented control.
10
+ */
11
+ size?: 'medium' | 'large';
12
+ /**
13
+ * The items to display in the segmented control.
14
+ */
15
+ items: SegmentedControlItemConfig[];
16
+ /**
17
+ * The value of the selected item.
18
+ */
19
+ value: string;
20
+ /**
21
+ * Test ID for testing purposes.
22
+ */
23
+ testID?: string;
24
+ /**
25
+ * The style of the segmented control.
26
+ */
27
+ style?: StyleProp<ViewStyle>;
28
+ /**
29
+ * The callback function to be called when the selected item changes.
30
+ */
31
+ onItemPress: (item: SegmentedControlItemConfig) => void;
32
+ }
33
+
34
+ const SegmentedControl = ({
35
+ size = 'medium',
36
+ items,
37
+ value,
38
+ testID,
39
+ style,
40
+ onItemPress,
41
+ }: SegmentedControlProps) => {
42
+ return (
43
+ <StyledSegmentedControlWrapper
44
+ themeSize={size}
45
+ testID={testID}
46
+ style={style}
47
+ >
48
+ {items.map((item) => (
49
+ <SegmentedItem
50
+ {...item}
51
+ key={item.value}
52
+ selected={item.value === value}
53
+ onPress={() => onItemPress(item)}
54
+ size={size}
55
+ />
56
+ ))}
57
+ </StyledSegmentedControlWrapper>
58
+ );
59
+ };
60
+
61
+ export default SegmentedControl;
@@ -0,0 +1,46 @@
1
+ import type { IconName } from '../Icon';
2
+
3
+ export type StatusBadgeType = {
4
+ type: 'status';
5
+ };
6
+
7
+ export type CounterBadgeType = {
8
+ type: 'counter';
9
+ value: number;
10
+ max?: number;
11
+ };
12
+
13
+ export interface SegmentedControlItemConfig {
14
+ /**
15
+ * The value of the segment option.
16
+ */
17
+ value: string;
18
+ /**
19
+ * The label of the segment option.
20
+ */
21
+ label?: string;
22
+ /**
23
+ * The subtext of the segment option.
24
+ */
25
+ subText?: string;
26
+ /**
27
+ * The prefix of the segment option.
28
+ */
29
+ prefix?: IconName;
30
+ /**
31
+ * The suffix of the segment option.
32
+ */
33
+ suffix?: IconName;
34
+ /**
35
+ * Test ID for testing purposes.
36
+ */
37
+ testID?: string;
38
+ /**
39
+ * Whether the segment option is disabled.
40
+ */
41
+ disabled?: boolean;
42
+ /**
43
+ * The badge configuration of the segment option.
44
+ */
45
+ badge?: StatusBadgeType | CounterBadgeType;
46
+ }