ncore-react-native-sheet 1.0.0-alpha.0

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/src/index.tsx ADDED
@@ -0,0 +1,631 @@
1
+ import React, {
2
+ useImperativeHandle,
3
+ forwardRef,
4
+ useEffect,
5
+ useState,
6
+ Fragment,
7
+ useMemo,
8
+ useRef
9
+ } from "react";
10
+ import {
11
+ PanResponderGestureState,
12
+ TouchableWithoutFeedback,
13
+ GestureResponderEvent,
14
+ PanResponder,
15
+ ScrollView,
16
+ Animated,
17
+ FlatList,
18
+ View
19
+ } from "react-native";
20
+ import styleSheet from "./styles";
21
+ import {
22
+ useSafeAreaInsets
23
+ } from "react-native-safe-area-context";
24
+ import {
25
+ RefForwardingComponent,
26
+ ISheetProps,
27
+ DistsType,
28
+ SheetRef
29
+ } from "./types";
30
+ import {
31
+ windowHeight
32
+ } from "./constants";
33
+
34
+ const ANIMATION_TO_SNAP_DURATION = 75;
35
+ const ANIMATION_CLOSE_DURATION = 400;
36
+ const ANIMATION_OPEN_DURATION = 300;
37
+ const VELOCITY_CLOSE = 1.2;
38
+
39
+ const Sheet: RefForwardingComponent<SheetRef, ISheetProps<any>> = ({
40
+ handlerColor = "#c4c2c2",
41
+ panGestureEnabled = true,
42
+ snapPoint: snapPointProp,
43
+ isCanFullScreenOnSnap,
44
+ withSafeArea = true,
45
+ handler = "inside",
46
+ autoHeight = true,
47
+ headerComponent,
48
+ footerComponent,
49
+ scrollViewProps,
50
+ onOverlayPress,
51
+ withFullScreen,
52
+ flatListProps,
53
+ contentStyle,
54
+ overlayStyle,
55
+ rootStyle,
56
+ onClosed,
57
+ onOpened,
58
+ children,
59
+ ...props
60
+ }, ref) => {
61
+ const insets = useSafeAreaInsets();
62
+ let maxHeight = withSafeArea ? windowHeight - insets.top : windowHeight;
63
+ let snapPoint = snapPointProp || maxHeight;
64
+
65
+ const [isVisible, setIsVisible] = useState<boolean>(false);
66
+
67
+ const contentContainerHeight = useRef(0);
68
+ const headerContainerHeight = useRef(0);
69
+ const isScrollable = useRef(false);
70
+ const contentHeight = useRef(0);
71
+ const listHeight = useRef(0);
72
+
73
+ const moveHeight = useRef(new Animated.Value(snapPoint)).current;
74
+ const beforeYPosition = useRef(new Animated.Value(0)).current;
75
+ const height = useRef(new Animated.Value(snapPoint)).current;
76
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
77
+ const moveYPosition = useRef(new Animated.Value(0)).current;
78
+ const opening = useRef(new Animated.Value(0)).current;
79
+
80
+ const isAtBottomRef = useRef(false);
81
+ const scrollOffsetRef = useRef(0);
82
+ const isScrolling = useRef(false);
83
+
84
+ const flatListRef = useRef<FlatList>(null);
85
+
86
+ useEffect(() => {
87
+ isAtBottomRef.current = false;
88
+ scrollOffsetRef.current = 0;
89
+
90
+ if (snapPointProp && snapPointProp > windowHeight || withFullScreen) {
91
+ snapPoint = maxHeight;
92
+ }
93
+ }, []);
94
+
95
+ const checkScrollability = (contentH: number, listH: number) => {
96
+ if (listH > 0 && contentH > listH) {
97
+ isScrollable.current = true;
98
+ } else {
99
+ isScrollable.current = false;
100
+ }
101
+ };
102
+
103
+ const calculateAutoHeight = (contentHeight: number, headerHeight: number) => {
104
+ moveHeight.setValue(contentHeight + headerHeight);
105
+ height.setValue(contentHeight + headerHeight);
106
+ snapPoint = contentHeight + headerHeight;
107
+ };
108
+
109
+ useImperativeHandle(
110
+ ref,
111
+ () => ({
112
+ close,
113
+ open
114
+ }),
115
+ []
116
+ );
117
+
118
+ const open = () => {
119
+ opening.setValue(1);
120
+ openAnimate();
121
+
122
+ setTimeout(() => {
123
+ if (onOpened) onOpened();
124
+ opening.setValue(0);
125
+ }, ANIMATION_OPEN_DURATION);
126
+ };
127
+
128
+ const close = () => {
129
+ closeAnimate();
130
+
131
+ setTimeout(() => {
132
+ if (flatListProps && flatListProps.data?.length) {
133
+ flatListRef?.current?.scrollToIndex({
134
+ index: 0,
135
+ animated: false
136
+ });
137
+ }
138
+ if (onClosed) onClosed();
139
+ }, ANIMATION_CLOSE_DURATION);
140
+ };
141
+
142
+ const openAnimate = () => {
143
+ setIsVisible(true);
144
+ let toValue = (withFullScreen ? withSafeArea ? windowHeight - (insets.top) : windowHeight : snapPoint) * -1;
145
+
146
+ Animated.parallel([
147
+ Animated.timing(moveYPosition, {
148
+ toValue: toValue,
149
+ useNativeDriver: false,
150
+ duration: ANIMATION_OPEN_DURATION
151
+ }),
152
+ Animated.timing(overlayOpacity, {
153
+ toValue: .1,
154
+ useNativeDriver: false,
155
+ duration: ANIMATION_OPEN_DURATION
156
+ }),
157
+ Animated.timing(beforeYPosition, {
158
+ toValue: toValue,
159
+ useNativeDriver: false,
160
+ duration: ANIMATION_OPEN_DURATION
161
+ })
162
+ ]).start();
163
+ };
164
+
165
+ const closeAnimate = () => {
166
+ Animated.parallel([
167
+ Animated.timing(beforeYPosition, {
168
+ toValue: 50,
169
+ useNativeDriver: false,
170
+ duration: ANIMATION_CLOSE_DURATION
171
+ }),
172
+ Animated.timing(moveYPosition, {
173
+ toValue: 50,
174
+ useNativeDriver: false,
175
+ duration: ANIMATION_CLOSE_DURATION
176
+ }),
177
+ Animated.timing(height, {
178
+ toValue: snapPoint,
179
+ useNativeDriver: false,
180
+ duration: ANIMATION_CLOSE_DURATION
181
+ }),
182
+ Animated.timing(moveHeight, {
183
+ toValue: snapPoint,
184
+ useNativeDriver: false,
185
+ duration: ANIMATION_CLOSE_DURATION
186
+ }),
187
+ Animated.timing(overlayOpacity, {
188
+ toValue: 0,
189
+ useNativeDriver: false,
190
+ duration: ANIMATION_CLOSE_DURATION
191
+ })
192
+ ]).start(e => {
193
+ if (e.finished) {
194
+ setIsVisible(false);
195
+ }
196
+ });
197
+ };
198
+
199
+ const disableScroll = () => {
200
+ if (flatListProps) {
201
+ flatListRef.current?.setNativeProps({
202
+ scrollEnabled: false
203
+ });
204
+ }
205
+ };
206
+
207
+ const enableScroll = () => {
208
+ isAtBottomRef.current = false;
209
+ if (flatListProps) {
210
+ flatListRef.current?.setNativeProps({
211
+ scrollEnabled: true
212
+ });
213
+ }
214
+ };
215
+
216
+ const onMoveShouldSetPanResponderContent = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => {
217
+ const {
218
+ dy
219
+ } = gestureState;
220
+
221
+ if (!isScrolling) {
222
+ return true;
223
+ }
224
+
225
+ // FlatList scroll ediyorsa panResponder devreye girmesin
226
+ if (scrollOffsetRef.current < 0 && dy < 0) {
227
+ // flatlist altta değilken yukarı kaydırma scroll'a gitmeli
228
+ return false;
229
+ }
230
+
231
+ // FlatList alttayken yukarı çekiyorsa → devreye girsin
232
+ if (isAtBottomRef.current && dy < 0) {
233
+ return true;
234
+ }
235
+
236
+ // Aşağı çekme (her zaman sheet)
237
+ if (dy > 5 && scrollOffsetRef.current <= 0) {
238
+ return true;
239
+ }
240
+
241
+ if (!isScrollable.current) {
242
+ return true;
243
+ }
244
+
245
+ return false;
246
+ };
247
+
248
+ const onPanResponderGrant = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => {
249
+ const {
250
+ dy, dx, y0
251
+ } = gestureState;
252
+
253
+ disableScroll();
254
+
255
+ // @ts-ignore
256
+ const extra = height._value - snapPoint;
257
+
258
+ // @ts-ignore
259
+ beforeYPosition.setValue(beforeYPosition._value - extra);
260
+ height.setValue(snapPoint);
261
+ };
262
+
263
+ const onPanResponderMove = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => {
264
+ const {
265
+ dy, moveY
266
+ } = gestureState;
267
+
268
+ // @ts-ignore
269
+ const _beforeYPosition = beforeYPosition._value;
270
+
271
+ const newYPosition = _beforeYPosition + dy;
272
+
273
+ if (newYPosition > -snapPoint) {
274
+ // Snap altı
275
+ moveHeight.setValue(snapPoint);
276
+ moveYPosition.setValue(newYPosition);
277
+ } else {
278
+ // Snap üstü
279
+ if (!panGestureEnabled) {
280
+ return;
281
+ }
282
+ const absNewY = Math.abs(newYPosition);
283
+ const extra = Math.max(0, absNewY - snapPoint);
284
+ const expandedHeight = Math.min(snapPoint + extra, maxHeight);
285
+
286
+ moveHeight.setValue(expandedHeight);
287
+ moveYPosition.setValue(-snapPoint);
288
+ }
289
+ };
290
+
291
+ const onPanResponderEnd = (e: GestureResponderEvent, gestureState: PanResponderGestureState) => {
292
+ const {
293
+ vy, dy
294
+ } = gestureState;
295
+
296
+ enableScroll();
297
+ // @ts-ignore
298
+ const moveHeightValue = moveHeight._value;
299
+ // @ts-ignore
300
+ const currentYValue = moveYPosition._value;
301
+ const isBelowSnap = moveHeightValue === snapPoint;
302
+
303
+ // @ts-ignore
304
+ const _moveYPosition = moveHeight._value;
305
+
306
+ let targetHeight = snapPoint;
307
+ let targetY = snapPoint * -1;
308
+
309
+ if (vy > VELOCITY_CLOSE) {
310
+ close();
311
+ return;
312
+ }
313
+
314
+ const dists: DistsType = {
315
+ distToTop: {
316
+ value: isBelowSnap && !withFullScreen ? windowHeight * 10 : Math.abs(moveHeightValue - windowHeight),
317
+ action: () => {
318
+ targetHeight = maxHeight;
319
+ targetY = snapPoint * -1;
320
+ },
321
+ },
322
+ distToBottom: {
323
+ value: isBelowSnap ? Math.abs(currentYValue) : Math.abs(moveHeightValue),
324
+ action: () => {
325
+ close();
326
+ },
327
+ },
328
+ };
329
+
330
+ if (snapPointProp) {
331
+ dists.distToSnap = {
332
+ value: isBelowSnap ? Math.abs(currentYValue + snapPoint) : Math.abs(moveHeightValue - snapPoint),
333
+ action: () => {
334
+ targetHeight = snapPoint;
335
+ targetY = snapPoint * -1;
336
+ }
337
+ };
338
+ }
339
+
340
+ const [key, distItem] = Object.entries(dists).reduce(
341
+ (min, curr) => (curr[1].value < min[1].value ? curr : min)
342
+ );
343
+
344
+ distItem.action();
345
+
346
+ if (key === "distToBottom") {
347
+ return;
348
+ }
349
+
350
+ Animated.parallel([
351
+ Animated.timing(moveHeight, {
352
+ toValue: targetHeight,
353
+ useNativeDriver: false,
354
+ duration: ANIMATION_TO_SNAP_DURATION
355
+ }),
356
+ Animated.timing(moveYPosition, {
357
+ toValue: targetY,
358
+ useNativeDriver: false,
359
+ duration: ANIMATION_TO_SNAP_DURATION
360
+ }),
361
+ ])
362
+ .start();
363
+
364
+ beforeYPosition.setValue(targetY);
365
+ height.setValue(targetHeight);
366
+ };
367
+
368
+ const panResponderHandle = useMemo(() => PanResponder.create({
369
+ //onStartShouldSetPanResponder: (evt, gestureState) => true,
370
+ onMoveShouldSetPanResponder: () => true,
371
+ onPanResponderGrant: onPanResponderGrant,
372
+ onPanResponderMove: onPanResponderMove,
373
+ onPanResponderEnd: onPanResponderEnd
374
+ }),
375
+ [
376
+ ]);
377
+
378
+ const panResponderContent = useMemo(() => PanResponder.create({
379
+ //onStartShouldSetPanResponder: (evt, gestureState) => true,
380
+ onMoveShouldSetPanResponder: onMoveShouldSetPanResponderContent,
381
+ onPanResponderGrant: onPanResponderGrant,
382
+ onPanResponderMove: onPanResponderMove,
383
+ onPanResponderEnd: onPanResponderEnd
384
+ }),
385
+ [
386
+
387
+ ]);
388
+
389
+ const renderOverlay = () => {
390
+ return <TouchableWithoutFeedback
391
+ style={{
392
+ ...styleSheet.overlayTouchable,
393
+ ...overlayStyle
394
+ }}
395
+ onPress={() => {
396
+ if (onOverlayPress) {
397
+ onOverlayPress();
398
+ }
399
+ }}
400
+ >
401
+ <Animated.View
402
+ style={{
403
+ ...styleSheet.overlayView,
404
+ opacity: overlayOpacity,
405
+ }}
406
+ />
407
+ </TouchableWithoutFeedback>;
408
+ };
409
+
410
+ const renderHandler = () => {
411
+ if (withFullScreen && withSafeArea) {
412
+ return null;
413
+ }
414
+
415
+ return <View
416
+ style={{
417
+ ...styleSheet.handler,
418
+ backgroundColor: handlerColor
419
+ }}
420
+ >
421
+ </View>;
422
+ };
423
+
424
+ const renderHeader = () => {
425
+ return <Animated.View
426
+ {...panResponderHandle.panHandlers}
427
+ key="bottomSheet-header-key"
428
+ onLayout={(e) => {
429
+ if (withFullScreen) {
430
+ return;
431
+ }
432
+
433
+ if (!autoHeight || snapPointProp) {
434
+ return;
435
+ }
436
+ //@ts-ignore
437
+ if (opening._value !== 1) {
438
+ return;
439
+ }
440
+
441
+ const absNewHeight = Math.abs(e.nativeEvent.layout.height);
442
+ const layoutHeight = Math.min(absNewHeight, maxHeight);
443
+
444
+ headerContainerHeight.current = layoutHeight;
445
+ calculateAutoHeight(contentContainerHeight.current, layoutHeight);
446
+ }}
447
+ style={[
448
+ styleSheet.handlerContainer,
449
+ handler === "outside" ?
450
+ {
451
+ position: "absolute",
452
+ top: -24
453
+ }
454
+ :
455
+ null
456
+ ]}
457
+ >
458
+ {renderHandler()}
459
+ {renderHeaderComponent()}
460
+ </Animated.View>;
461
+ };
462
+
463
+ const renderHeaderComponent = () => {
464
+ if (!headerComponent) {
465
+ return null;
466
+ }
467
+
468
+ return headerComponent;
469
+ };
470
+
471
+ const renderContentItems = () => {
472
+ if (flatListProps) {
473
+ return <FlatList
474
+ {...flatListProps}
475
+ key={"bottomSheet-flatlist-key"}
476
+ ref={flatListRef}
477
+ scrollEnabled={true}
478
+ onLayout={(e) => {
479
+ const height = e.nativeEvent.layout.height;
480
+ listHeight.current = height;
481
+
482
+ checkScrollability(contentHeight.current, height);
483
+ }}
484
+ onContentSizeChange={(w, h) => {
485
+ isAtBottomRef.current = false;
486
+ contentHeight.current = h;
487
+ checkScrollability(h, listHeight.current);
488
+ }}
489
+ onScroll={(e) => {
490
+ const {
491
+ layoutMeasurement,
492
+ contentOffset,
493
+ contentSize
494
+ } = e.nativeEvent;
495
+
496
+ scrollOffsetRef.current = contentOffset.y;
497
+
498
+ const paddingToBottom = 12; // tolerans
499
+ isAtBottomRef.current = layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom;
500
+ }}
501
+ onTouchStart={() => {
502
+ isScrolling.current = true;
503
+ }}
504
+ onTouchEnd={() => {
505
+ isScrolling.current = false;
506
+ }}
507
+ scrollEventThrottle={16}
508
+ />;
509
+ }
510
+
511
+ if (scrollViewProps) {
512
+ return <ScrollView
513
+ {...scrollViewProps}
514
+ scrollEnabled={true}
515
+ onLayout={(e) => {
516
+ const height = e.nativeEvent.layout.height;
517
+ listHeight.current = height;
518
+ checkScrollability(contentHeight.current, height);
519
+ }}
520
+ onContentSizeChange={(w, h) => {
521
+ isAtBottomRef.current = false;
522
+ contentHeight.current = h;
523
+ checkScrollability(h, listHeight.current);
524
+ }}
525
+ onScroll={(e) => {
526
+ const {
527
+ layoutMeasurement,
528
+ contentOffset,
529
+ contentSize
530
+ } = e.nativeEvent;
531
+
532
+ scrollOffsetRef.current = contentOffset.y;
533
+
534
+ const paddingToBottom = 12; // tolerans
535
+ isAtBottomRef.current = layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom;
536
+ }}
537
+ onTouchStart={() => {
538
+ isScrolling.current = true;
539
+ }}
540
+ onTouchEnd={() => {
541
+ isScrolling.current = false;
542
+ }}
543
+ scrollEventThrottle={16}
544
+ >
545
+ {children}
546
+ </ScrollView>;
547
+ }
548
+
549
+ return children;
550
+ };
551
+
552
+ const renderFooterComponent = () => {
553
+ if (!footerComponent) {
554
+ return null;
555
+ }
556
+
557
+ return footerComponent;
558
+ };
559
+
560
+ const renderContent = () => {
561
+ return <Animated.View
562
+ key="bottomSheet-content-key"
563
+ onLayout={(e) => {
564
+ if (withFullScreen) {
565
+ return;
566
+ }
567
+
568
+ if (!autoHeight || snapPointProp) {
569
+ return;
570
+ }
571
+ //@ts-ignore
572
+ if (opening._value !== 1) {
573
+ return;
574
+ }
575
+
576
+ const absNewHeight = Math.abs(withSafeArea ? e.nativeEvent.layout.height + insets.bottom : e.nativeEvent.layout.height);
577
+ const layoutHeight = Math.min(absNewHeight, maxHeight);
578
+
579
+ contentContainerHeight.current = layoutHeight;
580
+ calculateAutoHeight(layoutHeight, headerContainerHeight.current);
581
+ }}
582
+ style={{
583
+ ...styleSheet.bottomViewChildContainer,
584
+ ...contentStyle,
585
+ flex: withFullScreen || !autoHeight || snapPointProp ? 1 : 0
586
+ }}
587
+ {...panResponderContent.panHandlers}
588
+ >
589
+ {renderContentItems()}
590
+ {renderFooterComponent()}
591
+ </Animated.View>;
592
+ };
593
+
594
+ const renderView = () => {
595
+ return <Animated.View
596
+ key={"bottomSheet-root-view-key"}
597
+ style={{
598
+ backgroundColor: "#fff",
599
+ borderRadius: 8,
600
+ ...rootStyle,
601
+ ...styleSheet.bottomViewContainer,
602
+ //@ts-ignore
603
+ height: moveHeight._value > 0 ? moveHeight : "auto",
604
+ bottom: snapPoint * -1,
605
+ transform: [
606
+ {
607
+ translateY: moveYPosition
608
+ }
609
+ ]
610
+ }}
611
+ >
612
+ {renderHeader()}
613
+ {renderContent()}
614
+ </Animated.View>;
615
+ };
616
+
617
+ return isVisible ?
618
+ <Fragment>
619
+ {renderOverlay()}
620
+ {renderView()}
621
+ </Fragment>
622
+ :
623
+ null;
624
+ };
625
+
626
+ export type {
627
+ ISheetProps,
628
+ SheetRef
629
+ } from "./types";
630
+
631
+ export default forwardRef(Sheet);
package/src/styles.ts ADDED
@@ -0,0 +1,51 @@
1
+ import {
2
+ StyleSheet
3
+ } from "react-native";
4
+ import {
5
+ windowHeight,
6
+ windowWidth
7
+ } from "./constants";
8
+ const styleSheet = StyleSheet.create({
9
+ bottomViewContainer: {
10
+ position: "absolute",
11
+ overflow: "hidden",
12
+ width: windowWidth
13
+ },
14
+ bottomViewChildContainer: {
15
+ overflow: "hidden"
16
+ },
17
+ overlayTouchable: {
18
+ position: "absolute",
19
+ height: windowHeight,
20
+ width: windowWidth,
21
+ bottom: 0,
22
+ right: 0,
23
+ left: 0,
24
+ top: 0
25
+ },
26
+ handlerContainer: {
27
+ justifyContent: "center",
28
+ alignItems: "center",
29
+ width: "100%",
30
+ padding: 8
31
+ },
32
+ outsideHandlerContainer: {
33
+ justifyContent: "center",
34
+ alignItems: "center",
35
+ position: "absolute",
36
+ width: "100%",
37
+ padding: 8,
38
+ top: -24
39
+ },
40
+ handler: {
41
+ position: "relative",
42
+ borderRadius: 4,
43
+ height: 8,
44
+ width: 48
45
+ },
46
+ overlayView: {
47
+ backgroundColor: "black",
48
+ flex: 1
49
+ }
50
+ });
51
+ export default styleSheet;