@utilitywarehouse/hearth-react-native 0.14.1 → 0.15.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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/CHANGELOG.md +55 -0
  4. package/build/components/BottomSheet/index.d.ts +5 -5
  5. package/build/components/BottomSheet/index.js +4 -4
  6. package/build/components/Checkbox/Checkbox.d.ts +1 -1
  7. package/build/components/Checkbox/Checkbox.js +2 -4
  8. package/build/components/DescriptionList/DescriptionListItem.js +8 -1
  9. package/build/components/HTMLElements/ListItem.d.ts +9 -1
  10. package/build/components/HTMLElements/ListItem.js +1 -2
  11. package/build/components/HTMLElements/OrderedList.d.ts +3 -2
  12. package/build/components/HTMLElements/OrderedList.js +29 -4
  13. package/build/components/HTMLElements/UnorderedList.d.ts +3 -2
  14. package/build/components/HTMLElements/UnorderedList.js +29 -4
  15. package/build/components/Helper/Helper.js +1 -1
  16. package/build/components/Helper/HelperText.js +1 -0
  17. package/build/components/Input/Input.js +2 -2
  18. package/build/components/Modal/Modal.d.ts +1 -1
  19. package/build/components/Modal/Modal.js +49 -4
  20. package/build/components/Modal/Modal.props.d.ts +1 -0
  21. package/build/components/PillGroup/PillGroup.props.d.ts +19 -7
  22. package/package.json +1 -1
  23. package/src/components/BottomSheet/index.ts +7 -5
  24. package/src/components/Checkbox/Checkbox.tsx +7 -2
  25. package/src/components/DescriptionList/DescriptionListItem.tsx +8 -1
  26. package/src/components/HTMLElements/ListItem.tsx +11 -3
  27. package/src/components/HTMLElements/Lists.docs.mdx +64 -16
  28. package/src/components/HTMLElements/OrderedList.stories.tsx +33 -2
  29. package/src/components/HTMLElements/OrderedList.tsx +54 -6
  30. package/src/components/HTMLElements/UnorderedList.stories.tsx +63 -5
  31. package/src/components/HTMLElements/UnorderedList.tsx +50 -6
  32. package/src/components/Helper/Helper.tsx +1 -1
  33. package/src/components/Helper/HelperText.tsx +1 -0
  34. package/src/components/Input/Input.tsx +2 -0
  35. package/src/components/Modal/Modal.props.ts +1 -0
  36. package/src/components/Modal/Modal.tsx +86 -23
  37. package/src/components/PillGroup/PillGroup.props.ts +25 -11
  38. package/src/components/PillGroup/PillGroup.stories.tsx +5 -6
  39. package/src/components/PillGroup/PillGroup.tsx +3 -3
@@ -1,6 +1,7 @@
1
- import { Meta, Canvas, Story, Controls } from '@storybook/addon-docs/blocks';
2
- import { UL, OL, LI, Center, BodyText } from '../..';
3
- import { ViewFigmaButton, BackToTopButton, UsageWrap } from '../../../docs/components';
1
+ import { Canvas, Controls, Meta, Story } from '@storybook/addon-docs/blocks';
2
+ import { BodyText, Center, LI, OL, UL } from '../..';
3
+ import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
4
+ import * as UnorderedListStory from './UnorderedList.stories';
4
5
 
5
6
  <Meta title="Utility Components / UL & OL (Lists)" />
6
7
 
@@ -11,6 +12,9 @@ import { ViewFigmaButton, BackToTopButton, UsageWrap } from '../../../docs/compo
11
12
  The `UL` (Unordered List) and `OL` (Ordered List) components are used to display lists of items. `UL` displays a bulleted list, and `OL` displays a numbered list. The `LI` (List Item) component is used to define each item within these lists.
12
13
 
13
14
  - [Usage](#usage)
15
+ - [Unordered List (UL)](#unordered-list-ul)
16
+ - [Ordered List (OL)](#ordered-list-ol)
17
+ - [Customising List Styles](#customising-list-styles)
14
18
  - [Components](#components)
15
19
 
16
20
  ## Usage
@@ -73,32 +77,76 @@ const MyComponent = () => {
73
77
  };
74
78
  ```
75
79
 
80
+ ### Customising List Styles
81
+
82
+ You can customise the appearance of list markers using props like `listStyleIcon`, `listStyleImage`, `listStyleColour`, and dimensions. These props can be applied to the `UL`/`OL` container to affect all items, or on individual `LI` components to override them.
83
+
84
+ <Canvas of={UnorderedListStory.WithCustomIcon} />
85
+
86
+ ```tsx
87
+ import { UL, LI } from '@utilitywarehouse/native-ui';
88
+ import { TickMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
89
+ import { Image } from 'react-native';
90
+
91
+ const MyComponent = () => {
92
+ return (
93
+ <UL listStyleColour="feedbackDangerSurfaceDefault">
94
+ <LI>Primary colored bullet</LI>
95
+ <LI listStyleColour="feedbackPositiveSurfaceDefault">Danger colored bullet override</LI>
96
+ <LI listStyleIcon={TickMediumIcon}>Icon bullet</LI>
97
+ <LI
98
+ listStyleImage={<Image source={{ uri: '...' }} />}
99
+ listStyleWidth={20}
100
+ listStyleHeight={20}
101
+ >
102
+ Image bullet
103
+ </LI>
104
+ </UL>
105
+ );
106
+ };
107
+ ```
108
+
76
109
  ## Components
77
110
 
78
111
  ### `UL`
79
112
 
80
113
  The `UL` component is a container for unordered list items. It inherits all the properties of React Native's [`View` component](https://reactnative.dev/docs/view).
81
114
 
82
- | Property | Type | Default | Description |
83
- | ------------- | ----------------- | ------- | --------------------------------------------------- |
84
- | `gap` | `SpaceValue` | `'100'` | The gap between the list items. |
85
- | `bulletStyle` | `ViewStyle` | - | Custom style for the bullet points. |
86
- | `children` | `React.ReactNode` | - | The `LI` components to be rendered within the list. |
115
+ | Property | Type | Default | Description |
116
+ | ----------------- | --------------------- | ------- | -------------------------------------------------------- |
117
+ | `gap` | `SpaceValue` | `'100'` | The gap between the list items. |
118
+ | `bulletStyle` | `ViewStyle` | - | Custom style for the bullet points. |
119
+ | `children` | `React.ReactNode` | - | The `LI` components to be rendered within the list. |
120
+ | `listStyleImage` | `React.ReactElement` | - | Custom element (e.g. Image) to use as the bullet/marker. |
121
+ | `listStyleIcon` | `React.ComponentType` | - | Custom icon component to use as the bullet/marker. |
122
+ | `listStyleWidth` | `number` | `20` | Width of the custom bullet/marker. |
123
+ | `listStyleHeight` | `number` | `20` | Height of the custom bullet/marker. |
124
+ | `listStyleColour` | `ColorValue` | - | Color of the bullet/marker. |
87
125
 
88
126
  ### `OL`
89
127
 
90
128
  The `OL` component is a container for ordered list items. It inherits all the properties of React Native's [`View` component](https://reactnative.dev/docs/view).
91
129
 
92
- | Property | Type | Default | Description |
93
- | ------------- | ----------------- | ------- | --------------------------------------------------- |
94
- | `gap` | `SpaceValue` | `'100'` | The gap between the list items. |
95
- | `bulletStyle` | `ViewStyle` | - | Custom style for the numbers. |
96
- | `children` | `React.ReactNode` | - | The `LI` components to be rendered within the list. |
130
+ | Property | Type | Default | Description |
131
+ | ----------------- | --------------------- | ------- | -------------------------------------------------------- |
132
+ | `gap` | `SpaceValue` | `'100'` | The gap between the list items. |
133
+ | `bulletStyle` | `ViewStyle` | - | Custom style for the numbers. |
134
+ | `children` | `React.ReactNode` | - | The `LI` components to be rendered within the list. |
135
+ | `listStyleImage` | `React.ReactElement` | - | Custom element (e.g. Image) to use as the bullet/marker. |
136
+ | `listStyleIcon` | `React.ComponentType` | - | Custom icon component to use as the bullet/marker. |
137
+ | `listStyleWidth` | `number` | `20` | Width of the custom bullet/marker. |
138
+ | `listStyleHeight` | `number` | `20` | Height of the custom bullet/marker. |
139
+ | `listStyleColour` | `ColorValue` | - | Color of the number/marker. |
97
140
 
98
141
  ### `LI`
99
142
 
100
143
  The `LI` component represents an item in a list. It wraps its children in a `BodyText` component if the children are a string. It inherits all the properties of React Native's [`View` component](https://reactnative.dev/docs/view).
101
144
 
102
- | Property | Type | Default | Description |
103
- | ---------- | ----------------- | ------- | ------------------------------------------------- |
104
- | `children` | `React.ReactNode` | - | The content to be displayed inside the list item. |
145
+ | Property | Type | Default | Description |
146
+ | ----------------- | --------------------- | ------- | -------------------------------------------------------- |
147
+ | `children` | `React.ReactNode` | - | The content to be displayed inside the list item. |
148
+ | `listStyleImage` | `React.ReactElement` | - | Custom element (e.g. Image) to use as the bullet/marker. |
149
+ | `listStyleIcon` | `React.ComponentType` | - | Custom icon component to use as the bullet/marker. |
150
+ | `listStyleWidth` | `number` | `20` | Width of the custom bullet/marker. |
151
+ | `listStyleHeight` | `number` | `20` | Height of the custom bullet/marker. |
152
+ | `listStyleColour` | `ColorValue` | - | Color of the bullet/marker/number. |
@@ -1,9 +1,10 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-vite';
2
- import OrderedList from './OrderedList';
3
- import ListItem from './ListItem';
4
2
  import { primitive } from '@utilitywarehouse/hearth-tokens/js';
3
+ import { View } from 'react-native';
5
4
  import { InputType } from 'storybook/internal/types';
6
5
  import { SpaceValue } from '../../types';
6
+ import ListItem from './ListItem';
7
+ import OrderedList from './OrderedList';
7
8
 
8
9
  const gap: InputType = {
9
10
  options: Object.keys(primitive.space),
@@ -53,3 +54,33 @@ export const WithCustomGap: Story = {
53
54
  </OrderedList>
54
55
  ),
55
56
  };
57
+
58
+ export const WithColoredNumbers: Story = {
59
+ render: ({ ...args }) => (
60
+ <OrderedList {...args} listStyleColour="piggyPink300">
61
+ <ListItem>Item 1</ListItem>
62
+ <ListItem>Item 2</ListItem>
63
+ </OrderedList>
64
+ ),
65
+ };
66
+
67
+ export const WithIconOverride: Story = {
68
+ render: ({ ...args }) => {
69
+ const CustomIcon = (props: any) => (
70
+ <View
71
+ style={{
72
+ width: props.width,
73
+ height: props.height,
74
+ backgroundColor: props.color || 'blue',
75
+ borderRadius: 4,
76
+ }}
77
+ />
78
+ );
79
+ return (
80
+ <OrderedList {...args} listStyleIcon={CustomIcon}>
81
+ <ListItem>Item 1 (overridden)</ListItem>
82
+ <ListItem>Item 2 (overridden)</ListItem>
83
+ </OrderedList>
84
+ );
85
+ },
86
+ };
@@ -1,27 +1,75 @@
1
1
  import React from 'react';
2
2
  import { StyleSheet, View, ViewProps, ViewStyle } from 'react-native';
3
- import { useStyleProps } from '../../hooks';
3
+ import { useStyleProps, useTheme } from '../../hooks';
4
4
  import { SpaceValue } from '../../types';
5
+ import { getFlattenedColorValue } from '../../utils';
5
6
  import { BodyText } from '../BodyText';
7
+ import { ListItemProps, ListStyleProps } from './ListItem';
6
8
 
7
- export interface OrderedListProps extends ViewProps {
9
+ export interface OrderedListProps extends ViewProps, ListStyleProps {
8
10
  children: ViewProps['children'];
9
11
  gap?: SpaceValue;
10
12
  bulletStyle?: ViewStyle;
11
13
  }
12
14
 
13
- const OrderedList = ({ children, gap = '100', style, ...rest }: OrderedListProps) => {
15
+ const OrderedList = ({
16
+ children,
17
+ gap = '100',
18
+ style,
19
+ listStyleImage,
20
+ listStyleIcon,
21
+ listStyleWidth,
22
+ listStyleHeight,
23
+ listStyleColour,
24
+ ...rest
25
+ }: OrderedListProps) => {
14
26
  const { computedStyles } = useStyleProps({ gap });
27
+ const theme = useTheme();
15
28
  let itemNumber = 0;
29
+
16
30
  return (
17
31
  <View style={[computedStyles, style]} {...rest}>
18
32
  {React.Children.map(children, child => {
19
33
  if (React.isValidElement(child)) {
20
34
  itemNumber++;
35
+ const childProps = child.props as ListItemProps;
36
+
37
+ const image = childProps.listStyleImage ?? listStyleImage;
38
+ const Icon = childProps.listStyleIcon ?? listStyleIcon;
39
+ const width = childProps.listStyleWidth ?? listStyleWidth ?? 20;
40
+ const height = childProps.listStyleHeight ?? listStyleHeight ?? 20;
41
+ const colourRaw = childProps.listStyleColour ?? listStyleColour;
42
+
43
+ const colour = colourRaw ? getFlattenedColorValue(colourRaw, theme.color) : undefined;
44
+
45
+ let bullet;
46
+ if (image) {
47
+ const imageEl = image as React.ReactElement<any>;
48
+ bullet = React.cloneElement(imageEl, {
49
+ style: [{ width, height }, imageEl.props.style],
50
+ });
51
+ } else if (Icon) {
52
+ bullet = (
53
+ <Icon width={width} height={height} color={colour ?? theme.color.text.primary} />
54
+ );
55
+ } else {
56
+ bullet = (
57
+ <BodyText
58
+ style={[styles.number, colour && { color: colour }]}
59
+ >{`${itemNumber}.`}</BodyText>
60
+ );
61
+ }
62
+
63
+ const isCustom = !!(image || Icon);
64
+
21
65
  return (
22
66
  <View style={styles.listItemContainer}>
23
- <BodyText style={styles.number}>{`${itemNumber}.`}</BodyText>
24
- {React.cloneElement(child as React.ReactElement<any>, {}) as ViewProps['children']}
67
+ {isCustom ? <View style={{ marginRight: 8 }}>{bullet}</View> : bullet}
68
+ {
69
+ React.cloneElement(child as React.ReactElement<ListItemProps>, {
70
+ style: [childProps.style, { flex: 1 }],
71
+ }) as ViewProps['children']
72
+ }
25
73
  </View>
26
74
  );
27
75
  }
@@ -40,7 +88,7 @@ const styles = StyleSheet.create({
40
88
  },
41
89
  number: {
42
90
  marginRight: 8,
43
- lineHeight: undefined, // Allow number to align with first line of text
91
+ lineHeight: undefined,
44
92
  },
45
93
  });
46
94
 
@@ -1,10 +1,11 @@
1
- import { Meta, StoryObj } from '@storybook/react-vite';
2
- import UnorderedList from './UnorderedList';
3
- import ListItem from './ListItem';
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import { TickMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
3
  import { primitive } from '@utilitywarehouse/hearth-tokens/js';
5
- import { InputType } from 'storybook/internal/types';
4
+ import { Image, View } from 'react-native';
5
+ import ListItem from './ListItem';
6
+ import UnorderedList from './UnorderedList';
6
7
 
7
- const gap: InputType = {
8
+ const gap = {
8
9
  options: Object.keys(primitive.space),
9
10
  control: 'select',
10
11
  description: 'Gap between list items.',
@@ -50,3 +51,60 @@ export const WithCustomGap: Story = {
50
51
  </UnorderedList>
51
52
  ),
52
53
  };
54
+
55
+ export const WithCustomIcon: Story = {
56
+ render: ({ ...args }) => {
57
+ return (
58
+ <UnorderedList
59
+ {...args}
60
+ listStyleIcon={TickMediumIcon}
61
+ listStyleColour="feedbackDangerSurfaceDefault"
62
+ >
63
+ <ListItem>List item 1 with icon</ListItem>
64
+ <ListItem>List item 2 with icon</ListItem>
65
+ <ListItem>
66
+ List item 3 with icon is a long example to test alignment, lorem ipsum dolor sit amet
67
+ </ListItem>
68
+ </UnorderedList>
69
+ );
70
+ },
71
+ };
72
+
73
+ export const WithCustomImage: Story = {
74
+ render: ({ ...args }) => (
75
+ <UnorderedList
76
+ {...args}
77
+ listStyleImage={
78
+ <Image
79
+ source={{ uri: 'https://placehold.co/20x20.png' }}
80
+ style={{ width: 20, height: 20 }}
81
+ />
82
+ }
83
+ >
84
+ <ListItem>List item 1 with image</ListItem>
85
+ <ListItem>List item 2 with image</ListItem>
86
+ </UnorderedList>
87
+ ),
88
+ };
89
+
90
+ export const WithIndividualItemOverride: Story = {
91
+ render: ({ ...args }) => {
92
+ const CheckIcon = (props: any) => (
93
+ <View
94
+ style={{
95
+ width: props.width,
96
+ height: props.height,
97
+ backgroundColor: 'green',
98
+ borderRadius: 10,
99
+ }}
100
+ />
101
+ );
102
+ return (
103
+ <UnorderedList {...args}>
104
+ <ListItem>Default bullet item</ListItem>
105
+ <ListItem listStyleIcon={CheckIcon}>Success item</ListItem>
106
+ <ListItem listStyleColour="blue600">Colored bullet item</ListItem>
107
+ </UnorderedList>
108
+ );
109
+ },
110
+ };
@@ -1,25 +1,69 @@
1
1
  import React from 'react';
2
2
  import { StyleSheet, View, ViewProps, ViewStyle } from 'react-native';
3
- import { useStyleProps } from '../../hooks';
3
+ import { useStyleProps, useTheme } from '../../hooks';
4
4
  import { SpaceValue } from '../../types';
5
+ import { getFlattenedColorValue } from '../../utils';
5
6
  import { BodyText } from '../BodyText';
7
+ import { ListItemProps, ListStyleProps } from './ListItem';
6
8
 
7
- export interface UnorderedListProps extends ViewProps {
9
+ export interface UnorderedListProps extends ViewProps, ListStyleProps {
8
10
  children: ViewProps['children'];
9
11
  gap?: SpaceValue;
10
12
  bulletStyle?: ViewStyle;
11
13
  }
12
14
 
13
- const UnorderedList = ({ children, gap = '100', style, ...rest }: UnorderedListProps) => {
15
+ const UnorderedList = ({
16
+ children,
17
+ gap = '100',
18
+ style,
19
+ listStyleImage,
20
+ listStyleIcon,
21
+ listStyleWidth,
22
+ listStyleHeight,
23
+ listStyleColour,
24
+ ...rest
25
+ }: UnorderedListProps) => {
14
26
  const { computedStyles } = useStyleProps({ gap });
27
+ const theme = useTheme();
28
+
15
29
  return (
16
30
  <View style={[computedStyles, style]} {...rest}>
17
31
  {React.Children.map(children, child => {
18
32
  if (React.isValidElement(child)) {
33
+ const childProps = child.props as ListItemProps;
34
+
35
+ const image = childProps.listStyleImage ?? listStyleImage;
36
+ const Icon = childProps.listStyleIcon ?? listStyleIcon;
37
+ const width = childProps.listStyleWidth ?? listStyleWidth ?? 20;
38
+ const height = childProps.listStyleHeight ?? listStyleHeight ?? 20;
39
+ const colourRaw = childProps.listStyleColour ?? listStyleColour;
40
+
41
+ const colour = colourRaw ? getFlattenedColorValue(colourRaw, theme.color) : undefined;
42
+
43
+ let bullet;
44
+ if (image) {
45
+ const imageEl = image as React.ReactElement<any>;
46
+ bullet = React.cloneElement(imageEl, {
47
+ style: [{ width, height }, imageEl.props.style],
48
+ });
49
+ } else if (Icon) {
50
+ bullet = (
51
+ <Icon width={width} height={height} color={colour ?? theme.color.text.primary} />
52
+ );
53
+ } else {
54
+ bullet = <BodyText style={[styles.bullet, colour && { color: colour }]}>•</BodyText>;
55
+ }
56
+
57
+ const isCustom = !!(image || Icon);
58
+
19
59
  return (
20
60
  <View style={styles.listItemContainer}>
21
- <BodyText style={styles.bullet}>•</BodyText>
22
- {React.cloneElement(child as React.ReactElement<any>, {}) as ViewProps['children']}
61
+ {isCustom ? <View style={{ marginRight: 8 }}>{bullet}</View> : bullet}
62
+ {
63
+ React.cloneElement(child as React.ReactElement<ListItemProps>, {
64
+ style: [childProps.style, { flex: 1 }],
65
+ }) as ViewProps['children']
66
+ }
23
67
  </View>
24
68
  );
25
69
  }
@@ -38,7 +82,7 @@ const styles = StyleSheet.create({
38
82
  },
39
83
  bullet: {
40
84
  marginRight: 8,
41
- lineHeight: undefined, // Allow bullet to align with first line of text
85
+ lineHeight: undefined,
42
86
  },
43
87
  });
44
88
 
@@ -52,7 +52,7 @@ const styles = StyleSheet.create(theme => ({
52
52
  container: {
53
53
  flexDirection: 'row',
54
54
  gap: theme.components.formField.helper.gap,
55
- alignItems: 'center',
55
+ alignItems: 'flex-start',
56
56
  variants: {
57
57
  disabled: {
58
58
  true: {
@@ -23,6 +23,7 @@ HelperText.displayName = 'HelperText';
23
23
  const styles = StyleSheet.create(theme => ({
24
24
  text: {
25
25
  color: theme.color.text.secondary,
26
+ flexShrink: 1,
26
27
  variants: {
27
28
  validationStatus: {
28
29
  valid: {
@@ -48,6 +48,7 @@ const Input = forwardRef<TextInput, InputProps>(
48
48
  clearable = false,
49
49
  required,
50
50
  inBottomSheet = false,
51
+ style,
51
52
  ...props
52
53
  },
53
54
  ref
@@ -96,6 +97,7 @@ const Input = forwardRef<TextInput, InputProps>(
96
97
  isFocused={focused}
97
98
  type={type as undefined}
98
99
  isRequired={isRequired}
100
+ style={style}
99
101
  >
100
102
  {children ? (
101
103
  <>{children}</>
@@ -12,6 +12,7 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
12
12
  description?: string;
13
13
  inNavModal?: boolean;
14
14
  fullscreen?: boolean;
15
+ stickyFooter?: boolean;
15
16
  children?: ViewProps['children'];
16
17
  onPressPrimaryButton?: () => void;
17
18
  primaryButtonText?: string;
@@ -1,4 +1,9 @@
1
- import { BottomSheetScrollViewMethods, SNAP_POINT_TYPE } from '@gorhom/bottom-sheet';
1
+ import {
2
+ BottomSheetFooter,
3
+ BottomSheetFooterProps,
4
+ BottomSheetScrollViewMethods,
5
+ SNAP_POINT_TYPE,
6
+ } from '@gorhom/bottom-sheet';
2
7
  import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
3
8
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
9
  import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
@@ -43,6 +48,7 @@ const Modal = ({
43
48
  secondaryButtonProps,
44
49
  closeButtonProps,
45
50
  inNavModal = false,
51
+ stickyFooter = true,
46
52
  ...props
47
53
  }: ModalProps) => {
48
54
  const bottomSheetModalRef = useRef<BottomSheetModal>(null);
@@ -155,7 +161,37 @@ const Modal = ({
155
161
  }
156
162
  };
157
163
 
158
- styles.useVariants({ loading });
164
+ const noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
165
+
166
+ styles.useVariants({
167
+ loading,
168
+ bothButtons: !!(onPressPrimaryButton && onPressSecondaryButton),
169
+ noButtons,
170
+ stickyFooter,
171
+ });
172
+
173
+ const footer = (
174
+ <View style={styles.footer}>
175
+ {onPressPrimaryButton && primaryButtonText ? (
176
+ <Button
177
+ onPress={handlePrimaryButtonPress}
178
+ text={primaryButtonText}
179
+ {...primaryButtonProps}
180
+ variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
181
+ colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
182
+ />
183
+ ) : null}
184
+ {onPressSecondaryButton && secondaryButtonText ? (
185
+ <Button
186
+ onPress={handleSecondaryButtonPress}
187
+ text={secondaryButtonText}
188
+ {...secondaryButtonProps}
189
+ variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
190
+ colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
191
+ />
192
+ ) : null}
193
+ </View>
194
+ );
159
195
 
160
196
  const content = (
161
197
  <>
@@ -216,31 +252,28 @@ const Modal = ({
216
252
  </View>
217
253
  ) : null}
218
254
  {children}
219
- <View style={styles.footer}>
220
- {onPressPrimaryButton && primaryButtonText ? (
221
- <Button
222
- onPress={handlePrimaryButtonPress}
223
- text={primaryButtonText}
224
- {...primaryButtonProps}
225
- variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
226
- colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
227
- />
228
- ) : null}
229
- {onPressSecondaryButton && secondaryButtonText ? (
230
- <Button
231
- onPress={handleSecondaryButtonPress}
232
- text={secondaryButtonText}
233
- {...secondaryButtonProps}
234
- variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
235
- colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
236
- />
237
- ) : null}
238
- </View>
255
+ {!stickyFooter && !noButtons ? footer : null}
239
256
  </View>
240
257
  )}
241
258
  </>
242
259
  );
243
260
 
261
+ const renderFooter = useCallback(
262
+ (props: BottomSheetFooterProps) => (
263
+ <BottomSheetFooter {...props}>
264
+ <View style={styles.footerWrap}>{footer}</View>
265
+ </BottomSheetFooter>
266
+ ),
267
+ [
268
+ onPressPrimaryButton,
269
+ primaryButtonText,
270
+ onPressSecondaryButton,
271
+ secondaryButtonText,
272
+ primaryButtonProps,
273
+ secondaryButtonProps,
274
+ ]
275
+ );
276
+
244
277
  return inNavModal ? (
245
278
  <View style={{ flex: 1, backgroundColor: theme.color.background.primary }}>
246
279
  {Platform.OS === 'android' ? (
@@ -262,11 +295,12 @@ const Modal = ({
262
295
  showHandle={typeof loading !== 'undefined' && loading ? false : props.showHandle}
263
296
  accessible={false}
264
297
  style={styles.modal}
298
+ footerComponent={stickyFooter && !noButtons ? renderFooter : undefined}
265
299
  {...props}
266
300
  onChange={handleChange}
267
301
  >
268
302
  {loading ? <View style={styles.loadingTop} /> : null}
269
- <BottomSheetScrollView contentContainerStyle={styles.container} ref={scrollViewRef}>
303
+ <BottomSheetScrollView contentContainerStyle={styles.scrollView} ref={scrollViewRef}>
270
304
  {content}
271
305
  </BottomSheetScrollView>
272
306
  </BottomSheetModal>
@@ -288,6 +322,30 @@ const styles = StyleSheet.create((theme, rt) => ({
288
322
  },
289
323
  },
290
324
  },
325
+ scrollView: {
326
+ flex: 1,
327
+ variants: {
328
+ bothButtons: {
329
+ true: {
330
+ paddingBottom: 166 + rt.insets.bottom - theme.components.modal.padding,
331
+ },
332
+ false: {
333
+ paddingBottom: 102 + rt.insets.bottom - theme.components.modal.padding,
334
+ },
335
+ },
336
+ noButtons: {
337
+ true: {
338
+ paddingBottom: rt.insets.bottom + theme.components.modal.padding,
339
+ },
340
+ },
341
+ stickyFooter: {
342
+ true: {},
343
+ false: {
344
+ paddingBottom: rt.insets.bottom + theme.components.modal.padding,
345
+ },
346
+ },
347
+ },
348
+ },
291
349
  header: {
292
350
  flexDirection: 'row',
293
351
  gap: theme.components.modal.gap,
@@ -321,6 +379,11 @@ const styles = StyleSheet.create((theme, rt) => ({
321
379
  footer: {
322
380
  gap: theme.components.modal.action.gap,
323
381
  },
382
+ footerWrap: {
383
+ backgroundColor: theme.color.surface.neutral.strong,
384
+ paddingHorizontal: theme.components.bottomSheet.padding,
385
+ paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
386
+ },
324
387
  inNavModalContainer: {
325
388
  flex: 1,
326
389
  ...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
@@ -1,22 +1,36 @@
1
1
  import React from 'react';
2
2
  import { ScrollViewProps, ViewStyle } from 'react-native';
3
3
 
4
- export interface PillGroupProps
5
- extends Omit<ScrollViewProps, 'horizontal' | 'contentContainerStyle' | 'showsHorizontalScrollIndicator'> {
6
- /** Controlled selected value(s) */
7
- value: string | string[];
8
-
9
- /** Multi-select mode. Default = false */
10
- multiple?: boolean;
11
-
4
+ export interface PillGroupBaseProps
5
+ extends Omit<
6
+ ScrollViewProps,
7
+ 'horizontal' | 'contentContainerStyle' | 'showsHorizontalScrollIndicator'
8
+ > {
12
9
  /** Allow pills to wrap lines. Default = true */
13
10
  wrap?: boolean;
14
11
 
15
- /** Handle selection changes */
16
- onChange?: (value: string | string[]) => void;
17
-
18
12
  /** Children must be <Pill> elements */
19
13
  children: React.ReactNode;
20
14
 
21
15
  style?: ViewStyle | ViewStyle[];
22
16
  }
17
+
18
+ interface SinglePillGroupProps extends PillGroupBaseProps {
19
+ /** Multi-select mode. Default = false */
20
+ multiple?: false;
21
+ /** Controlled selected value */
22
+ value: string;
23
+ /** Handle selection changes */
24
+ onChange?: (value: string) => void;
25
+ }
26
+
27
+ interface MultiPillGroupProps extends PillGroupBaseProps {
28
+ /** Multi-select mode. Default = false */
29
+ multiple: true;
30
+ /** Controlled selected value(s) */
31
+ value: string[];
32
+ /** Handle selection changes */
33
+ onChange?: (value: string[]) => void;
34
+ }
35
+
36
+ export type PillGroupProps = SinglePillGroupProps | MultiPillGroupProps;