@utilitywarehouse/hearth-react-native 0.29.1 → 0.30.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 (54) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +82 -0
  4. package/build/components/BottomSheet/BottomSheet.context.d.ts +1 -0
  5. package/build/components/BottomSheet/BottomSheet.context.js +3 -1
  6. package/build/components/BottomSheet/BottomSheet.js +5 -1
  7. package/build/components/BottomSheet/BottomSheetFlatList.js +16 -2
  8. package/build/components/BottomSheet/BottomSheetModal.js +5 -1
  9. package/build/components/BottomSheet/BottomSheetModalProvider.d.ts +9 -0
  10. package/build/components/BottomSheet/BottomSheetModalProvider.js +14 -0
  11. package/build/components/BottomSheet/BottomSheetScrollView.js +16 -2
  12. package/build/components/BottomSheet/BottomSheetView.js +16 -2
  13. package/build/components/BottomSheet/index.d.ts +3 -1
  14. package/build/components/BottomSheet/index.js +2 -1
  15. package/build/components/Combobox/Combobox.js +3 -1
  16. package/build/components/DescriptionList/DescriptionListItem.js +1 -2
  17. package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +2 -2
  18. package/build/components/Heading/Heading.js +1 -1
  19. package/build/components/List/List.js +2 -2
  20. package/build/components/List/ListAction/ListAction.js +1 -1
  21. package/build/components/List/ListItem/ListItemRoot.js +2 -2
  22. package/build/components/Modal/Modal.d.ts +1 -1
  23. package/build/components/Modal/Modal.js +56 -10
  24. package/build/components/Modal/Modal.shared.types.d.ts +1 -0
  25. package/build/components/NavModal/NavModal.d.ts +1 -1
  26. package/build/components/NavModal/NavModal.js +2 -2
  27. package/build/components/Select/Select.js +3 -1
  28. package/docs/changelog.mdx +86 -0
  29. package/package.json +3 -3
  30. package/src/components/BottomSheet/BottomSheet.context.ts +4 -1
  31. package/src/components/BottomSheet/BottomSheet.docs.mdx +20 -0
  32. package/src/components/BottomSheet/BottomSheet.tsx +8 -1
  33. package/src/components/BottomSheet/BottomSheetFlatList.tsx +16 -2
  34. package/src/components/BottomSheet/BottomSheetModal.tsx +8 -1
  35. package/src/components/BottomSheet/BottomSheetModalProvider.tsx +33 -0
  36. package/src/components/BottomSheet/BottomSheetScrollView.tsx +16 -2
  37. package/src/components/BottomSheet/BottomSheetView.tsx +17 -2
  38. package/src/components/BottomSheet/index.ts +2 -1
  39. package/src/components/Combobox/Combobox.tsx +3 -1
  40. package/src/components/DescriptionList/DescriptionListItem.tsx +1 -2
  41. package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +4 -4
  42. package/src/components/Heading/Heading.docs.mdx +12 -3
  43. package/src/components/Heading/Heading.tsx +1 -0
  44. package/src/components/List/List.docs.mdx +2 -2
  45. package/src/components/List/List.stories.tsx +6 -5
  46. package/src/components/List/List.tsx +14 -2
  47. package/src/components/List/ListAction/ListAction.tsx +1 -0
  48. package/src/components/List/ListItem/ListItemRoot.tsx +3 -3
  49. package/src/components/Modal/Modal.docs.mdx +36 -21
  50. package/src/components/Modal/Modal.shared.types.ts +1 -0
  51. package/src/components/Modal/Modal.tsx +60 -9
  52. package/src/components/NavModal/NavModal.docs.mdx +1 -0
  53. package/src/components/NavModal/NavModal.tsx +10 -1
  54. package/src/components/Select/Select.tsx +3 -1
@@ -6,14 +6,16 @@ import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native'
6
6
  import { StyleSheet } from 'react-native-unistyles';
7
7
  import { BodyText } from '../BodyText';
8
8
  import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
9
+ import { useBottomSheetContext } from '../BottomSheet/BottomSheet.context';
9
10
  import { Button } from '../Button';
10
11
  import { Heading } from '../Heading';
11
12
  import { Spinner } from '../Spinner';
12
13
  import { UnstyledIconButton } from '../UnstyledIconButton';
13
- const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, ...props }) => {
14
+ const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', loadingDescription, fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, ...props }) => {
14
15
  const bottomSheetModalRef = useRef(null);
15
16
  const viewRef = useRef(null);
16
17
  const scrollViewRef = useRef(null);
18
+ const { useSafeAreaInsets } = useBottomSheetContext();
17
19
  useImperativeHandle(ref, () => ({
18
20
  ...bottomSheetModalRef.current,
19
21
  }));
@@ -64,6 +66,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
64
66
  noButtons,
65
67
  stickyFooter,
66
68
  showHandle: props.showHandle,
69
+ useSafeAreaInsets,
67
70
  });
68
71
  const footer = useMemo(() => (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] })), [
69
72
  handlePrimaryButtonPress,
@@ -75,7 +78,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
75
78
  secondaryButtonProps,
76
79
  secondaryButtonText,
77
80
  ]);
78
- 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" }), _jsx(Heading, { size: "lg", textAlign: "center", 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, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, !stickyFooter && !noButtons ? footer : null] })) }));
81
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? (loadingHeading ?? 'Loading') : undefined, accessibilityHint: Platform.OS === 'android' && loadingDescription ? loadingDescription : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: loadingHeading }), loadingDescription ? _jsx(BodyText, { textAlign: "center", children: loadingDescription }) : null] })) : (_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, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, !stickyFooter && !noButtons ? footer : null] })) }));
79
82
  const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [footer]);
80
83
  return (_jsxs(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, style: styles.modal, footerComponent: stickyFooter && !noButtons ? renderFooter : undefined, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.scrollView, ref: scrollViewRef, children: content })] }));
81
84
  };
@@ -99,34 +102,68 @@ const styles = StyleSheet.create((theme, rt) => ({
99
102
  variants: {
100
103
  bothButtons: {
101
104
  true: {
105
+ paddingBottom: 166,
106
+ },
107
+ false: {
108
+ paddingBottom: 102,
109
+ },
110
+ },
111
+ noButtons: {
112
+ true: {
113
+ paddingBottom: theme.components.modal.padding,
114
+ },
115
+ },
116
+ stickyFooter: {
117
+ true: {},
118
+ false: {
119
+ paddingBottom: theme.components.modal.padding + theme.components.bottomSheet.padding,
120
+ },
121
+ },
122
+ useSafeAreaInsets: {
123
+ true: {},
124
+ false: {},
125
+ },
126
+ },
127
+ compoundVariants: [
128
+ {
129
+ bothButtons: true,
130
+ useSafeAreaInsets: true,
131
+ styles: {
102
132
  paddingBottom: 166 +
103
133
  rt.insets.bottom -
104
134
  theme.components.modal.padding +
105
135
  theme.components.bottomSheet.padding,
106
136
  },
107
- false: {
137
+ },
138
+ {
139
+ bothButtons: false,
140
+ useSafeAreaInsets: true,
141
+ styles: {
108
142
  paddingBottom: 102 +
109
143
  rt.insets.bottom -
110
144
  theme.components.modal.padding +
111
145
  theme.components.bottomSheet.padding,
112
146
  },
113
147
  },
114
- noButtons: {
115
- true: {
148
+ {
149
+ noButtons: true,
150
+ useSafeAreaInsets: true,
151
+ styles: {
116
152
  paddingBottom: rt.insets.bottom +
117
153
  theme.components.modal.padding +
118
154
  theme.components.bottomSheet.padding,
119
155
  },
120
156
  },
121
- stickyFooter: {
122
- true: {},
123
- false: {
157
+ {
158
+ useSafeAreaInsets: true,
159
+ stickyFooter: false,
160
+ styles: {
124
161
  paddingBottom: rt.insets.bottom +
125
162
  theme.components.modal.padding +
126
163
  theme.components.bottomSheet.padding,
127
164
  },
128
165
  },
129
- },
166
+ ],
130
167
  },
131
168
  header: {
132
169
  flexDirection: 'row',
@@ -172,7 +209,16 @@ const styles = StyleSheet.create((theme, rt) => ({
172
209
  footerWrap: {
173
210
  backgroundColor: theme.color.surface.neutral.strong,
174
211
  paddingHorizontal: theme.components.bottomSheet.padding,
175
- paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
212
+ variants: {
213
+ useSafeAreaInsets: {
214
+ true: {
215
+ paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
216
+ },
217
+ false: {
218
+ paddingBottom: theme.components.bottomSheet.padding,
219
+ },
220
+ },
221
+ },
176
222
  },
177
223
  }));
178
224
  export default Modal;
@@ -8,6 +8,7 @@ export interface ModalCommonProps {
8
8
  showCloseButton?: boolean;
9
9
  heading?: string;
10
10
  loadingHeading?: string;
11
+ loadingDescription?: string;
11
12
  description?: string;
12
13
  stickyFooter?: boolean;
13
14
  children?: ViewProps['children'];
@@ -1,3 +1,3 @@
1
1
  import NavModalProps from './NavModal.props';
2
- declare const NavModal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter, background, scrollable, presentation, scrollViewProps, safeAreaViewProps, ...props }: NavModalProps) => import("react/jsx-runtime").JSX.Element;
2
+ declare const NavModal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading, loadingDescription, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter, background, scrollable, presentation, scrollViewProps, safeAreaViewProps, ...props }: NavModalProps) => import("react/jsx-runtime").JSX.Element;
3
3
  export default NavModal;
@@ -12,7 +12,7 @@ import { Button } from '../Button';
12
12
  import { Heading } from '../Heading';
13
13
  import { Spinner } from '../Spinner';
14
14
  import { UnstyledIconButton } from '../UnstyledIconButton';
15
- const NavModal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading = 'Loading...', image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, background = 'default', scrollable = true, presentation = 'modal', scrollViewProps, safeAreaViewProps, ...props }) => {
15
+ const NavModal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, loading, loadingHeading = 'Loading...', loadingDescription, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, stickyFooter = true, background = 'default', scrollable = true, presentation = 'modal', scrollViewProps, safeAreaViewProps, ...props }) => {
16
16
  const theme = useTheme();
17
17
  const backgroundOpacity = useSharedValue(0);
18
18
  const pretendContentTranslateY = useSharedValue(20);
@@ -78,7 +78,7 @@ const NavModal = ({ ref, children, heading, description, showCloseButton = true,
78
78
  secondaryButtonProps,
79
79
  secondaryButtonText,
80
80
  ]);
81
- 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, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground, ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground, children: description })) : null] })] })) : null, scrollable ? (_jsxs(ScrollView, { style: {
81
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? (loadingHeading ?? 'Loading') : undefined, accessibilityHint: Platform.OS === 'android' && loadingDescription ? loadingDescription : undefined, screenReaderFocusable: true, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground, children: loadingHeading }), loadingDescription ? (_jsx(BodyText, { size: "md", textAlign: "center", inverted: isBrandBackground, children: loadingDescription })) : null] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground, ...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, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground, children: description })) : null] })] })) : null, scrollable ? (_jsxs(ScrollView, { style: {
82
82
  flex: stickyFooter ? 1 : 0,
83
83
  marginHorizontal: -4,
84
84
  }, contentContainerStyle: { paddingHorizontal: 4 }, ...scrollViewProps, children: [children, !stickyFooter && !noButtons ? (_jsx(View, { style: styles.inNavModalFooterContainer, children: footer })) : null] })) : (_jsxs(View, { style: {
@@ -6,6 +6,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
6
6
  import { StyleSheet } from 'react-native-unistyles';
7
7
  import { BodyText } from '../BodyText';
8
8
  import { BottomSheetFlatList, BottomSheetModal, BottomSheetScrollView, BottomSheetView, } from '../BottomSheet';
9
+ import { useBottomSheetContext } from '../BottomSheet/BottomSheet.context';
9
10
  import { DetailText } from '../DetailText';
10
11
  import { FormField, useFormFieldContext } from '../FormField';
11
12
  import { Icon } from '../Icon';
@@ -18,6 +19,7 @@ const Select = ({ options = [], value, onValueChange, label, labelVariant = 'bod
18
19
  const isRequired = formFieldContext?.required ?? required;
19
20
  const isDisabled = formFieldContext?.disabled ?? disabled;
20
21
  const isReadonly = formFieldContext?.readonly ?? readonly;
22
+ const { useSafeAreaInsets } = useBottomSheetContext();
21
23
  const bottomSheetModalRef = useRef(null);
22
24
  const [search, setSearch] = useState('');
23
25
  const [isOpen, setIsOpen] = useState(false);
@@ -77,7 +79,7 @@ const Select = ({ options = [], value, onValueChange, label, labelVariant = 'bod
77
79
  selectedValue: value,
78
80
  onValueChange,
79
81
  close: closeBottomSheet,
80
- }, children: _jsxs(SafeAreaView, { edges: ['top'], style: { flex: 1 }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search", testID: testID ? `${testID}-search` : undefined }) })), children ? (_jsx(BottomSheetScrollView, { testID: testID ? `${testID}-options` : undefined, children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, testID: testID ? `${testID}-options` : undefined, ...listProps }))] }) }) })] }));
82
+ }, children: _jsxs(SafeAreaView, { edges: useSafeAreaInsets ? ['top'] : [], style: { flex: 1 }, children: [menuHeading && (_jsx(View, { style: styles.headingContainer, children: _jsx(DetailText, { size: "lg", children: menuHeading }) })), searchable && (_jsx(View, { style: styles.searchContainer, children: _jsx(Input, { placeholder: searchPlaceholder, value: search, inBottomSheet: true, onChangeText: setSearch, type: "search", testID: testID ? `${testID}-search` : undefined }) })), children ? (_jsx(BottomSheetScrollView, { testID: testID ? `${testID}-options` : undefined, children: children })) : (_jsx(BottomSheetFlatList, { data: filteredOptions, keyExtractor: (option) => option.value, renderItem: renderSelectOption, ListEmptyComponent: renderEmptyComponent, testID: testID ? `${testID}-options` : undefined, ...listProps }))] }) }) })] }));
81
83
  };
82
84
  const styles = StyleSheet.create(theme => ({
83
85
  container: {
@@ -9,6 +9,92 @@ import { BackToTopButton, NextPrevPage } from './components';
9
9
  The changelog for the Hearth React Native library. Here you can find all the changes, improvements, and bug fixes for each version.
10
10
 
11
11
 
12
+ ## 0.29.2
13
+
14
+ ### Patch Changes
15
+
16
+ - [#1067](https://github.com/utilitywarehouse/hearth/pull/1067) [`893cbfd`](https://github.com/utilitywarehouse/hearth/commit/893cbfd1bf090b8b75df6f58f2babaf8ba1e0033) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add a `useSafeAreaInsets` prop to `BottomSheetModalProvider` to control Hearth's bottom-sheet safe-area spacing.
17
+
18
+ Bottom-sheet wrappers such as `BottomSheetView`, `BottomSheetScrollView`, `BottomSheetFlatList`, and components that render `SafeAreaView` inside a bottom sheet now respect `BottomSheetModalProvider` configuration.
19
+
20
+ **Components affected**:
21
+ - `BottomSheetModalProvider`
22
+ - `BottomSheetView`
23
+ - `BottomSheetScrollView`
24
+ - `BottomSheetFlatList`
25
+ - `Modal`
26
+ - `Select`
27
+ - `Combobox`
28
+
29
+ **Developer changes**:
30
+
31
+ No changes are required if you want the current behaviour. If your app already applies its own safe-area padding around bottom-sheet content, opt out like this:
32
+
33
+ ```tsx
34
+ <BottomSheetModalProvider useSafeAreaInsets={false}>
35
+ {/* Your app content */}
36
+ </BottomSheetModalProvider>
37
+ ```
38
+
39
+ ## 0.29.1
40
+
41
+ ### Patch Changes
42
+
43
+ - [#1062](https://github.com/utilitywarehouse/hearth/pull/1062) [`0da3ffe`](https://github.com/utilitywarehouse/hearth/commit/0da3ffe12691a4287694ae9fcb2290d459e3c041) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Respect the selected `NavModal` background style
44
+
45
+ Fixed an issue where `NavModal` always rendered its inner content with the default surface background, which prevented the `background="brand"` treatment from being applied correctly.
46
+
47
+ **Components affected**:
48
+ - `NavModal`
49
+
50
+ **Developer changes**:
51
+
52
+ No changes are required.
53
+
54
+ ## 0.29.0
55
+
56
+ ### Minor Changes
57
+
58
+ - [#1060](https://github.com/utilitywarehouse/hearth/pull/1060) [`05d38f9`](https://github.com/utilitywarehouse/hearth/commit/05d38f9414fec6d6b4a0733829b4d290d96fccae) Thanks [@jordmccord](https://github.com/jordmccord)! - 💔 [BREAKING CHANGE]: Extract navigation-presented modal behaviour into `NavModal`
59
+
60
+ `Modal` now only supports the bottom-sheet modal behaviour. The React Navigation screen-based API that was previously exposed through `inNavModal`, `background`, and `scrollable` has moved to the new `NavModal` component.
61
+
62
+ `NavModal` also adds a `presentation` prop so the component can match the React Navigation screen presentation style for sheet-style and full-screen modal routes.
63
+
64
+ The package-owned `SafeAreaView` component has also been removed in favour of the `react-native-safe-area-context` `SafeAreaView`. Hearth now re-exports that implementation from the package root.
65
+
66
+ **Components affected**:
67
+ - `Modal`
68
+ - `NavModal`
69
+ - `SafeAreaView`
70
+
71
+ **Developer changes**:
72
+
73
+ Update navigation modal screens to use `NavModal` instead of `Modal`:
74
+
75
+ ```diff
76
+ - import { Modal } from '@utilitywarehouse/hearth-react-native';
77
+ + import { NavModal, type NavModalRef } from '@utilitywarehouse/hearth-react-native';
78
+
79
+ - const modalRef = useRef<Modal>(null);
80
+ + const modalRef = useRef<NavModalRef>(null);
81
+
82
+ - <Modal inNavModal background="brand" scrollable={false}>
83
+ + <NavModal background="brand" scrollable={false} presentation="modal">
84
+ {/* content */}
85
+ - </Modal>
86
+ + </NavModal>
87
+ ```
88
+
89
+ If you are using the bottom-sheet modal API, no changes are required.
90
+
91
+ If you were importing the old component from the component path, update it to use the package root re-export or import directly from `react-native-safe-area-context`:
92
+
93
+ ```diff
94
+ - import { SafeAreaView } from '@utilitywarehouse/hearth-react-native';
95
+ + import { SafeAreaView } from 'react-native-safe-area-context';
96
+ ```
97
+
12
98
  ## 0.28.6
13
99
 
14
100
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -60,8 +60,8 @@
60
60
  "@utilitywarehouse/hearth-fonts": "^0.0.4",
61
61
  "@utilitywarehouse/hearth-react-icons": "^0.8.0",
62
62
  "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
63
- "@utilitywarehouse/hearth-tokens": "^0.2.4",
64
- "@utilitywarehouse/hearth-svg-assets": "^0.5.0"
63
+ "@utilitywarehouse/hearth-svg-assets": "^0.5.0",
64
+ "@utilitywarehouse/hearth-tokens": "^0.2.4"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@gorhom/bottom-sheet": ">=5.0.0",
@@ -2,9 +2,12 @@ import { createContext, useContext } from 'react';
2
2
 
3
3
  interface BottomSheetContextProps {
4
4
  handle?: boolean;
5
+ useSafeAreaInsets: boolean;
5
6
  }
6
7
 
7
- export const BottomSheetContext = createContext<BottomSheetContextProps>({});
8
+ export const BottomSheetContext = createContext<BottomSheetContextProps>({
9
+ useSafeAreaInsets: true,
10
+ });
8
11
 
9
12
  export const useBottomSheetContext = (): BottomSheetContextProps => {
10
13
  const context = useContext(BottomSheetContext);
@@ -129,6 +129,20 @@ const App = () => {
129
129
  };
130
130
  ```
131
131
 
132
+ `BottomSheetModalProvider` adds Hearth's bottom-sheet safe-area spacing by default. If your app already handles safe-area padding, you can opt out and avoid double spacing:
133
+
134
+ ```tsx
135
+ import { BottomSheetModalProvider } from '@utilitywarehouse/hearth-react-native';
136
+
137
+ const App = () => {
138
+ return (
139
+ <BottomSheetModalProvider useSafeAreaInsets={false}>
140
+ {/* Your app content */}
141
+ </BottomSheetModalProvider>
142
+ );
143
+ };
144
+ ```
145
+
132
146
  ### `BottomSheetScrollView`
133
147
 
134
148
  For scrollable content within a bottom sheet, use the `BottomSheetScrollView` component instead of the standard ScrollView:
@@ -169,6 +183,12 @@ const App = () => {
169
183
  };
170
184
  ```
171
185
 
186
+ Use the `useSafeAreaInsets` prop to control whether Hearth applies bottom-sheet safe-area spacing for modal content, list wrappers, and the top `SafeAreaView` used by components such as `Select` and `Combobox`.
187
+
188
+ | Property | Type | Description | Default |
189
+ | ------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
190
+ | `useSafeAreaInsets` | `boolean` | Applies Hearth's safe-area insets inside bottom sheets. Set to `false` when your app already manages safe-area padding around bottom-sheet content. | `true` |
191
+
172
192
  ### `Input` in Bottom Sheets
173
193
 
174
194
  When using the `Input` component inside a `BottomSheet` or `BottomSheetModal`, you need to pass the `inBottomSheet` prop to ensure proper behavior with the keyboard and focus management.
@@ -22,6 +22,7 @@ const BottomSheet = ({
22
22
  ...rest
23
23
  }: BottomSheetProps) => {
24
24
  const bottomSheetRef = React.useRef<BottomSheet>(null);
25
+ const parentContext = React.useContext(BottomSheetContext);
25
26
 
26
27
  const { renderBackdrop, renderHandle } = useBottomSheetLogic<BottomSheet>({
27
28
  ref,
@@ -31,7 +32,13 @@ const BottomSheet = ({
31
32
  handleStyle,
32
33
  });
33
34
 
34
- const value = useMemo(() => ({ handle: showHandle }), [showHandle]);
35
+ const value = useMemo(
36
+ () => ({
37
+ ...parentContext,
38
+ handle: showHandle,
39
+ }),
40
+ [parentContext, showHandle]
41
+ );
35
42
 
36
43
  return (
37
44
  <StyledBottomSheetCore
@@ -11,10 +11,11 @@ const BottomSheetFlatList = ({
11
11
  isModal = true,
12
12
  ...props
13
13
  }: BottomSheetFlatListProps<any> & { isModal?: boolean }) => {
14
- const { handle } = useBottomSheetContext();
14
+ const { handle, useSafeAreaInsets } = useBottomSheetContext();
15
15
  styles.useVariants({
16
16
  isModal,
17
17
  handle,
18
+ useSafeAreaInsets,
18
19
  });
19
20
 
20
21
  return (
@@ -34,7 +35,7 @@ const styles = StyleSheet.create((theme, rt) => ({
34
35
  variants: {
35
36
  isModal: {
36
37
  true: {
37
- paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
38
+ paddingBottom: theme.components.bottomSheet.padding,
38
39
  },
39
40
  },
40
41
  handle: {
@@ -42,7 +43,20 @@ const styles = StyleSheet.create((theme, rt) => ({
42
43
  paddingTop: theme.components.bottomSheet.padding,
43
44
  },
44
45
  },
46
+ useSafeAreaInsets: {
47
+ true: {},
48
+ false: {},
49
+ },
45
50
  },
51
+ compoundVariants: [
52
+ {
53
+ isModal: true,
54
+ useSafeAreaInsets: true,
55
+ styles: {
56
+ paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
57
+ },
58
+ },
59
+ ],
46
60
  },
47
61
  }));
48
62
 
@@ -20,6 +20,7 @@ const BottomSheetModal = ({
20
20
  ...rest
21
21
  }: BottomSheetProps) => {
22
22
  const bottomSheetRef = React.useRef<BottomSheetModalMethods<any>>(null);
23
+ const parentContext = React.useContext(BottomSheetContext);
23
24
 
24
25
  const { renderBackdrop, renderHandle } = useBottomSheetLogic<BottomSheetModal>({
25
26
  // @ts-expect-error - ref
@@ -30,7 +31,13 @@ const BottomSheetModal = ({
30
31
  handleStyle,
31
32
  });
32
33
 
33
- const value = useMemo(() => ({ handle: showHandle }), [showHandle]);
34
+ const value = useMemo(
35
+ () => ({
36
+ ...parentContext,
37
+ handle: showHandle,
38
+ }),
39
+ [parentContext, showHandle]
40
+ );
34
41
 
35
42
  return (
36
43
  <StyledBottomSheetModalCore
@@ -0,0 +1,33 @@
1
+ import { BottomSheetModalProvider as GorhomBottomSheetModalProvider } from '@gorhom/bottom-sheet';
2
+ import type { BottomSheetModalProviderProps as GorhomBottomSheetModalProviderProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetModalProvider/types';
3
+ import { useMemo } from 'react';
4
+ import { BottomSheetContext, useBottomSheetContext } from './BottomSheet.context';
5
+
6
+ export interface BottomSheetModalProviderProps extends GorhomBottomSheetModalProviderProps {
7
+ useSafeAreaInsets?: boolean;
8
+ }
9
+
10
+ const BottomSheetModalProvider = ({
11
+ children,
12
+ useSafeAreaInsets = true,
13
+ ...props
14
+ }: BottomSheetModalProviderProps) => {
15
+ const parentContext = useBottomSheetContext();
16
+ const value = useMemo(
17
+ () => ({
18
+ ...parentContext,
19
+ useSafeAreaInsets,
20
+ }),
21
+ [parentContext, useSafeAreaInsets]
22
+ );
23
+
24
+ return (
25
+ <GorhomBottomSheetModalProvider {...props}>
26
+ <BottomSheetContext.Provider value={value}>{children}</BottomSheetContext.Provider>
27
+ </GorhomBottomSheetModalProvider>
28
+ );
29
+ };
30
+
31
+ BottomSheetModalProvider.displayName = 'BottomSheetModalProvider';
32
+
33
+ export default BottomSheetModalProvider;
@@ -14,10 +14,11 @@ const BottomSheetScrollView = ({
14
14
  isModal = true,
15
15
  ...props
16
16
  }: BottomSheetScrollViewProps & { isModal?: boolean }) => {
17
- const { handle } = useBottomSheetContext();
17
+ const { handle, useSafeAreaInsets } = useBottomSheetContext();
18
18
  styles.useVariants({
19
19
  isModal,
20
20
  handle,
21
+ useSafeAreaInsets,
21
22
  });
22
23
 
23
24
  return (
@@ -43,7 +44,7 @@ const styles = StyleSheet.create((theme, rt) => ({
43
44
  variants: {
44
45
  isModal: {
45
46
  true: {
46
- paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
47
+ paddingBottom: theme.components.bottomSheet.padding,
47
48
  },
48
49
  },
49
50
  handle: {
@@ -51,7 +52,20 @@ const styles = StyleSheet.create((theme, rt) => ({
51
52
  paddingTop: theme.components.bottomSheet.padding,
52
53
  },
53
54
  },
55
+ useSafeAreaInsets: {
56
+ true: {},
57
+ false: {},
58
+ },
54
59
  },
60
+ compoundVariants: [
61
+ {
62
+ isModal: true,
63
+ useSafeAreaInsets: true,
64
+ styles: {
65
+ paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
66
+ },
67
+ },
68
+ ],
55
69
  },
56
70
  }));
57
71
 
@@ -13,11 +13,13 @@ const BottomSheetView = ({
13
13
  isModal = true,
14
14
  ...props
15
15
  }: BottomSheetViewProps & { isModal?: boolean }) => {
16
- const { handle } = useBottomSheetContext();
16
+ const { handle, useSafeAreaInsets } = useBottomSheetContext();
17
17
  styles.useVariants({
18
18
  isModal,
19
19
  handle,
20
+ useSafeAreaInsets,
20
21
  });
22
+
21
23
  return (
22
24
  <StyledBottomSheetView style={[styles.contentContainer, style]} {...props}>
23
25
  {children}
@@ -35,7 +37,7 @@ const styles = StyleSheet.create((theme, rt) => ({
35
37
  variants: {
36
38
  isModal: {
37
39
  true: {
38
- paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
40
+ paddingBottom: theme.components.bottomSheet.padding,
39
41
  },
40
42
  },
41
43
  handle: {
@@ -43,7 +45,20 @@ const styles = StyleSheet.create((theme, rt) => ({
43
45
  paddingTop: theme.components.bottomSheet.padding,
44
46
  },
45
47
  },
48
+ useSafeAreaInsets: {
49
+ true: {},
50
+ false: {},
51
+ },
46
52
  },
53
+ compoundVariants: [
54
+ {
55
+ isModal: true,
56
+ useSafeAreaInsets: true,
57
+ styles: {
58
+ paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
59
+ },
60
+ },
61
+ ],
47
62
  },
48
63
  }));
49
64
 
@@ -1,7 +1,6 @@
1
1
  export {
2
2
  BottomSheetFlashList,
3
3
  BottomSheetFooter,
4
- BottomSheetModalProvider,
5
4
  BottomSheetSectionList,
6
5
  BottomSheetVirtualizedList,
7
6
  useBottomSheet,
@@ -15,5 +14,7 @@ export { default as BottomSheet } from './BottomSheet';
15
14
  export type { default as BottomSheetProps } from './BottomSheet.props';
16
15
  export { default as BottomSheetFlatList } from './BottomSheetFlatList';
17
16
  export { default as BottomSheetModal } from './BottomSheetModal';
17
+ export { default as BottomSheetModalProvider } from './BottomSheetModalProvider';
18
+ export type { BottomSheetModalProviderProps } from './BottomSheetModalProvider';
18
19
  export { default as BottomSheetScrollView } from './BottomSheetScrollView';
19
20
  export { default as BottomSheetView } from './BottomSheetView';
@@ -6,6 +6,7 @@ import { StyleSheet } from 'react-native-unistyles';
6
6
  import { useTheme } from '../../hooks';
7
7
  import { BodyText } from '../BodyText';
8
8
  import { BottomSheetFlatList, BottomSheetModal, BottomSheetView } from '../BottomSheet';
9
+ import { useBottomSheetContext } from '../BottomSheet/BottomSheet.context';
9
10
  import { DetailText } from '../DetailText';
10
11
  import { FormField, useFormFieldContext } from '../FormField';
11
12
  import { Icon } from '../Icon';
@@ -55,6 +56,7 @@ const Combobox = ({
55
56
  const isDisabled = formFieldContext?.disabled ?? disabled;
56
57
  const isReadonly = formFieldContext?.readonly ?? readonly;
57
58
  const { color } = useTheme();
59
+ const { useSafeAreaInsets } = useBottomSheetContext();
58
60
 
59
61
  const bottomSheetModalRef = useRef<BottomSheetModal>(null);
60
62
  const searchInputRef = useRef<TextInput>(null);
@@ -325,7 +327,7 @@ const Combobox = ({
325
327
  setSearch,
326
328
  }}
327
329
  >
328
- <SafeAreaView edges={['top']}>
330
+ <SafeAreaView edges={useSafeAreaInsets ? ['top'] : []}>
329
331
  {menuHeading && (
330
332
  <View style={styles.headingContainer}>
331
333
  <DetailText size="lg">{menuHeading}</DetailText>
@@ -3,7 +3,6 @@ import { View } from 'react-native';
3
3
  import { StyleSheet } from 'react-native-unistyles';
4
4
  import { useTheme } from '../../hooks';
5
5
  import { BodyText } from '../BodyText';
6
- import { DetailText } from '../DetailText';
7
6
  import Helper from '../Helper/Helper';
8
7
  import { useDescriptionListContext } from './DescriptionList.context';
9
8
  import type DescriptionListItemProps from './DescriptionListItem.props';
@@ -55,7 +54,7 @@ const DescriptionListItem = ({
55
54
  </View>
56
55
  {numericValue ? (
57
56
  <View style={styles.descriptionWrapper}>
58
- <DetailText size="lg">{numericValue}</DetailText>
57
+ <BodyText weight="semibold">{numericValue}</BodyText>
59
58
  </View>
60
59
  ) : null}
61
60
  </View>