@haroldtran/react-native-modals 0.0.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.
@@ -0,0 +1,53 @@
1
+ // @flow
2
+
3
+ import React, { Component } from "react";
4
+ import { Animated, StyleSheet, TouchableOpacity } from "react-native";
5
+ import { BackdropProps } from "../type";
6
+
7
+ export default class Backdrop extends Component<BackdropProps> {
8
+ static defaultProps: Partial<BackdropProps> = {
9
+ backgroundColor: "#000",
10
+ opacity: 0.5,
11
+ animationDuration: 200,
12
+ visible: false,
13
+ useNativeDriver: true,
14
+ onPress: () => {},
15
+ };
16
+
17
+ opacity = new Animated.Value(0);
18
+
19
+ componentDidUpdate(prevProps: BackdropProps) {
20
+ const {
21
+ visible,
22
+ useNativeDriver = true,
23
+ opacity: toOpacity = 0.5,
24
+ animationDuration: duration = 200,
25
+ } = this.props;
26
+ if (prevProps.visible !== visible) {
27
+ const toValue = visible ? toOpacity : 0;
28
+ Animated.timing(this.opacity, {
29
+ toValue,
30
+ duration,
31
+ useNativeDriver,
32
+ }).start();
33
+ }
34
+ }
35
+
36
+ setOpacity = (value: number) => {
37
+ this.opacity.setValue(value);
38
+ };
39
+
40
+ render() {
41
+ const { onPress, pointerEvents, backgroundColor } = this.props;
42
+ const { opacity } = this;
43
+ const overlayStyle: any = [
44
+ StyleSheet.absoluteFill,
45
+ { backgroundColor, opacity },
46
+ ];
47
+ return (
48
+ <Animated.View pointerEvents={pointerEvents as any} style={overlayStyle}>
49
+ <TouchableOpacity onPress={onPress} style={StyleSheet.absoluteFill} />
50
+ </Animated.View>
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,294 @@
1
+ import React, { Component, Fragment } from "react";
2
+ import {
3
+ Animated,
4
+ BackHandler,
5
+ Dimensions,
6
+ StyleSheet,
7
+ View,
8
+ } from "react-native";
9
+
10
+ import Animation from "../animations/Animation";
11
+ import FadeAnimation from "../animations/FadeAnimation";
12
+ import type { ModalProps } from "../type";
13
+ import Backdrop from "./Backdrop";
14
+ import DraggableView from "./DraggableView";
15
+ import ModalContext from "./ModalContext";
16
+
17
+ const HARDWARE_BACK_PRESS_EVENT = "hardwareBackPress" as const;
18
+
19
+ // dialog states
20
+ const MODAL_OPENING: string = "opening";
21
+ const MODAL_OPENED: string = "opened";
22
+ const MODAL_CLOSING: string = "closing";
23
+ const MODAL_CLOSED: string = "closed";
24
+
25
+ // default dialog config
26
+ const DEFAULT_ANIMATION_DURATION: number = 150;
27
+
28
+ const styles = StyleSheet.create({
29
+ container: {
30
+ ...StyleSheet.absoluteFillObject,
31
+ },
32
+ modal: {
33
+ overflow: "hidden",
34
+ backgroundColor: "#ffffff",
35
+ },
36
+ hidden: {
37
+ top: -10000,
38
+ left: 0,
39
+ height: 0,
40
+ width: 0,
41
+ },
42
+ round: {
43
+ borderRadius: 8,
44
+ },
45
+ draggableView: {
46
+ flex: 1,
47
+ justifyContent: "center",
48
+ alignItems: "center",
49
+ },
50
+ });
51
+
52
+ type ModalState =
53
+ | typeof MODAL_OPENING
54
+ | typeof MODAL_OPENED
55
+ | typeof MODAL_CLOSING
56
+ | typeof MODAL_CLOSED;
57
+
58
+ type State = {
59
+ modalAnimation: Animation;
60
+ modalState: ModalState;
61
+ };
62
+
63
+ class BaseModal extends Component<ModalProps, State> {
64
+ // internal refs / stateful properties
65
+ backHandler: any;
66
+ lastSwipeEvent: any | null = null;
67
+ isSwipingOut: boolean = false;
68
+ backdrop: any;
69
+
70
+ static defaultProps = {
71
+ rounded: true,
72
+ modalTitle: null,
73
+ visible: false,
74
+ style: null,
75
+ animationDuration: DEFAULT_ANIMATION_DURATION,
76
+ modalStyle: null,
77
+ width: null,
78
+ height: null,
79
+ onTouchOutside: () => {},
80
+ onHardwareBackPress: () => false,
81
+ hasOverlay: true,
82
+ overlayOpacity: 0.5,
83
+ overlayPointerEvents: null,
84
+ overlayBackgroundColor: "#000",
85
+ onShow: () => {},
86
+ onDismiss: () => {},
87
+ footer: null,
88
+ onMove: () => {},
89
+ onSwiping: () => {},
90
+ onSwipeRelease: () => {},
91
+ onSwipingOut: () => {},
92
+ useNativeDriver: true,
93
+ };
94
+
95
+ constructor(props: ModalProps) {
96
+ super(props);
97
+
98
+ this.state = {
99
+ modalAnimation:
100
+ props.modalAnimation ||
101
+ new FadeAnimation({
102
+ animationDuration: props.animationDuration,
103
+ }),
104
+ modalState: MODAL_CLOSED,
105
+ };
106
+ }
107
+
108
+ componentDidMount() {
109
+ if (this.props.visible) {
110
+ this.show();
111
+ }
112
+ this.backHandler = BackHandler.addEventListener(
113
+ HARDWARE_BACK_PRESS_EVENT,
114
+ this.onHardwareBackPress,
115
+ );
116
+ }
117
+
118
+ componentDidUpdate(prevProps: ModalProps) {
119
+ if (this.props.visible !== prevProps.visible) {
120
+ if (this.props.visible) {
121
+ this.show();
122
+ return;
123
+ }
124
+ this.dismiss();
125
+ }
126
+ }
127
+
128
+ componentWillUnmount() {
129
+ this.backHandler?.remove();
130
+ }
131
+
132
+ onHardwareBackPress = (): boolean =>
133
+ this.props.onHardwareBackPress ? this.props.onHardwareBackPress() : false;
134
+
135
+ get pointerEvents(): "auto" | "none" {
136
+ const { overlayPointerEvents } = this.props;
137
+ const { modalState } = this.state;
138
+ if (overlayPointerEvents) {
139
+ return overlayPointerEvents;
140
+ }
141
+ return modalState === MODAL_OPENED ? "auto" : "none";
142
+ }
143
+
144
+ get modalSize(): any {
145
+ const { width: screenWidth, height: screenHeight } =
146
+ Dimensions.get("window");
147
+ let { width, height } = this.props;
148
+ if (width && width > 0.0 && width <= 1.0) {
149
+ width *= screenWidth;
150
+ }
151
+ if (height && height > 0.0 && height <= 1.0) {
152
+ height *= screenHeight;
153
+ }
154
+ return { width, height };
155
+ }
156
+
157
+ show(): void {
158
+ this.setState({ modalState: MODAL_OPENING }, () => {
159
+ this.state.modalAnimation.in(() => {
160
+ this.setState({ modalState: MODAL_OPENED }, this.props.onShow);
161
+ });
162
+ });
163
+ }
164
+
165
+ dismiss(): void {
166
+ this.setState({ modalState: MODAL_CLOSING }, () => {
167
+ if (this.isSwipingOut) {
168
+ this.setState({ modalState: MODAL_CLOSED }, this.props.onDismiss);
169
+ return;
170
+ }
171
+ this.state.modalAnimation.out(() => {
172
+ this.setState({ modalState: MODAL_CLOSED }, this.props.onDismiss);
173
+ });
174
+ });
175
+ }
176
+
177
+ handleMove = (event: any): void => {
178
+ // prevent flashing when modal is closing and onMove callback invoked
179
+ if (this.state.modalState === MODAL_CLOSING) {
180
+ return;
181
+ }
182
+ if (!this.lastSwipeEvent) {
183
+ this.lastSwipeEvent = event;
184
+ }
185
+ let newOpacity;
186
+ const opacity = this.props.overlayOpacity ?? 0;
187
+ if (Math.abs(event.axis.y)) {
188
+ const lastAxis = Math.abs(this.lastSwipeEvent.layout.y);
189
+ const currAxis = Math.abs(event.axis.y);
190
+ newOpacity =
191
+ opacity -
192
+ (opacity * currAxis) / (Dimensions.get("window").height - lastAxis);
193
+ } else {
194
+ const lastAxis = Math.abs(this.lastSwipeEvent.layout.x);
195
+ const currAxis = Math.abs(event.axis.x);
196
+ newOpacity =
197
+ opacity -
198
+ (opacity * currAxis) / (Dimensions.get("window").width - lastAxis);
199
+ }
200
+ this.backdrop?.setOpacity(newOpacity);
201
+ };
202
+
203
+ handleSwipingOut = (event: any) => {
204
+ this.isSwipingOut = true;
205
+ this.props.onSwipingOut?.(event);
206
+ };
207
+
208
+ render() {
209
+ const { modalState, modalAnimation } = this.state;
210
+ const {
211
+ rounded,
212
+ modalTitle,
213
+ children,
214
+ onTouchOutside,
215
+ hasOverlay,
216
+ modalStyle,
217
+ animationDuration,
218
+ overlayOpacity,
219
+ useNativeDriver,
220
+ overlayBackgroundColor,
221
+ style,
222
+ footer,
223
+ onSwiping,
224
+ onSwipeRelease,
225
+ onSwipeOut,
226
+ swipeDirection,
227
+ swipeThreshold,
228
+ } = this.props;
229
+
230
+ const overlayVisible =
231
+ hasOverlay && [MODAL_OPENING, MODAL_OPENED].includes(modalState);
232
+ const round = rounded ? styles.round : null;
233
+ const hidden = modalState === MODAL_CLOSED && styles.hidden;
234
+
235
+ return (
236
+ <ModalContext.Provider
237
+ value={{
238
+ hasTitle: Boolean(modalTitle),
239
+ hasFooter: Boolean(footer),
240
+ }}
241
+ >
242
+ <View
243
+ pointerEvents={this.isSwipingOut ? "none" : "auto"}
244
+ style={[styles.container, hidden]}
245
+ >
246
+ <DraggableView
247
+ style={StyleSheet.flatten([styles.draggableView, style])}
248
+ onMove={this.handleMove}
249
+ onSwiping={onSwiping}
250
+ onRelease={onSwipeRelease}
251
+ onSwipingOut={this.handleSwipingOut}
252
+ onSwipeOut={onSwipeOut}
253
+ swipeDirection={swipeDirection}
254
+ swipeThreshold={swipeThreshold}
255
+ >
256
+ {({ pan, onLayout }) => (
257
+ <Fragment>
258
+ <Backdrop
259
+ ref={(ref) => {
260
+ this.backdrop = ref;
261
+ }}
262
+ pointerEvents={this.pointerEvents}
263
+ visible={overlayVisible}
264
+ onPress={onTouchOutside}
265
+ backgroundColor={overlayBackgroundColor}
266
+ opacity={overlayOpacity}
267
+ animationDuration={animationDuration}
268
+ useNativeDriver={useNativeDriver}
269
+ />
270
+ <Animated.View style={pan.getLayout()} onLayout={onLayout}>
271
+ <Animated.View
272
+ style={[
273
+ styles.modal,
274
+ round,
275
+ this.modalSize,
276
+ modalStyle,
277
+ modalAnimation.getAnimations(),
278
+ ]}
279
+ >
280
+ {modalTitle}
281
+ {children}
282
+ {footer}
283
+ </Animated.View>
284
+ </Animated.View>
285
+ </Fragment>
286
+ )}
287
+ </DraggableView>
288
+ </View>
289
+ </ModalContext.Provider>
290
+ );
291
+ }
292
+ }
293
+
294
+ export default BaseModal;
@@ -0,0 +1,36 @@
1
+ // @flow
2
+
3
+ import React from 'react';
4
+ import { StyleSheet } from 'react-native';
5
+ import type { ModalProps } from '../type';
6
+ import SlideAnimation from '../animations/SlideAnimation';
7
+ import BaseModal from './BaseModal';
8
+
9
+ const styles = StyleSheet.create({
10
+ container: {
11
+ justifyContent: 'flex-end',
12
+ },
13
+ modal: {
14
+ borderBottomRightRadius: 0,
15
+ borderBottomLeftRadius: 0,
16
+ },
17
+ });
18
+
19
+ const BottomModal = ({
20
+ style,
21
+ modalStyle,
22
+ ...restProps
23
+ }: ModalProps) => (
24
+ <BaseModal
25
+ modalAnimation={new SlideAnimation({
26
+ slideFrom: 'bottom',
27
+ })}
28
+ {...restProps}
29
+ style={StyleSheet.flatten([styles.container, style])}
30
+ modalStyle={StyleSheet.flatten([styles.modal, modalStyle])}
31
+ width={1}
32
+ swipeDirection="down"
33
+ />
34
+ );
35
+
36
+ export default BottomModal;
@@ -0,0 +1,248 @@
1
+ import React, { Component } from "react";
2
+ import { Animated, Dimensions, PanResponder } from "react-native";
3
+ import type { DragEvent, SwipeDirection } from "../type";
4
+
5
+ type Props = {
6
+ style?: any;
7
+ onMove?: (event: DragEvent) => void;
8
+ onSwiping?: (event: DragEvent) => void;
9
+ onRelease?: (event: DragEvent) => void;
10
+ onSwipingOut?: (event: DragEvent) => void;
11
+ onSwipeOut?: (event: DragEvent) => void;
12
+ swipeThreshold?: number;
13
+ swipeDirection?: SwipeDirection | Array<SwipeDirection>;
14
+ children: ({
15
+ onLayout,
16
+ pan,
17
+ }: {
18
+ onLayout: (event: any) => void;
19
+ pan: Animated.ValueXY;
20
+ }) => React.ReactNode;
21
+ };
22
+
23
+ export default class DraggableView extends Component<Props> {
24
+ static defaultProps = {
25
+ style: null,
26
+ onMove: () => {},
27
+ onSwiping: () => {},
28
+ onSwipingOut: () => {},
29
+ onSwipeOut: null,
30
+ onRelease: () => {},
31
+ swipeThreshold: 100,
32
+ swipeDirection: [],
33
+ };
34
+
35
+ // instance properties
36
+ pan: Animated.ValueXY;
37
+ allowedDirections: SwipeDirection[];
38
+ layout: { x: number; y: number; width: number; height: number } | null;
39
+ panEventListenerId: string | number | null = null;
40
+ currentSwipeDirection: SwipeDirection | null = null;
41
+
42
+ constructor(props: Props) {
43
+ super(props);
44
+
45
+ this.pan = new Animated.ValueXY();
46
+ this.allowedDirections = ([] as SwipeDirection[]).concat(
47
+ props.swipeDirection || [],
48
+ );
49
+ this.layout = null;
50
+ }
51
+
52
+ componentDidMount() {
53
+ this.panEventListenerId = this.pan.addListener(
54
+ (axis: { x: number; y: number }) => {
55
+ this.props.onMove?.(this.createDragEvent(axis));
56
+ },
57
+ );
58
+ }
59
+
60
+ componentWillUnmount() {
61
+ if (this.panEventListenerId != null) {
62
+ this.pan.removeListener(this.panEventListenerId as string);
63
+ }
64
+ }
65
+
66
+ onLayout = (event: any) => {
67
+ this.layout = event.nativeEvent.layout;
68
+ };
69
+
70
+ getSwipeDirection(gestureState: any): SwipeDirection | null {
71
+ if (this.isValidHorizontalSwipe(gestureState)) {
72
+ return gestureState.dx > 0 ? "right" : "left";
73
+ } else if (this.isValidVerticalSwipe(gestureState)) {
74
+ return gestureState.dy > 0 ? "down" : "up";
75
+ }
76
+ return null;
77
+ }
78
+
79
+ getDisappearDirection() {
80
+ const { width, height } = Dimensions.get("window");
81
+ const vertical = height / 2 + (this.layout ? this.layout.height / 2 : 0);
82
+ const horizontal = width / 2 + (this.layout ? this.layout.width / 2 : 0);
83
+ let toValue;
84
+ if (this.currentSwipeDirection === "up") {
85
+ toValue = {
86
+ x: 0,
87
+ y: -vertical,
88
+ };
89
+ } else if (this.currentSwipeDirection === "down") {
90
+ toValue = {
91
+ x: 0,
92
+ y: vertical,
93
+ };
94
+ } else if (this.currentSwipeDirection === "left") {
95
+ toValue = {
96
+ x: -horizontal,
97
+ y: 0,
98
+ };
99
+ } else if (this.currentSwipeDirection === "right") {
100
+ toValue = {
101
+ x: horizontal,
102
+ y: 0,
103
+ };
104
+ }
105
+ return toValue;
106
+ }
107
+
108
+ isValidHorizontalSwipe({ vx, dy }: any) {
109
+ return this.isValidSwipe(vx, dy);
110
+ }
111
+
112
+ isValidVerticalSwipe({ vy, dx }: any) {
113
+ return this.isValidSwipe(vy, dx);
114
+ }
115
+
116
+ // eslint-disable-next-line class-methods-use-this
117
+ isValidSwipe(velocity: number, directionalOffset: number) {
118
+ const velocityThreshold = 0.3;
119
+ const directionalOffsetThreshold = 80;
120
+ // eslint-disable-next-line max-len
121
+ return (
122
+ Math.abs(velocity) > velocityThreshold &&
123
+ Math.abs(directionalOffset) < directionalOffsetThreshold
124
+ );
125
+ }
126
+
127
+ isAllowedDirection({ dy, dx }: any) {
128
+ const draggedDown = dy > 0;
129
+ const draggedUp = dy < 0;
130
+ const draggedLeft = dx < 0;
131
+ const draggedRight = dx > 0;
132
+ const isAllowedDirection = (d: SwipeDirection) =>
133
+ this.currentSwipeDirection === d && this.allowedDirections.includes(d);
134
+ if (draggedDown && isAllowedDirection("down")) {
135
+ return true;
136
+ } else if (draggedUp && isAllowedDirection("up")) {
137
+ return true;
138
+ } else if (draggedLeft && isAllowedDirection("left")) {
139
+ return true;
140
+ } else if (draggedRight && isAllowedDirection("right")) {
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ createDragEvent(axis: { x: number; y: number }): DragEvent {
147
+ return {
148
+ axis,
149
+ layout: this.layout,
150
+ swipeDirection: this.currentSwipeDirection,
151
+ };
152
+ }
153
+
154
+ panResponder = PanResponder.create({
155
+ onMoveShouldSetPanResponder: (evt, gestureState) =>
156
+ gestureState.dx !== 0 && gestureState.dy !== 0,
157
+ onStartShouldSetPanResponder: () => true,
158
+ onPanResponderMove: (event: any, gestureState: any) => {
159
+ const isVerticalSwipe = (d: SwipeDirection | null) =>
160
+ ["up", "down"].includes(d as string);
161
+ const isHorizontalSwipe = (d: SwipeDirection | null) =>
162
+ ["left", "right"].includes(d as string);
163
+
164
+ const newSwipeDirection = this.getSwipeDirection(gestureState);
165
+ const isSameDirection =
166
+ isVerticalSwipe(this.currentSwipeDirection) ===
167
+ isVerticalSwipe(newSwipeDirection) ||
168
+ isHorizontalSwipe(this.currentSwipeDirection) ===
169
+ isHorizontalSwipe(newSwipeDirection);
170
+ // newDirection & currentSwipeDirection must be same direction
171
+ if (newSwipeDirection && isSameDirection) {
172
+ this.currentSwipeDirection = newSwipeDirection;
173
+ }
174
+ if (this.isAllowedDirection(gestureState)) {
175
+ let animEvent: any;
176
+ if (isVerticalSwipe(this.currentSwipeDirection)) {
177
+ animEvent = { dy: this.pan.y };
178
+ } else if (isHorizontalSwipe(this.currentSwipeDirection)) {
179
+ animEvent = { dx: this.pan.x };
180
+ }
181
+ if (animEvent) {
182
+ Animated.event([null, animEvent], { useNativeDriver: false })(
183
+ event,
184
+ gestureState,
185
+ );
186
+ }
187
+ this.props.onSwiping?.(
188
+ this.createDragEvent({
189
+ x: (this.pan.x as any)._value,
190
+ y: (this.pan.y as any)._value,
191
+ }),
192
+ );
193
+ }
194
+ },
195
+ onPanResponderRelease: () => {
196
+ this.pan.flattenOffset();
197
+ const event = this.createDragEvent({
198
+ x: (this.pan.x as any)._value,
199
+ y: (this.pan.y as any)._value,
200
+ });
201
+ // on swipe out
202
+ const threshold = this.props.swipeThreshold ?? 0;
203
+ if (
204
+ (this.props.onSwipeOut &&
205
+ Math.abs((this.pan.y as any)._value) > threshold) ||
206
+ Math.abs((this.pan.x as any)._value) > threshold
207
+ ) {
208
+ const toValue = this.getDisappearDirection();
209
+ this.props.onSwipingOut?.(event);
210
+ if (!toValue) return;
211
+ Animated.spring(this.pan, {
212
+ toValue,
213
+ velocity: 0,
214
+ tension: 65,
215
+ friction: 11,
216
+ useNativeDriver: false,
217
+ }).start(() => {
218
+ this.props.onSwipeOut?.(event);
219
+ });
220
+ return;
221
+ }
222
+ // on release
223
+ this.currentSwipeDirection = null;
224
+ this.props.onRelease?.(event);
225
+ Animated.spring(this.pan, {
226
+ toValue: { x: 0, y: 0 },
227
+ velocity: 0,
228
+ tension: 65,
229
+ friction: 11,
230
+ useNativeDriver: false,
231
+ }).start();
232
+ },
233
+ });
234
+
235
+ render() {
236
+ const { style, children: renderContent } = this.props;
237
+ const content = renderContent({
238
+ pan: this.pan,
239
+ onLayout: this.onLayout,
240
+ });
241
+
242
+ return (
243
+ <Animated.View {...this.panResponder.panHandlers} style={style}>
244
+ {content}
245
+ </Animated.View>
246
+ );
247
+ }
248
+ }
@@ -0,0 +1,67 @@
1
+ // @flow
2
+
3
+ import React from "react";
4
+ import {
5
+ PixelRatio,
6
+ Platform,
7
+ StyleSheet,
8
+ Text,
9
+ TouchableHighlight,
10
+ } from "react-native";
11
+ import { Positions } from "../constants/Constants";
12
+ import type { ModalButtonProps } from "../type";
13
+
14
+ const isAndroid = Platform.OS === "android";
15
+
16
+ const styles = StyleSheet.create({
17
+ button: {
18
+ flex: 1,
19
+ width: "100%",
20
+ justifyContent: "center",
21
+ alignItems: "center",
22
+ paddingTop: 16,
23
+ paddingBottom: 16,
24
+ },
25
+ border: {
26
+ borderLeftColor: "#CCD0D5",
27
+ borderLeftWidth: 1 / PixelRatio.get(),
28
+ },
29
+ text: {
30
+ fontWeight: isAndroid ? "400" : "500",
31
+ fontFamily: isAndroid ? "sans-serif-medium" : "System",
32
+ fontSize: isAndroid ? 19 : 16,
33
+ color: "#044DE0",
34
+ },
35
+ disable: {
36
+ color: "#C5C6C5",
37
+ },
38
+ });
39
+
40
+ const ModalButton = ({
41
+ text,
42
+ onPress,
43
+ style,
44
+ textStyle,
45
+ activeOpacity = 0.6,
46
+ align = "center",
47
+ disabled = false,
48
+ bordered = false,
49
+ }: ModalButtonProps) => {
50
+ const buttonAlign = { alignSelf: Positions[align as keyof typeof Positions] };
51
+ const disable = disabled ? styles.disable : null;
52
+ const border = bordered ? styles.border : null;
53
+
54
+ return (
55
+ <TouchableHighlight
56
+ underlayColor="#F1F2F2"
57
+ onPress={onPress}
58
+ disabled={disabled}
59
+ activeOpacity={activeOpacity}
60
+ style={[styles.button, buttonAlign, border, style]}
61
+ >
62
+ <Text style={[styles.text, disable, textStyle]}>{text}</Text>
63
+ </TouchableHighlight>
64
+ );
65
+ };
66
+
67
+ export default ModalButton;