@utilitywarehouse/hearth-react-native 0.28.6 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.storybook/preview.tsx +7 -4
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-lint.log +15 -18
  4. package/CHANGELOG.md +59 -0
  5. package/build/components/Combobox/Combobox.js +1 -1
  6. package/build/components/Modal/Modal.d.ts +1 -1
  7. package/build/components/Modal/Modal.js +6 -95
  8. package/build/components/Modal/Modal.props.d.ts +2 -31
  9. package/build/components/Modal/Modal.shared.types.d.ts +22 -0
  10. package/build/components/Modal/Modal.web.d.ts +1 -1
  11. package/build/components/Modal/Modal.web.js +6 -71
  12. package/build/components/NavModal/NavModal.d.ts +3 -0
  13. package/build/components/NavModal/NavModal.js +190 -0
  14. package/build/components/NavModal/NavModal.props.d.ts +15 -0
  15. package/build/components/NavModal/NavModal.props.js +1 -0
  16. package/build/components/NavModal/index.d.ts +2 -0
  17. package/build/components/NavModal/index.js +1 -0
  18. package/build/components/Select/Select.js +1 -1
  19. package/build/components/index.d.ts +2 -1
  20. package/build/components/index.js +2 -1
  21. package/docs/changelog.mdx +34 -0
  22. package/docs/components/AllComponents.web.tsx +709 -689
  23. package/package.json +5 -3
  24. package/src/components/Combobox/Combobox.tsx +1 -1
  25. package/src/components/Modal/Modal.docs.mdx +5 -122
  26. package/src/components/Modal/Modal.props.ts +2 -34
  27. package/src/components/Modal/Modal.shared.types.ts +23 -0
  28. package/src/components/Modal/Modal.stories.tsx +0 -1
  29. package/src/components/Modal/Modal.tsx +11 -174
  30. package/src/components/Modal/Modal.web.tsx +29 -127
  31. package/src/components/NavModal/NavModal.docs.mdx +178 -0
  32. package/src/components/NavModal/NavModal.figma.tsx +13 -0
  33. package/src/components/NavModal/NavModal.props.ts +23 -0
  34. package/src/components/NavModal/NavModal.stories.tsx +131 -0
  35. package/src/components/NavModal/NavModal.tsx +375 -0
  36. package/src/components/NavModal/index.ts +2 -0
  37. package/src/components/Select/Select.tsx +1 -1
  38. package/src/components/index.ts +3 -1
  39. package/build/components/SafeAreaView/SafeAreaView.d.ts +0 -5
  40. package/build/components/SafeAreaView/SafeAreaView.js +0 -117
  41. package/build/components/SafeAreaView/SafeAreaView.props.d.ts +0 -17
  42. package/build/components/SafeAreaView/index.d.ts +0 -2
  43. package/build/components/SafeAreaView/index.js +0 -1
  44. package/src/components/SafeAreaView/SafeAreaView.props.ts +0 -20
  45. package/src/components/SafeAreaView/SafeAreaView.tsx +0 -173
  46. package/src/components/SafeAreaView/index.ts +0 -2
  47. /package/build/components/{SafeAreaView/SafeAreaView.props.js → Modal/Modal.shared.types.js} +0 -0
@@ -1,8 +1,9 @@
1
1
  import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
2
2
  import '@utilitywarehouse/hearth-fonts';
3
- import '@utilitywarehouse/hearth-tokens/index.css';
4
3
  import { color } from '@utilitywarehouse/hearth-tokens';
4
+ import '@utilitywarehouse/hearth-tokens/index.css';
5
5
  import { useEffect } from 'react';
6
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
6
7
  import '../../../shared/storybook/styles/diff-highlighting.css';
7
8
  import '../../../shared/storybook/styles/preview.css';
8
9
  import theme from '../../../shared/storybook/theme';
@@ -159,9 +160,11 @@ const preview = {
159
160
  }, [isDarkMode, args.darkMode, args.surface, args.inverted]);
160
161
 
161
162
  return (
162
- <BottomSheetModalProvider>
163
- <Story args={{ ...args, darkMode: isDarkMode }} />
164
- </BottomSheetModalProvider>
163
+ <SafeAreaProvider>
164
+ <BottomSheetModalProvider>
165
+ <Story args={{ ...args, darkMode: isDarkMode }} />
166
+ </BottomSheetModalProvider>
167
+ </SafeAreaProvider>
165
168
  );
166
169
  },
167
170
  ],
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.28.6 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.29.1 build /home/runner/work/hearth/hearth/packages/react-native
3
3
  > tsc
4
4
 
@@ -1,5 +1,5 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.28.6 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.29.1 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint .
4
4
 
5
5
 
@@ -30,9 +30,6 @@
30
30
  /home/runner/work/hearth/hearth/packages/react-native/src/components/Input/Input.tsx
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
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.web.tsx
34
- 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
35
-
36
33
  /home/runner/work/hearth/hearth/packages/react-native/src/components/PillGroup/PillGroup.tsx
37
34
  17:9 warning The 'normalizedValue' conditional could make the dependencies of useMemo Hook (at line 33) change on every render. Move it inside the useMemo callback. Alternatively, wrap the initialization of 'normalizedValue' in its own useMemo() Hook react-hooks/exhaustive-deps
38
35
 
@@ -52,17 +49,17 @@
52
49
  14:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
53
50
  106:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
54
51
 
55
- 23 problems (0 errors, 23 warnings)
56
-
57
- Rule | Time (ms) | Relative
58
- :---------------------------------|----------:|--------:
59
- @typescript-eslint/no-unused-vars | 1617.178 | 58.4%
60
- react-hooks/exhaustive-deps | 164.271 | 5.9%
61
- react-hooks/rules-of-hooks | 89.591 | 3.2%
62
- no-global-assign | 84.280 | 3.0%
63
- no-unexpected-multiline | 56.606 | 2.0%
64
- no-loss-of-precision | 56.417 | 2.0%
65
- no-misleading-character-class | 48.013 | 1.7%
66
- no-useless-escape | 38.194 | 1.4%
67
- no-regex-spaces | 36.638 | 1.3%
68
- @typescript-eslint/ban-ts-comment | 35.602 | 1.3%
52
+ 22 problems (0 errors, 22 warnings)
53
+
54
+ Rule | Time (ms) | Relative
55
+ :-------------------------------------------------|----------:|--------:
56
+ @typescript-eslint/no-unused-vars | 1543.146 | 60.0%
57
+ react-hooks/exhaustive-deps | 137.989 | 5.4%
58
+ no-global-assign | 91.047 | 3.5%
59
+ react-hooks/rules-of-hooks | 90.077 | 3.5%
60
+ @typescript-eslint/no-unnecessary-type-constraint | 40.362 | 1.6%
61
+ no-loss-of-precision | 40.140 | 1.6%
62
+ no-unexpected-multiline | 37.188 | 1.4%
63
+ no-regex-spaces | 35.566 | 1.4%
64
+ @typescript-eslint/ban-ts-comment | 34.241 | 1.3%
65
+ no-fallthrough | 31.788 | 1.2%
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.29.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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
8
+
9
+ 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.
10
+
11
+ **Components affected**:
12
+ - `NavModal`
13
+
14
+ **Developer changes**:
15
+
16
+ No changes are required.
17
+
18
+ ## 0.29.0
19
+
20
+ ### Minor Changes
21
+
22
+ - [#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`
23
+
24
+ `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.
25
+
26
+ `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.
27
+
28
+ 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.
29
+
30
+ **Components affected**:
31
+ - `Modal`
32
+ - `NavModal`
33
+ - `SafeAreaView`
34
+
35
+ **Developer changes**:
36
+
37
+ Update navigation modal screens to use `NavModal` instead of `Modal`:
38
+
39
+ ```diff
40
+ - import { Modal } from '@utilitywarehouse/hearth-react-native';
41
+ + import { NavModal, type NavModalRef } from '@utilitywarehouse/hearth-react-native';
42
+
43
+ - const modalRef = useRef<Modal>(null);
44
+ + const modalRef = useRef<NavModalRef>(null);
45
+
46
+ - <Modal inNavModal background="brand" scrollable={false}>
47
+ + <NavModal background="brand" scrollable={false} presentation="modal">
48
+ {/* content */}
49
+ - </Modal>
50
+ + </NavModal>
51
+ ```
52
+
53
+ If you are using the bottom-sheet modal API, no changes are required.
54
+
55
+ 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`:
56
+
57
+ ```diff
58
+ - import { SafeAreaView } from '@utilitywarehouse/hearth-react-native';
59
+ + import { SafeAreaView } from 'react-native-safe-area-context';
60
+ ```
61
+
3
62
  ## 0.28.6
4
63
 
5
64
  ### Patch Changes
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { CloseSmallIcon, SearchMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
3
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { Pressable, View } from 'react-native';
5
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
6
  import { StyleSheet } from 'react-native-unistyles';
6
7
  import { useTheme } from '../../hooks';
7
8
  import { BodyText } from '../BodyText';
@@ -10,7 +11,6 @@ import { DetailText } from '../DetailText';
10
11
  import { FormField, useFormFieldContext } from '../FormField';
11
12
  import { Icon } from '../Icon';
12
13
  import { Input } from '../Input';
13
- import { SafeAreaView } from '../SafeAreaView';
14
14
  import { Spinner } from '../Spinner';
15
15
  import { UnstyledIconButton } from '../UnstyledIconButton';
16
16
  import { ComboboxContext } from './Combobox.context';
@@ -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, scrollable, ...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, stickyFooter, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export default Modal;
@@ -1,64 +1,21 @@
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, useMemo, useRef } from 'react';
5
- import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
6
- import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
4
+ import { useCallback, useImperativeHandle, useMemo, useRef } from 'react';
5
+ import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
7
6
  import { StyleSheet } from 'react-native-unistyles';
8
- import { useTheme } from '../../hooks';
9
- import { hexWithOpacity } from '../../utils';
10
7
  import { BodyText } from '../BodyText';
11
8
  import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
12
9
  import { Button } from '../Button';
13
10
  import { Heading } from '../Heading';
14
- import { SafeAreaView } from '../SafeAreaView';
15
11
  import { Spinner } from '../Spinner';
16
12
  import { UnstyledIconButton } from '../UnstyledIconButton';
17
- 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 }) => {
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 }) => {
18
14
  const bottomSheetModalRef = useRef(null);
19
15
  const viewRef = useRef(null);
20
16
  const scrollViewRef = useRef(null);
21
- const theme = useTheme();
22
- const backgroundOpacity = useSharedValue(0);
23
- const pretendContentTranslateY = useSharedValue(20);
24
- const isBrandBackground = background === 'brand';
25
- const triggerCloseAnimation = useCallback(() => {
26
- if (Platform.OS === 'android' && inNavModal) {
27
- pretendContentTranslateY.value = withTiming(20, {
28
- duration: 50,
29
- easing: Easing.in(Easing.quad),
30
- });
31
- backgroundOpacity.value = withTiming(0, {
32
- duration: 100,
33
- easing: Easing.in(Easing.quad),
34
- });
35
- }
36
- }, [inNavModal, pretendContentTranslateY, backgroundOpacity]);
37
17
  useImperativeHandle(ref, () => ({
38
18
  ...bottomSheetModalRef.current,
39
- triggerCloseAnimation,
40
- }));
41
- // Trigger animations on render for inNavModal Android modal
42
- useEffect(() => {
43
- if (Platform.OS === 'android' && inNavModal) {
44
- backgroundOpacity.value = withDelay(300, withTiming(1, {
45
- duration: 200,
46
- easing: Easing.out(Easing.quad),
47
- }));
48
- pretendContentTranslateY.value = withDelay(500, withTiming(0, {
49
- duration: 300,
50
- easing: Easing.out(Easing.quad),
51
- }));
52
- }
53
- }, [inNavModal, backgroundOpacity, pretendContentTranslateY]);
54
- const animatedBackgroundStyle = useAnimatedStyle(() => ({
55
- backgroundColor: hexWithOpacity(theme.components.overlay.backgroundColor, backgroundOpacity.value * (theme.components.overlay.opacity / 100)),
56
- }));
57
- const animatedInNavModalStyle = useAnimatedStyle(() => ({
58
- backgroundColor: hexWithOpacity(theme.components.overlay.backgroundColor, backgroundOpacity.value * (theme.components.overlay.opacity / 100)),
59
- }));
60
- const animatedPretendContentStyle = useAnimatedStyle(() => ({
61
- transform: [{ translateY: pretendContentTranslateY.value }],
62
19
  }));
63
20
  const handleChange = (index, position, type) => {
64
21
  if (index > -1) {
@@ -107,13 +64,10 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
107
64
  noButtons,
108
65
  stickyFooter,
109
66
  showHandle: props.showHandle,
110
- background: isBrandBackground ? 'brand' : 'primary',
111
67
  });
112
- const footer = useMemo(() => (_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] })), [
68
+ 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] })), [
113
69
  handlePrimaryButtonPress,
114
70
  handleSecondaryButtonPress,
115
- inNavModal,
116
- isBrandBackground,
117
71
  onPressPrimaryButton,
118
72
  onPressSecondaryButton,
119
73
  primaryButtonProps,
@@ -121,18 +75,9 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
121
75
  secondaryButtonProps,
122
76
  secondaryButtonText,
123
77
  ]);
124
- const InNavModalContainer = scrollable ? ScrollView : View;
125
- 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: {
126
- flex: stickyFooter ? 1 : 0,
127
- ...(scrollable ? { marginHorizontal: -1 } : {}),
128
- }, ...(scrollable ? { contentContainerStyle: { paddingHorizontal: 1 } } : {}), children: [children, !stickyFooter ? (_jsx(View, { style: styles.inNavModalFooterContainer, children: footer })) : null] })), !inNavModal && children, ((!stickyFooter && !inNavModal) || (inNavModal && stickyFooter)) && !noButtons
129
- ? footer
130
- : null] })) }));
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] })) }));
131
79
  const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [footer]);
132
- return inNavModal ? (_jsxs(View, { style: {
133
- flex: 1,
134
- backgroundColor: theme.color.background[isBrandBackground ? 'brand' : 'primary'],
135
- }, 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(SafeAreaView, { edges: ['top', 'bottom'], 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 })] }));
80
+ 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 })] }));
136
81
  };
137
82
  const styles = StyleSheet.create((theme, rt) => ({
138
83
  modal: {
@@ -229,39 +174,5 @@ const styles = StyleSheet.create((theme, rt) => ({
229
174
  paddingHorizontal: theme.components.bottomSheet.padding,
230
175
  paddingBottom: theme.components.bottomSheet.padding + rt.insets.bottom,
231
176
  },
232
- inNavModalContainer: {
233
- flex: 1,
234
- ...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
235
- },
236
- inNavModalContent: {
237
- flex: 1,
238
- borderTopLeftRadius: theme.components.modal.borderRadius,
239
- borderTopRightRadius: theme.components.modal.borderRadius,
240
- backgroundColor: theme.color.surface.neutral.strong,
241
- padding: theme.components.bottomSheet.padding,
242
- variants: {
243
- background: {
244
- primary: {},
245
- brand: {
246
- backgroundColor: theme.color.background.brand,
247
- },
248
- },
249
- },
250
- },
251
- inNavModalFooterContainer: {
252
- paddingTop: theme.components.bottomSheet.padding,
253
- },
254
- androidContainer: {
255
- height: rt.insets.top + 18,
256
- paddingLeft: theme.components.bottomSheet.padding,
257
- paddingRight: theme.components.bottomSheet.padding,
258
- justifyContent: 'flex-end',
259
- },
260
- pretendContent: {
261
- borderTopLeftRadius: theme.components.modal.borderRadius,
262
- borderTopRightRadius: theme.components.modal.borderRadius,
263
- height: 12,
264
- backgroundColor: theme.components.parts.modalStack.backgroundColorCardTop,
265
- },
266
177
  }));
267
178
  export default Modal;
@@ -1,37 +1,8 @@
1
- import { ReactNode } from 'react';
2
- import { ViewProps } from 'react-native';
3
1
  import { BottomSheetProps } from '../BottomSheet';
4
- import { ButtonWithoutChildrenProps } from '../Button/Button.props';
5
- import { UnstyledIconButtonProps } from '../UnstyledIconButton';
6
- interface ModalPropsBase extends Omit<BottomSheetProps, 'children'> {
7
- loading?: boolean;
8
- image?: ReactNode;
9
- showCloseButton?: boolean;
10
- heading?: string;
11
- loadingHeading?: string;
12
- description?: string;
2
+ import { ModalCommonProps } from './Modal.shared.types';
3
+ interface ModalProps extends Omit<BottomSheetProps, 'children'>, ModalCommonProps {
13
4
  fullscreen?: boolean;
14
- stickyFooter?: boolean;
15
- children?: ViewProps['children'];
16
- onPressPrimaryButton?: () => void;
17
- primaryButtonText?: string;
18
- onPressSecondaryButton?: () => void;
19
5
  closeOnPrimaryButtonPress?: boolean;
20
- secondaryButtonText?: string;
21
- onPressCloseButton?: () => void;
22
6
  closeOnSecondaryButtonPress?: boolean;
23
- primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
24
- secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
25
- closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
26
7
  }
27
- type ModalProps = (ModalPropsBase & {
28
- inNavModal?: false | undefined;
29
- scrollable?: never;
30
- background?: never;
31
- }) | (ModalPropsBase & {
32
- inNavModal: true;
33
- fullscreen?: never;
34
- scrollable?: boolean;
35
- background?: 'default' | 'brand';
36
- });
37
8
  export default ModalProps;
@@ -0,0 +1,22 @@
1
+ import { ReactNode } from 'react';
2
+ import { ViewProps } from 'react-native';
3
+ import { ButtonWithoutChildrenProps } from '../Button/Button.props';
4
+ import { UnstyledIconButtonProps } from '../UnstyledIconButton';
5
+ export interface ModalCommonProps {
6
+ loading?: boolean;
7
+ image?: ReactNode;
8
+ showCloseButton?: boolean;
9
+ heading?: string;
10
+ loadingHeading?: string;
11
+ description?: string;
12
+ stickyFooter?: boolean;
13
+ children?: ViewProps['children'];
14
+ onPressPrimaryButton?: () => void;
15
+ primaryButtonText?: string;
16
+ onPressSecondaryButton?: () => void;
17
+ secondaryButtonText?: string;
18
+ onPressCloseButton?: () => void;
19
+ primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
20
+ secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
21
+ closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
22
+ }
@@ -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, 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, loadingHeading, fullscreen, image, primaryButtonProps, secondaryButtonProps, closeButtonProps, ...props }: ModalProps) => import("react/jsx-runtime").JSX.Element;
7
7
  export default Modal;
@@ -1,61 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
- import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
3
+ import { useImperativeHandle, useRef } from 'react';
4
4
  import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
5
- import Animated, { Easing, useAnimatedStyle, useSharedValue, withDelay, withTiming, } from 'react-native-reanimated';
6
5
  import { StyleSheet } from 'react-native-unistyles';
7
- import { useTheme } from '../../hooks';
8
- import { hexWithOpacity } from '../../utils';
9
6
  import { BodyText } from '../BodyText';
10
7
  import { BottomSheetModal, BottomSheetScrollView } from '../BottomSheet';
11
8
  import { Button } from '../Button';
12
9
  import { Heading } from '../Heading';
13
10
  import { Spinner } from '../Spinner';
14
11
  import { UnstyledIconButton } from '../UnstyledIconButton';
15
- const Modal = ({ ref, children, heading, description, showCloseButton = true, primaryButtonText, secondaryButtonText, onPressPrimaryButton, onPressCloseButton, onPressSecondaryButton, closeOnPrimaryButtonPress = true, closeOnSecondaryButtonPress = true, loading, loadingHeading = 'Loading...', image, primaryButtonProps, secondaryButtonProps, closeButtonProps, inNavModal = false, ...props }) => {
12
+ 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, ...props }) => {
16
13
  const bottomSheetModalRef = useRef(null);
17
14
  const viewRef = useRef(null);
18
15
  const scrollViewRef = useRef(null);
19
- const theme = useTheme();
20
- const backgroundOpacity = useSharedValue(0);
21
- const pretendContentTranslateY = useSharedValue(20);
22
- const triggerCloseAnimation = useCallback(() => {
23
- if (Platform.OS === 'android' && inNavModal) {
24
- pretendContentTranslateY.value = withTiming(20, {
25
- duration: 50,
26
- easing: Easing.in(Easing.quad),
27
- });
28
- backgroundOpacity.value = withTiming(0, {
29
- duration: 100,
30
- easing: Easing.in(Easing.quad),
31
- });
32
- }
33
- }, [Platform.OS, inNavModal, pretendContentTranslateY, backgroundOpacity]);
34
16
  useImperativeHandle(ref, () => ({
35
17
  ...bottomSheetModalRef.current,
36
- triggerCloseAnimation,
37
- }));
38
- // Trigger animations on render for inNavModal Android modal
39
- useEffect(() => {
40
- if (Platform.OS === 'android' && inNavModal) {
41
- backgroundOpacity.value = withDelay(300, withTiming(1, {
42
- duration: 200,
43
- easing: Easing.out(Easing.quad),
44
- }));
45
- pretendContentTranslateY.value = withDelay(500, withTiming(0, {
46
- duration: 300,
47
- easing: Easing.out(Easing.quad),
48
- }));
49
- }
50
- }, [inNavModal, backgroundOpacity, pretendContentTranslateY]);
51
- const animatedBackgroundStyle = useAnimatedStyle(() => ({
52
- backgroundColor: hexWithOpacity(theme.components.overlay.backgroundColor, backgroundOpacity.value * (theme.components.overlay.opacity / 100)),
53
- }));
54
- const animatedInNavModalStyle = useAnimatedStyle(() => ({
55
- backgroundColor: hexWithOpacity(theme.components.overlay.backgroundColor, backgroundOpacity.value * (theme.components.overlay.opacity / 100)),
56
- }));
57
- const animatedPretendContentStyle = useAnimatedStyle(() => ({
58
- transform: [{ translateY: pretendContentTranslateY.value }],
59
18
  }));
60
19
  const handleChange = (index, position, type) => {
61
20
  if (index > -1) {
@@ -97,10 +56,11 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
97
56
  bottomSheetModalRef.current?.dismiss();
98
57
  }
99
58
  };
100
- 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, _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] })] })) }));
101
- 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 }) })] })) : (_jsx(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, ...props, onChange: handleChange, children: _jsx(BottomSheetScrollView, { contentContainerStyle: styles.container, ref: scrollViewRef, children: content }) }));
59
+ const noButtons = !onPressPrimaryButton && !onPressSecondaryButton;
60
+ 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, !noButtons ? (_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] })) : null] })) }));
61
+ return (_jsx(BottomSheetModal, { ref: bottomSheetModalRef, enableDynamicSizing: true, snapPoints: image || fullscreen ? ['90%'] : props.snapPoints, showHandle: typeof loading !== 'undefined' && loading ? false : props.showHandle, accessible: false, ...props, onChange: handleChange, children: _jsx(BottomSheetScrollView, { contentContainerStyle: styles.container, ref: scrollViewRef, children: content }) }));
102
62
  };
103
- const styles = StyleSheet.create((theme, rt) => ({
63
+ const styles = StyleSheet.create(theme => ({
104
64
  container: {
105
65
  flex: 1,
106
66
  gap: theme.components.modal.gap,
@@ -131,30 +91,5 @@ const styles = StyleSheet.create((theme, rt) => ({
131
91
  footer: {
132
92
  gap: theme.components.modal.action.gap,
133
93
  },
134
- inNavModalContainer: {
135
- flex: 1,
136
- ...(Platform.OS === 'ios' ? { backgroundColor: theme.components.overlay.backgroundColor } : {}),
137
- },
138
- inNavModalContent: {
139
- flex: 1,
140
- borderTopLeftRadius: theme.components.modal.borderRadius,
141
- borderTopRightRadius: theme.components.modal.borderRadius,
142
- backgroundColor: theme.color.surface.neutral.strong,
143
- gap: theme.components.modal.gap,
144
- padding: theme.components.bottomSheet.padding,
145
- paddingBottom: theme.components.modal.padding + rt.insets.bottom,
146
- },
147
- androidContainer: {
148
- height: rt.insets.top + 18,
149
- paddingLeft: theme.components.bottomSheet.padding,
150
- paddingRight: theme.components.bottomSheet.padding,
151
- justifyContent: 'flex-end',
152
- },
153
- pretendContent: {
154
- borderTopLeftRadius: theme.components.modal.borderRadius,
155
- borderTopRightRadius: theme.components.modal.borderRadius,
156
- height: 12,
157
- backgroundColor: theme.components.parts.modalStack.backgroundColorCardTop,
158
- },
159
94
  }));
160
95
  export default Modal;
@@ -0,0 +1,3 @@
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;
3
+ export default NavModal;