react-native-timer-picker 1.10.2 → 2.0.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.
Files changed (56) hide show
  1. package/README.md +82 -53
  2. package/dist/commonjs/components/DurationScroll/index.js +154 -76
  3. package/dist/commonjs/components/DurationScroll/index.js.map +1 -1
  4. package/dist/commonjs/components/DurationScroll/types.js.map +1 -1
  5. package/dist/commonjs/components/TimerPicker/index.js +31 -10
  6. package/dist/commonjs/components/TimerPicker/index.js.map +1 -1
  7. package/dist/commonjs/components/TimerPicker/types.js.map +1 -1
  8. package/dist/commonjs/tests/DurationScroll.test.js +3 -3
  9. package/dist/commonjs/tests/DurationScroll.test.js.map +1 -1
  10. package/dist/commonjs/tests/Modal.test.js +3 -3
  11. package/dist/commonjs/tests/Modal.test.js.map +1 -1
  12. package/dist/commonjs/tests/TimerPicker.test.js +4 -4
  13. package/dist/commonjs/tests/TimerPicker.test.js.map +1 -1
  14. package/dist/commonjs/tests/TimerPickerModal.test.js +6 -6
  15. package/dist/commonjs/tests/TimerPickerModal.test.js.map +1 -1
  16. package/dist/commonjs/utils/generateNumbers.js +7 -6
  17. package/dist/commonjs/utils/generateNumbers.js.map +1 -1
  18. package/dist/commonjs/utils/getAdjustedLimit.js +5 -4
  19. package/dist/commonjs/utils/getAdjustedLimit.js.map +1 -1
  20. package/dist/commonjs/utils/getDurationAndIndexFromScrollOffset.js +24 -0
  21. package/dist/commonjs/utils/getDurationAndIndexFromScrollOffset.js.map +1 -0
  22. package/dist/commonjs/utils/getInitialScrollIndex.js +19 -0
  23. package/dist/commonjs/utils/getInitialScrollIndex.js.map +1 -0
  24. package/dist/module/components/DurationScroll/index.js +154 -76
  25. package/dist/module/components/DurationScroll/index.js.map +1 -1
  26. package/dist/module/components/DurationScroll/types.js.map +1 -1
  27. package/dist/module/components/TimerPicker/index.js +31 -10
  28. package/dist/module/components/TimerPicker/index.js.map +1 -1
  29. package/dist/module/components/TimerPicker/types.js.map +1 -1
  30. package/dist/module/tests/DurationScroll.test.js +3 -3
  31. package/dist/module/tests/DurationScroll.test.js.map +1 -1
  32. package/dist/module/tests/Modal.test.js +3 -3
  33. package/dist/module/tests/Modal.test.js.map +1 -1
  34. package/dist/module/tests/TimerPicker.test.js +4 -4
  35. package/dist/module/tests/TimerPicker.test.js.map +1 -1
  36. package/dist/module/tests/TimerPickerModal.test.js +6 -6
  37. package/dist/module/tests/TimerPickerModal.test.js.map +1 -1
  38. package/dist/module/utils/generateNumbers.js +7 -6
  39. package/dist/module/utils/generateNumbers.js.map +1 -1
  40. package/dist/module/utils/getAdjustedLimit.js +5 -4
  41. package/dist/module/utils/getAdjustedLimit.js.map +1 -1
  42. package/dist/module/utils/getDurationAndIndexFromScrollOffset.js +17 -0
  43. package/dist/module/utils/getDurationAndIndexFromScrollOffset.js.map +1 -0
  44. package/dist/module/utils/getInitialScrollIndex.js +12 -0
  45. package/dist/module/utils/getInitialScrollIndex.js.map +1 -0
  46. package/dist/typescript/components/DurationScroll/types.d.ts +4 -1
  47. package/dist/typescript/components/TimerPicker/types.d.ts +7 -0
  48. package/dist/typescript/utils/generateNumbers.d.ts +3 -1
  49. package/dist/typescript/utils/getAdjustedLimit.d.ts +1 -1
  50. package/dist/typescript/utils/getDurationAndIndexFromScrollOffset.d.ts +11 -0
  51. package/dist/typescript/utils/{getScrollIndex.d.ts → getInitialScrollIndex.d.ts} +3 -1
  52. package/package.json +14 -10
  53. package/dist/commonjs/utils/getScrollIndex.js +0 -17
  54. package/dist/commonjs/utils/getScrollIndex.js.map +0 -1
  55. package/dist/module/utils/getScrollIndex.js +0 -10
  56. package/dist/module/utils/getScrollIndex.js.map +0 -1
package/README.md CHANGED
@@ -29,6 +29,7 @@ Includes iOS-style haptic and audio feedback 🍏
29
29
  - [Custom Styles 👗](#custom-styles-)
30
30
  - [Performance](#performance)
31
31
  - [Custom FlatList](#custom-flatlist)
32
+ - [Generic feedback](#generic-feedback)
32
33
  - [TimerPickerModal ⏰](#timerpickermodal-)
33
34
  - [Custom Styles 👕](#custom-styles--1)
34
35
  - [Methods 🔄](#methods-)
@@ -72,15 +73,17 @@ If you want the numbers to fade in/out at the top and bottom of the picker, you
72
73
 
73
74
  ### Haptic Feedback
74
75
 
75
- This is currently only supported on Expo with the [expo-haptics](https://www.npmjs.com/package/expo-haptics) module:
76
+ Enable haptic feedback with the [expo-haptics](https://www.npmjs.com/package/expo-haptics) module:
76
77
 
77
78
  `import * as Haptics from "expo-haptics";`
78
79
 
79
80
  **To enable haptic feedback, you need to supply the imported `Haptics` namespace as a prop to either TimerPickerModal or TimerPicker.**
80
81
 
82
+ [Generic feedback](#generic-feedback) support is possible with the `pickerFeeback` prop.
83
+
81
84
  ### Audio Feedback (Click Sound)
82
85
 
83
- This is currently only supported on Expo with the [expo-av](https://www.npmjs.com/package/expo-av) module:
86
+ Enable audio feedback with the [expo-av](https://www.npmjs.com/package/expo-av) module:
84
87
 
85
88
  `import { Audio } from "expo-av";`
86
89
 
@@ -89,6 +92,8 @@ This is currently only supported on Expo with the [expo-av](https://www.npmjs.co
89
92
  Please note that the default click sound uses a hosted mp3 file. To make the click sound work offline, you need to supply your own
90
93
  sound asset through the `clickSoundAsset` prop. You can download the default click sound [here](https://drive.google.com/uc?export=download&id=10e1YkbNsRh-vGx1jmS1Nntz8xzkBp4_I).
91
94
 
95
+ [Generic feedback](#generic-feedback) support is possible with the `pickerFeeback` prop.
96
+
92
97
  <br>
93
98
 
94
99
  ## Installation 🚀
@@ -424,45 +429,52 @@ return (
424
429
 
425
430
  ### TimerPicker ⏲️
426
431
 
427
- | Prop | Description | Type | Default | Required |
428
- | :------------------------------: | :------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----: | :------: |
429
- | onDurationChange | Callback when the duration changes | `(duration: { hours: number, minutes: number, seconds: number }) => void` | - | false |
430
- | initialValue | Initial value for the picker | `{ hours?: number, minutes?: number, seconds?: number }` | - | false |
431
- | hideHours | Hide the hours picker | Boolean | false | false |
432
- | hideMinutes | Hide the minutes picker | Boolean | false | false |
433
- | hideSeconds | Hide the seconds picker | Boolean | false | false |
434
- | hoursPickerIsDisabled | Disable the hours picker picker | Boolean | false | false |
435
- | minutesPickerIsDisabled | Disable the minutes picker picker | Boolean | false | false |
436
- | secondsPickerIsDisabled | Disable the seconds picker picker | Boolean | false | false |
437
- | hourLimit | Limit on the hours it is possible to select | `{ max?: Number, min?: Number }` | - | false |
438
- | minuteLimit | Limit on the minutes it is possible to select | `{ max?: Number, min?: Number }` | - | false |
439
- | secondLimit | Limit on the seconds it is possible to select | `{ max?: Number, min?: Number }` | - | false |
440
- | hourLabel | Label for the hours picker | String \| React.ReactElement | h | false |
441
- | minuteLabel | Label for the minutes picker | String \| React.ReactElement | m | false |
442
- | secondLabel | Label for the seconds picker | String \| React.ReactElement | s | false |
443
- | padHoursWithZero | Pad single-digit hours in the picker with a zero | Boolean | false | false |
444
- | padMinutesWithZero | Pad single-digit minutes in the picker with a zero | Boolean | true | false |
445
- | padSecondsWithZero | Pad single-digit seconds in the picker with a zero | Boolean | true | false |
446
- | padWithNItems | Number of items to pad the picker with on either side | Number | 1 | false |
447
- | aggressivelyGetLatestDuration | Set to True to ask DurationScroll to aggressively update the latestDuration ref | Boolean | false | false |
448
- | allowFontScaling | Allow font in the picker to scale with accessibility settings | Boolean | false | false |
449
- | use12HourPicker | Switch the hour picker to 12-hour format with an AM / PM label | Boolean | false | false |
450
- | amLabel | Set the AM label if using the 12-hour picker | String | am | false |
451
- | pmLabel | Set the PM label if using the 12-hour picker | String | pm | false |
452
- | repeatHourNumbersNTimes | Set the number of times the list of hours is repeated in the picker | Number | 6 | false |
453
- | repeatMinuteNumbersNTimes | Set the number of times the list of minutes is repeated in the picker | Number | 3 | false |
454
- | repeatSecondNumbersNTimes | Set the number of times the list of seconds is repeated in the picker | Number | 3 | false |
455
- | disableInfiniteScroll | Disable the infinite scroll feature | Boolean | false | false |
456
- | LinearGradient | Linear Gradient Component | [expo-linear-gradient](https://www.npmjs.com/package/expo-linear-gradient).LinearGradient or [react-native-linear-gradient](https://www.npmjs.com/package/react-native-linear-gradient).default | - | false |
457
- | Haptics | Haptics Namespace (required for Haptic feedback) | [expo-haptics](https://www.npmjs.com/package/expo-haptics) | - | false |
458
- | Audio | Audio Class (required for audio feedback i.e. click sound) | [expo-av](https://www.npmjs.com/package/expo-av).Audio | - | false |
459
- | FlatList | FlatList component used internally to implement each picker (hour, minutes and seconds). More info [below](#custom-flatlist) | [react-native](https://reactnative.dev/docs/flatlist).FlatList | `FlatList` from `react-native` | false |
460
- | clickSoundAsset | Custom sound asset for click sound (required for offline click sound - download default [here](https://drive.google.com/uc?export=download&id=10e1YkbNsRh-vGx1jmS1Nntz8xzkBp4_I)) | require(.../somefolderpath) or {uri: www.someurl} | - | false |
461
- | pickerContainerProps | Props for the picker container | `React.ComponentProps<typeof View>` | - | false |
462
- | pickerGradientOverlayProps | Props for both gradient overlays | `Partial<LinearGradientProps>` | - | false |
463
- | topPickerGradientOverlayProps | Props for the top gradient overlay | `Partial<LinearGradientProps>` | - | false |
464
- | bottomPickerGradientOverlayProps | Props for the bottom gradient overlay | `Partial<LinearGradientProps>` | - | false |
465
- | styles | Custom styles for the timer picker | [CustomTimerPickerStyles](#custom-styles-) | - | false |
432
+ | Prop | Description | Type | Default | Required |
433
+ | :------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------: | :------: |
434
+ | onDurationChange | Callback when the duration changes | `(duration: { hours: number, minutes: number, seconds: number }) => void` | - | false |
435
+ | initialValue | Initial value for the picker | `{ hours?: number, minutes?: number, seconds?: number }` | - | false |
436
+ | hideHours | Hide the hours picker | Boolean | false | false |
437
+ | hideMinutes | Hide the minutes picker | Boolean | false | false |
438
+ | hideSeconds | Hide the seconds picker | Boolean | false | false |
439
+ | hoursPickerIsDisabled | Disable the hours picker picker | Boolean | false | false |
440
+ | minutesPickerIsDisabled | Disable the minutes picker picker | Boolean | false | false |
441
+ | secondsPickerIsDisabled | Disable the seconds picker picker | Boolean | false | false |
442
+ | hourLimit | Limit on the hours it is possible to select | `{ max?: Number, min?: Number }` | - | false |
443
+ | minuteLimit | Limit on the minutes it is possible to select | `{ max?: Number, min?: Number }` | - | false |
444
+ | secondLimit | Limit on the seconds it is possible to select | `{ max?: Number, min?: Number }` | - | false |
445
+ | maximumHours | The highest value on the hours picker | Number | 23 | false |
446
+ | maximumMinutes | The highest value on the minutes picker | Number | 59 | false |
447
+ | maximumSeconds | The highest value on the seconds picker | Number | 59 | false |
448
+ | hourInterval | The interval between values on the hours picker | Number | 1 | false |
449
+ | minuteInterval | The interval between values on the minutes picker | Number | 1 | false |
450
+ | secondInterval | The interval between values on the seconds picker | Number | 1 | false |
451
+ | hourLabel | Label for the hours picker | String \| React.ReactElement | h | false |
452
+ | minuteLabel | Label for the minutes picker | String \| React.ReactElement | m | false |
453
+ | secondLabel | Label for the seconds picker | String \| React.ReactElement | s | false |
454
+ | padHoursWithZero | Pad single-digit hours in the picker with a zero | Boolean | false | false |
455
+ | padMinutesWithZero | Pad single-digit minutes in the picker with a zero | Boolean | true | false |
456
+ | padSecondsWithZero | Pad single-digit seconds in the picker with a zero | Boolean | true | false |
457
+ | padWithNItems | Number of items to pad the picker with on either side | Number | 1 | false |
458
+ | aggressivelyGetLatestDuration | Set to True to ask DurationScroll to aggressively update the latestDuration ref | Boolean | false | false |
459
+ | allowFontScaling | Allow font in the picker to scale with accessibility settings | Boolean | false | false |
460
+ | use12HourPicker | Switch the hour picker to 12-hour format with an AM / PM label | Boolean | false | false |
461
+ | amLabel | Set the AM label if using the 12-hour picker | String | am | false |
462
+ | pmLabel | Set the PM label if using the 12-hour picker | String | pm | false |
463
+ | repeatHourNumbersNTimes | Set the number of times the list of hours is repeated in the picker | Number | 7 | false |
464
+ | repeatMinuteNumbersNTimes | Set the number of times the list of minutes is repeated in the picker | Number | 3 | false |
465
+ | repeatSecondNumbersNTimes | Set the number of times the list of seconds is repeated in the picker | Number | 3 | false |
466
+ | disableInfiniteScroll | Disable the infinite scroll feature | Boolean | false | false |
467
+ | LinearGradient | Linear Gradient Component | [expo-linear-gradient](https://www.npmjs.com/package/expo-linear-gradient).LinearGradient or [react-native-linear-gradient](https://www.npmjs.com/package/react-native-linear-gradient).default | - | false |
468
+ | Haptics | Haptics Namespace (required for Haptic feedback) | [expo-haptics](https://www.npmjs.com/package/expo-haptics) | - | false |
469
+ | Audio | Audio Class (required for audio feedback i.e. click sound) | [expo-av](https://www.npmjs.com/package/expo-av).Audio | - | false |
470
+ | pickerFeedback | Generic picker feedback as alternative to the below Expo feedback support | `() => void \| Promise<void> ` | - | false |
471
+ | FlatList | FlatList component used internally to implement each picker (hour, minutes and seconds). More info [below](#custom-flatlist) | [react-native](https://reactnative.dev/docs/flatlist).FlatList | `FlatList` from `react-native` | false |
472
+ | clickSoundAsset | Custom sound asset for click sound (required for offline click sound - download default [here](https://drive.google.com/uc?export=download&id=10e1YkbNsRh-vGx1jmS1Nntz8xzkBp4_I)) | require(.../somefolderpath) or {uri: www.someurl} | - | false |
473
+ | pickerContainerProps | Props for the picker container | `React.ComponentProps<typeof View>` | - | false |
474
+ | pickerGradientOverlayProps | Props for both gradient overlays | `Partial<LinearGradientProps>` | - | false |
475
+ | topPickerGradientOverlayProps | Props for the top gradient overlay | `Partial<LinearGradientProps>` | - | false |
476
+ | bottomPickerGradientOverlayProps | Props for the bottom gradient overlay | `Partial<LinearGradientProps>` | - | false |
477
+ | styles | Custom styles for the timer picker | [CustomTimerPickerStyles](#custom-styles-) | - | false |
466
478
 
467
479
  #### Custom Styles 👗
468
480
 
@@ -486,16 +498,14 @@ The following custom styles can be supplied to re-style the component in any way
486
498
 
487
499
  Note the minor limitations to the allowed styles for `pickerContainer` and `pickerItemContainer`. These are made because these styles are used for internal calculations and all possible `backgroundColor`/`height` types are not supported.
488
500
 
489
-
490
501
  #### Performance
491
502
 
492
503
  When the `disableInfiniteScroll` prop is not set, the picker gives the appearance of an infinitely scrolling picker by auto-scrolling forward/back when you near the start/end of the list. When the picker auto-scrolls, a momentary flicker is visible if you are scrolling very slowly.
493
504
 
494
- To mitigate for this, you can modify the `repeatHourNumbersNTimes`, `repeatMinuteNumbersNTimes` and `repeatSecondNumbersNTimes` props. These set the number of times the list of numbers in each picker is repeated. These have a performance trade-off: higher values mean the picker has to auto-scroll less to maintain the infinite scroll, but has to render a longer list of numbers. By default, the props are set to 6, 3 and 3, respectively, which balances that trade-off effectively.
505
+ To mitigate for this, you can modify the `repeatHourNumbersNTimes`, `repeatMinuteNumbersNTimes` and `repeatSecondNumbersNTimes` props. These set the number of times the list of numbers in each picker is repeated. These have a performance trade-off: higher values mean the picker has to auto-scroll less to maintain the infinite scroll, but has to render a longer list of numbers. By default, the props are set to 7, 3 and 3, respectively, which balances that trade-off effectively.
495
506
 
496
507
  Note that you can avoid the auto-scroll flickering entirely by disabling infinite scroll. You could then set the above props to high values, so that a user has to scroll far down/up the list to reach the end of the list.
497
508
 
498
-
499
509
  #### Custom FlatList
500
510
 
501
511
  The library offers the ability to provide a custom component for the `<FlatList />`, instead of the default React Native component. This allows for more flexibility and integration with libraries like [react-native-gesture-handler](react-native-gesture-handler) or other components built on top of it, like [https://ui.gorhom.dev/components/bottom-sheet](https://ui.gorhom.dev/components/bottom-sheet).
@@ -518,6 +528,23 @@ import { TimerPicker } from "react-native-timer-picker";
518
528
  **Important**:
519
529
  The custom component needs to have the same interface as React Native's `<FlatList />` in order for it to work as expected. A complete reference of the current usage can be found [here](/src/components/DurationScroll/index.tsx)
520
530
 
531
+ #### Generic feedback
532
+
533
+ To enable haptic feedback from the non-Expo module [react-native-haptic-feedback](https://github.com/mkuczera/react-native-haptic-feedback) or provide feedback in any other form you can use the generic feedback callback prop `pickerFeedback`. This function is called whenever any of the pickers tick onto a new number.
534
+
535
+ ```Jsx
536
+ import { trigger } from 'react-native-haptic-feedback';
537
+ import { TimerPicker } from "react-native-timer-picker";
538
+
539
+ // ...
540
+
541
+ <TimerPicker
542
+ {...props}
543
+ pickerFeedback={() => trigger('impactLight')}
544
+ />
545
+
546
+ ```
547
+
521
548
  ### TimerPickerModal ⏰
522
549
 
523
550
  The TimerPickerModal component accepts all [TimerPicker props](#timerpicker-️), and the below additional props.
@@ -609,16 +636,12 @@ Contributions to this project are more than welcome.
609
636
  To get this project running locally:
610
637
 
611
638
  1. Clone the Git repo.
612
- 2. Run `yarn` to install the base dependencies
613
- 3. Run `yarn setup` from the project root (this installs the example's additional dependencies)
614
- 4. Run `yarn start` to start the example in Expo Go.
615
- 5. Start adding cool stuff! Your changes should be immediately reflected in the Expo Go app.
639
+ 2. Run `yarn setup` from the project root (this installs the project dependencies and the examples' additional dependencies)
616
640
 
617
- You can also run the library in bare React Native:
618
- 1. Clone the Git repo.
619
- 2. Run `yarn` to install the base dependencies
620
- 3. Run `yarn setup-dev`.
621
- 4. Run `yarn start-bare:android` or `start-bare:ios` to start the project on an emulator/device.
641
+ You can then start either the Expo example or the bare React Native example:
642
+
643
+ - For Expo, run `yarn start` to start the Expo example in Expo Go.
644
+ - For bare React Native, run `yarn start-bare:android` or `start-bare:ios` to start the project on an emulator/device.
622
645
 
623
646
  ### GitHub Guidelines
624
647
 
@@ -630,6 +653,12 @@ There are two permenant branches: `main` and `develop`. You should never work di
630
653
 
631
654
  <br>
632
655
 
656
+ ## Limitations ⚠
657
+
658
+ The project is not compatibile with React Native versions prior to `v0.72.0` due to this [React Native issue](https://github.com/facebook/react-native/issues/36329).
659
+
660
+ <br>
661
+
633
662
  ## License 📝
634
663
 
635
664
  This project is licensed under the [MIT License](LICENSE).
@@ -9,7 +9,8 @@ var _reactNative = require("react-native");
9
9
  var _colorToRgba = require("../../utils/colorToRgba");
10
10
  var _generateNumbers = require("../../utils/generateNumbers");
11
11
  var _getAdjustedLimit = require("../../utils/getAdjustedLimit");
12
- var _getScrollIndex = require("../../utils/getScrollIndex");
12
+ var _getDurationAndIndexFromScrollOffset = require("../../utils/getDurationAndIndexFromScrollOffset");
13
+ var _getInitialScrollIndex = require("../../utils/getInitialScrollIndex");
13
14
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
14
15
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
15
16
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
@@ -25,45 +26,78 @@ const DurationScroll = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
25
26
  FlatList = _reactNative.FlatList,
26
27
  Haptics,
27
28
  initialValue = 0,
29
+ interval,
28
30
  is12HourPicker,
29
31
  isDisabled,
30
32
  label,
31
33
  limit,
32
34
  LinearGradient,
33
- numberOfItems,
35
+ maximumValue,
34
36
  onDurationChange,
35
37
  padNumbersWithZero = false,
36
38
  padWithNItems,
39
+ pickerFeedback,
37
40
  pickerGradientOverlayProps,
38
41
  pmLabel,
39
42
  repeatNumbersNTimes = 3,
43
+ repeatNumbersNTimesNotExplicitlySet,
40
44
  styles,
41
45
  testID,
42
46
  topPickerGradientOverlayProps
43
47
  } = props;
44
- const data = (0, _react.useMemo)(() => {
48
+ const numberOfItems = (0, _react.useMemo)(() => {
49
+ // guard against negative maximum values
50
+ if (maximumValue < 0) {
51
+ return 1;
52
+ }
53
+ return Math.floor(maximumValue / interval) + 1;
54
+ }, [interval, maximumValue]);
55
+ const safeRepeatNumbersNTimes = (0, _react.useMemo)(() => {
56
+ // do not repeat numbers if there is only one option
57
+ if (numberOfItems === 1) {
58
+ return 1;
59
+ }
60
+ if (!disableInfiniteScroll && repeatNumbersNTimes < 2) {
61
+ return 2;
62
+ } else if (repeatNumbersNTimes < 1) {
63
+ return 1;
64
+ }
65
+
66
+ // if this variable is not explicitly set, we calculate a reasonable value based on
67
+ // the number of items in the picker, avoiding regular jumps up/down the list
68
+ // whilst avoiding rendering too many items in the picker
69
+ if (repeatNumbersNTimesNotExplicitlySet) {
70
+ return Math.max(Math.round(180 / numberOfItems), 1);
71
+ }
72
+ return Math.round(repeatNumbersNTimes);
73
+ }, [disableInfiniteScroll, numberOfItems, repeatNumbersNTimes, repeatNumbersNTimesNotExplicitlySet]);
74
+ const numbersForFlatList = (0, _react.useMemo)(() => {
45
75
  if (is12HourPicker) {
46
76
  return (0, _generateNumbers.generate12HourNumbers)({
47
77
  padNumbersWithZero,
48
- repeatNTimes: repeatNumbersNTimes,
78
+ repeatNTimes: safeRepeatNumbersNTimes,
49
79
  disableInfiniteScroll,
50
- padWithNItems
80
+ padWithNItems,
81
+ interval
51
82
  });
52
83
  }
53
84
  return (0, _generateNumbers.generateNumbers)(numberOfItems, {
54
85
  padNumbersWithZero,
55
- repeatNTimes: repeatNumbersNTimes,
86
+ repeatNTimes: safeRepeatNumbersNTimes,
56
87
  disableInfiniteScroll,
57
- padWithNItems
88
+ padWithNItems,
89
+ interval
58
90
  });
59
- }, [disableInfiniteScroll, is12HourPicker, numberOfItems, padNumbersWithZero, padWithNItems, repeatNumbersNTimes]);
60
- const initialScrollIndex = (0, _react.useMemo)(() => (0, _getScrollIndex.getScrollIndex)({
91
+ }, [disableInfiniteScroll, is12HourPicker, interval, numberOfItems, padNumbersWithZero, padWithNItems, safeRepeatNumbersNTimes]);
92
+ const initialScrollIndex = (0, _react.useMemo)(() => (0, _getInitialScrollIndex.getInitialScrollIndex)({
93
+ disableInfiniteScroll,
94
+ interval,
61
95
  numberOfItems,
62
96
  padWithNItems,
63
- repeatNumbersNTimes,
97
+ repeatNumbersNTimes: safeRepeatNumbersNTimes,
64
98
  value: initialValue
65
- }), [initialValue, numberOfItems, padWithNItems, repeatNumbersNTimes]);
66
- const adjustedLimited = (0, _react.useMemo)(() => (0, _getAdjustedLimit.getAdjustedLimit)(limit, numberOfItems), [limit, numberOfItems]);
99
+ }), [disableInfiniteScroll, initialValue, interval, numberOfItems, padWithNItems, safeRepeatNumbersNTimes]);
100
+ const adjustedLimited = (0, _react.useMemo)(() => (0, _getAdjustedLimit.getAdjustedLimit)(limit, numberOfItems, interval), [interval, limit, numberOfItems]);
67
101
  const numberOfItemsToShow = 1 + padWithNItems * 2;
68
102
 
69
103
  // keep track of the latest duration as it scrolls
@@ -97,28 +131,6 @@ const DurationScroll = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
97
131
  };
98
132
  // eslint-disable-next-line react-hooks/exhaustive-deps
99
133
  }, [Audio]);
100
- (0, _react.useImperativeHandle)(ref, () => ({
101
- reset: options => {
102
- var _flatListRef$current;
103
- (_flatListRef$current = flatListRef.current) === null || _flatListRef$current === void 0 || _flatListRef$current.scrollToIndex({
104
- animated: (options === null || options === void 0 ? void 0 : options.animated) ?? false,
105
- index: initialScrollIndex
106
- });
107
- },
108
- setValue: (value, options) => {
109
- var _flatListRef$current2;
110
- (_flatListRef$current2 = flatListRef.current) === null || _flatListRef$current2 === void 0 || _flatListRef$current2.scrollToIndex({
111
- animated: (options === null || options === void 0 ? void 0 : options.animated) ?? false,
112
- index: (0, _getScrollIndex.getScrollIndex)({
113
- numberOfItems,
114
- padWithNItems,
115
- repeatNumbersNTimes,
116
- value: value
117
- })
118
- });
119
- },
120
- latestDuration: latestDuration
121
- }));
122
134
  const renderItem = (0, _react.useCallback)(({
123
135
  item
124
136
  }) => {
@@ -151,23 +163,29 @@ const DurationScroll = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
151
163
  // this function is only used when the picker is in a modal and/or has Haptic/Audio feedback
152
164
  // it is used to ensure that the modal gets the latest duration on clicking
153
165
  // the confirm button, even if the scrollview is still scrolling
154
- if (!aggressivelyGetLatestDuration && !Haptics && !Audio) {
166
+ if (!aggressivelyGetLatestDuration && !Haptics && !Audio && !pickerFeedback) {
155
167
  return;
156
168
  }
157
169
  if (aggressivelyGetLatestDuration) {
158
- const newIndex = Math.round(e.nativeEvent.contentOffset.y / styles.pickerItemContainer.height);
159
- let newDuration = (disableInfiniteScroll ? newIndex : newIndex + padWithNItems) % (numberOfItems + 1);
160
- if (newDuration !== latestDuration.current) {
170
+ const newValues = (0, _getDurationAndIndexFromScrollOffset.getDurationAndIndexFromScrollOffset)({
171
+ disableInfiniteScroll,
172
+ interval,
173
+ itemHeight: styles.pickerItemContainer.height,
174
+ numberOfItems,
175
+ padWithNItems,
176
+ yContentOffset: e.nativeEvent.contentOffset.y
177
+ });
178
+ if (newValues.duration !== latestDuration.current) {
161
179
  // check limits
162
- if (newDuration > adjustedLimited.max) {
163
- newDuration = adjustedLimited.max;
164
- } else if (newDuration < adjustedLimited.min) {
165
- newDuration = adjustedLimited.min;
180
+ if (newValues.duration > adjustedLimited.max) {
181
+ newValues.duration = adjustedLimited.max;
182
+ } else if (newValues.duration < adjustedLimited.min) {
183
+ newValues.duration = adjustedLimited.min;
166
184
  }
167
- latestDuration.current = newDuration;
185
+ latestDuration.current = newValues.duration;
168
186
  }
169
187
  }
170
- if (Haptics || Audio) {
188
+ if (Haptics || Audio || pickerFeedback) {
171
189
  const feedbackIndex = Math.round((e.nativeEvent.contentOffset.y + styles.pickerItemContainer.height / 2) / styles.pickerItemContainer.height);
172
190
  if (feedbackIndex !== lastFeedbackIndex.current) {
173
191
  // this check stops the feedback firing when the component mounts
@@ -185,70 +203,129 @@ const DurationScroll = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
185
203
  } catch {
186
204
  // do nothing
187
205
  }
206
+
207
+ // fire custom feedback if available
208
+ try {
209
+ pickerFeedback === null || pickerFeedback === void 0 || pickerFeedback();
210
+ } catch {
211
+ // do nothing
212
+ }
188
213
  }
189
214
  lastFeedbackIndex.current = feedbackIndex;
190
215
  }
191
216
  }
192
217
  },
193
218
  // eslint-disable-next-line react-hooks/exhaustive-deps
194
- [adjustedLimited.max, adjustedLimited.min, aggressivelyGetLatestDuration, clickSound, disableInfiniteScroll, numberOfItems, padWithNItems, styles.pickerItemContainer.height]);
219
+ [adjustedLimited.max, adjustedLimited.min, aggressivelyGetLatestDuration, clickSound, disableInfiniteScroll, interval, numberOfItems, padWithNItems, styles.pickerItemContainer.height]);
195
220
  const onMomentumScrollEnd = (0, _react.useCallback)(e => {
196
- const newIndex = Math.round(e.nativeEvent.contentOffset.y / styles.pickerItemContainer.height);
197
- let newDuration = (disableInfiniteScroll ? newIndex : newIndex + padWithNItems) % (numberOfItems + 1);
221
+ const newValues = (0, _getDurationAndIndexFromScrollOffset.getDurationAndIndexFromScrollOffset)({
222
+ disableInfiniteScroll,
223
+ interval,
224
+ itemHeight: styles.pickerItemContainer.height,
225
+ numberOfItems,
226
+ padWithNItems,
227
+ yContentOffset: e.nativeEvent.contentOffset.y
228
+ });
198
229
 
199
230
  // check limits
200
- if (newDuration > adjustedLimited.max) {
201
- var _flatListRef$current3;
202
- const targetScrollIndex = newIndex - (newDuration - adjustedLimited.max);
203
- (_flatListRef$current3 = flatListRef.current) === null || _flatListRef$current3 === void 0 || _flatListRef$current3.scrollToIndex({
231
+ if (newValues.duration > adjustedLimited.max) {
232
+ var _flatListRef$current;
233
+ const targetScrollIndex = newValues.index - (newValues.duration - adjustedLimited.max);
234
+ (_flatListRef$current = flatListRef.current) === null || _flatListRef$current === void 0 || _flatListRef$current.scrollToIndex({
204
235
  animated: true,
205
236
  index:
206
237
  // guard against scrolling beyond end of list
207
238
  targetScrollIndex >= 0 ? targetScrollIndex : adjustedLimited.max - 1
208
239
  }); // scroll down to max
209
- newDuration = adjustedLimited.max;
210
- } else if (newDuration < adjustedLimited.min) {
211
- var _flatListRef$current4;
212
- const targetScrollIndex = newIndex + (adjustedLimited.min - newDuration);
213
- (_flatListRef$current4 = flatListRef.current) === null || _flatListRef$current4 === void 0 || _flatListRef$current4.scrollToIndex({
240
+ newValues.duration = adjustedLimited.max;
241
+ } else if (newValues.duration < adjustedLimited.min) {
242
+ var _flatListRef$current2;
243
+ const targetScrollIndex = newValues.index + (adjustedLimited.min - newValues.duration);
244
+ (_flatListRef$current2 = flatListRef.current) === null || _flatListRef$current2 === void 0 || _flatListRef$current2.scrollToIndex({
214
245
  animated: true,
215
246
  index:
216
247
  // guard against scrolling beyond end of list
217
- targetScrollIndex <= data.length - 1 ? targetScrollIndex : adjustedLimited.min
248
+ targetScrollIndex <= numbersForFlatList.length - 1 ? targetScrollIndex : adjustedLimited.min
218
249
  }); // scroll up to min
219
- newDuration = adjustedLimited.min;
250
+ newValues.duration = adjustedLimited.min;
220
251
  }
221
- onDurationChange(newDuration);
222
- }, [adjustedLimited.max, adjustedLimited.min, data.length, disableInfiniteScroll, numberOfItems, onDurationChange, padWithNItems, styles.pickerItemContainer.height]);
252
+ onDurationChange(newValues.duration);
253
+ }, [disableInfiniteScroll, interval, styles.pickerItemContainer.height, numberOfItems, padWithNItems, adjustedLimited.max, adjustedLimited.min, onDurationChange, numbersForFlatList.length]);
223
254
  const onViewableItemsChanged = (0, _react.useCallback)(({
224
255
  viewableItems
225
256
  }) => {
226
257
  var _viewableItems$, _viewableItems$2;
258
+ if (numberOfItems === 1) {
259
+ return;
260
+ }
227
261
  if ((_viewableItems$ = viewableItems[0]) !== null && _viewableItems$ !== void 0 && _viewableItems$.index && viewableItems[0].index < numberOfItems * 0.5) {
228
- var _flatListRef$current5;
229
- (_flatListRef$current5 = flatListRef.current) === null || _flatListRef$current5 === void 0 || _flatListRef$current5.scrollToIndex({
262
+ var _flatListRef$current3;
263
+ (_flatListRef$current3 = flatListRef.current) === null || _flatListRef$current3 === void 0 || _flatListRef$current3.scrollToIndex({
230
264
  animated: false,
231
265
  index: viewableItems[0].index + numberOfItems
232
266
  });
233
- } else if ((_viewableItems$2 = viewableItems[0]) !== null && _viewableItems$2 !== void 0 && _viewableItems$2.index && viewableItems[0].index >= numberOfItems * (repeatNumbersNTimes - 0.5)) {
234
- var _flatListRef$current6;
235
- (_flatListRef$current6 = flatListRef.current) === null || _flatListRef$current6 === void 0 || _flatListRef$current6.scrollToIndex({
267
+ } else if ((_viewableItems$2 = viewableItems[0]) !== null && _viewableItems$2 !== void 0 && _viewableItems$2.index && viewableItems[0].index >= numberOfItems * (safeRepeatNumbersNTimes - 0.5)) {
268
+ var _flatListRef$current4;
269
+ (_flatListRef$current4 = flatListRef.current) === null || _flatListRef$current4 === void 0 || _flatListRef$current4.scrollToIndex({
236
270
  animated: false,
237
- index: viewableItems[0].index - numberOfItems - 1
271
+ index: viewableItems[0].index - numberOfItems
238
272
  });
239
273
  }
240
- }, [numberOfItems, repeatNumbersNTimes]);
274
+ }, [numberOfItems, safeRepeatNumbersNTimes]);
275
+ const [viewabilityConfigCallbackPairs, setViewabilityConfigCallbackPairs] = (0, _react.useState)(!disableInfiniteScroll ? [{
276
+ viewabilityConfig: {
277
+ viewAreaCoveragePercentThreshold: 0
278
+ },
279
+ onViewableItemsChanged: onViewableItemsChanged
280
+ }] : undefined);
281
+ const [flatListRenderKey, setFlatListRenderKey] = (0, _react.useState)(0);
282
+ const initialRender = (0, _react.useRef)(true);
283
+ (0, _react.useEffect)(() => {
284
+ // don't run on first render
285
+ if (initialRender.current) {
286
+ initialRender.current = false;
287
+ return;
288
+ }
289
+
290
+ // if the onViewableItemsChanged callback changes, we need to update viewabilityConfigCallbackPairs
291
+ // which requires the FlatList to be remounted, hence the increase of the FlatList key
292
+ setFlatListRenderKey(prev => prev + 1);
293
+ setViewabilityConfigCallbackPairs(!disableInfiniteScroll ? [{
294
+ viewabilityConfig: {
295
+ viewAreaCoveragePercentThreshold: 0
296
+ },
297
+ onViewableItemsChanged: onViewableItemsChanged
298
+ }] : undefined);
299
+ }, [disableInfiniteScroll, onViewableItemsChanged]);
241
300
  const getItemLayout = (0, _react.useCallback)((_, index) => ({
242
301
  length: styles.pickerItemContainer.height,
243
302
  offset: styles.pickerItemContainer.height * index,
244
303
  index
245
304
  }), [styles.pickerItemContainer.height]);
246
- const viewabilityConfigCallbackPairs = (0, _react.useRef)([{
247
- viewabilityConfig: {
248
- viewAreaCoveragePercentThreshold: 0
305
+ (0, _react.useImperativeHandle)(ref, () => ({
306
+ reset: options => {
307
+ var _flatListRef$current5;
308
+ (_flatListRef$current5 = flatListRef.current) === null || _flatListRef$current5 === void 0 || _flatListRef$current5.scrollToIndex({
309
+ animated: (options === null || options === void 0 ? void 0 : options.animated) ?? false,
310
+ index: initialScrollIndex
311
+ });
249
312
  },
250
- onViewableItemsChanged: onViewableItemsChanged
251
- }]);
313
+ setValue: (value, options) => {
314
+ var _flatListRef$current6;
315
+ (_flatListRef$current6 = flatListRef.current) === null || _flatListRef$current6 === void 0 || _flatListRef$current6.scrollToIndex({
316
+ animated: (options === null || options === void 0 ? void 0 : options.animated) ?? false,
317
+ index: (0, _getInitialScrollIndex.getInitialScrollIndex)({
318
+ disableInfiniteScroll,
319
+ interval,
320
+ numberOfItems,
321
+ padWithNItems,
322
+ repeatNumbersNTimes: safeRepeatNumbersNTimes,
323
+ value: value
324
+ })
325
+ });
326
+ },
327
+ latestDuration: latestDuration
328
+ }));
252
329
  return /*#__PURE__*/_react.default.createElement(_reactNative.View, {
253
330
  pointerEvents: isDisabled ? "none" : undefined,
254
331
  style: [{
@@ -257,8 +334,9 @@ const DurationScroll = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
257
334
  }, isDisabled && styles.disabledPickerContainer],
258
335
  testID: testID
259
336
  }, /*#__PURE__*/_react.default.createElement(FlatList, {
337
+ key: flatListRenderKey,
260
338
  ref: flatListRef,
261
- data: data,
339
+ data: numbersForFlatList,
262
340
  decelerationRate: 0.88,
263
341
  getItemLayout: getItemLayout,
264
342
  initialScrollIndex: initialScrollIndex,
@@ -271,11 +349,11 @@ const DurationScroll = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
271
349
  scrollEventThrottle: 16,
272
350
  showsVerticalScrollIndicator: false,
273
351
  snapToAlignment: "start"
274
- // used in place of snapToOffset due to bug on Android
352
+ // used in place of snapToInterval due to bug on Android
275
353
  ,
276
- snapToOffsets: [...Array(data.length)].map((_, i) => i * styles.pickerItemContainer.height),
354
+ snapToOffsets: [...Array(numbersForFlatList.length)].map((_, i) => i * styles.pickerItemContainer.height),
277
355
  testID: "duration-scroll-flatlist",
278
- viewabilityConfigCallbackPairs: !disableInfiniteScroll ? viewabilityConfigCallbackPairs === null || viewabilityConfigCallbackPairs === void 0 ? void 0 : viewabilityConfigCallbackPairs.current : undefined,
356
+ viewabilityConfigCallbackPairs: viewabilityConfigCallbackPairs,
279
357
  windowSize: numberOfItemsToShow
280
358
  }), /*#__PURE__*/_react.default.createElement(_reactNative.View, {
281
359
  pointerEvents: "none",