ferns-ui 0.36.3 → 0.36.5

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.
Files changed (77) hide show
  1. package/package.json +3 -2
  2. package/src/ActionSheet.tsx +1231 -0
  3. package/src/Avatar.tsx +317 -0
  4. package/src/Badge.tsx +65 -0
  5. package/src/Banner.tsx +124 -0
  6. package/src/BlurBox.native.tsx +40 -0
  7. package/src/BlurBox.tsx +31 -0
  8. package/src/Body.tsx +32 -0
  9. package/src/Box.tsx +308 -0
  10. package/src/Button.tsx +219 -0
  11. package/src/Card.tsx +23 -0
  12. package/src/CheckBox.tsx +118 -0
  13. package/src/Common.ts +2743 -0
  14. package/src/Constants.ts +53 -0
  15. package/src/CustomSelect.tsx +85 -0
  16. package/src/DateTimeActionSheet.tsx +409 -0
  17. package/src/DateTimeField.android.tsx +101 -0
  18. package/src/DateTimeField.ios.tsx +83 -0
  19. package/src/DateTimeField.tsx +69 -0
  20. package/src/DecimalRangeActionSheet.tsx +113 -0
  21. package/src/ErrorBoundary.tsx +37 -0
  22. package/src/ErrorPage.tsx +44 -0
  23. package/src/FernsProvider.tsx +21 -0
  24. package/src/Field.tsx +299 -0
  25. package/src/FieldWithLabels.tsx +36 -0
  26. package/src/FlatList.tsx +2 -0
  27. package/src/Form.tsx +182 -0
  28. package/src/HeaderButtons.tsx +107 -0
  29. package/src/Heading.tsx +53 -0
  30. package/src/HeightActionSheet.tsx +104 -0
  31. package/src/Hyperlink.tsx +181 -0
  32. package/src/Icon.tsx +24 -0
  33. package/src/IconButton.tsx +165 -0
  34. package/src/Image.tsx +50 -0
  35. package/src/ImageBackground.tsx +14 -0
  36. package/src/InfoTooltipButton.tsx +23 -0
  37. package/src/Layer.tsx +17 -0
  38. package/src/Link.tsx +17 -0
  39. package/src/Mask.tsx +21 -0
  40. package/src/MediaQuery.ts +46 -0
  41. package/src/Meta.tsx +9 -0
  42. package/src/Modal.tsx +248 -0
  43. package/src/ModalSheet.tsx +58 -0
  44. package/src/NumberPickerActionSheet.tsx +66 -0
  45. package/src/Page.tsx +133 -0
  46. package/src/Permissions.ts +44 -0
  47. package/src/PickerSelect.tsx +553 -0
  48. package/src/Pill.tsx +24 -0
  49. package/src/Pog.tsx +87 -0
  50. package/src/ProgressBar.tsx +55 -0
  51. package/src/ScrollView.tsx +2 -0
  52. package/src/SegmentedControl.tsx +102 -0
  53. package/src/SelectList.tsx +89 -0
  54. package/src/SideDrawer.tsx +62 -0
  55. package/src/Spinner.tsx +20 -0
  56. package/src/SplitPage.native.tsx +160 -0
  57. package/src/SplitPage.tsx +302 -0
  58. package/src/Switch.tsx +19 -0
  59. package/src/Table.tsx +87 -0
  60. package/src/TableHeader.tsx +36 -0
  61. package/src/TableHeaderCell.tsx +76 -0
  62. package/src/TableRow.tsx +87 -0
  63. package/src/TapToEdit.tsx +221 -0
  64. package/src/Text.tsx +131 -0
  65. package/src/TextArea.tsx +16 -0
  66. package/src/TextField.tsx +401 -0
  67. package/src/TextFieldNumberActionSheet.tsx +61 -0
  68. package/src/Toast.tsx +106 -0
  69. package/src/Tooltip.tsx +269 -0
  70. package/src/UnifiedScreens.ts +24 -0
  71. package/src/Unifier.ts +371 -0
  72. package/src/Utilities.tsx +159 -0
  73. package/src/WithLabel.tsx +57 -0
  74. package/src/dayjsExtended.ts +10 -0
  75. package/src/index.tsx +1346 -0
  76. package/src/polyfill.d.ts +11 -0
  77. package/src/tableContext.tsx +80 -0
@@ -0,0 +1,1231 @@
1
+ /* eslint-disable react/prop-types */
2
+
3
+ import React, {Component, createRef} from "react";
4
+ import {
5
+ Animated,
6
+ Dimensions,
7
+ EmitterSubscription,
8
+ findNodeHandle,
9
+ FlatList,
10
+ Keyboard,
11
+ KeyboardEvent,
12
+ LayoutChangeEvent,
13
+ Modal,
14
+ NativeScrollEvent,
15
+ NativeSyntheticEvent,
16
+ Platform,
17
+ Pressable,
18
+ SafeAreaView,
19
+ StatusBar,
20
+ StyleSheet,
21
+ TextInput,
22
+ UIManager,
23
+ View,
24
+ ViewStyle,
25
+ } from "react-native";
26
+
27
+ export type ActionSheetProps = {
28
+ children?: React.ReactNode;
29
+ ref?: React.MutableRefObject<{
30
+ /**
31
+ * Open or close the ActionSheet.
32
+ */
33
+ setModalVisible(visible?: boolean): void;
34
+
35
+ /**
36
+ * Open the Action Sheet.
37
+ */
38
+ show(): void;
39
+
40
+ /**
41
+ * Close the ActionSheet.
42
+ */
43
+ hide(): void;
44
+
45
+ /**
46
+ * Attach this to any child ScrollView Component's onScrollEndDrag,
47
+ * onMomentumScrollEnd,onScrollAnimationEnd callbacks to handle the ActionSheet
48
+ * closing and bouncing back properly.
49
+ */
50
+ handleChildScrollEnd(): void;
51
+
52
+ /**
53
+ * Snap ActionSheet to given offset
54
+ */
55
+ snapToOffset(offset: number): void;
56
+ }>;
57
+ /**
58
+ * Animate the opening and closing of ActionSheet.
59
+
60
+ | Type | Required |
61
+ | ---- | -------- |
62
+ | boolean | no |
63
+
64
+ Default: `true`
65
+ */
66
+ animated?: boolean;
67
+
68
+ /**
69
+ * Use if you want to show the ActionSheet Partially on Opening. **Requires `gestureEnabled=true`**
70
+
71
+ | Type | Required |
72
+ | ---- | -------- |
73
+ | boolean | no |
74
+
75
+ Default:`1`
76
+ */
77
+
78
+ initialOffsetFromBottom?: number;
79
+
80
+ /**
81
+ * When touch ends and user has not moved farther from the set springOffset, the ActionSheet will return to previous position.
82
+
83
+ | Type | Required |
84
+ | ---- | -------- |
85
+ | number | no |
86
+
87
+ Default: `50`
88
+ */
89
+ springOffset?: number;
90
+ /**
91
+ * Add elevation to the ActionSheet container.
92
+
93
+ | Type | Required |
94
+ | ---- | -------- |
95
+ | number | no |
96
+
97
+ Default: `0`
98
+
99
+ #
100
+ */
101
+ elevation?: number;
102
+
103
+ /**
104
+ * Color of the gestureEnabled Indicator.
105
+
106
+ | Type | Required |
107
+ | ---- | -------- |
108
+ | string | no |
109
+
110
+ Default: `"#f0f0f0"`
111
+ */
112
+ indicatorColor?: string;
113
+
114
+ /**
115
+ * Normally when the ActionSheet is fully opened, a small portion from the bottom is hidden by default. Use this prop if you want the ActionSheet to hover over the bottom of screen and not hide a little behind it.
116
+
117
+ | Type | Required |
118
+ | ---- | -------- |
119
+ | number | no |
120
+
121
+ Default:`0`
122
+ */
123
+ extraScroll?: number;
124
+ /**
125
+ * Color of the overlay/backdrop.
126
+
127
+ | Type | Required |
128
+ | ---- | -------- |
129
+ | string | no |
130
+
131
+ Default: `"black"`
132
+ */
133
+ overlayColor?: string;
134
+
135
+ /**
136
+ * Keep the header always visible even when gestures are disabled.
137
+
138
+ | Type | Required |
139
+ | ---- | -------- |
140
+ | boolean | no |
141
+
142
+ Default: `false`
143
+ */
144
+ headerAlwaysVisible?: boolean;
145
+
146
+ /**
147
+ * Delay draw of ActionSheet on open for android.
148
+
149
+ | Type | Required |
150
+ | ---- | -------- |
151
+ | boolean | no |
152
+
153
+ Default: `false`
154
+ */
155
+
156
+ delayActionSheetDraw?: boolean;
157
+
158
+ /**
159
+ * Delay draw of ActionSheet on open for android time.
160
+
161
+ | Type | Required |
162
+ | ---- | -------- |
163
+ | number (ms) | no |
164
+
165
+ Default: `50`
166
+ */
167
+
168
+ delayActionSheetDrawTime?: number;
169
+
170
+ /**
171
+ * Your custom header component. Using this will hide the default indicator.
172
+
173
+ | Type | Required |
174
+ | ---- | -------- |
175
+ | React.Component | no |
176
+ */
177
+ CustomHeaderComponent?: React.ReactNode;
178
+
179
+ /**
180
+ * Any custom styles for the container.
181
+
182
+ | Type | Required |
183
+ | ---- | -------- |
184
+ | Object | no |
185
+ */
186
+ containerStyle?: ViewStyle;
187
+
188
+ /**
189
+ * Control closing ActionSheet by touching on backdrop.
190
+
191
+ | Type | Required |
192
+ | ---- | -------- |
193
+ | boolean | no |
194
+
195
+ Default: `true`
196
+ */
197
+ closeOnTouchBackdrop?: boolean;
198
+
199
+ /**
200
+ * Speed of opening animation. Higher means the ActionSheet will open more quickly.
201
+
202
+ | Type | Required |
203
+ | ---- | -------- |
204
+ | number | no |
205
+
206
+ Default: `12`
207
+ */
208
+ openAnimationSpeed?: number;
209
+ /**
210
+ * Duration of closing animation.
211
+
212
+ | Type | Required |
213
+ | ---- | -------- |
214
+ | number | no |
215
+
216
+ Default: `300`
217
+ */
218
+ closeAnimationDuration?: number;
219
+ /**
220
+ *
221
+ How much you want the ActionSheet to bounce when it is opened.
222
+
223
+ | Type | Required |
224
+ | ---- | -------- |
225
+ | number | no |
226
+
227
+ Default: `8`
228
+ */
229
+ bounciness?: number;
230
+
231
+ /**
232
+ * Will the ActionSheet close on `hardwareBackPress` event.
233
+
234
+ | Type | Required |
235
+ | ---- | -------- |
236
+ | boolean | no |
237
+
238
+ Default: `true`
239
+ */
240
+ closeOnPressBack?: boolean;
241
+ /**
242
+ * Default opacity of the overlay/backdrop.
243
+
244
+ | Type | Required |
245
+ | ---- | -------- |
246
+ | number 0 - 1 | no |
247
+
248
+ Default: `0.3`
249
+ */
250
+ defaultOverlayOpacity?: number;
251
+
252
+ /**
253
+ * Enables gesture control of ActionSheet
254
+
255
+ | Type | Required |
256
+ | ---- | -------- |
257
+ | boolean | no |
258
+
259
+ Default: `false`
260
+ */
261
+ gestureEnabled?: boolean;
262
+
263
+ /**
264
+ * Bounces the ActionSheet on open.
265
+
266
+ | Type | Required |
267
+ | ---- | -------- |
268
+ | boolean | no |
269
+
270
+ Default: `false`
271
+ */
272
+ bounceOnOpen?: boolean;
273
+
274
+ /**
275
+ * Setting the keyboard persistence of the ScrollView component, should be one of "never", "always", or "handled"
276
+
277
+ | Type | Required |
278
+ | ---- | -------- |
279
+ | string | no |
280
+
281
+ Default: `"never"`
282
+ */
283
+ keyboardShouldPersistTaps?: boolean | "always" | "never" | "handled";
284
+
285
+ /**
286
+ * Determine whether the modal should go under the system statusbar.
287
+
288
+ | Type | Required |
289
+ | ---- | -------- |
290
+ | boolean | no |
291
+
292
+ Default: `true`
293
+ */
294
+ statusBarTranslucent?: boolean;
295
+
296
+ /**
297
+ * Prevent ActionSheet from closing on
298
+ * gesture or tapping on backdrop.
299
+ * Instead snap it to `bottomOffset` location
300
+ *
301
+ *
302
+ * | Type | Required |
303
+ | ---- | -------- |
304
+ | boolean | no |
305
+ */
306
+ closable?: boolean;
307
+
308
+ /**
309
+ * Allow ActionSheet to draw under the StatusBar.
310
+ * This is enabled by default.
311
+ *
312
+ *
313
+ * | Type | Required |
314
+ | ---- | -------- |
315
+ | boolean | no |
316
+ Default: `true`
317
+ */
318
+ drawUnderStatusBar?: boolean;
319
+
320
+ /**
321
+ * Snap ActionSheet to this location if `closable` is set to false;
322
+ *
323
+ *
324
+ * | Type | Required |
325
+ | ---- | -------- |
326
+ | number | no |
327
+ */
328
+
329
+ bottomOffset?: number;
330
+
331
+ /**
332
+ * Change how ActionSheet behaves when keyboard is opened.
333
+ *
334
+ *
335
+ * | Type | Required |
336
+ | ---- | -------- |
337
+ | "padding" | "position" | no |
338
+ Default:`padding`
339
+ */
340
+
341
+ keyboardMode?: "padding" | "position";
342
+
343
+ /**
344
+ * Test ID for unit testing
345
+ */
346
+ testID?: string;
347
+
348
+ /**
349
+ *
350
+ Event called when the ActionSheet closes.
351
+
352
+
353
+ * | Type | Required |
354
+ | ---- | -------- |
355
+ | function | no |
356
+
357
+
358
+ #
359
+ */
360
+
361
+ onClose?: () => void;
362
+
363
+ /**
364
+ * An event called when the ActionSheet Opens.
365
+
366
+ | Type | Required |
367
+ | ---- | -------- |
368
+ | function | no |
369
+ */
370
+ onOpen?: () => void;
371
+
372
+ /**
373
+ * Event called when position of ActionSheet changes.
374
+ */
375
+ onPositionChanged?: (hasReachedTop: boolean) => void;
376
+ };
377
+
378
+ export const styles = StyleSheet.create({
379
+ scrollView: {
380
+ height: "100%",
381
+ width: "100%",
382
+ backgroundColor: "transparent",
383
+ },
384
+ container: {
385
+ width: "100%",
386
+ backgroundColor: "white",
387
+ alignSelf: "center",
388
+ },
389
+ safearea: {
390
+ position: "absolute",
391
+ top: 999999,
392
+ left: 999999,
393
+ },
394
+ indicator: {
395
+ height: 6,
396
+ width: 45,
397
+ borderRadius: 100,
398
+ backgroundColor: "#f0f0f0",
399
+ marginVertical: 5,
400
+ alignSelf: "center",
401
+ },
402
+ parentContainer: {
403
+ width: "100%",
404
+ height: "100%",
405
+ justifyContent: "center",
406
+ alignItems: "center",
407
+ },
408
+ });
409
+
410
+ export function getDeviceHeight(statusBarTranslucent: boolean | undefined): number {
411
+ const height = Dimensions.get("window").height;
412
+
413
+ if (Platform.OS === "android" && !statusBarTranslucent) {
414
+ return StatusBar.currentHeight ? height - StatusBar.currentHeight : height;
415
+ }
416
+
417
+ return height;
418
+ }
419
+
420
+ export const getElevation = (elevation?: number) => {
421
+ if (!elevation) {
422
+ return {};
423
+ }
424
+ return {
425
+ elevation,
426
+ shadowColor: "black",
427
+ shadowOffset: {width: 0.3 * elevation, height: 0.5 * elevation},
428
+ shadowOpacity: 0.2,
429
+ shadowRadius: 0.7 * elevation,
430
+ };
431
+ };
432
+
433
+ export const SUPPORTED_ORIENTATIONS: (
434
+ | "portrait"
435
+ | "portrait-upside-down"
436
+ | "landscape"
437
+ | "landscape-left"
438
+ | "landscape-right"
439
+ )[] = ["portrait", "portrait-upside-down", "landscape", "landscape-left", "landscape-right"];
440
+
441
+ export const waitAsync = (ms: number): Promise<null> =>
442
+ new Promise((resolve) => {
443
+ setTimeout(() => {
444
+ resolve(null);
445
+ }, ms);
446
+ });
447
+
448
+ const safeAreaInnerHeight = 0;
449
+ const dummyData = ["dummy"];
450
+ let safeAreaPaddingTop = Platform.OS === "android" ? StatusBar.currentHeight || 0 : 0;
451
+ let calculatedDeviceHeight = Dimensions.get("window").height;
452
+
453
+ type State = {
454
+ modalVisible: boolean;
455
+ scrollable: boolean;
456
+ layoutHasCalled: boolean;
457
+ keyboard: boolean;
458
+ deviceHeight: number;
459
+ deviceWidth: number;
460
+ portrait: boolean;
461
+ safeAreaInnerHeight: number;
462
+ paddingTop: number;
463
+ };
464
+
465
+ const defaultProps = {
466
+ animated: true,
467
+ closeOnPressBack: true,
468
+ bounciness: 8,
469
+ extraScroll: 0,
470
+ closeAnimationDuration: 300,
471
+ delayActionSheetDrawTime: 0,
472
+ openAnimationSpeed: 12,
473
+ springOffset: 100,
474
+ elevation: 5,
475
+ initialOffsetFromBottom: 1,
476
+ indicatorColor: "#f0f0f0",
477
+ defaultOverlayOpacity: 0.3,
478
+ overlayColor: "black",
479
+ closable: true,
480
+ bottomOffset: 0,
481
+ closeOnTouchBackdrop: true,
482
+ drawUnderStatusBar: true,
483
+ statusBarTranslucent: true,
484
+ keyboardMode: "padding",
485
+ gestureEnabled: false,
486
+ };
487
+
488
+ type Props = Partial<typeof defaultProps> & ActionSheetProps;
489
+
490
+ export class ActionSheet extends Component<Props, State, any> {
491
+ static defaultProps = defaultProps;
492
+
493
+ actionSheetHeight = 0;
494
+
495
+ keyboardDidShowListener: EmitterSubscription | null = null;
496
+
497
+ keyboardDidHideListener: EmitterSubscription | null = null;
498
+
499
+ prevScroll = 0;
500
+
501
+ timeout: any | null = null;
502
+
503
+ offsetY = 0;
504
+
505
+ currentOffsetFromBottom = 0;
506
+
507
+ scrollAnimationEndValue = 0;
508
+
509
+ hasBounced = false;
510
+
511
+ layoutHasCalled = false;
512
+
513
+ isClosing = false;
514
+
515
+ isRecoiling = false;
516
+
517
+ isReachedTop = false;
518
+
519
+ deviceLayoutCalled = false;
520
+
521
+ scrollViewRef: React.RefObject<any>;
522
+
523
+ safeAreaViewRef: React.RefObject<any>;
524
+
525
+ transformValue: Animated.Value;
526
+
527
+ opacityValue: Animated.Value;
528
+
529
+ borderRadius: Animated.Value;
530
+
531
+ underlayTranslateY: Animated.Value;
532
+
533
+ underlayScale: Animated.Value;
534
+
535
+ indicatorTranslateY: Animated.Value;
536
+
537
+ constructor(props: ActionSheetProps) {
538
+ super(props);
539
+ this.state = {
540
+ modalVisible: false,
541
+ scrollable: false,
542
+ layoutHasCalled: false,
543
+ keyboard: false,
544
+ deviceHeight: calculatedDeviceHeight || getDeviceHeight(this.props.statusBarTranslucent),
545
+ deviceWidth: Dimensions.get("window").width,
546
+ portrait: true,
547
+ safeAreaInnerHeight,
548
+ paddingTop: safeAreaPaddingTop,
549
+ };
550
+
551
+ this.actionSheetHeight = 0;
552
+ this.prevScroll = 0;
553
+ this.scrollAnimationEndValue = 0;
554
+ this.hasBounced = false;
555
+ this.scrollViewRef = createRef();
556
+ this.layoutHasCalled = false;
557
+ this.isClosing = false;
558
+ this.isRecoiling = false;
559
+ this.offsetY = 0;
560
+ this.safeAreaViewRef = createRef();
561
+ this.transformValue = new Animated.Value(0);
562
+ this.opacityValue = new Animated.Value(0);
563
+ this.borderRadius = new Animated.Value(10);
564
+ this.currentOffsetFromBottom = this.props.initialOffsetFromBottom as number;
565
+ this.underlayTranslateY = new Animated.Value(100);
566
+ this.underlayScale = new Animated.Value(1);
567
+ this.indicatorTranslateY = new Animated.Value(-this.state.paddingTop | 0);
568
+ this.isReachedTop = false;
569
+ this.deviceLayoutCalled = false;
570
+ this.timeout = null;
571
+ }
572
+
573
+ /**
574
+ * Snap ActionSheet to Offset
575
+ */
576
+
577
+ snapToOffset = (offset: number) => {
578
+ const correction = this.state.deviceHeight * 0.15;
579
+ const extraScroll = this.props.extraScroll || 0;
580
+ const scrollOffset = this.props.gestureEnabled
581
+ ? offset + correction + extraScroll
582
+ : offset + correction + extraScroll;
583
+ this.currentOffsetFromBottom = offset / this.actionSheetHeight;
584
+ this._scrollTo(scrollOffset);
585
+ this.updateActionSheetPosition(scrollOffset);
586
+ };
587
+
588
+ // Open the ActionSheet
589
+ show = () => {
590
+ this.setModalVisible(true);
591
+ };
592
+
593
+ // Close the ActionSheet
594
+ hide = () => {
595
+ this.setModalVisible(false);
596
+ };
597
+
598
+ /**
599
+ * Open/Close the ActionSheet
600
+ */
601
+ setModalVisible = (visible: boolean) => {
602
+ let modalVisible = this.state.modalVisible;
603
+ if (visible !== undefined) {
604
+ if (modalVisible === visible) {
605
+ return;
606
+ }
607
+ modalVisible = !visible;
608
+ }
609
+
610
+ if (!modalVisible) {
611
+ this.setState({
612
+ modalVisible: true,
613
+ scrollable: this.props.gestureEnabled || false,
614
+ });
615
+ } else {
616
+ this._hideModal();
617
+ }
618
+ };
619
+
620
+ _hideAnimation() {
621
+ const {
622
+ animated,
623
+ closeAnimationDuration,
624
+ bottomOffset,
625
+ initialOffsetFromBottom,
626
+ extraScroll,
627
+ closable,
628
+ } = this.props;
629
+
630
+ Animated.parallel([
631
+ Animated.timing(this.opacityValue, {
632
+ toValue: closable ? 0 : 1,
633
+ duration: animated ? closeAnimationDuration : 1,
634
+ useNativeDriver: true,
635
+ }),
636
+ Animated.timing(this.transformValue, {
637
+ toValue: closable ? this.actionSheetHeight * 2 : 0,
638
+ duration: animated ? closeAnimationDuration : 1,
639
+ useNativeDriver: true,
640
+ }),
641
+ ]).start();
642
+
643
+ waitAsync((closeAnimationDuration as number) / 1.5).then(() => {
644
+ if (!closable) {
645
+ if (bottomOffset && bottomOffset > 0) {
646
+ this.snapToOffset(bottomOffset);
647
+ } else {
648
+ this._scrollTo(
649
+ ((this.actionSheetHeight * (initialOffsetFromBottom || 0)) as number) +
650
+ this.state.deviceHeight * 0.1 +
651
+ (extraScroll ?? 0),
652
+ true
653
+ );
654
+ this.currentOffsetFromBottom = initialOffsetFromBottom as number;
655
+ }
656
+
657
+ this.isClosing = false;
658
+ } else {
659
+ this._scrollTo(0, false);
660
+ this.currentOffsetFromBottom = initialOffsetFromBottom as number;
661
+ this.setState(
662
+ {
663
+ modalVisible: !closable,
664
+ },
665
+ () => {
666
+ this.isClosing = false;
667
+ this.isReachedTop = false;
668
+ this.props.onPositionChanged && this.props.onPositionChanged(false);
669
+ this.indicatorTranslateY.setValue(-this.state.paddingTop);
670
+ this.layoutHasCalled = false;
671
+ this.deviceLayoutCalled = false;
672
+ this.props.onClose && this.props.onClose();
673
+ }
674
+ );
675
+ }
676
+ });
677
+ }
678
+
679
+ _hideModal = () => {
680
+ if (this.isClosing) return;
681
+ this.isClosing = true;
682
+ this._hideAnimation();
683
+ };
684
+
685
+ measure = async (): Promise<number> => {
686
+ return new Promise((resolve) => {
687
+ setTimeout(() => {
688
+ UIManager.measureInWindow(
689
+ this.safeAreaViewRef.current._nativeTag,
690
+ (x, y, width, height) => {
691
+ safeAreaPaddingTop = height;
692
+ resolve(height === 0 ? 20 : height);
693
+ }
694
+ );
695
+ }, 100);
696
+ });
697
+ };
698
+
699
+ _showModal = async (event: LayoutChangeEvent) => {
700
+ const {gestureEnabled, delayActionSheetDraw, delayActionSheetDrawTime} = this.props;
701
+
702
+ if (!event?.nativeEvent) return;
703
+ const height = event.nativeEvent.layout.height;
704
+ if (this.layoutHasCalled) {
705
+ this.actionSheetHeight = height;
706
+ this._returnToPrevScrollPosition(height);
707
+ return;
708
+ } else {
709
+ this.layoutHasCalled = true;
710
+ this.actionSheetHeight = height;
711
+ const scrollOffset = this.getInitialScrollPosition();
712
+ this.isRecoiling = false;
713
+ if (Platform.OS === "ios") {
714
+ await waitAsync(delayActionSheetDrawTime as number);
715
+ } else {
716
+ if (delayActionSheetDraw) {
717
+ await waitAsync(delayActionSheetDrawTime as number);
718
+ }
719
+ }
720
+ this._scrollTo(scrollOffset, false);
721
+ this.prevScroll = scrollOffset;
722
+ if (Platform.OS === "ios") {
723
+ await waitAsync(delayActionSheetDrawTime ?? 0 / 2);
724
+ } else {
725
+ if (delayActionSheetDraw) {
726
+ await waitAsync(delayActionSheetDrawTime ?? 0 / 2);
727
+ }
728
+ }
729
+ this._openAnimation(scrollOffset);
730
+ this.underlayScale.setValue(1);
731
+ this.underlayTranslateY.setValue(100);
732
+ if (!gestureEnabled) {
733
+ this.props.onPositionChanged && this.props.onPositionChanged(true);
734
+ }
735
+ this.updateActionSheetPosition(scrollOffset);
736
+ }
737
+ };
738
+
739
+ _openAnimation = (scrollOffset: number) => {
740
+ const {bounciness, bounceOnOpen, animated, openAnimationSpeed} = this.props;
741
+
742
+ if (animated) {
743
+ this.transformValue.setValue(scrollOffset);
744
+ Animated.parallel([
745
+ Animated.spring(this.transformValue, {
746
+ toValue: 0,
747
+ bounciness: bounceOnOpen ? bounciness : 1,
748
+ speed: openAnimationSpeed,
749
+ useNativeDriver: true,
750
+ }),
751
+ Animated.timing(this.opacityValue, {
752
+ toValue: 1,
753
+ duration: 150,
754
+ useNativeDriver: true,
755
+ }),
756
+ ]).start();
757
+ } else {
758
+ this.opacityValue.setValue(1);
759
+ }
760
+ };
761
+
762
+ _onScrollBegin = async () => {};
763
+
764
+ _onScrollBeginDrag = async (event: NativeSyntheticEvent<NativeScrollEvent>) => {
765
+ this.prevScroll = event.nativeEvent.contentOffset.y;
766
+ };
767
+
768
+ _applyHeightLimiter() {
769
+ if (this.actionSheetHeight > this.state.deviceHeight) {
770
+ this.actionSheetHeight =
771
+ this.actionSheetHeight - (this.actionSheetHeight - this.state.deviceHeight);
772
+ }
773
+ }
774
+
775
+ _onScrollEnd = async (event: NativeSyntheticEvent<NativeScrollEvent>) => {
776
+ const {springOffset, extraScroll} = this.props;
777
+ const verticalOffset = event.nativeEvent.contentOffset.y;
778
+
779
+ const correction = this.state.deviceHeight * 0.15;
780
+ if (this.isRecoiling) return;
781
+
782
+ if (this.prevScroll < verticalOffset) {
783
+ if (verticalOffset - this.prevScroll > (springOffset ?? 0) * 0.75) {
784
+ this.isRecoiling = true;
785
+
786
+ this._applyHeightLimiter();
787
+ this.currentOffsetFromBottom =
788
+ this.currentOffsetFromBottom < (this.props.initialOffsetFromBottom ?? 0)
789
+ ? (this.props.initialOffsetFromBottom as number)
790
+ : 1;
791
+ const scrollOffset =
792
+ this.actionSheetHeight * this.currentOffsetFromBottom + correction + (extraScroll ?? 0);
793
+
794
+ this._scrollTo(scrollOffset);
795
+ await waitAsync(300);
796
+ this.isRecoiling = false;
797
+ this.props.onPositionChanged && this.props.onPositionChanged(true);
798
+ } else {
799
+ this._returnToPrevScrollPosition(this.actionSheetHeight);
800
+ }
801
+ } else {
802
+ if (this.prevScroll - verticalOffset > (springOffset ?? 0)) {
803
+ this._hideModal();
804
+ } else {
805
+ if (this.isRecoiling) {
806
+ return;
807
+ }
808
+
809
+ this.isRecoiling = true;
810
+ this._returnToPrevScrollPosition(this.actionSheetHeight);
811
+ await waitAsync(300);
812
+ this.isRecoiling = false;
813
+ }
814
+ }
815
+ };
816
+
817
+ updateActionSheetPosition(scrollPosition: number) {
818
+ if (this.actionSheetHeight >= this.state.deviceHeight - 1) {
819
+ const correction = this.state.deviceHeight * 0.15;
820
+ const distanceFromTop = this.actionSheetHeight + correction - scrollPosition;
821
+ if (distanceFromTop < safeAreaPaddingTop) {
822
+ if (!this.props.drawUnderStatusBar) return;
823
+ this.indicatorTranslateY.setValue(0);
824
+ } else {
825
+ this.indicatorTranslateY.setValue(-safeAreaPaddingTop);
826
+ }
827
+ }
828
+ }
829
+
830
+ _returnToPrevScrollPosition(height: number) {
831
+ const correction = this.state.deviceHeight * 0.15;
832
+ const scrollOffset =
833
+ height * this.currentOffsetFromBottom + correction + (this.props.extraScroll ?? 0);
834
+
835
+ this.updateActionSheetPosition(scrollOffset);
836
+ this._scrollTo(scrollOffset);
837
+ }
838
+
839
+ _scrollTo = (y: number, animated = true) => {
840
+ this.scrollAnimationEndValue = y;
841
+ this.prevScroll = y;
842
+ this.scrollViewRef.current?._listRef._scrollRef.scrollTo({
843
+ x: 0,
844
+ y: this.scrollAnimationEndValue,
845
+ animated,
846
+ });
847
+ };
848
+
849
+ _onTouchMove = () => {
850
+ if (this.props.closeOnTouchBackdrop) {
851
+ this._hideModal();
852
+ }
853
+ this.setState({
854
+ scrollable: false,
855
+ });
856
+ };
857
+
858
+ _onTouchStart = () => {
859
+ if (this.props.closeOnTouchBackdrop) {
860
+ this._hideModal();
861
+ }
862
+ this.setState({
863
+ scrollable: false,
864
+ });
865
+ };
866
+
867
+ _onTouchEnd = () => {
868
+ this._returnToPrevScrollPosition(this.actionSheetHeight);
869
+ if (this.props.gestureEnabled) {
870
+ this.setState({
871
+ scrollable: true,
872
+ });
873
+ }
874
+ };
875
+
876
+ _onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
877
+ this.offsetY = event.nativeEvent.contentOffset.y;
878
+
879
+ const correction = this.state.deviceHeight * 0.15;
880
+ const distanceFromTop = this.actionSheetHeight + correction - this.offsetY;
881
+
882
+ if (this.actionSheetHeight < this.offsetY) {
883
+ if (!this.isReachedTop) {
884
+ this.isReachedTop = true;
885
+ this.props.onPositionChanged && this.props.onPositionChanged(true);
886
+ }
887
+ } else {
888
+ if (this.isReachedTop) {
889
+ this.isReachedTop = false;
890
+ this.props.onPositionChanged && this.props.onPositionChanged(false);
891
+ }
892
+ }
893
+
894
+ if (this.actionSheetHeight >= this.state.deviceHeight - 1) {
895
+ if (distanceFromTop < safeAreaPaddingTop) {
896
+ if (!this.props.drawUnderStatusBar) return;
897
+
898
+ this.indicatorTranslateY.setValue(
899
+ -this.state.paddingTop + (safeAreaPaddingTop - distanceFromTop)
900
+ );
901
+ } else {
902
+ this.indicatorTranslateY.setValue(-safeAreaPaddingTop);
903
+ }
904
+ }
905
+ };
906
+
907
+ _onRequestClose = () => {
908
+ if (this.props.closeOnPressBack) this._hideModal();
909
+ };
910
+
911
+ _onTouchBackdrop = () => {
912
+ if (this.props.closeOnTouchBackdrop) {
913
+ this._hideModal();
914
+ }
915
+ };
916
+
917
+ componentDidMount() {
918
+ this.keyboardDidShowListener = Keyboard.addListener(
919
+ Platform.OS === "android" ? "keyboardDidShow" : "keyboardWillShow",
920
+ this._onKeyboardShow
921
+ );
922
+
923
+ this.keyboardDidHideListener = Keyboard.addListener(
924
+ Platform.OS === "android" ? "keyboardDidHide" : "keyboardWillHide",
925
+ this._onKeyboardHide
926
+ );
927
+ }
928
+
929
+ _onKeyboardShow = (event: KeyboardEvent) => {
930
+ this.setState({
931
+ keyboard: true,
932
+ });
933
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
934
+ const ReactNativeVersion = require("react-native/Libraries/Core/ReactNativeVersion");
935
+
936
+ let v = ReactNativeVersion.version.major + ReactNativeVersion.version.minor;
937
+ v = parseInt(v);
938
+
939
+ if (v >= 63 || Platform.OS === "ios") {
940
+ const keyboardHeight = event.endCoordinates.height;
941
+ const {height: windowHeight} = Dimensions.get("window");
942
+
943
+ const currentlyFocusedField = TextInput.State.currentlyFocusedField
944
+ ? findNodeHandle(TextInput.State.currentlyFocusedField())
945
+ : TextInput.State.currentlyFocusedField();
946
+
947
+ if (!currentlyFocusedField) {
948
+ return;
949
+ }
950
+
951
+ UIManager.measure(currentlyFocusedField, (originX, originY, width, height, pageX, pageY) => {
952
+ const fieldHeight = height;
953
+ const gap = windowHeight - keyboardHeight - (pageY + fieldHeight);
954
+ if (gap >= 0) {
955
+ return;
956
+ }
957
+ const toValue = this.props.keyboardMode === "position" ? -(keyboardHeight + 15) : gap - 10;
958
+
959
+ Animated.timing(this.transformValue, {
960
+ toValue,
961
+ duration: 250,
962
+ useNativeDriver: true,
963
+ }).start();
964
+ });
965
+ } else {
966
+ Animated.timing(this.transformValue, {
967
+ toValue: -10,
968
+ duration: 250,
969
+ useNativeDriver: true,
970
+ }).start();
971
+ }
972
+ };
973
+
974
+ /**
975
+ * Attach this to any child ScrollView Component's onScrollEndDrag,
976
+ * onMomentumScrollEnd,onScrollAnimationEnd callbacks to handle the ActionSheet
977
+ * closing and bouncing back properly.
978
+ */
979
+
980
+ handleChildScrollEnd = async () => {
981
+ if (this.offsetY > this.prevScroll) return;
982
+ if (this.prevScroll - (this.props.springOffset ?? 0) > this.offsetY) {
983
+ const scrollOffset = this.getInitialScrollPosition();
984
+ if (this.offsetY > scrollOffset - 100) {
985
+ this.isRecoiling = true;
986
+ this._scrollTo(scrollOffset);
987
+ this.currentOffsetFromBottom = this.props.initialOffsetFromBottom ?? 0;
988
+ this.prevScroll = scrollOffset;
989
+ setTimeout(() => {
990
+ this.isRecoiling = false;
991
+ }, 500);
992
+ } else {
993
+ this._hideModal();
994
+ }
995
+ } else {
996
+ this.isRecoiling = true;
997
+ this._scrollTo(this.prevScroll, true);
998
+ setTimeout(() => {
999
+ this.isRecoiling = false;
1000
+ }, 500);
1001
+ }
1002
+ };
1003
+
1004
+ _onKeyboardHide = () => {
1005
+ this.setState({
1006
+ keyboard: false,
1007
+ });
1008
+ this.opacityValue.setValue(1);
1009
+ Animated.timing(this.transformValue, {
1010
+ toValue: 0,
1011
+ duration: 100,
1012
+ useNativeDriver: true,
1013
+ }).start();
1014
+ };
1015
+
1016
+ componentWillUnmount() {
1017
+ this.keyboardDidShowListener?.remove();
1018
+ this.keyboardDidHideListener?.remove();
1019
+ }
1020
+
1021
+ _onDeviceLayout = async (_event: any) => {
1022
+ const event = {..._event};
1023
+
1024
+ if (this.timeout) {
1025
+ clearTimeout(this.timeout);
1026
+ }
1027
+
1028
+ this.timeout = setTimeout(async () => {
1029
+ let safeMarginFromTop = 0;
1030
+ const measuredPadding =
1031
+ Platform.OS === "ios" ? await this.measure() : StatusBar.currentHeight;
1032
+
1033
+ if (!this.props.drawUnderStatusBar) {
1034
+ if (Platform.OS === "android" && !this.props.statusBarTranslucent) return;
1035
+ safeMarginFromTop = measuredPadding ?? 0;
1036
+ this.indicatorTranslateY.setValue(-(measuredPadding as number));
1037
+ } else {
1038
+ this.updateActionSheetPosition(this.offsetY);
1039
+ }
1040
+ const height = event.nativeEvent.layout.height - safeMarginFromTop;
1041
+ const width = Dimensions.get("window").width;
1042
+ if (
1043
+ height?.toFixed(0) === calculatedDeviceHeight?.toFixed(0) &&
1044
+ width?.toFixed(0) === this.state.deviceWidth?.toFixed(0) &&
1045
+ this.deviceLayoutCalled
1046
+ )
1047
+ return;
1048
+ this.deviceLayoutCalled = true;
1049
+ calculatedDeviceHeight = height;
1050
+ this.setState({
1051
+ deviceHeight: height,
1052
+ deviceWidth: width,
1053
+ portrait: height > width,
1054
+ paddingTop: measuredPadding as any,
1055
+ });
1056
+ }, 1);
1057
+ };
1058
+
1059
+ getInitialScrollPosition() {
1060
+ this._applyHeightLimiter();
1061
+ const correction = this.state.deviceHeight * 0.15;
1062
+ const scrollPosition = this.props.gestureEnabled
1063
+ ? this.actionSheetHeight * (this.props.initialOffsetFromBottom ?? 0) +
1064
+ correction +
1065
+ (this.props.extraScroll ?? 0)
1066
+ : this.actionSheetHeight + correction + (this.props.extraScroll ?? 0);
1067
+ this.currentOffsetFromBottom = this.props.initialOffsetFromBottom ?? 0;
1068
+ this.updateActionSheetPosition(scrollPosition);
1069
+
1070
+ return scrollPosition;
1071
+ }
1072
+
1073
+ _keyExtractor = (item: any) => item;
1074
+
1075
+ render() {
1076
+ const {scrollable, modalVisible, keyboard} = this.state;
1077
+ const {
1078
+ onOpen,
1079
+ overlayColor,
1080
+ gestureEnabled,
1081
+ elevation,
1082
+ indicatorColor,
1083
+ defaultOverlayOpacity,
1084
+ children,
1085
+ containerStyle,
1086
+ CustomHeaderComponent,
1087
+ headerAlwaysVisible,
1088
+ keyboardShouldPersistTaps,
1089
+ statusBarTranslucent,
1090
+ } = this.props;
1091
+
1092
+ return (
1093
+ <Modal
1094
+ animationType="none"
1095
+ statusBarTranslucent={statusBarTranslucent}
1096
+ // testID={testID}
1097
+ supportedOrientations={SUPPORTED_ORIENTATIONS}
1098
+ transparent
1099
+ visible={modalVisible}
1100
+ onRequestClose={this._onRequestClose}
1101
+ onShow={onOpen}
1102
+ >
1103
+ <Animated.View
1104
+ style={[
1105
+ styles.parentContainer,
1106
+ {
1107
+ opacity: this.opacityValue,
1108
+ width: this.state.deviceWidth,
1109
+ },
1110
+ ]}
1111
+ onLayout={this._onDeviceLayout}
1112
+ >
1113
+ <SafeAreaView ref={this.safeAreaViewRef} style={styles.safearea}>
1114
+ <View />
1115
+ </SafeAreaView>
1116
+ <FlatList
1117
+ ref={this.scrollViewRef}
1118
+ bounces={false}
1119
+ contentContainerStyle={{
1120
+ width: this.state.deviceWidth,
1121
+ }}
1122
+ data={dummyData}
1123
+ keyExtractor={this._keyExtractor}
1124
+ keyboardShouldPersistTaps={keyboardShouldPersistTaps}
1125
+ renderItem={() => (
1126
+ <View
1127
+ style={{
1128
+ width: "100%",
1129
+ }}
1130
+ >
1131
+ <Animated.View
1132
+ style={{
1133
+ height: "100%",
1134
+ width: "100%",
1135
+ position: "absolute",
1136
+ zIndex: 1,
1137
+ backgroundColor: overlayColor,
1138
+ opacity: defaultOverlayOpacity,
1139
+ }}
1140
+ onTouchEnd={this._onTouchBackdrop}
1141
+ onTouchMove={this._onTouchBackdrop}
1142
+ onTouchStart={this._onTouchBackdrop}
1143
+ />
1144
+ <View
1145
+ style={{
1146
+ height: this.state.deviceHeight * 1.15,
1147
+ width: "100%",
1148
+ zIndex: 10,
1149
+ }}
1150
+ onTouchEnd={this._onTouchEnd}
1151
+ onTouchMove={this._onTouchMove}
1152
+ onTouchStart={this._onTouchStart}
1153
+ >
1154
+ <Pressable
1155
+ style={{
1156
+ height: this.state.deviceHeight * 1.15,
1157
+ width: "100%",
1158
+ }}
1159
+ onLongPress={this._onTouchBackdrop}
1160
+ onPress={this._onTouchBackdrop}
1161
+ />
1162
+ </View>
1163
+
1164
+ <Animated.View
1165
+ style={[
1166
+ styles.container,
1167
+ {
1168
+ borderRadius: 10,
1169
+ },
1170
+ containerStyle,
1171
+ {
1172
+ ...getElevation(elevation),
1173
+ zIndex: 11,
1174
+ opacity: this.opacityValue,
1175
+ transform: [
1176
+ {
1177
+ translateY: this.transformValue,
1178
+ },
1179
+ ],
1180
+ maxHeight: this.state.deviceHeight,
1181
+ },
1182
+ ]}
1183
+ onLayout={this._showModal}
1184
+ >
1185
+ <Animated.View
1186
+ style={{
1187
+ maxHeight: this.state.deviceHeight,
1188
+ transform: [
1189
+ {
1190
+ translateY: this.indicatorTranslateY,
1191
+ },
1192
+ ],
1193
+ marginTop: this.state.paddingTop,
1194
+ }}
1195
+ >
1196
+ {gestureEnabled || headerAlwaysVisible ? (
1197
+ CustomHeaderComponent ? (
1198
+ CustomHeaderComponent
1199
+ ) : (
1200
+ <Animated.View
1201
+ style={[styles.indicator, {backgroundColor: indicatorColor}]}
1202
+ />
1203
+ )
1204
+ ) : null}
1205
+
1206
+ {children}
1207
+ </Animated.View>
1208
+ </Animated.View>
1209
+ </View>
1210
+ )}
1211
+ scrollEnabled={scrollable && !keyboard}
1212
+ scrollEventThrottle={5}
1213
+ scrollsToTop={false}
1214
+ showsVerticalScrollIndicator={false}
1215
+ style={[
1216
+ styles.scrollView,
1217
+ {
1218
+ width: this.state.deviceWidth,
1219
+ },
1220
+ ]}
1221
+ onMomentumScrollBegin={this._onScrollBegin}
1222
+ onMomentumScrollEnd={this._onScrollEnd}
1223
+ onScroll={this._onScroll}
1224
+ onScrollBeginDrag={this._onScrollBeginDrag}
1225
+ onTouchEnd={this._onTouchEnd}
1226
+ />
1227
+ </Animated.View>
1228
+ </Modal>
1229
+ );
1230
+ }
1231
+ }