@utilitywarehouse/hearth-react-native 0.15.0 → 0.15.2

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.15.0 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.15.2 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.15.0 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.15.2 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint --max-warnings 0
4
4
 
5
5
  Rule | Time (ms) | Relative
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.15.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#771](https://github.com/utilitywarehouse/hearth/pull/771) [`5320be5`](https://github.com/utilitywarehouse/hearth/commit/5320be544992c137e201be7750d30ea098a7d399) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Adds missing `ref` prop to the `CurrencyInput` component
8
+
9
+ ## 0.15.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [#768](https://github.com/utilitywarehouse/hearth/pull/768) [`570f240`](https://github.com/utilitywarehouse/hearth/commit/570f240a448eae546b893ed3ad69235213ee5fac) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Makes `Modal` footer sticky by default
14
+
15
+ This change updates the `Modal` component to have a sticky footer by default, enhancing user experience by keeping action buttons accessible. The `stickyFooter` prop has been added to allow developers to disable this behavior if needed.
16
+
3
17
  ## 0.15.0
4
18
 
5
19
  ### Minor Changes
@@ -1,7 +1,7 @@
1
+ export { BottomSheetFlashList, BottomSheetFooter, BottomSheetModalProvider, BottomSheetSectionList, BottomSheetVirtualizedList, useBottomSheet, useBottomSheetModal, type BottomSheetBackdropProps, type BottomSheetBackgroundProps, type BottomSheetHandleProps, type BottomSheetModalProps, } from '@gorhom/bottom-sheet';
1
2
  export { default as BottomSheet } from './BottomSheet';
2
- export { default as BottomSheetView } from './BottomSheetView';
3
- export { default as BottomSheetScrollView } from './BottomSheetScrollView';
4
- export { default as BottomSheetModal } from './BottomSheetModal';
5
- export { default as BottomSheetFlatList } from './BottomSheetFlatList';
6
- export { BottomSheetModalProvider, BottomSheetSectionList, BottomSheetVirtualizedList, useBottomSheet, useBottomSheetModal, type BottomSheetBackdropProps, type BottomSheetBackgroundProps, type BottomSheetHandleProps, type BottomSheetModalProps, } from '@gorhom/bottom-sheet';
7
3
  export type { default as BottomSheetProps } from './BottomSheet.props';
4
+ export { default as BottomSheetFlatList } from './BottomSheetFlatList';
5
+ export { default as BottomSheetModal } from './BottomSheetModal';
6
+ export { default as BottomSheetScrollView } from './BottomSheetScrollView';
7
+ export { default as BottomSheetView } from './BottomSheetView';
@@ -1,6 +1,6 @@
1
+ export { BottomSheetFlashList, BottomSheetFooter, BottomSheetModalProvider, BottomSheetSectionList, BottomSheetVirtualizedList, useBottomSheet, useBottomSheetModal, } from '@gorhom/bottom-sheet';
1
2
  export { default as BottomSheet } from './BottomSheet';
2
- export { default as BottomSheetView } from './BottomSheetView';
3
- export { default as BottomSheetScrollView } from './BottomSheetScrollView';
4
- export { default as BottomSheetModal } from './BottomSheetModal';
5
3
  export { default as BottomSheetFlatList } from './BottomSheetFlatList';
6
- export { BottomSheetModalProvider, BottomSheetSectionList, BottomSheetVirtualizedList, useBottomSheet, useBottomSheetModal, } from '@gorhom/bottom-sheet';
4
+ export { default as BottomSheetModal } from './BottomSheetModal';
5
+ export { default as BottomSheetScrollView } from './BottomSheetScrollView';
6
+ export { default as BottomSheetView } from './BottomSheetView';
@@ -1,6 +1,6 @@
1
1
  import type CurrencyInputProps from './CurrencyInput.props';
2
2
  declare const CurrencyInput: {
3
- ({ validationStatus, disabled, focused, readonly, placeholder, inBottomSheet, required, disableGroupSeparator, value, onChangeText, ...rest }: CurrencyInputProps): import("react/jsx-runtime").JSX.Element;
3
+ ({ validationStatus, disabled, focused, readonly, placeholder, inBottomSheet, required, disableGroupSeparator, value, onChangeText, ref, ...rest }: CurrencyInputProps): import("react/jsx-runtime").JSX.Element;
4
4
  displayName: string;
5
5
  };
6
6
  export default CurrencyInput;
@@ -5,7 +5,7 @@ import { formatThousands } from '../../utils';
5
5
  import { DetailText } from '../DetailText';
6
6
  import { useFormFieldContext } from '../FormField';
7
7
  import { Input, InputField, InputSlot } from '../Input';
8
- const CurrencyInput = ({ validationStatus = 'initial', disabled, focused, readonly, placeholder, inBottomSheet = false, required, disableGroupSeparator = false, value, onChangeText, ...rest }) => {
8
+ const CurrencyInput = ({ validationStatus = 'initial', disabled, focused, readonly, placeholder, inBottomSheet = false, required, disableGroupSeparator = false, value, onChangeText, ref, ...rest }) => {
9
9
  const formFieldContext = useFormFieldContext();
10
10
  const { disabled: formFieldDisabled } = formFieldContext;
11
11
  const validationStatusFromContext = formFieldContext?.validationStatus ?? validationStatus;
@@ -21,7 +21,7 @@ const CurrencyInput = ({ validationStatus = 'initial', disabled, focused, readon
21
21
  }
22
22
  };
23
23
  const displayValue = !disableGroupSeparator && typeof value === 'string' ? formatThousands(value) : value;
24
- return (_jsxs(Input, { validationStatus: validationStatusFromContext, disabled: formFieldDisabled ?? disabled, readonly: readonly, focused: focused, style: styles.wrap, children: [_jsx(InputSlot, { children: _jsx(DetailText, { size: "4xl", style: styles.text, accessible: false, children: "\u00A3" }) }), _jsx(InputField, { inputMode: "decimal", inBottomSheet: inBottomSheet, accessibilityHint: 'Enter the amount in pounds and pence, for example "10.99"', ...rest, placeholder: getPlaceholder, keyboardType: "decimal-pad", style: styles.input, value: displayValue, onChangeText: handleChangeText })] }));
24
+ return (_jsxs(Input, { validationStatus: validationStatusFromContext, disabled: formFieldDisabled ?? disabled, readonly: readonly, focused: focused, style: styles.wrap, children: [_jsx(InputSlot, { children: _jsx(DetailText, { size: "4xl", style: styles.text, accessible: false, children: "\u00A3" }) }), _jsx(InputField, { ref: ref, inputMode: "decimal", inBottomSheet: inBottomSheet, accessibilityHint: 'Enter the amount in pounds and pence, for example "10.99"', ...rest, placeholder: getPlaceholder, keyboardType: "decimal-pad", style: styles.input, value: displayValue, onChangeText: handleChangeText })] }));
25
25
  };
26
26
  CurrencyInput.displayName = 'CurrencyInput';
27
27
  const styles = StyleSheet.create(theme => ({
@@ -1,5 +1,7 @@
1
- import type { TextInputProps, ViewProps } from 'react-native';
1
+ import type { Ref } from 'react';
2
+ import type { TextInput, TextInputProps, ViewProps } from 'react-native';
2
3
  export interface CurrencyInputBaseProps {
4
+ ref?: Ref<TextInput>;
3
5
  disabled?: boolean;
4
6
  validationStatus?: 'initial' | 'valid' | 'invalid';
5
7
  readonly?: boolean;
@@ -3,5 +3,5 @@ import ModalProps from './Modal.props';
3
3
  type Modal<T = any> = BottomSheetModalMethods<T> & {
4
4
  triggerCloseAnimation?: () => void;
5
5
  };
6
- declare const Modal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress, closeOnSecondaryButtonPress, loading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
6
+ declare const Modal: ({ ref, children, heading, description, showCloseButton, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress, closeOnSecondaryButtonPress, loading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal, stickyFooter, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export default Modal;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { BottomSheetFooter, } from '@gorhom/bottom-sheet';
2
3
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
4
  import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
4
5
  import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
@@ -12,7 +13,7 @@ import { Button } from '../Button';
12
13
  import { Heading } from '../Heading';
13
14
  import { Spinner } from '../Spinner';
14
15
  import { UnstyledIconButton } from '../UnstyledIconButton';
15
- const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, ...props }) => {
16
+ const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, fullscreen = false, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, stickyFooter = true, ...props }) => {
16
17
  const bottomSheetModalRef = useRef(null);
17
18
  const viewRef = useRef(null);
18
19
  const scrollViewRef = useRef(null);
@@ -97,9 +98,24 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
97
98
  bottomSheetModalRef.current?.dismiss();
98
99
  }
99
100
  };
100
- styles.useVariants({ loading });
101
- 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: "Loading..." })] })) : (_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, _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] })] })) }));
102
- return inNavModal ? (_jsxs(View, { style: { flex: 1, backgroundColor: theme.color.background.primary }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_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, ...props, onChange: handleChange, children: [loading ? _jsx(View, { style: styles.loadingTop }) : null, _jsx(BottomSheetScrollView, { contentContainerStyle: styles.container, ref: scrollViewRef, children: content })] }));
101
+ const noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
102
+ styles.useVariants({
103
+ loading,
104
+ bothButtons: !!(onPressPrimaryButton && onPressSecondaryButton),
105
+ noButtons,
106
+ stickyFooter,
107
+ });
108
+ const footer = (_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] }));
109
+ 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: "Loading..." })] })) : (_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] })) }));
110
+ const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
111
+ onPressPrimaryButton,
112
+ primaryButtonText,
113
+ onPressSecondaryButton,
114
+ secondaryButtonText,
115
+ primaryButtonProps,
116
+ secondaryButtonProps,
117
+ ]);
118
+ return inNavModal ? (_jsxs(View, { style: { flex: 1, backgroundColor: theme.color.background.primary }, children: [Platform.OS === 'android' ? (_jsx(Animated.View, { style: [styles.androidContainer, animatedBackgroundStyle], children: _jsx(Animated.View, { style: [styles.pretendContent, animatedPretendContentStyle] }) })) : null, _jsx(Animated.View, { style: [styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle], children: _jsx(View, { style: styles.inNavModalContent, children: content }) })] })) : (_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 })] }));
103
119
  };
104
120
  const styles = StyleSheet.create((theme, rt) => ({
105
121
  modal: {
@@ -116,6 +132,30 @@ const styles = StyleSheet.create((theme, rt) => ({
116
132
  },
117
133
  },
118
134
  },
135
+ scrollView: {
136
+ flex: 1,
137
+ variants: {
138
+ bothButtons: {
139
+ true: {
140
+ paddingBottom: 166 + rt.insets.bottom - theme.components.modal.padding,
141
+ },
142
+ false: {
143
+ paddingBottom: 102 + rt.insets.bottom - theme.components.modal.padding,
144
+ },
145
+ },
146
+ noButtons: {
147
+ true: {
148
+ paddingBottom: rt.insets.bottom + theme.components.modal.padding,
149
+ },
150
+ },
151
+ stickyFooter: {
152
+ true: {},
153
+ false: {
154
+ paddingBottom: rt.insets.bottom + theme.components.modal.padding,
155
+ },
156
+ },
157
+ },
158
+ },
119
159
  header: {
120
160
  flexDirection: 'row',
121
161
  gap: theme.components.modal.gap,
@@ -149,6 +189,11 @@ const styles = StyleSheet.create((theme, rt) => ({
149
189
  footer: {
150
190
  gap: theme.components.modal.action.gap,
151
191
  },
192
+ footerWrap: {
193
+ backgroundColor: theme.color.surface.neutral.strong,
194
+ paddingHorizontal: theme.components.bottomSheet.padding,
195
+ paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
196
+ },
152
197
  inNavModalContainer: {
153
198
  flex: 1,
154
199
  ...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
@@ -11,6 +11,7 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
11
11
  description?: string;
12
12
  inNavModal?: boolean;
13
13
  fullscreen?: boolean;
14
+ stickyFooter?: boolean;
14
15
  children?: ViewProps['children'];
15
16
  onPressPrimaryButton?: () => void;
16
17
  primaryButtonText?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,9 +1,6 @@
1
- export { default as BottomSheet } from './BottomSheet';
2
- export { default as BottomSheetView } from './BottomSheetView';
3
- export { default as BottomSheetScrollView } from './BottomSheetScrollView';
4
- export { default as BottomSheetModal } from './BottomSheetModal';
5
- export { default as BottomSheetFlatList } from './BottomSheetFlatList';
6
1
  export {
2
+ BottomSheetFlashList,
3
+ BottomSheetFooter,
7
4
  BottomSheetModalProvider,
8
5
  BottomSheetSectionList,
9
6
  BottomSheetVirtualizedList,
@@ -14,4 +11,9 @@ export {
14
11
  type BottomSheetHandleProps,
15
12
  type BottomSheetModalProps,
16
13
  } from '@gorhom/bottom-sheet';
14
+ export { default as BottomSheet } from './BottomSheet';
17
15
  export type { default as BottomSheetProps } from './BottomSheet.props';
16
+ export { default as BottomSheetFlatList } from './BottomSheetFlatList';
17
+ export { default as BottomSheetModal } from './BottomSheetModal';
18
+ export { default as BottomSheetScrollView } from './BottomSheetScrollView';
19
+ export { default as BottomSheetView } from './BottomSheetView';
@@ -1,6 +1,8 @@
1
- import type { TextInputProps, ViewProps } from 'react-native';
1
+ import type { Ref } from 'react';
2
+ import type { TextInput, TextInputProps, ViewProps } from 'react-native';
2
3
 
3
4
  export interface CurrencyInputBaseProps {
5
+ ref?: Ref<TextInput>;
4
6
  disabled?: boolean;
5
7
  validationStatus?: 'initial' | 'valid' | 'invalid';
6
8
  readonly?: boolean;
@@ -17,6 +17,7 @@ const CurrencyInput = ({
17
17
  disableGroupSeparator = false,
18
18
  value,
19
19
  onChangeText,
20
+ ref,
20
21
  ...rest
21
22
  }: CurrencyInputProps) => {
22
23
  const formFieldContext = useFormFieldContext();
@@ -52,6 +53,7 @@ const CurrencyInput = ({
52
53
  </DetailText>
53
54
  </InputSlot>
54
55
  <InputField
56
+ ref={ref as any}
55
57
  inputMode="decimal"
56
58
  inBottomSheet={inBottomSheet}
57
59
  accessibilityHint='Enter the amount in pounds and pence, for example "10.99"'
@@ -67,7 +69,6 @@ const CurrencyInput = ({
67
69
  };
68
70
 
69
71
  CurrencyInput.displayName = 'CurrencyInput';
70
-
71
72
  const styles = StyleSheet.create(theme => ({
72
73
  wrap: {
73
74
  height: theme.components.input.currency.height,
@@ -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 } : {}),