@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.
- package/LICENSE.md +21 -0
- package/README.md +184 -0
- package/package.json +83 -0
- package/src/BottomModal.tsx +13 -0
- package/src/Modal.tsx +61 -0
- package/src/ModalPortal.tsx +137 -0
- package/src/animations/Animation.tsx +37 -0
- package/src/animations/FadeAnimation.tsx +41 -0
- package/src/animations/ScaleAnimation.tsx +37 -0
- package/src/animations/SlideAnimation.tsx +88 -0
- package/src/components/Backdrop.tsx +53 -0
- package/src/components/BaseModal.tsx +294 -0
- package/src/components/BottomModal.tsx +36 -0
- package/src/components/DraggableView.tsx +248 -0
- package/src/components/ModalButton.tsx +67 -0
- package/src/components/ModalContent.tsx +31 -0
- package/src/components/ModalContext.tsx +8 -0
- package/src/components/ModalFooter.tsx +48 -0
- package/src/components/ModalTitle.tsx +47 -0
- package/src/components/SlideAnimation.tsx +91 -0
- package/src/constants/Constants.ts +5 -0
- package/src/index.tsx +32 -0
- package/src/type.ts +90 -0
|
@@ -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;
|