@haroldtran/react-native-modals 0.0.6 → 0.0.10

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.
@@ -1,59 +1,48 @@
1
- // @flow
2
-
3
- import { BlurView } from "@sbaiahmed1/react-native-blur";
4
- import React, { Component } from "react";
5
- import { Animated, StyleSheet, TouchableOpacity } from "react-native";
6
- import { BackdropProps } from "../type";
7
-
8
- export default class Backdrop extends Component<BackdropProps> {
9
- static defaultProps: Partial<BackdropProps> = {
10
- backgroundColor: "#000",
11
- opacity: 0.5,
12
- animationDuration: 200,
13
- visible: false,
14
- useNativeDriver: true,
15
- useBlurView: false,
16
- onPress: () => {},
17
- blurProps: {
18
- blurType: "extraDark",
19
- blurAmount: 20,
20
- reducedTransparencyFallbackColor: "#000000",
21
- },
22
- };
23
-
24
- opacity = new Animated.Value(0);
25
-
26
- componentDidUpdate(prevProps: BackdropProps) {
27
- const { visible, useNativeDriver = true, opacity: toOpacity = 0.5, animationDuration: duration = 200 } = this.props;
28
- if (prevProps.visible !== visible) {
1
+ import React, { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react';
2
+ import { Animated, StyleSheet, TouchableOpacity } from 'react-native';
3
+ import { BackdropProps } from '../type';
4
+
5
+ const Backdrop = memo(
6
+ forwardRef((props: BackdropProps, ref) => {
7
+ const {
8
+ backgroundColor = '#000',
9
+ opacity: toOpacity = 0.5,
10
+ animationDuration: duration = 200,
11
+ animationDelay: delay = 0,
12
+ visible = false,
13
+ useNativeDriver = true,
14
+ onPress = () => {},
15
+ pointerEvents,
16
+ } = props;
17
+
18
+ const opacity = useRef(new Animated.Value(0)).current;
19
+
20
+ useImperativeHandle(ref, () => ({
21
+ setOpacity: (value: number) => {
22
+ opacity.setValue(value);
23
+ },
24
+ }));
25
+
26
+ useEffect(() => {
29
27
  const toValue = visible ? toOpacity : 0;
30
- Animated.timing(this.opacity, {
28
+ Animated.timing(opacity, {
31
29
  toValue,
32
30
  duration,
33
31
  useNativeDriver,
32
+ delay: visible ? 0 : delay,
34
33
  }).start();
35
- }
36
- }
37
-
38
- setOpacity = (value: number) => {
39
- this.opacity.setValue(value);
40
- };
41
-
42
- render() {
43
- const { onPress, pointerEvents, backgroundColor, useBlurView, blurProps } = this.props;
44
- const { opacity } = this;
45
- const overlayStyle: any = [StyleSheet.absoluteFill, { backgroundColor, opacity }];
46
- const _children = (
34
+ }, [visible, toOpacity, duration, delay, useNativeDriver]);
35
+
36
+ const overlayStyle = [StyleSheet.absoluteFill, { backgroundColor, opacity }];
37
+
38
+ return (
47
39
  <Animated.View pointerEvents={pointerEvents as any} style={overlayStyle}>
48
- <TouchableOpacity onPress={onPress} style={StyleSheet.absoluteFill} />
40
+ <TouchableOpacity activeOpacity={1} onPress={onPress} style={StyleSheet.absoluteFill} />
49
41
  </Animated.View>
50
42
  );
51
- return useBlurView ? (
52
- <BlurView {...blurProps} style={StyleSheet.absoluteFill}>
53
- {_children}
54
- </BlurView>
55
- ) : (
56
- _children
57
- );
58
- }
59
- }
43
+ }),
44
+ );
45
+
46
+ Backdrop.displayName = 'Backdrop';
47
+
48
+ export default Backdrop;
@@ -1,275 +1,312 @@
1
- import React, { Component, Fragment } from "react";
2
- import { Animated, BackHandler, Dimensions, StyleSheet, View } from "react-native";
1
+ import React, {
2
+ forwardRef,
3
+ Fragment,
4
+ memo,
5
+ useCallback,
6
+ useEffect,
7
+ useImperativeHandle,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import { Animated, BackHandler, StyleSheet, useWindowDimensions, View } from 'react-native';
3
13
 
4
- import Animation from "../animations/Animation";
5
- import FadeAnimation from "../animations/FadeAnimation";
6
- import type { ModalProps } from "../type";
7
- import Backdrop from "./Backdrop";
8
- import DraggableView from "./DraggableView";
9
- import ModalContext from "./ModalContext";
14
+ import FadeAnimation from '../animations/FadeAnimation';
15
+ import type { ModalProps } from '../type';
16
+ import Backdrop from './Backdrop';
17
+ import DraggableView from './DraggableView';
18
+ import ModalContext from './ModalContext';
10
19
 
11
- const HARDWARE_BACK_PRESS_EVENT = "hardwareBackPress" as const;
20
+ const HARDWARE_BACK_PRESS_EVENT = 'hardwareBackPress' as const;
12
21
 
13
22
  // dialog states
14
- const MODAL_OPENING: string = "opening";
15
- const MODAL_OPENED: string = "opened";
16
- const MODAL_CLOSING: string = "closing";
17
- const MODAL_CLOSED: string = "closed";
23
+ const MODAL_OPENING = 'opening' as const;
24
+ const MODAL_OPENED = 'opened' as const;
25
+ const MODAL_CLOSING = 'closing' as const;
26
+ const MODAL_CLOSED = 'closed' as const;
18
27
 
19
- // default dialog config
20
- const DEFAULT_ANIMATION_DURATION: number = 150;
28
+ const DEFAULT_ANIMATION_DURATION = 150;
21
29
 
22
30
  const styles = StyleSheet.create({
23
31
  container: {
24
32
  ...StyleSheet.absoluteFillObject,
25
33
  },
26
34
  modal: {
27
- overflow: "hidden",
28
- backgroundColor: "#ffffff",
35
+ overflow: 'hidden',
36
+ backgroundColor: '#ffffff',
29
37
  },
30
38
  hidden: {
31
- top: -10000,
32
- left: 0,
33
- height: 0,
34
- width: 0,
39
+ display: 'none',
35
40
  },
36
41
  round: {
37
42
  borderRadius: 8,
38
43
  },
39
44
  draggableView: {
40
45
  flex: 1,
41
- justifyContent: "center",
42
- alignItems: "center",
46
+ justifyContent: 'center',
47
+ alignItems: 'center',
43
48
  },
44
49
  });
45
50
 
46
51
  type ModalState = typeof MODAL_OPENING | typeof MODAL_OPENED | typeof MODAL_CLOSING | typeof MODAL_CLOSED;
47
52
 
48
- type State = {
49
- modalAnimation: Animation;
50
- modalState: ModalState;
51
- };
52
-
53
- class BaseModal extends Component<ModalProps, State> {
54
- // internal refs / stateful properties
55
- backHandler: any;
56
- lastSwipeEvent: any | null = null;
57
- isSwipingOut: boolean = false;
58
- backdrop: any;
59
-
60
- static defaultProps = {
61
- rounded: true,
62
- modalTitle: null,
63
- visible: false,
64
- style: null,
65
- animationDuration: DEFAULT_ANIMATION_DURATION,
66
- modalStyle: null,
67
- width: null,
68
- height: null,
69
- onTouchOutside: () => {},
70
- onHardwareBackPress: () => false,
71
- hasOverlay: true,
72
- overlayOpacity: 0.5,
73
- overlayPointerEvents: null,
74
- overlayBackgroundColor: "#000",
75
- onShow: () => {},
76
- onDismiss: () => {},
77
- footer: null,
78
- onMove: () => {},
79
- onSwiping: () => {},
80
- onSwipeRelease: () => {},
81
- onSwipingOut: () => {},
82
- useNativeDriver: true,
83
- useBlurView: false,
84
- isDelay: false,
85
- blurProps: {
86
- blurType: "extraDark",
87
- blurAmount: 20,
88
- reducedTransparencyFallbackColor: "#000000",
89
- },
90
- };
91
-
92
- constructor(props: ModalProps) {
93
- super(props);
94
-
95
- this.state = {
96
- modalAnimation:
97
- props.modalAnimation ||
53
+ const BaseModal = memo(
54
+ forwardRef((props: ModalProps, ref) => {
55
+ const {
56
+ rounded = true,
57
+ modalTitle = null,
58
+ visible = false,
59
+ style = null,
60
+ animationDuration = DEFAULT_ANIMATION_DURATION,
61
+ animationDurationIn,
62
+ animationDurationOut,
63
+ modalStyle = null,
64
+ width: propWidth = null,
65
+ height: propHeight = null,
66
+ onTouchOutside = () => {},
67
+ onHardwareBackPress: propOnHardwareBackPress = () => false,
68
+ hasOverlay = true,
69
+ overlayOpacity = 0.5,
70
+ overlayPointerEvents = null,
71
+ overlayBackgroundColor = '#000',
72
+ overlayAnimationDelay = 250,
73
+ onShow = () => {},
74
+ onDismiss = () => {},
75
+ footer = null,
76
+ onMove = () => {},
77
+ onSwiping = () => {},
78
+ onSwipeRelease = () => {},
79
+ onSwipingOut: propOnSwipingOut = () => {},
80
+ onSwipeOut,
81
+ swipeDirection,
82
+ swipeThreshold,
83
+ useNativeDriver = true,
84
+ isDelay = false,
85
+ children,
86
+ modalAnimation: propModalAnimation,
87
+ } = props;
88
+
89
+ const { width: screenWidth, height: screenHeight } = useWindowDimensions();
90
+
91
+ const [modalState, setModalState] = useState<ModalState>(MODAL_CLOSED);
92
+
93
+ // Instances that don't need re-renders but need to persist
94
+ const modalAnimation = useRef(
95
+ propModalAnimation ||
98
96
  new FadeAnimation({
99
- animationDuration: props.animationDuration,
97
+ animationDuration: animationDuration || DEFAULT_ANIMATION_DURATION,
100
98
  }),
101
- modalState: MODAL_CLOSED,
102
- };
103
- }
104
-
105
- componentDidMount() {
106
- if (this.props.visible) {
107
- this.show();
108
- }
109
- this.backHandler = BackHandler.addEventListener(HARDWARE_BACK_PRESS_EVENT, this.onHardwareBackPress);
110
- }
111
-
112
- componentDidUpdate(prevProps: ModalProps) {
113
- if (this.props.visible !== prevProps.visible) {
114
- if (this.props.visible) {
115
- this.show();
99
+ ).current;
100
+
101
+ const backdropRef = useRef<any>(null);
102
+ const closeTimerRef = useRef<any>(null);
103
+ const lastSwipeEventRef = useRef<any | null>(null);
104
+ const isSwipingOutRef = useRef<boolean>(false);
105
+
106
+ const onHardwareBackPress = useCallback((): boolean => {
107
+ return propOnHardwareBackPress ? propOnHardwareBackPress() : false;
108
+ }, [propOnHardwareBackPress]);
109
+
110
+ const show = useCallback((): void => {
111
+ isSwipingOutRef.current = false;
112
+ clearTimeout(closeTimerRef.current);
113
+ setModalState(MODAL_OPENING);
114
+ modalAnimation.in(() => {
115
+ setModalState(MODAL_OPENED);
116
+ onShow?.();
117
+ }, animationDurationIn || animationDuration);
118
+ }, [modalAnimation, onShow, animationDurationIn, animationDuration]);
119
+
120
+ const dismiss = useCallback((): void => {
121
+ const duration = animationDurationOut || animationDuration;
122
+ clearTimeout(closeTimerRef.current);
123
+ setModalState(MODAL_CLOSING);
124
+
125
+ const delay = hasOverlay ? overlayAnimationDelay || 0 : 0;
126
+
127
+ const finishDismiss = () => {
128
+ if (delay > 0) {
129
+ closeTimerRef.current = setTimeout(() => {
130
+ setModalState(MODAL_CLOSED);
131
+ onDismiss?.();
132
+ }, delay);
133
+ } else {
134
+ setModalState(MODAL_CLOSED);
135
+ onDismiss?.();
136
+ }
137
+ };
138
+
139
+ if (isSwipingOutRef.current) {
140
+ finishDismiss();
116
141
  return;
117
142
  }
118
- this.dismiss();
119
- }
120
- }
121
-
122
- componentWillUnmount() {
123
- this.backHandler?.remove();
124
- }
125
-
126
- onHardwareBackPress = (): boolean => (this.props.onHardwareBackPress ? this.props.onHardwareBackPress() : false);
127
-
128
- get pointerEvents(): "auto" | "none" {
129
- const { overlayPointerEvents } = this.props;
130
- const { modalState } = this.state;
131
- if (overlayPointerEvents) {
132
- return overlayPointerEvents;
133
- }
134
- return modalState === MODAL_OPENED ? "auto" : "none";
135
- }
136
-
137
- get modalSize(): any {
138
- const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
139
- let { width, height } = this.props;
140
- if (width && width > 0.0 && width <= 1.0) {
141
- width *= screenWidth;
142
- }
143
- if (height && height > 0.0 && height <= 1.0) {
144
- height *= screenHeight;
145
- }
146
- return { width, height };
147
- }
148
-
149
- show(): void {
150
- this.setState({ modalState: MODAL_OPENING }, () => {
151
- this.state.modalAnimation.in(() => {
152
- this.setState({ modalState: MODAL_OPENED }, this.props.onShow);
153
- });
154
- });
155
- }
156
-
157
- dismiss(): void {
158
- this.setState({ modalState: MODAL_CLOSING }, () => {
159
- if (this.isSwipingOut) {
160
- this.setState({ modalState: MODAL_CLOSED }, this.props.onDismiss);
161
- return;
143
+
144
+ modalAnimation.out(finishDismiss, duration);
145
+ }, [modalAnimation, onDismiss, animationDurationOut, animationDuration, hasOverlay, overlayAnimationDelay]);
146
+
147
+ useImperativeHandle(ref, () => ({
148
+ show,
149
+ dismiss,
150
+ }));
151
+
152
+ useEffect(() => {
153
+ const backHandler = BackHandler.addEventListener(HARDWARE_BACK_PRESS_EVENT, onHardwareBackPress);
154
+ return () => {
155
+ backHandler.remove();
156
+ clearTimeout(closeTimerRef.current);
157
+ };
158
+ }, [onHardwareBackPress]);
159
+
160
+ useEffect(() => {
161
+ if (visible) {
162
+ if (modalState === MODAL_CLOSED || modalState === MODAL_CLOSING) {
163
+ show();
164
+ }
165
+ } else {
166
+ if (modalState === MODAL_OPENED || modalState === MODAL_OPENING) {
167
+ dismiss();
168
+ } else if (modalState === MODAL_CLOSED) {
169
+ onDismiss?.();
170
+ }
162
171
  }
163
- this.state.modalAnimation.out(() => {
164
- this.setState({ modalState: MODAL_CLOSED }, this.props.onDismiss);
165
- });
166
- });
167
- }
168
-
169
- handleMove = (event: any): void => {
170
- // prevent flashing when modal is closing and onMove callback invoked
171
- if (this.state.modalState === MODAL_CLOSING) {
172
- return;
173
- }
174
- if (!this.lastSwipeEvent) {
175
- this.lastSwipeEvent = event;
176
- }
177
- let newOpacity;
178
- const opacity = this.props.overlayOpacity ?? 0;
179
- if (Math.abs(event.axis.y)) {
180
- const lastAxis = Math.abs(this.lastSwipeEvent.layout.y);
181
- const currAxis = Math.abs(event.axis.y);
182
- newOpacity = opacity - (opacity * currAxis) / (Dimensions.get("window").height - lastAxis);
183
- } else {
184
- const lastAxis = Math.abs(this.lastSwipeEvent.layout.x);
185
- const currAxis = Math.abs(event.axis.x);
186
- newOpacity = opacity - (opacity * currAxis) / (Dimensions.get("window").width - lastAxis);
187
- }
188
- this.backdrop?.setOpacity(newOpacity);
189
- };
190
-
191
- handleSwipingOut = (event: any) => {
192
- this.isSwipingOut = true;
193
- this.props.onSwipingOut?.(event);
194
- };
195
-
196
- render() {
197
- const { modalState, modalAnimation } = this.state;
198
- const {
199
- rounded,
200
- modalTitle,
201
- children,
202
- onTouchOutside,
203
- hasOverlay,
204
- modalStyle,
205
- animationDuration,
206
- overlayOpacity,
207
- useNativeDriver,
208
- overlayBackgroundColor,
209
- style,
210
- footer,
211
- onSwiping,
212
- onSwipeRelease,
213
- onSwipeOut,
214
- swipeDirection,
215
- swipeThreshold,
216
- useBlurView,
217
- blurProps,
218
- isDelay,
219
- } = this.props;
172
+ }, [visible, modalState, show, dismiss, onDismiss]);
173
+
174
+ const pointerEvents = useMemo(() => {
175
+ if (overlayPointerEvents) {
176
+ return overlayPointerEvents;
177
+ }
178
+ return modalState === MODAL_OPENED || modalState === MODAL_OPENING ? 'auto' : 'none';
179
+ }, [overlayPointerEvents, modalState]);
180
+
181
+ const modalSizeStyle = useMemo(() => {
182
+ let w = propWidth;
183
+ let h = propHeight;
184
+ if (w && w > 0.0 && w <= 1.0) {
185
+ w *= screenWidth;
186
+ }
187
+ if (h && h > 0.0 && h <= 1.0) {
188
+ h *= screenHeight;
189
+ }
190
+ return { width: w ?? undefined, height: h ?? undefined };
191
+ }, [propWidth, propHeight, screenWidth, screenHeight]);
220
192
 
221
- const overlayVisible = hasOverlay && [MODAL_OPENING, MODAL_OPENED].includes(modalState);
222
- const round = rounded ? styles.round : null;
223
- const hidden = modalState === MODAL_CLOSED && styles.hidden;
193
+ const handleMove = useCallback(
194
+ (event: any): void => {
195
+ if (modalState === MODAL_CLOSING) {
196
+ return;
197
+ }
198
+ if (!lastSwipeEventRef.current) {
199
+ lastSwipeEventRef.current = event;
200
+ }
201
+ let newOpacity;
202
+ const opacity = overlayOpacity ?? 0;
203
+ if (Math.abs(event.axis.y)) {
204
+ const lastAxis = Math.abs(lastSwipeEventRef.current.layout.y);
205
+ const currAxis = Math.abs(event.axis.y);
206
+ newOpacity = opacity - (opacity * currAxis) / (screenHeight - lastAxis);
207
+ } else {
208
+ const lastAxis = Math.abs(lastSwipeEventRef.current.layout.x);
209
+ const currAxis = Math.abs(event.axis.x);
210
+ newOpacity = opacity - (opacity * currAxis) / (screenWidth - lastAxis);
211
+ }
212
+ backdropRef.current?.setOpacity(newOpacity);
213
+ onMove?.(event);
214
+ },
215
+ [modalState, overlayOpacity, screenWidth, screenHeight, onMove],
216
+ );
217
+
218
+ const handleSwipingOut = useCallback(
219
+ (event: any) => {
220
+ isSwipingOutRef.current = true;
221
+ propOnSwipingOut?.(event);
222
+ },
223
+ [propOnSwipingOut],
224
+ );
225
+
226
+ const overlayVisible = hasOverlay && (modalState === MODAL_OPENING || modalState === MODAL_OPENED);
227
+ const currentOverlayDuration =
228
+ modalState === MODAL_CLOSING
229
+ ? animationDurationOut || animationDuration
230
+ : animationDurationIn || animationDuration;
231
+
232
+ const roundStyle = rounded ? styles.round : null;
233
+ const hiddenStyle = modalState === MODAL_CLOSED ? styles.hidden : null;
234
+
235
+ const draggableViewStyle = useMemo(() => StyleSheet.flatten([styles.draggableView, style]), [style]);
236
+ const modalViewStyle = useMemo(
237
+ () => [styles.modal, roundStyle, modalSizeStyle, modalStyle, modalAnimation.getAnimations()],
238
+ [roundStyle, modalSizeStyle, modalStyle, modalAnimation],
239
+ );
240
+
241
+ const renderDraggableContent = useCallback(
242
+ ({ pan, onLayout }: any) => (
243
+ <Fragment>
244
+ <Backdrop
245
+ ref={backdropRef}
246
+ pointerEvents={pointerEvents}
247
+ visible={overlayVisible}
248
+ onPress={onTouchOutside}
249
+ backgroundColor={overlayBackgroundColor}
250
+ opacity={overlayOpacity}
251
+ animationDuration={currentOverlayDuration}
252
+ animationDelay={overlayAnimationDelay}
253
+ useNativeDriver={useNativeDriver}
254
+ />
255
+ <Animated.View style={pan.getLayout()} onLayout={onLayout}>
256
+ <Animated.View style={modalViewStyle}>
257
+ {modalTitle}
258
+ {isDelay ? (modalState === MODAL_OPENED ? children : null) : children}
259
+ {footer}
260
+ </Animated.View>
261
+ </Animated.View>
262
+ </Fragment>
263
+ ),
264
+ [
265
+ pointerEvents,
266
+ overlayVisible,
267
+ onTouchOutside,
268
+ overlayBackgroundColor,
269
+ overlayOpacity,
270
+ currentOverlayDuration,
271
+ overlayAnimationDelay,
272
+ useNativeDriver,
273
+ modalViewStyle,
274
+ modalTitle,
275
+ isDelay,
276
+ modalState,
277
+ children,
278
+ footer,
279
+ ],
280
+ );
224
281
 
225
282
  return (
226
283
  <ModalContext.Provider
227
284
  value={{
228
285
  hasTitle: Boolean(modalTitle),
229
286
  hasFooter: Boolean(footer),
230
- }}
231
- >
232
- <View pointerEvents={this.isSwipingOut ? "none" : "auto"} style={[styles.container, hidden]}>
287
+ }}>
288
+ <View
289
+ pointerEvents={modalState === MODAL_CLOSED || isSwipingOutRef.current ? 'none' : 'box-none'}
290
+ style={[styles.container, hiddenStyle]}
291
+ >
233
292
  <DraggableView
234
- style={StyleSheet.flatten([styles.draggableView, style])}
235
- onMove={this.handleMove}
293
+ style={draggableViewStyle}
294
+ pointerEvents={pointerEvents}
295
+ onMove={handleMove}
236
296
  onSwiping={onSwiping}
237
297
  onRelease={onSwipeRelease}
238
- onSwipingOut={this.handleSwipingOut}
298
+ onSwipingOut={handleSwipingOut}
239
299
  onSwipeOut={onSwipeOut}
240
300
  swipeDirection={swipeDirection}
241
- swipeThreshold={swipeThreshold}
242
- >
243
- {({ pan, onLayout }) => (
244
- <Fragment>
245
- <Backdrop
246
- ref={(ref) => {
247
- this.backdrop = ref;
248
- }}
249
- pointerEvents={this.pointerEvents}
250
- visible={overlayVisible}
251
- onPress={onTouchOutside}
252
- backgroundColor={overlayBackgroundColor}
253
- opacity={overlayOpacity}
254
- animationDuration={animationDuration}
255
- useNativeDriver={useNativeDriver}
256
- useBlurView={useBlurView}
257
- blurProps={blurProps}
258
- />
259
- <Animated.View style={pan.getLayout()} onLayout={onLayout}>
260
- <Animated.View style={[styles.modal, round, this.modalSize, modalStyle, modalAnimation.getAnimations()]}>
261
- {modalTitle}
262
- {isDelay ? (modalState === MODAL_OPENED ? children : null) : children}
263
- {footer}
264
- </Animated.View>
265
- </Animated.View>
266
- </Fragment>
267
- )}
301
+ swipeThreshold={swipeThreshold}>
302
+ {renderDraggableContent}
268
303
  </DraggableView>
269
304
  </View>
270
305
  </ModalContext.Provider>
271
306
  );
272
- }
273
- }
307
+ }),
308
+ );
309
+
310
+ BaseModal.displayName = 'BaseModal';
274
311
 
275
312
  export default BaseModal;