@utilitywarehouse/hearth-react-native 0.25.0 → 0.26.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.25.0 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.26.0 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.25.0 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.26.0 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint .
4
4
 
5
5
 
@@ -31,8 +31,8 @@
31
31
  78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
32
32
 
33
33
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.tsx
34
- 75:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
35
- 291:5 warning React Hook useCallback has a missing dependency: 'footer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
34
+ 86:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
35
+ 313:5 warning React Hook useCallback has a missing dependency: 'footer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
36
36
 
37
37
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.web.tsx
38
38
  66:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
@@ -60,13 +60,13 @@
60
60
 
61
61
  Rule | Time (ms) | Relative
62
62
  :-----------------------------------------|----------:|--------:
63
- @typescript-eslint/no-unused-vars | 1535.077 | 58.6%
64
- react-hooks/exhaustive-deps | 186.011 | 7.1%
65
- no-global-assign | 92.645 | 3.5%
66
- react-hooks/rules-of-hooks | 90.323 | 3.4%
67
- no-misleading-character-class | 64.601 | 2.5%
68
- no-unexpected-multiline | 47.391 | 1.8%
69
- @typescript-eslint/ban-ts-comment | 39.233 | 1.5%
70
- @typescript-eslint/triple-slash-reference | 36.020 | 1.4%
71
- no-useless-escape | 31.051 | 1.2%
72
- @typescript-eslint/no-unused-expressions | 28.768 | 1.1%
63
+ @typescript-eslint/no-unused-vars | 1316.387 | 59.6%
64
+ react-hooks/exhaustive-deps | 96.698 | 4.4%
65
+ no-global-assign | 93.436 | 4.2%
66
+ react-hooks/rules-of-hooks | 81.760 | 3.7%
67
+ no-unexpected-multiline | 47.972 | 2.2%
68
+ no-misleading-character-class | 43.286 | 2.0%
69
+ @typescript-eslint/triple-slash-reference | 33.417 | 1.5%
70
+ @typescript-eslint/ban-ts-comment | 32.258 | 1.5%
71
+ no-regex-spaces | 29.418 | 1.3%
72
+ no-loss-of-precision | 25.945 | 1.2%
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#981](https://github.com/utilitywarehouse/hearth/pull/981) [`df56387`](https://github.com/utilitywarehouse/hearth/commit/df563872e6bf040d419f6c7fce2343ebe560edb9) Thanks [@declanelcocks](https://github.com/declanelcocks)! - 🌟 [ENHANCEMENT]: Improve `Modal` behaviour when used inside a React Navigation modal (`inNavModal`).
8
+
9
+ The following improvements have been made to the `Modal` component when used in a navigation context with `inNavModal={true}`:
10
+ - **`scrollable` prop**: Content is now rendered inside a `ScrollView` by default. Set `scrollable={false}` to opt out, for example when you need to centre content or use a custom layout.
11
+ - **`stickyFooter` support**: The `stickyFooter` prop now works correctly in `inNavModal` mode.
12
+ - **Auto full-screen detection**: When the modal fills the entire screen (e.g. with `presentation: 'fullScreenModal'`), the `fullscreen` style is applied automatically. The `fullscreen` prop is no longer available when `inNavModal` is `true`.
13
+
14
+ **Components affected**:
15
+ - `Modal`
16
+
17
+ **Developer changes**:
18
+
19
+ No changes are required for existing usage. If you were passing `fullscreen` alongside `inNavModal={true}`, remove the `fullscreen` prop — full-screen styling is now detected automatically:
20
+
21
+ ```diff
22
+ - <Modal inNavModal fullscreen>
23
+ + <Modal inNavModal>
24
+ ```
25
+
26
+ To disable the default `ScrollView` wrapping in `inNavModal` mode:
27
+
28
+ ```tsx
29
+ <Modal inNavModal scrollable={false}>
30
+ {/* custom layout */}
31
+ </Modal>
32
+ ```
33
+
3
34
  ## 0.25.0
4
35
 
5
36
  ### Minor Changes
@@ -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, loadingHeading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal, stickyFooter, background, ...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, loadingHeading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal, stickyFooter, background, scrollable, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export default Modal;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { BottomSheetFooter, } from '@gorhom/bottom-sheet';
3
3
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
- import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
5
- import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
4
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
5
+ import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle } from 'react-native';
6
6
  import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
7
7
  import { StyleSheet } from 'react-native-unistyles';
8
8
  import { useTheme } from '../../hooks';
@@ -13,7 +13,7 @@ import { Button } from '../Button';
13
13
  import { Heading } from '../Heading';
14
14
  import { Spinner } from '../Spinner';
15
15
  import { UnstyledIconButton } from '../UnstyledIconButton';
16
- 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, inNavModal = false, stickyFooter = true, background = 'default', ...props }) => {
16
+ 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, inNavModal = false, stickyFooter = true, background = 'default', scrollable = true, ...props }) => {
17
17
  const bottomSheetModalRef = useRef(null);
18
18
  const viewRef = useRef(null);
19
19
  const scrollViewRef = useRef(null);
@@ -21,6 +21,13 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
21
21
  const backgroundOpacity = useSharedValue(0);
22
22
  const pretendContentTranslateY = useSharedValue(20);
23
23
  const isBrandBackground = background === 'brand';
24
+ const [inNavModalHeight, setInNavModalHeight] = useState();
25
+ const isNavModalFullScreen = useMemo(() => {
26
+ if (!inNavModalHeight || !inNavModal)
27
+ return false;
28
+ const screenHeight = Dimensions.get('window').height;
29
+ return inNavModalHeight >= screenHeight;
30
+ }, [inNavModalHeight, inNavModal]);
24
31
  const triggerCloseAnimation = useCallback(() => {
25
32
  if (Platform.OS === 'android' && inNavModal) {
26
33
  pretendContentTranslateY.value = withTiming(20, {
@@ -107,9 +114,13 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
107
114
  stickyFooter,
108
115
  showHandle: props.showHandle,
109
116
  background: isBrandBackground ? 'brand' : 'primary',
117
+ ...(inNavModal && {
118
+ fullscreen: isNavModalFullScreen,
119
+ }),
110
120
  });
111
121
  const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, inverted: isBrandBackground && inNavModal, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, inverted: isBrandBackground && inNavModal, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
112
- const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal ? _jsx(ScrollView, { style: { flex: 1 }, children: children }) : children, (!stickyFooter || inNavModal) && !noButtons ? footer : null] })) }));
122
+ const InNavModalContainer = scrollable ? ScrollView : View;
123
+ const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg", color: isBrandBackground && inNavModal ? theme.color.icon.inverted : undefined }), _jsx(Heading, { size: "lg", textAlign: "center", inverted: isBrandBackground && inNavModal, children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description && !image ? (_jsx(BodyText, { accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", inverted: isBrandBackground && inNavModal, ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, inverted: isBrandBackground && inNavModal, children: description })) : null] })] })) : null, inNavModal && (_jsxs(InNavModalContainer, { style: { flexGrow: stickyFooter ? 1 : 0 }, children: [children, !stickyFooter ? _jsx(View, { style: styles.inNavModalFooterContainer, children: footer }) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null] })) }));
113
124
  const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
114
125
  onPressPrimaryButton,
115
126
  primaryButtonText,
@@ -118,7 +129,9 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
118
129
  primaryButtonProps,
119
130
  secondaryButtonProps,
120
131
  ]);
121
- return inNavModal ? (_jsxs(View, { style: {
132
+ return inNavModal ? (_jsxs(View, { onLayout: (e) => {
133
+ setInNavModalHeight(e.nativeEvent.layout.height);
134
+ }, style: {
122
135
  flex: 1,
123
136
  backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
124
137
  }, 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 })] }));
@@ -227,8 +240,6 @@ const styles = StyleSheet.create((theme, rt) => ({
227
240
  borderTopLeftRadius: theme.components.modal.borderRadius,
228
241
  borderTopRightRadius: theme.components.modal.borderRadius,
229
242
  backgroundColor: theme.color.surface.neutral.strong,
230
- gap: theme.components.modal.gap,
231
- padding: theme.components.modal.padding,
232
243
  paddingBottom: theme.components.modal.padding + rt.insets.bottom,
233
244
  variants: {
234
245
  background: {
@@ -237,8 +248,20 @@ const styles = StyleSheet.create((theme, rt) => ({
237
248
  backgroundColor: theme.color.background.brand,
238
249
  },
239
250
  },
251
+ fullscreen: {
252
+ true: {
253
+ padding: theme.components.modal.padding,
254
+ paddingTop: rt.insets.top,
255
+ },
256
+ false: {
257
+ padding: theme.components.modal.padding,
258
+ }
259
+ }
240
260
  },
241
261
  },
262
+ inNavModalFooterContainer: {
263
+ paddingTop: theme.components.modal.padding,
264
+ },
242
265
  androidContainer: {
243
266
  height: rt.insets.top + 18,
244
267
  paddingLeft: theme.components.modal.padding,
@@ -11,6 +11,7 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
11
11
  loadingHeading?: string;
12
12
  description?: string;
13
13
  fullscreen?: boolean;
14
+ stickyFooter?: boolean;
14
15
  children?: ViewProps['children'];
15
16
  onPressPrimaryButton?: () => void;
16
17
  primaryButtonText?: string;
@@ -25,11 +26,12 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
25
26
  }
26
27
  type ModalProps = (ModalPropsBase & {
27
28
  inNavModal?: false | undefined;
28
- stickyFooter?: boolean;
29
+ scrollable?: never;
29
30
  background?: never;
30
31
  }) | (ModalPropsBase & {
31
32
  inNavModal: true;
32
- stickyFooter?: never;
33
+ fullscreen?: never;
34
+ scrollable?: boolean;
33
35
  background?: 'default' | 'brand';
34
36
  });
35
37
  export default ModalProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -58,8 +58,8 @@
58
58
  "vitest": "^3.2.4",
59
59
  "@utilitywarehouse/hearth-fonts": "^0.0.4",
60
60
  "@utilitywarehouse/hearth-react-icons": "^0.8.0",
61
- "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
62
61
  "@utilitywarehouse/hearth-svg-assets": "^0.5.0",
62
+ "@utilitywarehouse/hearth-react-native-icons": "^0.8.0",
63
63
  "@utilitywarehouse/hearth-tokens": "^0.2.3"
64
64
  },
65
65
  "peerDependencies": {
@@ -107,9 +107,10 @@ The Modal component extends the `BottomSheetModal` component and accepts all of
107
107
  | `primaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the primary button (colorScheme defaults to 'highlight', variant to 'solid') | - |
108
108
  | `secondaryButtonProps` | `Omit<ButtonWithoutChildrenProps, 'children'>` | Additional props to pass to the secondary button (colorScheme defaults to 'functional', variant to 'outline') | - |
109
109
  | `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
110
- | `fullscreen` | `boolean` | Whether the modal should take up the full screen height | `false` |
110
+ | `fullscreen` | `boolean` | Whether the modal should take up the full screen height. Only applies when `inNavModal` is `false` | `false` |
111
111
  | `inNavModal` | `boolean` | Renders the modal correctly when used inside a navigation modal | `false` |
112
112
  | `background` | `'default' \| 'brand'` | Sets the modal background. Only applies when `inNavModal` is `true` | `'default'` |
113
+ | `scrollable` | `boolean` | Whether the modal's content should be placed in a `ScrollView`. Only applies when `inNavModal` is `true` | `true` |
113
114
 
114
115
  \* use this to detect if the modal has been opened or closed, index 0 indicates open state and -1 indicates closed state
115
116
 
@@ -462,11 +463,16 @@ const AlertModal = () => {
462
463
 
463
464
  ### Modal In Navigation Modal
464
465
 
465
- When using the Modal component in a navigation context, you can set it to `inNavModal` mode, this will make it behave like a standard modal screen.
466
+ When wanting to use the Modal component in a navigation context using [React Navigation](https://reactnavigation.org/docs/modal), you can set `inNavModal` to `true` to make it behave like a standard modal screen.
467
+
468
+ Within React Navigation, you can set `presentation: 'modal'` in the screen's settings to have the Modal look and behave like a standard modal/bottom sheet, or you can set `presentation: 'fullScreenModal'` to have the Modal fill the entire screen.
469
+
470
+ When using `inNavModal`, by default the content will be rendered inside a `ScrollView` to ensure it is scrollable, especially on smaller devices or smaller modals. You can disable this by setting `scrollable={false}` if, for example, you need to center your content or add some custom content.
471
+
466
472
  Here's an example of how to implement this with custom close animations for Android:
467
473
 
468
474
  ```tsx
469
- import { useNavigation } from 'react-navigation/native';
475
+ import { useNavigation } from '@react-navigation/native';
470
476
  import { useCallback, useEffect, useRef } from 'react';
471
477
  import { Platform, StyleSheet, View } from 'react-native';
472
478
 
@@ -12,6 +12,7 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
12
12
  loadingHeading?: string;
13
13
  description?: string;
14
14
  fullscreen?: boolean;
15
+ stickyFooter?: boolean;
15
16
  children?: ViewProps['children'];
16
17
  onPressPrimaryButton?: () => void;
17
18
  primaryButtonText?: string;
@@ -28,12 +29,13 @@ interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
28
29
  type ModalProps =
29
30
  | (ModalPropsBase & {
30
31
  inNavModal?: false | undefined;
31
- stickyFooter?: boolean;
32
+ scrollable?: never;
32
33
  background?: never;
33
34
  })
34
35
  | (ModalPropsBase & {
35
36
  inNavModal: true;
36
- stickyFooter?: never;
37
+ fullscreen?: never;
38
+ scrollable?: boolean;
37
39
  background?: 'default' | 'brand';
38
40
  });
39
41
 
@@ -6,8 +6,8 @@ import {
6
6
  } from '@gorhom/bottom-sheet';
7
7
  import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
8
8
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
9
- import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
10
- import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
9
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
10
+ import { AccessibilityInfo, Dimensions, Platform, ScrollView, View, findNodeHandle } from 'react-native';
11
11
  import Animated, {
12
12
  Easing,
13
13
  useAnimatedStyle,
@@ -51,6 +51,7 @@ const Modal = ({
51
51
  inNavModal = false,
52
52
  stickyFooter = true,
53
53
  background = 'default',
54
+ scrollable = true,
54
55
  ...props
55
56
  }: ModalProps) => {
56
57
  const bottomSheetModalRef = useRef<BottomSheetModal>(null);
@@ -61,6 +62,16 @@ const Modal = ({
61
62
  const pretendContentTranslateY = useSharedValue(20);
62
63
  const isBrandBackground = background === 'brand';
63
64
 
65
+ const [inNavModalHeight, setInNavModalHeight] = useState<number>();
66
+
67
+ const isNavModalFullScreen = useMemo(() => {
68
+ if (!inNavModalHeight || !inNavModal) return false;
69
+
70
+ const screenHeight = Dimensions.get('window').height;
71
+
72
+ return inNavModalHeight >= screenHeight;
73
+ }, [inNavModalHeight, inNavModal]);
74
+
64
75
  const triggerCloseAnimation = useCallback(() => {
65
76
  if (Platform.OS === 'android' && inNavModal) {
66
77
  pretendContentTranslateY.value = withTiming(20, {
@@ -173,6 +184,9 @@ const Modal = ({
173
184
  stickyFooter,
174
185
  showHandle: props.showHandle,
175
186
  background: isBrandBackground ? 'brand' : 'primary',
187
+ ...(inNavModal && {
188
+ fullscreen: isNavModalFullScreen,
189
+ }),
176
190
  });
177
191
 
178
192
  const footer = (
@@ -200,6 +214,8 @@ const Modal = ({
200
214
  </View>
201
215
  );
202
216
 
217
+ const InNavModalContainer = scrollable ? ScrollView : View;
218
+
203
219
  const content = (
204
220
  <>
205
221
  {loading ? (
@@ -275,8 +291,14 @@ const Modal = ({
275
291
  </View>
276
292
  </View>
277
293
  ) : null}
278
- {inNavModal ? <ScrollView style={{ flex: 1 }}>{children}</ScrollView> : children}
279
- {(!stickyFooter || inNavModal) && !noButtons ? footer : null}
294
+ {inNavModal && (
295
+ <InNavModalContainer style={{ flexGrow: stickyFooter ? 1 : 0 }}>
296
+ {children}
297
+ {!stickyFooter ? <View style={styles.inNavModalFooterContainer}>{footer}</View> : null}
298
+ </InNavModalContainer>
299
+ )}
300
+ {!inNavModal && children}
301
+ {((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons ? footer : null}
280
302
  </View>
281
303
  )}
282
304
  </>
@@ -300,6 +322,9 @@ const Modal = ({
300
322
 
301
323
  return inNavModal ? (
302
324
  <View
325
+ onLayout={(e) => {
326
+ setInNavModalHeight(e.nativeEvent.layout.height);
327
+ }}
303
328
  style={{
304
329
  flex: 1,
305
330
  backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
@@ -313,7 +338,9 @@ const Modal = ({
313
338
  <Animated.View
314
339
  style={[styles.inNavModalContainer, Platform.OS === 'android' && animatedInNavModalStyle]}
315
340
  >
316
- <View style={styles.inNavModalContent}>{content}</View>
341
+ <View style={styles.inNavModalContent}>
342
+ {content}
343
+ </View>
317
344
  </Animated.View>
318
345
  </View>
319
346
  ) : (
@@ -444,8 +471,6 @@ const styles = StyleSheet.create((theme, rt) => ({
444
471
  borderTopLeftRadius: theme.components.modal.borderRadius,
445
472
  borderTopRightRadius: theme.components.modal.borderRadius,
446
473
  backgroundColor: theme.color.surface.neutral.strong,
447
- gap: theme.components.modal.gap,
448
- padding: theme.components.modal.padding,
449
474
  paddingBottom: theme.components.modal.padding + rt.insets.bottom,
450
475
  variants: {
451
476
  background: {
@@ -454,8 +479,20 @@ const styles = StyleSheet.create((theme, rt) => ({
454
479
  backgroundColor: theme.color.background.brand,
455
480
  },
456
481
  },
482
+ fullscreen: {
483
+ true: {
484
+ padding: theme.components.modal.padding,
485
+ paddingTop: rt.insets.top,
486
+ },
487
+ false: {
488
+ padding: theme.components.modal.padding,
489
+ }
490
+ }
457
491
  },
458
492
  },
493
+ inNavModalFooterContainer: {
494
+ paddingTop: theme.components.modal.padding,
495
+ },
459
496
  androidContainer: {
460
497
  height: rt.insets.top + 18,
461
498
  paddingLeft: theme.components.modal.padding,