@hero-design/rn 8.41.2 → 8.41.3-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/es/index.js +681 -583
  3. package/lib/index.js +685 -585
  4. package/package.json +8 -7
  5. package/rollup.config.js +1 -0
  6. package/src/components/Error/StyledError.tsx +1 -2
  7. package/src/components/Error/__tests__/__snapshots__/index.spec.tsx.snap +97 -115
  8. package/src/components/Error/__tests__/index.spec.tsx +6 -9
  9. package/src/components/Modal/ModalContentWrapper.tsx +112 -0
  10. package/src/components/Modal/ModalPresenter/ModalPresenter.tsx +135 -0
  11. package/src/components/Modal/ModalPresenter/index.tsx +9 -0
  12. package/src/components/Modal/ModalProvider.tsx +8 -0
  13. package/src/components/Modal/index.tsx +82 -178
  14. package/src/components/Success/StyledSuccess.tsx +1 -2
  15. package/src/components/Success/__tests__/__snapshots__/index.spec.tsx.snap +95 -115
  16. package/src/components/Success/__tests__/index.spec.tsx +6 -9
  17. package/src/index.ts +2 -0
  18. package/testUtils/setup.tsx +18 -0
  19. package/types/components/Error/StyledError.d.ts +5 -3
  20. package/types/components/Modal/ModalContentWrapper.d.ts +16 -0
  21. package/types/components/Modal/ModalPresenter/ModalPresenter.d.ts +34 -0
  22. package/types/components/Modal/ModalPresenter/index.d.ts +3 -0
  23. package/types/components/Modal/ModalProvider.d.ts +5 -0
  24. package/types/components/Modal/index.d.ts +8 -12
  25. package/types/components/Success/StyledSuccess.d.ts +5 -3
  26. package/types/index.d.ts +2 -1
  27. package/src/components/Modal/__tests__/__snapshots__/index.spec.tsx.snap +0 -117
  28. package/src/components/Modal/__tests__/index.spec.tsx +0 -99
@@ -1,41 +1,21 @@
1
- import React, {
2
- ReactNode,
3
- forwardRef,
4
- useEffect,
5
- useImperativeHandle,
6
- useRef,
7
- useState,
8
- useCallback,
9
- } from 'react';
10
- import {
11
- Animated,
12
- BackHandler,
13
- Dimensions,
14
- Easing,
15
- Platform,
16
- StyleSheet,
17
- } from 'react-native';
18
- import { useTheme } from '../../theme';
19
- import Portal from '../Portal';
20
-
21
- type ModalHandles = {
22
- show: () => void;
23
- hide: (callback: () => void) => void;
24
- };
25
-
26
- const DEFAULT_BACKDROP_OPACITY = 0.4;
27
-
28
- const DEFAULT_ANIMATION_CONFIG = {
29
- easing: Easing.inOut(Easing.cubic),
30
- useNativeDriver: Platform.OS !== 'web',
31
- duration: 400,
1
+ import React from 'react';
2
+ import { BackHandler, Dimensions, View, ViewStyle } from 'react-native';
3
+ import ModalContentWrapper, {
4
+ ModalContentWrapperHandler,
5
+ } from './ModalContentWrapper';
6
+ import ModalProvider from './ModalProvider';
7
+ import { ModalHandler, showModal } from './ModalPresenter';
8
+
9
+ const wrapperStyle: ViewStyle = {
10
+ width: Dimensions.get('window').width,
11
+ height: Dimensions.get('window').height,
32
12
  };
33
13
 
34
14
  export interface ModalProps {
35
15
  /**
36
16
  * Content of the modal.
37
17
  */
38
- children: ReactNode;
18
+ children: React.ReactElement;
39
19
  /**
40
20
  * Visibility of the modal
41
21
  */
@@ -56,162 +36,86 @@ export interface ModalProps {
56
36
  * Animation type of the modal content.
57
37
  */
58
38
  animationType?: 'none' | 'slide' | 'fade';
59
- /**
60
- * Whether to show the modal backdrop
61
- */
62
- transparent?: boolean;
63
- /**
64
- * Callback when the modal is dismissed. iOS only.
65
- */
66
- onDismiss?: () => void;
67
39
  }
68
40
 
69
- const Modal = forwardRef<ModalHandles, Omit<ModalProps, 'visible'>>(
70
- (
71
- {
72
- children,
73
- onShow,
74
- onRequestClose,
75
- testID,
76
- animationType = 'none',
77
- transparent = false,
78
- onDismiss,
79
- },
80
- ref
81
- ) => {
82
- const theme = useTheme();
83
- const animatedBackdropValue = useRef(new Animated.Value(0)).current;
84
- const animatedModalValue = useRef(new Animated.Value(0)).current;
85
-
86
- // Show or hide the backdrop and modal content
87
- const animateBackdropAndContent = useCallback(
88
- ({ toValue, callback }: { toValue: number; callback?: () => void }) => {
89
- if (animationType !== 'none') {
90
- // Backdrop animation
91
- if (!transparent) {
92
- Animated.timing(animatedBackdropValue, {
93
- toValue,
94
- ...DEFAULT_ANIMATION_CONFIG,
95
- }).start();
96
- }
97
-
98
- // Modal content animation
99
- Animated.timing(animatedModalValue, {
100
- toValue,
101
- ...DEFAULT_ANIMATION_CONFIG,
102
- }).start(callback);
103
- } else {
104
- callback?.();
105
- }
106
- },
107
- [animationType, onShow, transparent]
108
- );
109
-
110
- const backdropOpacityAnimation = animatedBackdropValue.interpolate({
111
- inputRange: [0, 1],
112
- outputRange: [0, DEFAULT_BACKDROP_OPACITY],
113
- });
114
-
115
- const modalAnimation = animatedModalValue.interpolate({
116
- inputRange: [0, 1],
117
- outputRange:
118
- animationType === 'slide'
119
- ? [Dimensions.get('window').height, 0]
120
- : [0, 1],
121
- });
122
-
123
- useImperativeHandle(
124
- ref,
125
- () => {
126
- return {
127
- show: () => {
128
- animateBackdropAndContent({ toValue: 1, callback: onShow });
129
- },
130
-
131
- hide: (wrapperCallback) => {
132
- animateBackdropAndContent({
133
- toValue: 0,
134
- callback: () => {
135
- if (Platform.OS === 'ios') {
136
- onDismiss?.();
137
- }
138
-
139
- wrapperCallback();
140
- },
141
- });
142
- },
143
- };
144
- },
145
- [onDismiss, onShow, animateBackdropAndContent]
146
- );
147
-
148
- // Back button handler
149
- useEffect(() => {
150
- const backHandler = BackHandler.addEventListener(
151
- 'hardwareBackPress',
152
- () => {
153
- onRequestClose?.();
154
- return true;
155
- }
156
- );
157
-
158
- return () => backHandler.remove();
159
- }, [onRequestClose]);
160
-
161
- return (
162
- <Portal>
163
- <Animated.View
164
- style={{
165
- ...StyleSheet.absoluteFillObject,
166
- backgroundColor: transparent
167
- ? 'transparent'
168
- : theme.colors.overlayGlobalSurface,
169
- opacity:
170
- animationType !== 'none'
171
- ? backdropOpacityAnimation
172
- : DEFAULT_BACKDROP_OPACITY,
173
- }}
174
- />
175
-
176
- <Animated.View
41
+ const Modal = ({
42
+ children,
43
+ onShow,
44
+ onRequestClose,
45
+ testID,
46
+ visible = true,
47
+ animationType = 'none',
48
+ }: ModalProps) => {
49
+ const [modalHandler, setModalHandler] = React.useState<ModalHandler>();
50
+ const modalContentWrapperRef = React.useRef<ModalContentWrapperHandler>(null);
51
+
52
+ const getModalContent = React.useCallback(
53
+ (isUpdate = false) => {
54
+ return animationType === 'none' ? (
55
+ <View style={wrapperStyle} testID={testID}>
56
+ {children}
57
+ </View>
58
+ ) : (
59
+ <ModalContentWrapper
60
+ visible={visible}
61
+ style={wrapperStyle}
62
+ animationType={animationType}
177
63
  testID={testID}
178
- style={{
179
- ...StyleSheet.absoluteFillObject,
180
- opacity: animationType === 'fade' ? modalAnimation : 1,
181
- transform: [
182
- {
183
- translateY: animationType === 'slide' ? modalAnimation : 0,
184
- },
185
- ],
186
- }}
64
+ onShow={onShow}
65
+ ref={modalContentWrapperRef}
66
+ animated={!isUpdate}
187
67
  >
188
68
  {children}
189
- </Animated.View>
190
- </Portal>
191
- );
192
- }
193
- );
194
-
195
- const ModalWrapper = ({ visible = true, ...props }: ModalProps) => {
196
- const modalRef = useRef<ModalHandles>(null);
197
- const [internalVisible, setInternalVisible] = useState(visible);
69
+ </ModalContentWrapper>
70
+ );
71
+ },
72
+ [visible, children, onShow, testID, animationType]
73
+ );
198
74
 
199
- useEffect(() => {
75
+ React.useEffect(() => {
200
76
  if (visible) {
201
- setInternalVisible(true);
77
+ // Modal does not exist, create a new one
78
+ if (!modalHandler) {
79
+ const newModalHandler = showModal(getModalContent(false));
80
+ setModalHandler(newModalHandler);
81
+
82
+ // If animationType is slide for fade, onShow would be run after animation on ModalContentWrapper,
83
+ // else run on this component.
84
+ if (animationType === 'none') {
85
+ onShow?.();
86
+ }
87
+ }
88
+ // Modal already exists, update it
89
+ else {
90
+ modalHandler.update(getModalContent(true));
91
+ }
92
+ } else if (animationType === 'none') {
93
+ modalHandler?.dismiss();
94
+ setModalHandler(undefined);
202
95
  } else {
203
- // Wait for animation to finish before hiding the modal
204
- modalRef.current?.hide(() => setInternalVisible(false));
96
+ // Wait to finish animation before dismissing
97
+ modalContentWrapperRef.current?.hide(() => {
98
+ modalHandler?.dismiss();
99
+ setModalHandler(undefined);
100
+ });
205
101
  }
206
- }, [visible]);
102
+ }, [getModalContent]);
207
103
 
208
- useEffect(() => {
209
- if (internalVisible) {
210
- modalRef.current?.show();
211
- }
212
- }, [internalVisible]);
104
+ React.useEffect(() => {
105
+ const backHandler = BackHandler.addEventListener(
106
+ 'hardwareBackPress',
107
+ () => {
108
+ onRequestClose?.();
109
+ return true;
110
+ }
111
+ );
112
+
113
+ return () => backHandler.remove();
114
+ }, [onRequestClose]);
213
115
 
214
- return internalVisible ? <Modal ref={modalRef} {...props} /> : null;
116
+ return null;
215
117
  };
216
118
 
217
- export default ModalWrapper;
119
+ export default Object.assign(Modal, {
120
+ Provider: ModalProvider,
121
+ });
@@ -1,9 +1,8 @@
1
1
  import styled from '@emotion/native';
2
- import { View } from 'react-native';
2
+ import { Modal, View } from 'react-native';
3
3
  import Image from '../Image';
4
4
  import Typography from '../Typography';
5
5
  import Button from '../Button';
6
- import Modal from '../Modal';
7
6
 
8
7
  type SuccessVariant = 'full-screen' | 'in-page';
9
8
 
@@ -1,164 +1,144 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
3
  exports[`Success renders full screen success page correctly 1`] = `
4
- [
5
- <View
6
- collapsable={false}
7
- style={
4
+ <Modal
5
+ animationType="slide"
6
+ hardwareAccelerated={false}
7
+ style={
8
+ [
8
9
  {
9
- "backgroundColor": "#000000",
10
- "bottom": 0,
11
- "left": 0,
12
- "opacity": 0,
13
- "position": "absolute",
14
- "right": 0,
15
- "top": 0,
16
- }
17
- }
18
- />,
10
+ "height": "100%",
11
+ "width": "100%",
12
+ },
13
+ undefined,
14
+ ]
15
+ }
16
+ visible={true}
17
+ >
19
18
  <View
20
- collapsable={false}
21
19
  style={
22
- {
23
- "bottom": 0,
24
- "left": 0,
25
- "opacity": 1,
26
- "position": "absolute",
27
- "right": 0,
28
- "top": 0,
29
- "transform": [
30
- {
31
- "translateY": 1334,
32
- },
33
- ],
34
- }
20
+ [
21
+ {
22
+ "backgroundColor": "#ccd2d3",
23
+ "display": "flex",
24
+ "flex": 1,
25
+ "flexDirection": "column",
26
+ },
27
+ undefined,
28
+ ]
35
29
  }
30
+ themeVariant="full-screen"
36
31
  >
37
32
  <View
38
33
  style={
39
34
  [
40
35
  {
41
- "backgroundColor": "#ccd2d3",
36
+ "alignItems": "center",
42
37
  "display": "flex",
43
38
  "flex": 1,
44
39
  "flexDirection": "column",
40
+ "justifyContent": "center",
41
+ "padding": 24,
45
42
  },
46
43
  undefined,
47
44
  ]
48
45
  }
49
- themeVariant="full-screen"
50
46
  >
51
47
  <View
52
48
  style={
53
49
  [
54
50
  {
55
- "alignItems": "center",
56
- "display": "flex",
57
- "flex": 1,
58
- "flexDirection": "column",
59
- "justifyContent": "center",
60
- "padding": 24,
51
+ "height": 176,
52
+ "marginBottom": 32,
53
+ "width": 176,
61
54
  },
62
55
  undefined,
63
56
  ]
64
57
  }
65
58
  >
66
- <View
67
- style={
68
- [
69
- {
70
- "height": 176,
71
- "marginBottom": 32,
72
- "width": 176,
73
- },
74
- undefined,
75
- ]
76
- }
77
- >
78
- <Image
79
- source={
80
- {
81
- "uri": "path_to_image",
82
- }
83
- }
84
- style={
85
- [
86
- {
87
- "borderRadius": 0,
88
- "height": 72,
89
- "width": 72,
90
- },
91
- [
92
- {
93
- "height": 176,
94
- "marginBottom": 32,
95
- "resizeMode": "contain",
96
- "width": 176,
97
- },
98
- undefined,
99
- ],
100
- ]
59
+ <Image
60
+ source={
61
+ {
62
+ "uri": "path_to_image",
101
63
  }
102
- testID="success-image"
103
- />
104
- </View>
105
- <Text
106
- allowFontScaling={false}
64
+ }
107
65
  style={
108
66
  [
109
67
  {
110
- "color": "#001f23",
111
- "fontFamily": "RebondGrotesque-SemiBold",
112
- "fontSize": 24,
113
- "letterSpacing": 0.24,
114
- "lineHeight": 32,
68
+ "borderRadius": 0,
69
+ "height": 72,
70
+ "width": 72,
115
71
  },
116
72
  [
117
73
  {
118
- "color": "#001f23",
119
- "marginBottom": 8,
120
- "textAlign": "center",
74
+ "height": 176,
75
+ "marginBottom": 32,
76
+ "resizeMode": "contain",
77
+ "width": 176,
121
78
  },
122
79
  undefined,
123
80
  ],
124
81
  ]
125
82
  }
126
- themeIntent="body"
127
- themeLevel="h4"
128
- themeTypeface="playful"
129
- >
130
- We’re sorry, something went wrong
131
- </Text>
132
- <Text
133
- allowFontScaling={false}
134
- style={
83
+ testID="success-image"
84
+ />
85
+ </View>
86
+ <Text
87
+ allowFontScaling={false}
88
+ style={
89
+ [
90
+ {
91
+ "color": "#001f23",
92
+ "fontFamily": "RebondGrotesque-SemiBold",
93
+ "fontSize": 24,
94
+ "letterSpacing": 0.24,
95
+ "lineHeight": 32,
96
+ },
135
97
  [
136
98
  {
137
99
  "color": "#001f23",
138
- "fontFamily": "BeVietnamPro-Regular",
139
- "fontSize": 16,
140
- "letterSpacing": 0.48,
141
- "lineHeight": 24,
100
+ "marginBottom": 8,
101
+ "textAlign": "center",
142
102
  },
143
- [
144
- {
145
- "color": "#4d6265",
146
- "textAlign": "center",
147
- },
148
- undefined,
149
- ],
150
- ]
151
- }
152
- themeIntent="body"
153
- themeTypeface="neutral"
154
- themeVariant="regular"
155
- >
156
- Please try again later
157
- </Text>
158
- </View>
103
+ undefined,
104
+ ],
105
+ ]
106
+ }
107
+ themeIntent="body"
108
+ themeLevel="h4"
109
+ themeTypeface="playful"
110
+ >
111
+ We’re sorry, something went wrong
112
+ </Text>
113
+ <Text
114
+ allowFontScaling={false}
115
+ style={
116
+ [
117
+ {
118
+ "color": "#001f23",
119
+ "fontFamily": "BeVietnamPro-Regular",
120
+ "fontSize": 16,
121
+ "letterSpacing": 0.48,
122
+ "lineHeight": 24,
123
+ },
124
+ [
125
+ {
126
+ "color": "#4d6265",
127
+ "textAlign": "center",
128
+ },
129
+ undefined,
130
+ ],
131
+ ]
132
+ }
133
+ themeIntent="body"
134
+ themeTypeface="neutral"
135
+ themeVariant="regular"
136
+ >
137
+ Please try again later
138
+ </Text>
159
139
  </View>
160
- </View>,
161
- ]
140
+ </View>
141
+ </Modal>
162
142
  `;
163
143
 
164
144
  exports[`Success renders succe screen with custom image element correctly 1`] = `
@@ -3,7 +3,6 @@ import { fireEvent } from '@testing-library/react-native';
3
3
  import renderWithTheme from '../../../testHelpers/renderWithTheme';
4
4
  import Success from '..';
5
5
  import Image from '../../Image';
6
- import Portal from '../../Portal';
7
6
 
8
7
  const title = `We’re sorry, something went wrong`;
9
8
  const description = 'Please try again later';
@@ -47,14 +46,12 @@ describe('Success', () => {
47
46
  });
48
47
  it('renders full screen success page correctly', () => {
49
48
  const { toJSON, getByText, getByTestId } = renderWithTheme(
50
- <Portal.Provider>
51
- <Success
52
- variant="full-screen"
53
- title={title}
54
- description={description}
55
- image="path_to_image"
56
- />
57
- </Portal.Provider>
49
+ <Success
50
+ variant="full-screen"
51
+ title={title}
52
+ description={description}
53
+ image="path_to_image"
54
+ />
58
55
  );
59
56
 
60
57
  expect(getByText(title)).toBeTruthy();
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ import HeroDesignProvider from './components/HeroDesignProvider';
38
38
  import Icon from './components/Icon';
39
39
  import Image from './components/Image';
40
40
  import List from './components/List';
41
+ import Modal from './components/Modal';
41
42
  import PinInput from './components/PinInput';
42
43
  import Progress from './components/Progress';
43
44
  import Slider from './components/Slider';
@@ -102,6 +103,7 @@ export {
102
103
  Image,
103
104
  HeroDesignProvider,
104
105
  List,
106
+ Modal,
105
107
  PinInput,
106
108
  Progress,
107
109
  Portal,
@@ -132,4 +132,22 @@ jest.mock('react-native/Libraries/Utilities/BackHandler', () => {
132
132
  );
133
133
  });
134
134
 
135
+ jest.mock('react-native-root-siblings', () => {
136
+ const React = jest.requireActual('react');
137
+
138
+ class RootSiblingsManager extends React.Component {
139
+ constructor(props) {
140
+ super(props);
141
+ }
142
+ render() {
143
+ return React.createElement('RootSiblingsManager', this.props, this.props.children);
144
+ }
145
+ }
146
+
147
+ return {
148
+ __esModule: true,
149
+ default: RootSiblingsManager,
150
+ };
151
+ });
152
+
135
153
  export {};
@@ -1,10 +1,12 @@
1
1
  /// <reference types="react" />
2
- import { View } from 'react-native';
2
+ import { Modal, View } from 'react-native';
3
3
  type ErrorVariant = 'full-screen' | 'in-page';
4
- declare const StyledErrorModal: import("@emotion/native").StyledComponent<import("../Modal").ModalProps & {
4
+ declare const StyledErrorModal: import("@emotion/native").StyledComponent<import("react-native").ModalBaseProps & import("react-native").ModalPropsIOS & import("react-native").ModalPropsAndroid & import("react-native").ViewProps & {
5
5
  theme?: import("@emotion/react").Theme | undefined;
6
6
  as?: import("react").ElementType<any> | undefined;
7
- }, {}, {}>;
7
+ }, {}, {
8
+ ref?: import("react").Ref<Modal> | undefined;
9
+ }>;
8
10
  declare const StyledErrorContainer: import("@emotion/native").StyledComponent<import("react-native").ViewProps & {
9
11
  theme?: import("@emotion/react").Theme | undefined;
10
12
  as?: import("react").ElementType<any> | undefined;
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ type ModalContentWrapperProps = {
4
+ children: React.ReactElement;
5
+ visible?: boolean;
6
+ onShow?: () => void;
7
+ testID?: string;
8
+ animationType?: 'none' | 'slide' | 'fade';
9
+ style?: StyleProp<ViewStyle>;
10
+ animated?: boolean;
11
+ };
12
+ export type ModalContentWrapperHandler = {
13
+ hide: (callback?: () => void) => void;
14
+ };
15
+ declare const ModalContentWrapper: React.ForwardRefExoticComponent<ModalContentWrapperProps & React.RefAttributes<ModalContentWrapperHandler>>;
16
+ export default ModalContentWrapper;