@hero-design/rn 8.33.2 → 8.34.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.
- package/.turbo/turbo-build.log +1 -1
- package/assets/fonts/hero-icons-mobile.ttf +0 -0
- package/es/index.js +797 -488
- package/lib/assets/fonts/hero-icons-mobile.ttf +0 -0
- package/lib/index.js +800 -489
- package/package.json +8 -5
- package/rollup.config.js +2 -0
- package/src/components/Calendar/__tests__/index.spec.tsx +47 -0
- package/src/components/Calendar/index.tsx +161 -59
- package/src/components/Carousel/CardCarousel.tsx +2 -0
- package/src/components/DatePicker/DatePickerAndroid.tsx +4 -1
- package/src/components/DatePicker/DatePickerCalendar.tsx +18 -1
- package/src/components/DatePicker/DatePickerIOS.tsx +4 -1
- package/src/components/DatePicker/__tests__/DatePickerCalendar.spec.tsx +39 -0
- package/src/components/DatePicker/types.ts +8 -0
- package/src/components/Icon/HeroIcon/glyphMap.json +1 -1
- package/src/components/Icon/IconList.ts +2 -0
- package/src/components/Modal/ModalContentWrapper.tsx +112 -0
- package/src/components/Modal/ModalPresenter/ModalPresenter.tsx +135 -0
- package/src/components/Modal/ModalPresenter/index.tsx +9 -0
- package/src/components/Modal/ModalProvider.tsx +8 -0
- package/src/components/Modal/__tests__/ModalContentWrapper.spec.tsx +25 -0
- package/src/components/Modal/__tests__/ModalPresenter.spec.tsx +57 -0
- package/src/components/Modal/__tests__/__snapshots__/ModalContentWrapper.spec.tsx.snap +35 -0
- package/src/components/Modal/__tests__/__snapshots__/ModalPresenter.spec.tsx.snap +55 -0
- package/src/components/Modal/__tests__/index.spec.tsx +50 -0
- package/src/components/Modal/index.tsx +121 -0
- package/src/components/PinInput/index.tsx +2 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +5 -2
- package/src/components/Toast/ToastContainer.tsx +2 -0
- package/src/index.ts +2 -0
- package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +2 -0
- package/src/theme/components/calendar.ts +2 -0
- package/testUtils/setup.tsx +43 -0
- package/types/components/Calendar/index.d.ts +13 -1
- package/types/components/DatePicker/DatePickerAndroid.d.ts +1 -1
- package/types/components/DatePicker/DatePickerCalendar.d.ts +1 -1
- package/types/components/DatePicker/DatePickerIOS.d.ts +1 -1
- package/types/components/DatePicker/types.d.ts +8 -0
- package/types/components/Icon/IconList.d.ts +1 -1
- package/types/components/Icon/index.d.ts +1 -1
- package/types/components/Icon/utils.d.ts +1 -1
- package/types/components/Modal/ModalContentWrapper.d.ts +16 -0
- package/types/components/Modal/ModalPresenter/ModalPresenter.d.ts +34 -0
- package/types/components/Modal/ModalPresenter/index.d.ts +3 -0
- package/types/components/Modal/ModalProvider.d.ts +5 -0
- package/types/components/Modal/index.d.ts +33 -0
- package/types/components/RichTextEditor/RichTextEditor.d.ts +2 -2
- package/types/index.d.ts +2 -1
- package/types/theme/components/calendar.d.ts +2 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Dimensions,
|
|
5
|
+
Easing,
|
|
6
|
+
Platform,
|
|
7
|
+
StyleProp,
|
|
8
|
+
ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
type ModalContentWrapperProps = {
|
|
12
|
+
children: React.ReactElement;
|
|
13
|
+
visible?: boolean;
|
|
14
|
+
onShow?: () => void;
|
|
15
|
+
testID?: string;
|
|
16
|
+
animationType?: 'none' | 'slide' | 'fade';
|
|
17
|
+
style?: StyleProp<ViewStyle>;
|
|
18
|
+
animated?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ModalContentWrapperHandler = {
|
|
22
|
+
hide: (callback?: () => void) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const windowHeight = Dimensions.get('window').height;
|
|
26
|
+
const defaultAnimationConfig = {
|
|
27
|
+
easing: Easing.inOut(Easing.cubic),
|
|
28
|
+
duration: 400,
|
|
29
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ModalContentWrapper = forwardRef<
|
|
33
|
+
ModalContentWrapperHandler,
|
|
34
|
+
ModalContentWrapperProps
|
|
35
|
+
>(
|
|
36
|
+
(
|
|
37
|
+
{
|
|
38
|
+
animationType,
|
|
39
|
+
children,
|
|
40
|
+
testID,
|
|
41
|
+
onShow,
|
|
42
|
+
style,
|
|
43
|
+
visible,
|
|
44
|
+
animated = true,
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) => {
|
|
48
|
+
const animatedValue = React.useRef(new Animated.Value(0)).current;
|
|
49
|
+
|
|
50
|
+
const modalAnimation = animatedValue.interpolate(
|
|
51
|
+
animationType === 'fade'
|
|
52
|
+
? {
|
|
53
|
+
inputRange: [0, 1],
|
|
54
|
+
outputRange: [0, 1],
|
|
55
|
+
}
|
|
56
|
+
: {
|
|
57
|
+
inputRange: [0, 1],
|
|
58
|
+
outputRange: [windowHeight, 0],
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
React.useImperativeHandle(
|
|
63
|
+
ref,
|
|
64
|
+
() => ({
|
|
65
|
+
hide: (callback) => {
|
|
66
|
+
Animated.timing(animatedValue, {
|
|
67
|
+
toValue: 0,
|
|
68
|
+
...defaultAnimationConfig,
|
|
69
|
+
}).start(callback);
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
[animatedValue]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
// Hiding animation will be called from the modal component
|
|
77
|
+
if (visible) {
|
|
78
|
+
Animated.timing(animatedValue, {
|
|
79
|
+
toValue: 1,
|
|
80
|
+
...defaultAnimationConfig,
|
|
81
|
+
// Prevent animation when updating the modal content
|
|
82
|
+
duration: animated ? defaultAnimationConfig.duration : 0,
|
|
83
|
+
}).start(() => {
|
|
84
|
+
onShow?.();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}, [visible, animatedValue, onShow, animated]);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Animated.View
|
|
91
|
+
testID={testID}
|
|
92
|
+
style={[
|
|
93
|
+
style,
|
|
94
|
+
{
|
|
95
|
+
...(animationType === 'fade' ? { opacity: modalAnimation } : {}),
|
|
96
|
+
...(animationType === 'slide'
|
|
97
|
+
? {
|
|
98
|
+
transform: [{ translateY: modalAnimation }],
|
|
99
|
+
}
|
|
100
|
+
: {}),
|
|
101
|
+
},
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</Animated.View>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
ModalContentWrapper.displayName = 'ModalContentWrapper';
|
|
111
|
+
|
|
112
|
+
export default ModalContentWrapper;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { forwardRef, useRef } from 'react';
|
|
2
|
+
import { Animated, StyleSheet, ViewProps } from 'react-native';
|
|
3
|
+
import RootSiblings from 'react-native-root-siblings';
|
|
4
|
+
import { useTheme } from '../../../theme';
|
|
5
|
+
import Box from '../../Box';
|
|
6
|
+
|
|
7
|
+
export type ModalPresenterHandles = {
|
|
8
|
+
animatedOut: (completion?: () => void) => void;
|
|
9
|
+
};
|
|
10
|
+
export type ModalDismissFunc = (onDismiss?: () => void) => void;
|
|
11
|
+
export type ModalUpdateFunc = (content: React.ReactNode) => void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Modal handler is returned by `showModal` function.
|
|
15
|
+
*/
|
|
16
|
+
export type ModalHandler = {
|
|
17
|
+
/**
|
|
18
|
+
* Same `dismiss` function as in `ModalContentProps`.
|
|
19
|
+
*/
|
|
20
|
+
dismiss: ModalDismissFunc;
|
|
21
|
+
/**
|
|
22
|
+
* Same `update` function as in `ModalContentProps`.
|
|
23
|
+
*/
|
|
24
|
+
update: ModalUpdateFunc;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ModalPresenter = forwardRef<ModalPresenterHandles, ViewProps>(
|
|
28
|
+
({ style, children, ...props }, ref) => {
|
|
29
|
+
const animatedOpacity = useRef(new Animated.Value(0));
|
|
30
|
+
const theme = useTheme();
|
|
31
|
+
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
Animated.spring(animatedOpacity.current, {
|
|
34
|
+
toValue: 1,
|
|
35
|
+
useNativeDriver: true,
|
|
36
|
+
}).start();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
React.useImperativeHandle(ref, () => ({
|
|
40
|
+
animatedOut: (completion?: () => void) => {
|
|
41
|
+
Animated.spring(animatedOpacity.current, {
|
|
42
|
+
toValue: 0,
|
|
43
|
+
useNativeDriver: true,
|
|
44
|
+
}).start(() => {
|
|
45
|
+
completion?.();
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box style={StyleSheet.absoluteFill}>
|
|
52
|
+
<Animated.View
|
|
53
|
+
style={[
|
|
54
|
+
{
|
|
55
|
+
width: '100%',
|
|
56
|
+
height: '100%',
|
|
57
|
+
backgroundColor: `${theme.colors.overlayGlobalSurface}66`, // 66 = 40% opacity as suggested by the mobile color guidelines
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
opacity: animatedOpacity.current,
|
|
61
|
+
},
|
|
62
|
+
style,
|
|
63
|
+
]}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</Animated.View>
|
|
68
|
+
</Box>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Present a modal on screen immediately.
|
|
75
|
+
*
|
|
76
|
+
* The new presented modal will be on top of existing modals if there are any.
|
|
77
|
+
*
|
|
78
|
+
* @param Content A component to be presented as a modal on screen.
|
|
79
|
+
* This component will be centered horizontally and vertically on screen with
|
|
80
|
+
* a semitransparent black overlay underneath.
|
|
81
|
+
* @param contentProps Props for this modal component.
|
|
82
|
+
* @returns A `ModalHandler` you can use to dismiss the modal.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
export const showModal = (content: React.ReactNode): ModalHandler => {
|
|
86
|
+
let ref: ModalPresenterHandles | null = null;
|
|
87
|
+
let rootSiblings: RootSiblings | null = null;
|
|
88
|
+
|
|
89
|
+
const dismiss: ModalDismissFunc = (onDismiss) => {
|
|
90
|
+
if (rootSiblings) {
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
rootSiblings?.destroy();
|
|
93
|
+
rootSiblings = null;
|
|
94
|
+
ref = null;
|
|
95
|
+
onDismiss?.();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (ref) {
|
|
99
|
+
ref.animatedOut(cleanup);
|
|
100
|
+
} else {
|
|
101
|
+
cleanup();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const update: ModalUpdateFunc = (newContent) => {
|
|
107
|
+
rootSiblings?.update(
|
|
108
|
+
<ModalPresenter
|
|
109
|
+
ref={(_ref) => {
|
|
110
|
+
ref = _ref;
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{newContent}
|
|
114
|
+
</ModalPresenter>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
rootSiblings = new RootSiblings(
|
|
119
|
+
(
|
|
120
|
+
<ModalPresenter
|
|
121
|
+
ref={(_ref) => {
|
|
122
|
+
ref = _ref;
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{content}
|
|
126
|
+
</ModalPresenter>
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return { dismiss, update };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
ModalPresenter.displayName = 'ModalPresenter';
|
|
134
|
+
|
|
135
|
+
export default ModalPresenter;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { RootSiblingParent } from 'react-native-root-siblings';
|
|
3
|
+
|
|
4
|
+
const ModalProvider = ({ children }: { children: ReactNode }) => {
|
|
5
|
+
return <RootSiblingParent>{children}</RootSiblingParent>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default ModalProvider;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../testHelpers/renderWithTheme';
|
|
3
|
+
import ModalContentWrapper from '../ModalContentWrapper';
|
|
4
|
+
import Typography from '../../Typography';
|
|
5
|
+
|
|
6
|
+
describe('ModalContentWrapper', () => {
|
|
7
|
+
it('should render correctly', () => {
|
|
8
|
+
const wrapper = renderWithTheme(
|
|
9
|
+
<ModalContentWrapper
|
|
10
|
+
testID="modal-content-wrapper"
|
|
11
|
+
style={{ backgroundColor: 'blue' }}
|
|
12
|
+
visible
|
|
13
|
+
>
|
|
14
|
+
<Typography.Text>Modal content</Typography.Text>
|
|
15
|
+
</ModalContentWrapper>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
19
|
+
expect(wrapper.getByText('Modal content')).toBeTruthy();
|
|
20
|
+
expect(wrapper.getByTestId('modal-content-wrapper')).toBeTruthy();
|
|
21
|
+
expect(wrapper.getByTestId('modal-content-wrapper')).toHaveStyle({
|
|
22
|
+
backgroundColor: 'blue',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../testHelpers/renderWithTheme';
|
|
3
|
+
import ModalPresenter, {
|
|
4
|
+
ModalPresenterHandles,
|
|
5
|
+
} from '../ModalPresenter/ModalPresenter';
|
|
6
|
+
import Typography from '../../Typography';
|
|
7
|
+
|
|
8
|
+
jest.mock('react-native', () => {
|
|
9
|
+
const RN = jest.requireActual('react-native');
|
|
10
|
+
|
|
11
|
+
const mockedAnimatedFunctions = {
|
|
12
|
+
start: (callback: () => void) => {
|
|
13
|
+
if (callback) {
|
|
14
|
+
callback();
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
_isUsingNativeDriver: () => jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
RN.Animated.spring = () => mockedAnimatedFunctions;
|
|
21
|
+
|
|
22
|
+
return RN;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('ModalPresenter', () => {
|
|
26
|
+
it('should render correctly', () => {
|
|
27
|
+
const wrapper = renderWithTheme(
|
|
28
|
+
<ModalPresenter
|
|
29
|
+
testID="modal-presenter"
|
|
30
|
+
style={{ backgroundColor: 'blue' }}
|
|
31
|
+
>
|
|
32
|
+
<Typography.Text>Modal content</Typography.Text>
|
|
33
|
+
</ModalPresenter>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
37
|
+
expect(wrapper.getByText('Modal content')).toBeTruthy();
|
|
38
|
+
expect(wrapper.getByTestId('modal-presenter')).toBeTruthy();
|
|
39
|
+
expect(wrapper.getByTestId('modal-presenter')).toHaveStyle({
|
|
40
|
+
backgroundColor: 'blue',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('allow using ref', () => {
|
|
45
|
+
const ref = React.createRef<ModalPresenterHandles>();
|
|
46
|
+
const completion = jest.fn();
|
|
47
|
+
|
|
48
|
+
renderWithTheme(
|
|
49
|
+
<ModalPresenter ref={ref}>
|
|
50
|
+
<Typography.Text>Modal content</Typography.Text>
|
|
51
|
+
</ModalPresenter>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
ref.current?.animatedOut(completion);
|
|
55
|
+
expect(completion).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`ModalContentWrapper should render correctly 1`] = `
|
|
4
|
+
<View
|
|
5
|
+
collapsable={false}
|
|
6
|
+
style={
|
|
7
|
+
Object {
|
|
8
|
+
"backgroundColor": "blue",
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
testID="modal-content-wrapper"
|
|
12
|
+
>
|
|
13
|
+
<Text
|
|
14
|
+
allowFontScaling={false}
|
|
15
|
+
style={
|
|
16
|
+
Array [
|
|
17
|
+
Object {
|
|
18
|
+
"color": "#001f23",
|
|
19
|
+
"fontFamily": "BeVietnamPro-Regular",
|
|
20
|
+
"fontSize": 14,
|
|
21
|
+
"letterSpacing": 0.42,
|
|
22
|
+
"lineHeight": 22,
|
|
23
|
+
},
|
|
24
|
+
undefined,
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
themeFontSize="medium"
|
|
28
|
+
themeFontWeight="regular"
|
|
29
|
+
themeIntent="body"
|
|
30
|
+
themeTypeface="neutral"
|
|
31
|
+
>
|
|
32
|
+
Modal content
|
|
33
|
+
</Text>
|
|
34
|
+
</View>
|
|
35
|
+
`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`ModalPresenter should render correctly 1`] = `
|
|
4
|
+
<View
|
|
5
|
+
style={
|
|
6
|
+
Array [
|
|
7
|
+
Object {},
|
|
8
|
+
Object {
|
|
9
|
+
"bottom": 0,
|
|
10
|
+
"left": 0,
|
|
11
|
+
"position": "absolute",
|
|
12
|
+
"right": 0,
|
|
13
|
+
"top": 0,
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
>
|
|
18
|
+
<View
|
|
19
|
+
collapsable={false}
|
|
20
|
+
style={
|
|
21
|
+
Object {
|
|
22
|
+
"alignItems": "center",
|
|
23
|
+
"backgroundColor": "blue",
|
|
24
|
+
"height": "100%",
|
|
25
|
+
"justifyContent": "center",
|
|
26
|
+
"opacity": 0,
|
|
27
|
+
"width": "100%",
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
testID="modal-presenter"
|
|
31
|
+
>
|
|
32
|
+
<Text
|
|
33
|
+
allowFontScaling={false}
|
|
34
|
+
style={
|
|
35
|
+
Array [
|
|
36
|
+
Object {
|
|
37
|
+
"color": "#001f23",
|
|
38
|
+
"fontFamily": "BeVietnamPro-Regular",
|
|
39
|
+
"fontSize": 14,
|
|
40
|
+
"letterSpacing": 0.42,
|
|
41
|
+
"lineHeight": 22,
|
|
42
|
+
},
|
|
43
|
+
undefined,
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
themeFontSize="medium"
|
|
47
|
+
themeFontWeight="regular"
|
|
48
|
+
themeIntent="body"
|
|
49
|
+
themeTypeface="neutral"
|
|
50
|
+
>
|
|
51
|
+
Modal content
|
|
52
|
+
</Text>
|
|
53
|
+
</View>
|
|
54
|
+
</View>
|
|
55
|
+
`;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BackHandler } from 'react-native';
|
|
3
|
+
import Modal from '..';
|
|
4
|
+
import Typography from '../../Typography';
|
|
5
|
+
import renderWithTheme from '../../../testHelpers/renderWithTheme';
|
|
6
|
+
|
|
7
|
+
jest.mock('react-native', () => {
|
|
8
|
+
const RN = jest.requireActual('react-native');
|
|
9
|
+
|
|
10
|
+
const mockedAnimatedFunctions = {
|
|
11
|
+
start: (callback: () => void) => {
|
|
12
|
+
if (callback) {
|
|
13
|
+
callback();
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
_isUsingNativeDriver: () => jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
RN.Animated.timing = () => mockedAnimatedFunctions;
|
|
20
|
+
|
|
21
|
+
return RN;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('Modal', () => {
|
|
25
|
+
it('onShow should be called correctly', () => {
|
|
26
|
+
const onShow = jest.fn();
|
|
27
|
+
|
|
28
|
+
renderWithTheme(
|
|
29
|
+
<Modal onShow={onShow} visible>
|
|
30
|
+
<Typography.Text>Modal content</Typography.Text>
|
|
31
|
+
</Modal>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(onShow).toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('onRequestClose should be called correctly', () => {
|
|
38
|
+
const onRequestClose = jest.fn();
|
|
39
|
+
|
|
40
|
+
renderWithTheme(
|
|
41
|
+
<Modal onRequestClose={onRequestClose} visible>
|
|
42
|
+
<Typography.Text>Modal content</Typography.Text>
|
|
43
|
+
</Modal>
|
|
44
|
+
);
|
|
45
|
+
// @ts-expect-error: BackHandler mock
|
|
46
|
+
BackHandler.mockPressBack();
|
|
47
|
+
|
|
48
|
+
expect(onRequestClose).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
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,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface ModalProps {
|
|
15
|
+
/**
|
|
16
|
+
* Content of the modal.
|
|
17
|
+
*/
|
|
18
|
+
children: React.ReactElement;
|
|
19
|
+
/**
|
|
20
|
+
* Visibility of the modal
|
|
21
|
+
*/
|
|
22
|
+
visible?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Callback when the modal is shown.
|
|
25
|
+
*/
|
|
26
|
+
onShow?: () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Callback when the user taps the hardware back button.
|
|
29
|
+
*/
|
|
30
|
+
onRequestClose?: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* TestID of the modal.
|
|
33
|
+
*/
|
|
34
|
+
testID?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Animation type of the modal content.
|
|
37
|
+
*/
|
|
38
|
+
animationType?: 'none' | 'slide' | 'fade';
|
|
39
|
+
}
|
|
40
|
+
|
|
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}
|
|
63
|
+
testID={testID}
|
|
64
|
+
onShow={onShow}
|
|
65
|
+
ref={modalContentWrapperRef}
|
|
66
|
+
animated={!isUpdate}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</ModalContentWrapper>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
[visible, children, onShow, testID, animationType]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (visible) {
|
|
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);
|
|
95
|
+
} else {
|
|
96
|
+
// Wait to finish animation before dismissing
|
|
97
|
+
modalContentWrapperRef.current?.hide(() => {
|
|
98
|
+
modalHandler?.dismiss();
|
|
99
|
+
setModalHandler(undefined);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}, [getModalContent]);
|
|
103
|
+
|
|
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]);
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default Object.assign(Modal, {
|
|
120
|
+
Provider: ModalProvider,
|
|
121
|
+
});
|
|
@@ -378,8 +378,11 @@ const RichTextEditor: ComponentType<RichTextEditorProps> = ({
|
|
|
378
378
|
</StyledContainer>
|
|
379
379
|
);
|
|
380
380
|
};
|
|
381
|
-
|
|
382
|
-
export default forwardRef<
|
|
381
|
+
const RichTextEditorWithRef = forwardRef<
|
|
383
382
|
RichTextEditorRef,
|
|
384
383
|
Omit<RichTextEditorProps, 'forwardedRef'>
|
|
385
384
|
>((props, ref) => <RichTextEditor {...props} forwardedRef={ref} />);
|
|
385
|
+
|
|
386
|
+
RichTextEditorWithRef.displayName = 'RichTextEditor';
|
|
387
|
+
|
|
388
|
+
export default RichTextEditorWithRef;
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import FAB from './components/FAB';
|
|
|
37
37
|
import Icon from './components/Icon';
|
|
38
38
|
import Image from './components/Image';
|
|
39
39
|
import List from './components/List';
|
|
40
|
+
import Modal from './components/Modal';
|
|
40
41
|
import PinInput from './components/PinInput';
|
|
41
42
|
import Progress from './components/Progress';
|
|
42
43
|
import Slider from './components/Slider';
|
|
@@ -99,6 +100,7 @@ export {
|
|
|
99
100
|
Icon,
|
|
100
101
|
Image,
|
|
101
102
|
List,
|
|
103
|
+
Modal,
|
|
102
104
|
PinInput,
|
|
103
105
|
Progress,
|
|
104
106
|
PageControl,
|
|
@@ -24,6 +24,8 @@ const getCalendarTheme = (theme: GlobalTheme) => {
|
|
|
24
24
|
rowVerticalPadding: theme.space.medium,
|
|
25
25
|
headerVerticalPadding: theme.space.medium,
|
|
26
26
|
headerHorizontalPadding: theme.space.smallMedium,
|
|
27
|
+
headerMarginRight: theme.space.small,
|
|
28
|
+
iosPickerMarginVertical: theme.space['5xlarge'],
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const radii = {
|