@utilitywarehouse/hearth-react-native 0.27.0 → 0.27.1

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.27.0 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.27.1 build /home/runner/work/hearth/hearth/packages/react-native
3
3
  > tsc
4
4
 
@@ -1,5 +1,5 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.27.0 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.27.1 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint .
4
4
 
5
5
 
@@ -58,15 +58,15 @@
58
58
 
59
59
  ✖ 25 problems (0 errors, 25 warnings)
60
60
 
61
- Rule | Time (ms) | Relative
62
- :---------------------------------|----------:|--------:
63
- @typescript-eslint/no-unused-vars | 1531.354 | 62.0%
64
- react-hooks/exhaustive-deps | 115.862 | 4.7%
65
- no-global-assign | 82.745 | 3.3%
66
- react-hooks/rules-of-hooks | 81.422 | 3.3%
67
- @typescript-eslint/ban-ts-comment | 54.976 | 2.2%
68
- no-misleading-character-class | 43.812 | 1.8%
69
- no-unexpected-multiline | 39.804 | 1.6%
70
- no-fallthrough | 33.437 | 1.4%
71
- no-regex-spaces | 28.539 | 1.2%
72
- no-shadow-restricted-names | 25.451 | 1.0%
61
+ Rule | Time (ms) | Relative
62
+ :-----------------------------------------|----------:|--------:
63
+ @typescript-eslint/no-unused-vars | 1499.293 | 61.6%
64
+ react-hooks/exhaustive-deps | 127.430 | 5.2%
65
+ react-hooks/rules-of-hooks | 84.158 | 3.5%
66
+ no-global-assign | 65.981 | 2.7%
67
+ no-unexpected-multiline | 44.926 | 1.8%
68
+ @typescript-eslint/ban-ts-comment | 39.212 | 1.6%
69
+ @typescript-eslint/triple-slash-reference | 36.913 | 1.5%
70
+ no-misleading-character-class | 36.268 | 1.5%
71
+ no-useless-escape | 28.431 | 1.2%
72
+ @typescript-eslint/no-unused-expressions | 25.564 | 1.1%
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.27.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#990](https://github.com/utilitywarehouse/hearth/pull/990) [`958e0e1`](https://github.com/utilitywarehouse/hearth/commit/958e0e1a9d5451d1e11fecadc69ae3c5ad9d42ca) Thanks [@declanelcocks](https://github.com/declanelcocks)! - 🐛 [FIX]: Fix `Modal` layout when `inNavModal` and `stickyFooter={false}`.
8
+
9
+ Corrects the container flex style for `inNavModal` modals with a non-sticky footer, where the UX was not great when scrolling.
10
+
11
+ **Components affected**:
12
+ - `Modal`
13
+
14
+ **Developer changes**:
15
+
16
+ No changes required.
17
+
18
+ - [#992](https://github.com/utilitywarehouse/hearth/pull/992) [`2560b3d`](https://github.com/utilitywarehouse/hearth/commit/2560b3dcba7ed4981fad585628f96afd07d8de4f) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Add optional leading `icon` support to `SegmentedControlOption`.
19
+
20
+ This adds an optional `icon` prop to `SegmentedControlOption`, allowing icons to be displayed before option labels in segmented controls.
21
+
22
+ Docs and stories were updated to include icon usage examples.
23
+
24
+ **Components affected**:
25
+ - `SegmentedControlOption`
26
+
27
+ **Developer changes**:
28
+
29
+ No changes required for existing usage.
30
+
31
+ To use the new optional icon prop:
32
+
33
+ ```tsx
34
+ import { SegmentedControl, SegmentedControlOption } from '@utilitywarehouse/hearth-react-native';
35
+ import { ElectricitySmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
36
+
37
+ <SegmentedControl defaultValue="energy">
38
+ <SegmentedControlOption value="energy" icon={ElectricitySmallIcon}>
39
+ Energy
40
+ </SegmentedControlOption>
41
+ <SegmentedControlOption value="broadband">Broadband</SegmentedControlOption>
42
+ </SegmentedControl>;
43
+ ```
44
+
3
45
  ## 0.27.0
4
46
 
5
47
  ### Minor Changes
@@ -120,7 +120,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
120
120
  });
121
121
  const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
122
122
  const InNavModalContainer = scrollable ? ScrollView : View;
123
- const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal && (_jsxs(InNavModalContainer, { style: { flexGrow: stickyFooter ? 1 : 0 }, children: [children, !stickyFooter ? _jsx(View, { style: styles.inNavModalFooterContainer, children: footer }) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null] })) }));
123
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal && (_jsxs(InNavModalContainer, { style: { flex: stickyFooter ? 1 : 0 }, children: [children, !stickyFooter ? _jsx(View, { style: styles.inNavModalFooterContainer, children: footer }) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null] })) }));
124
124
  const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
125
125
  onPressPrimaryButton,
126
126
  primaryButtonText,
@@ -5,9 +5,10 @@ import { Platform, Pressable, View } from 'react-native';
5
5
  import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming, } from 'react-native-reanimated';
6
6
  import { StyleSheet } from 'react-native-unistyles';
7
7
  import { BodyText } from '../BodyText';
8
+ import { Icon } from '../Icon';
8
9
  import { useSegmentedControlContext } from './SegmentedControl.context';
9
10
  const AnimatedView = Animated.createAnimatedComponent(View);
10
- const SegmentedControlOptionRoot = ({ value, children, accessibilityLabel, disabled = false, style, states = {}, ...props }) => {
11
+ const SegmentedControlOptionRoot = ({ value, children, icon, accessibilityLabel, disabled = false, style, states = {}, ...props }) => {
11
12
  const { value: selectedValue, select, disabled: allDisabled, size, registerOptionLayout, } = useSegmentedControlContext();
12
13
  const { active = false } = states;
13
14
  const reducedMotion = useReducedMotion();
@@ -35,7 +36,7 @@ const SegmentedControlOptionRoot = ({ value, children, accessibilityLabel, disab
35
36
  const accessibleLabel = typeof children === 'string' || typeof children === 'number' ? String(children) : value;
36
37
  return (_jsx(Pressable, { ...props, accessibilityRole: "radio", accessibilityState: { checked: selected, disabled: isDisabled }, accessibilityLabel: accessibilityLabel ?? accessibleLabel, onPress: onPress, onLayout: e => registerOptionLayout(value, e.nativeEvent.layout), disabled: isDisabled, style: [styles.option, style], ...(Platform.OS === 'web'
37
38
  ? { 'aria-label': accessibilityLabel ?? accessibleLabel }
38
- : null), children: _jsxs(View, { style: styles.labelWrap, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.labelSizer, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: children }), _jsx(AnimatedView, { pointerEvents: "none", style: [styles.textLayer, regularLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "regular", style: styles.textRegular, children: children }) }), _jsx(AnimatedView, { pointerEvents: "none", style: [styles.textLayer, selectedLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "semibold", style: styles.textSelected, children: children }) })] }) }));
39
+ : null), children: _jsxs(View, { style: styles.contentWrap, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: [icon ? _jsx(Icon, { as: icon, size: "sm", style: styles.icon }) : null, _jsxs(View, { style: styles.labelWrap, children: [_jsx(BodyText, { size: "md", weight: "semibold", style: styles.labelSizer, accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: children }), _jsx(AnimatedView, { pointerEvents: "none", style: [styles.textLayer, regularLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "regular", style: styles.textRegular, children: children }) }), _jsx(AnimatedView, { pointerEvents: "none", style: [styles.textLayer, selectedLabelStyle], accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...(Platform.OS === 'web' ? { 'aria-hidden': true } : null), children: _jsx(BodyText, { size: "md", weight: "semibold", style: styles.textSelected, children: children }) })] })] }) }));
39
40
  };
40
41
  const SegmentedControlOption = createPressable({ Root: SegmentedControlOptionRoot });
41
42
  SegmentedControlOption.displayName = 'SegmentedControlOption';
@@ -90,6 +91,12 @@ const styles = StyleSheet.create(theme => ({
90
91
  },
91
92
  },
92
93
  },
94
+ contentWrap: {
95
+ flexDirection: 'row',
96
+ alignItems: 'center',
97
+ justifyContent: 'center',
98
+ gap: theme.components.segmentedControl.gap,
99
+ },
93
100
  labelWrap: {
94
101
  position: 'relative',
95
102
  alignItems: 'center',
@@ -105,6 +112,18 @@ const styles = StyleSheet.create(theme => ({
105
112
  alignItems: 'center',
106
113
  justifyContent: 'center',
107
114
  },
115
+ icon: {
116
+ variants: {
117
+ selected: {
118
+ true: {
119
+ color: theme.color.icon.inverted,
120
+ },
121
+ false: {
122
+ color: theme.color.icon.primary,
123
+ },
124
+ },
125
+ },
126
+ },
108
127
  textRegular: {
109
128
  color: theme.color.text.primary,
110
129
  },
@@ -1,10 +1,12 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ComponentType, ReactNode } from 'react';
2
2
  import type { PressableProps, ViewProps } from 'react-native';
3
3
  export interface SegmentedControlOptionProps extends Omit<PressableProps, 'children'> {
4
4
  /** Unique option value. */
5
5
  value: string;
6
6
  /** Option label/content. */
7
7
  children: ReactNode;
8
+ /** Optional leading icon. */
9
+ icon?: ComponentType<any>;
8
10
  /** Disables only this option. */
9
11
  disabled?: boolean;
10
12
  style?: ViewProps['style'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -56,11 +56,11 @@
56
56
  "vite": "^7.1.3",
57
57
  "vite-plugin-svgr": "^4.5.0",
58
58
  "vitest": "^3.2.4",
59
+ "@utilitywarehouse/hearth-react-icons": "^0.8.0",
59
60
  "@utilitywarehouse/hearth-fonts": "^0.0.4",
60
61
  "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
61
- "@utilitywarehouse/hearth-svg-assets": "^0.5.0",
62
62
  "@utilitywarehouse/hearth-tokens": "^0.2.3",
63
- "@utilitywarehouse/hearth-react-icons": "^0.8.0"
63
+ "@utilitywarehouse/hearth-svg-assets": "^0.5.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@gorhom/bottom-sheet": "^5.0.0",
@@ -292,7 +292,7 @@ const Modal = ({
292
292
  </View>
293
293
  ) : null}
294
294
  {inNavModal && (
295
- <InNavModalContainer style={{ flexGrow: stickyFooter ? 1 : 0 }}>
295
+ <InNavModalContainer style={{ flex: stickyFooter ? 1 : 0 }}>
296
296
  {children}
297
297
  {!stickyFooter ? <View style={styles.inNavModalFooterContainer}>{footer}</View> : null}
298
298
  </InNavModalContainer>
@@ -16,8 +16,10 @@ Each option is presented as an equal-priority segment in a single horizontal gro
16
16
 
17
17
  - [Playground](#playground)
18
18
  - [Usage](#usage)
19
- - [Sizes](#sizes)
20
19
  - [Props](#props)
20
+ - [Examples](#examples)
21
+ - [Sizes](#sizes)
22
+ - [Icons](#icons)
21
23
  - [Accessibility](#accessibility)
22
24
 
23
25
  ## Playground
@@ -50,15 +52,6 @@ const Example = () => {
50
52
  };
51
53
  ```
52
54
 
53
- ## Sizes
54
-
55
- Figma defines two size variants for Segmented Control:
56
-
57
- - `sm` maps to `SM-32`
58
- - `md` maps to `MD-48`
59
-
60
- <Canvas of={Stories.Sizes} />
61
-
62
55
  ## Props
63
56
 
64
57
  ### SegmentedControl
@@ -74,11 +67,45 @@ Figma defines two size variants for Segmented Control:
74
67
 
75
68
  ### SegmentedControlOption
76
69
 
77
- | Property | Type | Description | Default |
78
- | ---------- | ----------- | ------------------------------ | -------- |
79
- | `value` | `string` | Unique value for this segment. | required |
80
- | `children` | `ReactNode` | Option label/content. | required |
81
- | `disabled` | `boolean` | Disables only this option. | `false` |
70
+ | Property | Type | Description | Default |
71
+ | ---------- | --------------- | ------------------------------ | -------- |
72
+ | `value` | `string` | Unique value for this segment. | required |
73
+ | `children` | `ReactNode` | Option label/content. | required |
74
+ | `icon` | `ComponentType` | Optional leading icon. | - |
75
+ | `disabled` | `boolean` | Disables only this option. | `false` |
76
+
77
+ ## Examples
78
+
79
+ ### Sizes
80
+
81
+ Figma defines two size variants for Segmented Control:
82
+
83
+ - `sm` maps to `SM-32`
84
+ - `md` maps to `MD-48`
85
+
86
+ <Canvas of={Stories.Sizes} />
87
+
88
+ ```tsx
89
+ <SegmentedControl size="sm">...</SegmentedControl>
90
+ <SegmentedControl size="md">...</SegmentedControl>
91
+ ```
92
+
93
+ ### Icons
94
+
95
+ SegmentedControlOption supports an optional leading icon.
96
+
97
+ <Canvas of={Stories.WithIcons} />
98
+
99
+ ```tsx
100
+ import { MobileSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
101
+
102
+ <SegmentedControl>
103
+ <SegmentedControlOption value="mobile" icon={MobileSmallIcon}>
104
+ Mobile
105
+ </SegmentedControlOption>
106
+ ...
107
+ </SegmentedControl>;
108
+ ```
82
109
 
83
110
  ## Accessibility
84
111
 
@@ -3,20 +3,17 @@ import { SegmentedControl, SegmentedControlOption } from '../';
3
3
 
4
4
  figma.connect(
5
5
  SegmentedControl,
6
- 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6185-1021&t=c7xg5X0N2EL0t87h-4',
6
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6185-1021&m=dev',
7
7
  {
8
8
  props: {
9
9
  size: figma.enum('Size', {
10
10
  'SM-32': 'sm',
11
11
  'MD-48': 'md',
12
12
  }),
13
- disabled: figma.enum('State', {
14
- Disabled: true,
15
- }),
16
13
  options: figma.children('Option'),
17
14
  },
18
15
  example: props => (
19
- <SegmentedControl defaultValue="option-1" size={props.size} disabled={props.disabled}>
16
+ <SegmentedControl defaultValue="option-1" size={props.size}>
20
17
  {props.options}
21
18
  </SegmentedControl>
22
19
  ),
@@ -25,15 +22,17 @@ figma.connect(
25
22
 
26
23
  figma.connect(
27
24
  SegmentedControlOption,
28
- 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=4340-1252&t=c7xg5X0N2EL0t87h-4',
25
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=4340-1252&m=dev',
29
26
  {
30
27
  props: {
31
28
  label: figma.string('Label'),
32
- value: figma.string('Value'),
29
+ icon: figma.boolean('Icon?', {
30
+ true: figma.instance('Icon-20'),
31
+ }),
33
32
  },
34
33
  example: props => (
35
- <SegmentedControlOption value={props.value ?? 'option'}>
36
- {props.label ?? 'Option'}
34
+ <SegmentedControlOption value={'option'} icon={props.icon}>
35
+ {props.label}
37
36
  </SegmentedControlOption>
38
37
  ),
39
38
  }
@@ -1,3 +1,8 @@
1
+ import {
2
+ BroadbandSmallIcon,
3
+ ElectricitySmallIcon,
4
+ MobileSmallIcon,
5
+ } from '@utilitywarehouse/hearth-react-native-icons';
1
6
  import { useState } from 'react';
2
7
  import { BodyText, Flex, SegmentedControl, SegmentedControlOption } from '../';
3
8
 
@@ -75,3 +80,19 @@ export const Disabled = {
75
80
  </SegmentedControl>
76
81
  ),
77
82
  };
83
+
84
+ export const WithIcons = {
85
+ render: () => (
86
+ <SegmentedControl defaultValue="energy" size="md">
87
+ <SegmentedControlOption value="energy" icon={ElectricitySmallIcon}>
88
+ Energy
89
+ </SegmentedControlOption>
90
+ <SegmentedControlOption value="broadband" icon={BroadbandSmallIcon}>
91
+ Broadband
92
+ </SegmentedControlOption>
93
+ <SegmentedControlOption value="mobile" icon={MobileSmallIcon}>
94
+ Mobile
95
+ </SegmentedControlOption>
96
+ </SegmentedControl>
97
+ ),
98
+ };
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ComponentType, ReactNode } from 'react';
2
2
  import type { PressableProps, ViewProps } from 'react-native';
3
3
 
4
4
  export interface SegmentedControlOptionProps extends Omit<PressableProps, 'children'> {
@@ -6,6 +6,8 @@ export interface SegmentedControlOptionProps extends Omit<PressableProps, 'child
6
6
  value: string;
7
7
  /** Option label/content. */
8
8
  children: ReactNode;
9
+ /** Optional leading icon. */
10
+ icon?: ComponentType<any>;
9
11
  /** Disables only this option. */
10
12
  disabled?: boolean;
11
13
  style?: ViewProps['style'];
@@ -10,6 +10,7 @@ import Animated, {
10
10
  } from 'react-native-reanimated';
11
11
  import { StyleSheet } from 'react-native-unistyles';
12
12
  import { BodyText } from '../BodyText';
13
+ import { Icon } from '../Icon';
13
14
  import { useSegmentedControlContext } from './SegmentedControl.context';
14
15
  import type SegmentedControlOptionProps from './SegmentedControlOption.props';
15
16
 
@@ -18,6 +19,7 @@ const AnimatedView = Animated.createAnimatedComponent(View);
18
19
  const SegmentedControlOptionRoot = ({
19
20
  value,
20
21
  children,
22
+ icon,
21
23
  accessibilityLabel,
22
24
  disabled = false,
23
25
  style,
@@ -79,47 +81,50 @@ const SegmentedControlOptionRoot = ({
79
81
  : null)}
80
82
  >
81
83
  <View
82
- style={styles.labelWrap}
84
+ style={styles.contentWrap}
83
85
  accessible={false}
84
86
  accessibilityElementsHidden
85
87
  importantForAccessibility="no-hide-descendants"
86
88
  {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
87
89
  >
88
- <BodyText
89
- size="md"
90
- weight="semibold"
91
- style={styles.labelSizer}
92
- accessible={false}
93
- accessibilityElementsHidden
94
- importantForAccessibility="no-hide-descendants"
95
- {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
96
- >
97
- {children}
98
- </BodyText>
99
- <AnimatedView
100
- pointerEvents="none"
101
- style={[styles.textLayer, regularLabelStyle]}
102
- accessible={false}
103
- accessibilityElementsHidden
104
- importantForAccessibility="no-hide-descendants"
105
- {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
106
- >
107
- <BodyText size="md" weight="regular" style={styles.textRegular}>
90
+ {icon ? <Icon as={icon} size="sm" style={styles.icon} /> : null}
91
+ <View style={styles.labelWrap}>
92
+ <BodyText
93
+ size="md"
94
+ weight="semibold"
95
+ style={styles.labelSizer}
96
+ accessible={false}
97
+ accessibilityElementsHidden
98
+ importantForAccessibility="no-hide-descendants"
99
+ {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
100
+ >
108
101
  {children}
109
102
  </BodyText>
110
- </AnimatedView>
111
- <AnimatedView
112
- pointerEvents="none"
113
- style={[styles.textLayer, selectedLabelStyle]}
114
- accessible={false}
115
- accessibilityElementsHidden
116
- importantForAccessibility="no-hide-descendants"
117
- {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
118
- >
119
- <BodyText size="md" weight="semibold" style={styles.textSelected}>
120
- {children}
121
- </BodyText>
122
- </AnimatedView>
103
+ <AnimatedView
104
+ pointerEvents="none"
105
+ style={[styles.textLayer, regularLabelStyle]}
106
+ accessible={false}
107
+ accessibilityElementsHidden
108
+ importantForAccessibility="no-hide-descendants"
109
+ {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
110
+ >
111
+ <BodyText size="md" weight="regular" style={styles.textRegular}>
112
+ {children}
113
+ </BodyText>
114
+ </AnimatedView>
115
+ <AnimatedView
116
+ pointerEvents="none"
117
+ style={[styles.textLayer, selectedLabelStyle]}
118
+ accessible={false}
119
+ accessibilityElementsHidden
120
+ importantForAccessibility="no-hide-descendants"
121
+ {...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
122
+ >
123
+ <BodyText size="md" weight="semibold" style={styles.textSelected}>
124
+ {children}
125
+ </BodyText>
126
+ </AnimatedView>
127
+ </View>
123
128
  </View>
124
129
  </Pressable>
125
130
  );
@@ -180,6 +185,12 @@ const styles = StyleSheet.create(theme => ({
180
185
  },
181
186
  },
182
187
  },
188
+ contentWrap: {
189
+ flexDirection: 'row',
190
+ alignItems: 'center',
191
+ justifyContent: 'center',
192
+ gap: theme.components.segmentedControl.gap,
193
+ },
183
194
  labelWrap: {
184
195
  position: 'relative',
185
196
  alignItems: 'center',
@@ -195,6 +206,18 @@ const styles = StyleSheet.create(theme => ({
195
206
  alignItems: 'center',
196
207
  justifyContent: 'center',
197
208
  },
209
+ icon: {
210
+ variants: {
211
+ selected: {
212
+ true: {
213
+ color: theme.color.icon.inverted,
214
+ },
215
+ false: {
216
+ color: theme.color.icon.primary,
217
+ },
218
+ },
219
+ },
220
+ },
198
221
  textRegular: {
199
222
  color: theme.color.text.primary,
200
223
  },
@@ -3,10 +3,10 @@ import { SelectOption } from '../';
3
3
 
4
4
  figma.connect(
5
5
  SelectOption,
6
- 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR?node-id=4340%3A1252',
6
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=3394-3663&m=dev',
7
7
  {
8
8
  props: {
9
- label: figma.string('Label'),
9
+ label: figma.string('Text'),
10
10
  disabled: figma.enum('State', {
11
11
  Disabled: true,
12
12
  }),