UNPKG

15.6 kBJavaScriptView Raw
1function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
2
3function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
4
5import * as React from 'react';
6import { StyleSheet, I18nManager, Platform, Keyboard, StatusBar } from 'react-native';
7import { PanGestureHandler, TapGestureHandler, State } from 'react-native-gesture-handler';
8import Animated from 'react-native-reanimated';
9import DrawerProgressContext from '../utils/DrawerProgressContext';
10const {
11 Clock,
12 Value,
13 onChange,
14 clockRunning,
15 startClock,
16 stopClock,
17 interpolate,
18 spring,
19 abs,
20 add,
21 and,
22 block,
23 call,
24 cond,
25 divide,
26 eq,
27 event,
28 greaterThan,
29 lessThan,
30 max,
31 min,
32 multiply,
33 neq,
34 or,
35 set,
36 sub
37} = Animated;
38const TRUE = 1;
39const FALSE = 0;
40const NOOP = 0;
41const UNSET = -1;
42const PROGRESS_EPSILON = 0.05;
43const DIRECTION_LEFT = 1;
44const DIRECTION_RIGHT = -1;
45const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60;
46const SWIPE_DISTANCE_MINIMUM = 5;
47const SPRING_CONFIG = {
48 stiffness: 1000,
49 damping: 500,
50 mass: 3,
51 overshootClamping: true,
52 restDisplacementThreshold: 0.01,
53 restSpeedThreshold: 0.01
54};
55export default class Drawer extends React.PureComponent {
56 constructor(...args) {
57 super(...args);
58
59 _defineProperty(this, "clock", new Clock());
60
61 _defineProperty(this, "isDrawerTypeFront", new Value(this.props.drawerType === 'front' ? TRUE : FALSE));
62
63 _defineProperty(this, "isOpen", new Value(this.props.open ? TRUE : FALSE));
64
65 _defineProperty(this, "nextIsOpen", new Value(UNSET));
66
67 _defineProperty(this, "isSwiping", new Value(FALSE));
68
69 _defineProperty(this, "gestureState", new Value(State.UNDETERMINED));
70
71 _defineProperty(this, "touchX", new Value(0));
72
73 _defineProperty(this, "velocityX", new Value(0));
74
75 _defineProperty(this, "gestureX", new Value(0));
76
77 _defineProperty(this, "offsetX", new Value(0));
78
79 _defineProperty(this, "position", new Value(0));
80
81 _defineProperty(this, "containerWidth", new Value(0));
82
83 _defineProperty(this, "drawerWidth", new Value(0));
84
85 _defineProperty(this, "drawerOpacity", new Value(0));
86
87 _defineProperty(this, "drawerPosition", new Value(this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT));
88
89 _defineProperty(this, "touchDistanceFromDrawer", cond(this.isDrawerTypeFront, cond(eq(this.drawerPosition, DIRECTION_LEFT), max( // Distance of touch start from left screen edge - Drawer width
90 sub(sub(this.touchX, this.gestureX), this.drawerWidth), 0), min(multiply( // Distance of drawer from left screen edge - Touch start point
91 sub(sub(this.containerWidth, this.drawerWidth), sub(this.touchX, this.gestureX)), DIRECTION_RIGHT), 0)), 0));
92
93 _defineProperty(this, "swipeDistanceThreshold", new Value(this.props.swipeDistanceThreshold !== undefined ? this.props.swipeDistanceThreshold : SWIPE_DISTANCE_THRESHOLD_DEFAULT));
94
95 _defineProperty(this, "swipeVelocityThreshold", new Value(this.props.swipeVelocityThreshold));
96
97 _defineProperty(this, "currentOpenValue", this.props.open);
98
99 _defineProperty(this, "pendingOpenValue", void 0);
100
101 _defineProperty(this, "isStatusBarHidden", false);
102
103 _defineProperty(this, "manuallyTriggerSpring", new Value(FALSE));
104
105 _defineProperty(this, "transitionTo", isOpen => {
106 const toValue = new Value(0);
107 const frameTime = new Value(0);
108 const state = {
109 position: this.position,
110 time: new Value(0),
111 finished: new Value(FALSE),
112 velocity: new Value(0)
113 };
114 return block([cond(clockRunning(this.clock), NOOP, [// Animation wasn't running before
115 // Set the initial values and start the clock
116 set(toValue, multiply(isOpen, this.drawerWidth, this.drawerPosition)), set(frameTime, 0), set(state.time, 0), set(state.finished, FALSE), set(state.velocity, this.velocityX), set(this.isOpen, isOpen), startClock(this.clock), set(this.manuallyTriggerSpring, FALSE)]), spring(this.clock, state, { ...SPRING_CONFIG,
117 toValue
118 }), cond(state.finished, [// Reset gesture and velocity from previous gesture
119 set(this.touchX, 0), set(this.gestureX, 0), set(this.velocityX, 0), set(this.offsetX, 0), // When the animation finishes, stop the clock
120 stopClock(this.clock), call([this.isOpen], ([value]) => {
121 const open = Boolean(value);
122
123 if (open !== this.props.open) {
124 // Sync drawer's state after animation finished
125 // This shouldn't be necessary, but there seems to be an issue on iOS
126 this.toggleDrawer(this.props.open);
127 }
128 })])]);
129 });
130
131 _defineProperty(this, "dragX", block([onChange(this.isOpen, call([this.isOpen], ([value]) => {
132 const open = Boolean(value);
133 this.currentOpenValue = open; // Without this check, the drawer can go to an infinite update <-> animate loop for sync updates
134
135 if (open !== this.props.open) {
136 // If the mode changed, update state
137 if (open) {
138 this.props.onOpen();
139 } else {
140 this.props.onClose();
141 }
142
143 this.pendingOpenValue = open; // Force componentDidUpdate to fire, whether user does a setState or not
144 // This allows us to detect when the user drops the update and revert back
145 // It's necessary to make sure that the state stays in sync
146
147 this.forceUpdate();
148 }
149 })), onChange(this.nextIsOpen, cond(neq(this.nextIsOpen, UNSET), [// Stop any running animations
150 cond(clockRunning(this.clock), stopClock(this.clock)), // Update the open value to trigger the transition
151 set(this.isOpen, this.nextIsOpen), set(this.gestureX, 0), set(this.nextIsOpen, UNSET)])), // This block must be after the this.isOpen listener since we check for current value
152 onChange(this.isSwiping, // Listen to updates for this value only when it changes
153 // Without `onChange`, this will fire even if the value didn't change
154 // We don't want to call the listeners if the value didn't change
155 call([this.isSwiping], ([value]) => {
156 const {
157 keyboardDismissMode
158 } = this.props;
159
160 if (value === TRUE) {
161 if (keyboardDismissMode === 'on-drag') {
162 Keyboard.dismiss();
163 }
164
165 this.toggleStatusBar(true);
166 } else {
167 this.toggleStatusBar(this.currentOpenValue);
168 }
169 })), cond(eq(this.gestureState, State.ACTIVE), [cond(this.isSwiping, NOOP, [// We weren't dragging before, set it to true
170 set(this.isSwiping, TRUE), // Also update the drag offset to the last position
171 set(this.offsetX, this.position)]), // Update position with previous offset + gesture distance
172 set(this.position, add(this.offsetX, this.gestureX, this.touchDistanceFromDrawer)), // Stop animations while we're dragging
173 stopClock(this.clock)], [set(this.isSwiping, FALSE), set(this.touchX, 0), this.transitionTo(cond(this.manuallyTriggerSpring, this.isOpen, cond(or(and(greaterThan(abs(this.gestureX), SWIPE_DISTANCE_MINIMUM), greaterThan(abs(this.velocityX), this.swipeVelocityThreshold)), greaterThan(abs(this.gestureX), this.swipeDistanceThreshold)), cond(eq(this.drawerPosition, DIRECTION_LEFT), // If swiped to right, open the drawer, otherwise close it
174 greaterThan(cond(eq(this.velocityX, 0), this.gestureX, this.velocityX), 0), // If swiped to left, open the drawer, otherwise close it
175 lessThan(cond(eq(this.velocityX, 0), this.gestureX, this.velocityX), 0)), this.isOpen)))]), this.position]));
176
177 _defineProperty(this, "translateX", cond(eq(this.drawerPosition, DIRECTION_RIGHT), min(max(multiply(this.drawerWidth, -1), this.dragX), 0), max(min(this.drawerWidth, this.dragX), 0)));
178
179 _defineProperty(this, "progress", cond( // Check if the drawer width is available to avoid division by zero
180 eq(this.drawerWidth, 0), 0, abs(divide(this.translateX, this.drawerWidth))));
181
182 _defineProperty(this, "handleGestureEvent", event([{
183 nativeEvent: {
184 x: this.touchX,
185 translationX: this.gestureX,
186 velocityX: this.velocityX
187 }
188 }]));
189
190 _defineProperty(this, "handleGestureStateChange", event([{
191 nativeEvent: {
192 state: s => set(this.gestureState, s)
193 }
194 }]));
195
196 _defineProperty(this, "handleTapStateChange", event([{
197 nativeEvent: {
198 oldState: s => cond(eq(s, State.ACTIVE), set(this.manuallyTriggerSpring, TRUE))
199 }
200 }]));
201
202 _defineProperty(this, "handleContainerLayout", e => this.containerWidth.setValue(e.nativeEvent.layout.width));
203
204 _defineProperty(this, "handleDrawerLayout", e => {
205 this.drawerWidth.setValue(e.nativeEvent.layout.width);
206 this.toggleDrawer(this.props.open); // Until layout is available, drawer is hidden with opacity: 0 by default
207 // Show it in the next frame when layout is available
208 // If we don't delay it until the next frame, there's a visible flicker
209
210 requestAnimationFrame(() => requestAnimationFrame(() => this.drawerOpacity.setValue(1)));
211 });
212
213 _defineProperty(this, "toggleDrawer", open => {
214 if (this.currentOpenValue !== open) {
215 this.nextIsOpen.setValue(open ? TRUE : FALSE); // This value will also be set shortly after as changing this.nextIsOpen changes this.isOpen
216 // However, there's a race condition on Android, so we need to set a bit earlier
217
218 this.currentOpenValue = open;
219 }
220 });
221
222 _defineProperty(this, "toggleStatusBar", hidden => {
223 const {
224 hideStatusBar,
225 statusBarAnimation
226 } = this.props;
227
228 if (hideStatusBar && this.isStatusBarHidden !== hidden) {
229 this.isStatusBarHidden = hidden;
230 StatusBar.setHidden(hidden, statusBarAnimation);
231 }
232 });
233 }
234
235 componentDidUpdate(prevProps) {
236 const {
237 open,
238 drawerPosition,
239 drawerType,
240 swipeDistanceThreshold,
241 swipeVelocityThreshold,
242 hideStatusBar
243 } = this.props;
244
245 if ( // If we're not in the middle of a transition, sync the drawer's open state
246 typeof this.pendingOpenValue !== 'boolean' || open !== this.pendingOpenValue) {
247 this.toggleDrawer(open);
248 }
249
250 this.pendingOpenValue = undefined;
251
252 if (open !== prevProps.open && hideStatusBar) {
253 this.toggleStatusBar(open);
254 }
255
256 if (prevProps.drawerPosition !== drawerPosition) {
257 this.drawerPosition.setValue(drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT);
258 }
259
260 if (prevProps.drawerType !== drawerType) {
261 this.isDrawerTypeFront.setValue(drawerType === 'front' ? TRUE : FALSE);
262 }
263
264 if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) {
265 this.swipeDistanceThreshold.setValue(swipeDistanceThreshold !== undefined ? swipeDistanceThreshold : SWIPE_DISTANCE_THRESHOLD_DEFAULT);
266 }
267
268 if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) {
269 this.swipeVelocityThreshold.setValue(swipeVelocityThreshold);
270 }
271 }
272
273 componentWillUnmount() {
274 this.toggleStatusBar(false);
275 }
276
277 render() {
278 const {
279 open,
280 gestureEnabled,
281 drawerPosition,
282 drawerType,
283 swipeEdgeWidth,
284 sceneContainerStyle,
285 drawerStyle,
286 overlayStyle,
287 onGestureRef,
288 renderDrawerContent,
289 renderSceneContent,
290 gestureHandlerProps
291 } = this.props;
292 const right = drawerPosition === 'right';
293 const contentTranslateX = drawerType === 'front' ? 0 : this.translateX;
294 const drawerTranslateX = drawerType === 'back' ? I18nManager.isRTL ? multiply(this.drawerWidth, DIRECTION_RIGHT) : this.drawerWidth : this.translateX;
295 const offset = I18nManager.isRTL ? '100%' : multiply(this.drawerWidth, -1); // FIXME: Currently hitSlop is broken when on Android when drawer is on right
296 // https://github.com/kmagiera/react-native-gesture-handler/issues/569
297
298 const hitSlop = right ? // Extend hitSlop to the side of the screen when drawer is closed
299 // This lets the user drag the drawer from the side of the screen
300 {
301 right: 0,
302 width: open ? undefined : swipeEdgeWidth
303 } : {
304 left: 0,
305 width: open ? undefined : swipeEdgeWidth
306 };
307 return /*#__PURE__*/React.createElement(DrawerProgressContext.Provider, {
308 value: this.progress
309 }, /*#__PURE__*/React.createElement(PanGestureHandler, _extends({
310 ref: onGestureRef,
311 activeOffsetX: [-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM],
312 failOffsetY: [-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM],
313 onGestureEvent: this.handleGestureEvent,
314 onHandlerStateChange: this.handleGestureStateChange,
315 hitSlop: hitSlop,
316 enabled: gestureEnabled
317 }, gestureHandlerProps), /*#__PURE__*/React.createElement(Animated.View, {
318 onLayout: this.handleContainerLayout,
319 style: styles.main
320 }, /*#__PURE__*/React.createElement(Animated.View, {
321 style: [styles.content, {
322 transform: [{
323 translateX: contentTranslateX
324 }]
325 }, sceneContainerStyle],
326 importantForAccessibility: open ? 'no-hide-descendants' : 'yes'
327 }, renderSceneContent({
328 progress: this.progress
329 }), /*#__PURE__*/React.createElement(TapGestureHandler, {
330 enabled: gestureEnabled,
331 onHandlerStateChange: this.handleTapStateChange
332 }, /*#__PURE__*/React.createElement(Animated.View, {
333 style: [styles.overlay, {
334 opacity: interpolate(this.progress, {
335 inputRange: [PROGRESS_EPSILON, 1],
336 outputRange: [0, 1]
337 }),
338 // We don't want the user to be able to press through the overlay when drawer is open
339 // One approach is to adjust the pointerEvents based on the progress
340 // But we can also send the overlay behind the screen, which works, and is much less code
341 zIndex: cond(greaterThan(this.progress, PROGRESS_EPSILON), 0, -1)
342 }, overlayStyle]
343 }))), /*#__PURE__*/React.createElement(Animated.Code, {
344 exec: block([onChange(this.manuallyTriggerSpring, [cond(eq(this.manuallyTriggerSpring, TRUE), [set(this.nextIsOpen, FALSE), call([], () => this.currentOpenValue = false)])])])
345 }), /*#__PURE__*/React.createElement(Animated.View, {
346 accessibilityViewIsModal: open,
347 removeClippedSubviews: Platform.OS !== 'ios',
348 onLayout: this.handleDrawerLayout,
349 style: [styles.container, right ? {
350 right: offset
351 } : {
352 left: offset
353 }, {
354 transform: [{
355 translateX: drawerTranslateX
356 }],
357 opacity: this.drawerOpacity,
358 zIndex: drawerType === 'back' ? -1 : 0
359 }, drawerStyle]
360 }, renderDrawerContent({
361 progress: this.progress
362 })))));
363 }
364
365}
366
367_defineProperty(Drawer, "defaultProps", {
368 gestureEnabled: true,
369 drawerPostion: I18nManager.isRTL ? 'left' : 'right',
370 drawerType: 'front',
371 swipeEdgeWidth: 32,
372 swipeVelocityThreshold: 500,
373 keyboardDismissMode: 'on-drag',
374 hideStatusBar: false,
375 statusBarAnimation: 'slide'
376});
377
378const styles = StyleSheet.create({
379 container: {
380 backgroundColor: 'white',
381 position: 'absolute',
382 top: 0,
383 bottom: 0,
384 width: '80%',
385 maxWidth: '100%'
386 },
387 overlay: { ...StyleSheet.absoluteFillObject,
388 backgroundColor: 'rgba(0, 0, 0, 0.5)'
389 },
390 content: {
391 flex: 1
392 },
393 main: {
394 flex: 1,
395 overflow: 'hidden'
396 }
397});
398//# sourceMappingURL=Drawer.js.map
\No newline at end of file