react-native-timer-picker 1.6.0 → 1.8.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 (121) hide show
  1. package/README.md +74 -25
  2. package/dist/commonjs/assets/select_click.mp3 +0 -0
  3. package/dist/commonjs/components/{TimerPicker/DurationScroll.js → DurationScroll/index.js} +113 -63
  4. package/dist/commonjs/components/DurationScroll/index.js.map +1 -0
  5. package/dist/commonjs/components/DurationScroll/types.js +6 -0
  6. package/dist/commonjs/components/DurationScroll/types.js.map +1 -0
  7. package/dist/commonjs/components/Modal/index.js +21 -20
  8. package/dist/commonjs/components/Modal/index.js.map +1 -1
  9. package/dist/commonjs/components/Modal/{Modal.styles.js → styles.js} +1 -1
  10. package/dist/commonjs/components/Modal/styles.js.map +1 -0
  11. package/dist/commonjs/components/Modal/types.js +6 -0
  12. package/dist/commonjs/components/Modal/types.js.map +1 -0
  13. package/dist/commonjs/components/TimerPicker/index.js +68 -79
  14. package/dist/commonjs/components/TimerPicker/index.js.map +1 -1
  15. package/dist/commonjs/components/TimerPicker/{TimerPicker.styles.js → styles.js} +1 -3
  16. package/dist/commonjs/components/TimerPicker/styles.js.map +1 -0
  17. package/dist/commonjs/components/TimerPicker/types.js +6 -0
  18. package/dist/commonjs/components/TimerPicker/types.js.map +1 -0
  19. package/dist/commonjs/components/{index.js → TimerPickerModal/index.js} +42 -99
  20. package/dist/commonjs/components/TimerPickerModal/index.js.map +1 -0
  21. package/dist/commonjs/components/{TimerPickerModal.styles.js → TimerPickerModal/styles.js} +1 -3
  22. package/dist/commonjs/components/TimerPickerModal/styles.js.map +1 -0
  23. package/dist/commonjs/components/TimerPickerModal/types.js +6 -0
  24. package/dist/commonjs/components/TimerPickerModal/types.js.map +1 -0
  25. package/dist/commonjs/index.js +14 -13
  26. package/dist/commonjs/index.js.map +1 -1
  27. package/dist/commonjs/tests/DurationScroll.test.js +1 -1
  28. package/dist/commonjs/tests/DurationScroll.test.js.map +1 -1
  29. package/dist/commonjs/tests/Modal.test.js +7 -7
  30. package/dist/commonjs/tests/Modal.test.js.map +1 -1
  31. package/dist/commonjs/tests/TimerPicker.test.js.map +1 -1
  32. package/dist/commonjs/tests/TimerPickerModal.test.js +2 -2
  33. package/dist/commonjs/tests/TimerPickerModal.test.js.map +1 -1
  34. package/dist/commonjs/utils/generateNumbers.js.map +1 -1
  35. package/dist/commonjs/utils/getAdjustedLimit.js.map +1 -1
  36. package/dist/commonjs/utils/getScrollIndex.js +2 -2
  37. package/dist/commonjs/utils/getScrollIndex.js.map +1 -1
  38. package/dist/module/assets/select_click.mp3 +0 -0
  39. package/dist/module/components/{TimerPicker/DurationScroll.js → DurationScroll/index.js} +114 -64
  40. package/dist/module/components/DurationScroll/index.js.map +1 -0
  41. package/dist/module/components/DurationScroll/types.js +2 -0
  42. package/dist/module/components/DurationScroll/types.js.map +1 -0
  43. package/dist/module/components/Modal/index.js +19 -19
  44. package/dist/module/components/Modal/index.js.map +1 -1
  45. package/dist/module/components/Modal/{Modal.styles.js → styles.js} +1 -1
  46. package/dist/module/components/Modal/styles.js.map +1 -0
  47. package/dist/module/components/Modal/types.js +2 -0
  48. package/dist/module/components/Modal/types.js.map +1 -0
  49. package/dist/module/components/TimerPicker/index.js +67 -78
  50. package/dist/module/components/TimerPicker/index.js.map +1 -1
  51. package/dist/module/components/TimerPicker/{TimerPicker.styles.js → styles.js} +1 -2
  52. package/dist/module/components/TimerPicker/styles.js.map +1 -0
  53. package/dist/module/components/TimerPicker/types.js +2 -0
  54. package/dist/module/components/TimerPicker/types.js.map +1 -0
  55. package/dist/module/components/{index.js → TimerPickerModal/index.js} +41 -98
  56. package/dist/module/components/TimerPickerModal/index.js.map +1 -0
  57. package/dist/module/components/{TimerPickerModal.styles.js → TimerPickerModal/styles.js} +1 -2
  58. package/dist/module/components/TimerPickerModal/styles.js.map +1 -0
  59. package/dist/module/components/TimerPickerModal/types.js +2 -0
  60. package/dist/module/components/TimerPickerModal/types.js.map +1 -0
  61. package/dist/module/index.js +6 -4
  62. package/dist/module/index.js.map +1 -1
  63. package/dist/module/tests/DurationScroll.test.js +1 -1
  64. package/dist/module/tests/DurationScroll.test.js.map +1 -1
  65. package/dist/module/tests/Modal.test.js +1 -1
  66. package/dist/module/tests/Modal.test.js.map +1 -1
  67. package/dist/module/tests/TimerPicker.test.js.map +1 -1
  68. package/dist/module/tests/TimerPickerModal.test.js +2 -2
  69. package/dist/module/tests/TimerPickerModal.test.js.map +1 -1
  70. package/dist/module/utils/generateNumbers.js.map +1 -1
  71. package/dist/module/utils/getAdjustedLimit.js.map +1 -1
  72. package/dist/module/utils/getScrollIndex.js +2 -2
  73. package/dist/module/utils/getScrollIndex.js.map +1 -1
  74. package/dist/typescript/components/DurationScroll/index.d.ts +4 -0
  75. package/dist/typescript/components/{TimerPicker/DurationScroll.d.ts → DurationScroll/types.d.ts} +36 -29
  76. package/dist/typescript/components/Modal/index.d.ts +3 -14
  77. package/dist/typescript/components/Modal/types.d.ts +15 -0
  78. package/dist/typescript/components/TimerPicker/index.d.ts +2 -57
  79. package/dist/typescript/components/TimerPicker/styles.d.ts +1022 -0
  80. package/dist/typescript/components/TimerPicker/types.d.ts +61 -0
  81. package/dist/typescript/components/TimerPickerModal/index.d.ts +4 -0
  82. package/dist/typescript/components/TimerPickerModal/styles.d.ts +738 -0
  83. package/dist/typescript/components/{index.d.ts → TimerPickerModal/types.d.ts} +24 -26
  84. package/dist/typescript/index.d.ts +6 -4
  85. package/dist/typescript/utils/generateNumbers.d.ts +4 -4
  86. package/dist/typescript/utils/getAdjustedLimit.d.ts +1 -1
  87. package/dist/typescript/utils/getScrollIndex.d.ts +2 -2
  88. package/package.json +14 -11
  89. package/src/assets/select_click.mp3 +0 -0
  90. package/src/components/{TimerPicker/DurationScroll.tsx → DurationScroll/index.tsx} +126 -110
  91. package/src/components/DurationScroll/types.ts +63 -0
  92. package/src/components/Modal/index.tsx +20 -30
  93. package/src/components/Modal/types.ts +17 -0
  94. package/src/components/TimerPicker/index.tsx +70 -138
  95. package/src/components/TimerPicker/{TimerPicker.styles.ts → styles.ts} +13 -13
  96. package/src/components/TimerPicker/types.ts +72 -0
  97. package/src/components/{index.tsx → TimerPickerModal/index.tsx} +44 -147
  98. package/src/components/{TimerPickerModal.styles.ts → TimerPickerModal/styles.ts} +9 -9
  99. package/src/components/TimerPickerModal/types.ts +52 -0
  100. package/src/index.ts +6 -7
  101. package/src/tests/DurationScroll.test.tsx +3 -1
  102. package/src/tests/Modal.test.tsx +3 -1
  103. package/src/tests/TimerPicker.test.tsx +2 -0
  104. package/src/tests/TimerPickerModal.test.tsx +3 -1
  105. package/src/utils/generateNumbers.ts +4 -4
  106. package/src/utils/getAdjustedLimit.ts +1 -1
  107. package/src/utils/getScrollIndex.ts +3 -3
  108. package/dist/commonjs/components/Modal/Modal.styles.js.map +0 -1
  109. package/dist/commonjs/components/TimerPicker/DurationScroll.js.map +0 -1
  110. package/dist/commonjs/components/TimerPicker/TimerPicker.styles.js.map +0 -1
  111. package/dist/commonjs/components/TimerPickerModal.styles.js.map +0 -1
  112. package/dist/commonjs/components/index.js.map +0 -1
  113. package/dist/module/components/Modal/Modal.styles.js.map +0 -1
  114. package/dist/module/components/TimerPicker/DurationScroll.js.map +0 -1
  115. package/dist/module/components/TimerPicker/TimerPicker.styles.js.map +0 -1
  116. package/dist/module/components/TimerPickerModal.styles.js.map +0 -1
  117. package/dist/module/components/index.js.map +0 -1
  118. package/dist/typescript/components/TimerPicker/TimerPicker.styles.d.ts +0 -29
  119. package/dist/typescript/components/TimerPickerModal.styles.d.ts +0 -19
  120. /package/dist/typescript/components/Modal/{Modal.styles.d.ts → styles.d.ts} +0 -0
  121. /package/src/components/Modal/{Modal.styles.ts → styles.ts} +0 -0
@@ -1,9 +1,14 @@
1
- import React, { MutableRefObject } from "react";
2
- import { View, Text, TouchableOpacity } from "react-native";
3
- import { TimerPickerProps } from "./TimerPicker";
4
- import Modal from "./Modal";
5
- import { CustomTimerPickerModalStyles } from "./TimerPickerModal.styles";
1
+ import type { MutableRefObject } from "react";
2
+ import type { View, TouchableOpacity, Text } from "react-native";
3
+ import type Modal from "../Modal";
4
+ import type { TimerPickerProps } from "../TimerPicker/types";
5
+ import type { CustomTimerPickerModalStyles } from "./styles";
6
6
  export interface TimerPickerModalRef {
7
+ latestDuration: {
8
+ hours: MutableRefObject<number> | undefined;
9
+ minutes: MutableRefObject<number> | undefined;
10
+ seconds: MutableRefObject<number> | undefined;
11
+ };
7
12
  reset: (options?: {
8
13
  animated?: boolean;
9
14
  }) => void;
@@ -14,33 +19,26 @@ export interface TimerPickerModalRef {
14
19
  }, options?: {
15
20
  animated?: boolean;
16
21
  }) => void;
17
- latestDuration: {
18
- hours: MutableRefObject<number> | undefined;
19
- minutes: MutableRefObject<number> | undefined;
20
- seconds: MutableRefObject<number> | undefined;
21
- };
22
22
  }
23
23
  export interface TimerPickerModalProps extends TimerPickerProps {
24
- visible: boolean;
25
- setIsVisible: (isVisible: boolean) => void;
26
- onConfirm: ({ hours, minutes, seconds, }: {
27
- hours: number;
28
- minutes: number;
29
- seconds: number;
30
- }) => void;
31
- onCancel?: () => void;
24
+ buttonContainerProps?: React.ComponentProps<typeof View>;
25
+ buttonTouchableOpacityProps?: React.ComponentProps<typeof TouchableOpacity>;
26
+ cancelButtonText?: string;
32
27
  closeOnOverlayPress?: boolean;
33
- hideCancelButton?: boolean;
34
28
  confirmButtonText?: string;
35
- cancelButtonText?: string;
36
- modalTitle?: string;
37
- modalProps?: React.ComponentProps<typeof Modal>;
38
29
  containerProps?: React.ComponentProps<typeof View>;
39
30
  contentContainerProps?: React.ComponentProps<typeof View>;
40
- buttonContainerProps?: React.ComponentProps<typeof View>;
41
- buttonTouchableOpacityProps?: React.ComponentProps<typeof TouchableOpacity>;
31
+ hideCancelButton?: boolean;
32
+ modalProps?: React.ComponentProps<typeof Modal>;
33
+ modalTitle?: string;
42
34
  modalTitleProps?: React.ComponentProps<typeof Text>;
35
+ onCancel?: () => void;
36
+ onConfirm: ({ hours, minutes, seconds, }: {
37
+ hours: number;
38
+ minutes: number;
39
+ seconds: number;
40
+ }) => void;
41
+ setIsVisible: (isVisible: boolean) => void;
43
42
  styles?: CustomTimerPickerModalStyles;
43
+ visible: boolean;
44
44
  }
45
- declare const _default: React.MemoExoticComponent<React.ForwardRefExoticComponent<TimerPickerModalProps & React.RefAttributes<TimerPickerModalRef>>>;
46
- export default _default;
@@ -1,4 +1,6 @@
1
- export { default as TimerPickerModal, TimerPickerModalProps, TimerPickerModalRef, } from "./components";
2
- export { default as TimerPicker, TimerPickerProps, TimerPickerRef, } from "./components/TimerPicker";
3
- export { CustomTimerPickerModalStyles } from "./components/TimerPickerModal.styles";
4
- export { CustomTimerPickerStyles } from "./components/TimerPicker/TimerPicker.styles";
1
+ export { default as TimerPickerModal } from "./components/TimerPickerModal";
2
+ export { TimerPickerModalProps, TimerPickerModalRef, } from "./components/TimerPickerModal/types";
3
+ export { CustomTimerPickerModalStyles } from "./components/TimerPickerModal/styles";
4
+ export { default as TimerPicker } from "./components/TimerPicker";
5
+ export { TimerPickerProps, TimerPickerRef, } from "./components/TimerPicker/types";
6
+ export { CustomTimerPickerStyles } from "./components/TimerPicker/styles";
@@ -1,12 +1,12 @@
1
1
  export declare const generateNumbers: (numberOfItems: number, options: {
2
- repeatNTimes?: number;
3
- padNumbersWithZero?: boolean;
4
2
  disableInfiniteScroll?: boolean;
3
+ padNumbersWithZero?: boolean;
5
4
  padWithNItems: number;
5
+ repeatNTimes?: number;
6
6
  }) => string[];
7
7
  export declare const generate12HourNumbers: (options: {
8
- repeatNTimes?: number;
9
- padNumbersWithZero?: boolean;
10
8
  disableInfiniteScroll?: boolean;
9
+ padNumbersWithZero?: boolean;
11
10
  padWithNItems: number;
11
+ repeatNTimes?: number;
12
12
  }) => string[];
@@ -1,4 +1,4 @@
1
- import type { LimitType } from "../components/TimerPicker/DurationScroll";
1
+ import type { LimitType } from "../components/DurationScroll/types";
2
2
  export declare const getAdjustedLimit: (limit: LimitType | undefined, numberOfItems: number) => {
3
3
  max: number;
4
4
  min: number;
@@ -1,6 +1,6 @@
1
1
  export declare const getScrollIndex: (variables: {
2
- value: number;
2
+ disableInfiniteScroll?: boolean;
3
3
  numberOfItems: number;
4
4
  padWithNItems: number;
5
- disableInfiniteScroll?: boolean;
5
+ value: number;
6
6
  }) => number;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "react-native-timer-picker",
3
- "description": "A simple, flexible, performant duration picker for React Native apps 🔥\n\nGreat for timers, alarms and duration inputs ⏰🕰️⏳",
3
+ "description": "A simple, flexible, performant duration picker for React Native apps 🔥\n\nGreat for timers, alarms and duration inputs ⏰🕰️⏳\n\nIncludes iOS-style haptic and audio feedback 🍏",
4
4
  "author": {
5
5
  "name": "Tim Roberts",
6
6
  "url": "https://github.com/troberts-28"
7
7
  },
8
8
  "license": "MIT",
9
- "version": "1.6.0",
9
+ "version": "1.8.0",
10
10
  "main": "dist/commonjs/index.js",
11
11
  "types": "dist/typescript/src/index.d.ts",
12
12
  "scripts": {
@@ -100,25 +100,28 @@
100
100
  "react-native": ">=0.59.0"
101
101
  },
102
102
  "devDependencies": {
103
- "@babel/core": ">=7.20.0",
103
+ "@babel/core": "^7.20.0",
104
104
  "@babel/plugin-transform-class-properties": "^7.22.5",
105
105
  "@babel/plugin-transform-flow-strip-types": "^7.22.5",
106
106
  "@babel/plugin-transform-private-methods": "^7.22.5",
107
107
  "@testing-library/react-native": "^12.0.0",
108
108
  "@types/jest": "^29.0.0",
109
- "@types/react": ">=18.0.27",
110
- "@types/react-native": ">=0.70.6",
111
- "@typescript-eslint/eslint-plugin": ">=5.49.0",
112
- "@typescript-eslint/parser": ">=5.49.0",
109
+ "@types/react": "^18.0.27",
110
+ "@types/react-native": "^0.70.6",
111
+ "@typescript-eslint/eslint-plugin": "^5.49.0",
112
+ "@typescript-eslint/parser": "^5.49.0",
113
113
  "babel-jest": "^29.6.2",
114
- "eslint": ">=8.44.0",
115
- "eslint-plugin-react": ">=7.33.1",
116
- "eslint-plugin-react-hooks": ">=4.6.0",
114
+ "eslint": "^8.44.0",
115
+ "eslint-plugin-import": "^2.29.0",
116
+ "eslint-plugin-react": "^7.33.1",
117
+ "eslint-plugin-react-hooks": "^4.6.0",
118
+ "eslint-plugin-sort-destructure-keys": "^1.5.0",
119
+ "eslint-plugin-typescript-sort-keys": "^3.1.0",
117
120
  "jest": "^29.0.0",
118
121
  "metro-react-native-babel-preset": "^0.71.1",
119
122
  "react-native-builder-bob": "^0.18.3",
120
123
  "react-test-renderer": "^18.0.0",
121
- "typescript": ">=4.7.4"
124
+ "typescript": "^4.7.4"
122
125
  },
123
126
  "react-native": "src/index.ts",
124
127
  "source": "src/index.ts",
Binary file
@@ -3,103 +3,56 @@ import React, {
3
3
  useCallback,
4
4
  forwardRef,
5
5
  useImperativeHandle,
6
- MutableRefObject,
6
+ useState,
7
+ useEffect,
7
8
  } from "react";
8
- import {
9
- View,
10
- Text,
11
- FlatList,
9
+
10
+ import { View, Text, FlatList } from "react-native";
11
+ import type {
12
12
  ViewabilityConfigCallbackPairs,
13
13
  ViewToken,
14
14
  NativeSyntheticEvent,
15
15
  NativeScrollEvent,
16
16
  } from "react-native";
17
17
 
18
+ import { colorToRgba } from "../../utils/colorToRgba";
18
19
  import {
19
20
  generate12HourNumbers,
20
21
  generateNumbers,
21
22
  } from "../../utils/generateNumbers";
22
- import { colorToRgba } from "../../utils/colorToRgba";
23
- import { generateStyles } from "./TimerPicker.styles";
24
23
  import { getAdjustedLimit } from "../../utils/getAdjustedLimit";
25
24
  import { getScrollIndex } from "../../utils/getScrollIndex";
26
25
 
27
- export interface DurationScrollRef {
28
- reset: (options?: { animated?: boolean }) => void;
29
- setValue: (value: number, options?: { animated?: boolean }) => void;
30
- latestDuration: MutableRefObject<number>;
31
- }
32
-
33
- type LinearGradientPoint = {
34
- x: number;
35
- y: number;
36
- };
37
-
38
- export type LinearGradientProps = React.ComponentProps<typeof View> & {
39
- colors: string[];
40
- locations?: number[] | null;
41
- start?: LinearGradientPoint | null;
42
- end?: LinearGradientPoint | null;
43
- };
44
-
45
- export type LimitType = {
46
- max?: number;
47
- min?: number;
48
- };
49
-
50
- interface DurationScrollProps {
51
- allowFontScaling?: boolean;
52
- numberOfItems: number;
53
- label?: string | React.ReactElement;
54
- initialValue?: number;
55
- onDurationChange: (duration: number) => void;
56
- padNumbersWithZero?: boolean;
57
- disableInfiniteScroll?: boolean;
58
- isDisabled?: boolean;
59
- limit?: LimitType;
60
- aggressivelyGetLatestDuration: boolean;
61
- is12HourPicker?: boolean;
62
- amLabel?: string;
63
- pmLabel?: string;
64
- padWithNItems: number;
65
- pickerGradientOverlayProps?: Partial<LinearGradientProps>;
66
- topPickerGradientOverlayProps?: Partial<LinearGradientProps>;
67
- bottomPickerGradientOverlayProps?: Partial<LinearGradientProps>;
68
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
- LinearGradient?: any;
70
- testID?: string;
71
- styles: ReturnType<typeof generateStyles>;
72
- }
73
-
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- const KEY_EXTRACTOR = (_: any, index: number) => index.toString();
26
+ import type { DurationScrollProps, DurationScrollRef } from "./types";
76
27
 
77
28
  const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
78
- (
79
- {
80
- numberOfItems,
81
- label,
82
- initialValue = 0,
83
- onDurationChange,
84
- padNumbersWithZero = false,
85
- disableInfiniteScroll = false,
86
- limit,
87
- isDisabled,
29
+ (props, ref) => {
30
+ const {
88
31
  aggressivelyGetLatestDuration,
89
32
  allowFontScaling = false,
90
- is12HourPicker,
91
33
  amLabel,
92
- pmLabel,
93
- padWithNItems,
94
- pickerGradientOverlayProps,
95
- topPickerGradientOverlayProps,
34
+ Audio,
96
35
  bottomPickerGradientOverlayProps,
36
+ clickSoundAsset,
37
+ disableInfiniteScroll = false,
38
+ Haptics,
39
+ initialValue = 0,
40
+ is12HourPicker,
41
+ isDisabled,
42
+ label,
43
+ limit,
97
44
  LinearGradient,
98
- testID,
45
+ numberOfItems,
46
+ onDurationChange,
47
+ padNumbersWithZero = false,
48
+ padWithNItems,
49
+ pickerGradientOverlayProps,
50
+ pmLabel,
99
51
  styles,
100
- },
101
- ref
102
- ): React.ReactElement => {
52
+ testID,
53
+ topPickerGradientOverlayProps,
54
+ } = props;
55
+
103
56
  const data = !is12HourPicker
104
57
  ? generateNumbers(numberOfItems, {
105
58
  padNumbersWithZero,
@@ -125,10 +78,42 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
125
78
  disableInfiniteScroll,
126
79
  });
127
80
 
81
+ // keep track of the latest duration as it scrolls
128
82
  const latestDuration = useRef(0);
83
+ // keep track of the last index scrolled past for haptic/audio feedback
84
+ const lastFeedbackIndex = useRef(0);
129
85
 
130
86
  const flatListRef = useRef<FlatList | null>(null);
131
87
 
88
+ const [clickSound, setClickSound] = useState<
89
+ | {
90
+ replayAsync: () => Promise<void>;
91
+ unloadAsync: () => Promise<void>;
92
+ }
93
+ | undefined
94
+ >();
95
+
96
+ // Preload the sound when the component mounts
97
+ useEffect(() => {
98
+ const loadSound = async () => {
99
+ if (Audio) {
100
+ const { sound } = await Audio.Sound.createAsync(
101
+ clickSoundAsset ??
102
+ require("../../assets/select_click.mp3"),
103
+ { shouldPlay: false }
104
+ );
105
+ setClickSound(sound);
106
+ }
107
+ };
108
+ loadSound();
109
+
110
+ // Unload sound when component unmounts
111
+ return () => {
112
+ clickSound?.unloadAsync();
113
+ };
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [Audio]);
116
+
132
117
  useImperativeHandle(ref, () => ({
133
118
  reset: (options) => {
134
119
  flatListRef.current?.scrollToIndex({
@@ -182,11 +167,11 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
182
167
  </Text>
183
168
  {is12HourPicker ? (
184
169
  <View
185
- style={styles.pickerAmPmContainer}
186
- pointerEvents="none">
170
+ pointerEvents="none"
171
+ style={styles.pickerAmPmContainer}>
187
172
  <Text
188
- style={[styles.pickerAmPmLabel]}
189
- allowFontScaling={allowFontScaling}>
173
+ allowFontScaling={allowFontScaling}
174
+ style={[styles.pickerAmPmLabel]}>
190
175
  {isAm ? amLabel : pmLabel}
191
176
  </Text>
192
177
  </View>
@@ -211,30 +196,63 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
211
196
 
212
197
  const onScroll = useCallback(
213
198
  (e: NativeSyntheticEvent<NativeScrollEvent>) => {
214
- // this function is only used when the picker is in a modal
199
+ // this function is only used when the picker is in a modal and/or has Haptic/Audio feedback
215
200
  // it is used to ensure that the modal gets the latest duration on clicking
216
201
  // the confirm button, even if the scrollview is still scrolling
217
- const newIndex = Math.round(
218
- e.nativeEvent.contentOffset.y /
219
- styles.pickerItemContainer.height
220
- );
221
- let newDuration =
222
- (disableInfiniteScroll
223
- ? newIndex
224
- : newIndex + padWithNItems) %
225
- (numberOfItems + 1);
202
+ if (!aggressivelyGetLatestDuration && !Haptics && !Audio) {
203
+ return;
204
+ }
226
205
 
227
- // check limits
228
- if (newDuration > adjustedLimited.max) {
229
- newDuration = adjustedLimited.max;
230
- } else if (newDuration < adjustedLimited.min) {
231
- newDuration = adjustedLimited.min;
206
+ if (aggressivelyGetLatestDuration) {
207
+ const newIndex = Math.round(
208
+ e.nativeEvent.contentOffset.y /
209
+ styles.pickerItemContainer.height
210
+ );
211
+ let newDuration =
212
+ (disableInfiniteScroll
213
+ ? newIndex
214
+ : newIndex + padWithNItems) %
215
+ (numberOfItems + 1);
216
+
217
+ if (newDuration !== latestDuration.current) {
218
+ // check limits
219
+ if (newDuration > adjustedLimited.max) {
220
+ newDuration = adjustedLimited.max;
221
+ } else if (newDuration < adjustedLimited.min) {
222
+ newDuration = adjustedLimited.min;
223
+ }
224
+
225
+ latestDuration.current = newDuration;
226
+ }
227
+ }
228
+
229
+ if (Haptics || Audio) {
230
+ const feedbackIndex = Math.round(
231
+ (e.nativeEvent.contentOffset.y +
232
+ styles.pickerItemContainer.height / 2) /
233
+ styles.pickerItemContainer.height
234
+ );
235
+
236
+ if (feedbackIndex !== lastFeedbackIndex.current) {
237
+ // this check stops the feedback firing when the component mounts
238
+ if (lastFeedbackIndex.current) {
239
+ // fire haptic feedback if available
240
+ Haptics?.selectionAsync();
241
+
242
+ // play click sound if available
243
+ clickSound?.replayAsync();
244
+ }
245
+
246
+ lastFeedbackIndex.current = feedbackIndex;
247
+ }
232
248
  }
233
- latestDuration.current = newDuration;
234
249
  },
250
+ // eslint-disable-next-line react-hooks/exhaustive-deps
235
251
  [
236
252
  adjustedLimited.max,
237
253
  adjustedLimited.min,
254
+ aggressivelyGetLatestDuration,
255
+ clickSound,
238
256
  disableInfiniteScroll,
239
257
  numberOfItems,
240
258
  padWithNItems,
@@ -337,7 +355,6 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
337
355
 
338
356
  return (
339
357
  <View
340
- testID={testID}
341
358
  pointerEvents={isDisabled ? "none" : undefined}
342
359
  style={[
343
360
  {
@@ -347,36 +364,35 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
347
364
  overflow: "visible",
348
365
  },
349
366
  isDisabled && styles.disabledPickerContainer,
350
- ]}>
367
+ ]}
368
+ testID={testID}>
351
369
  <FlatList
352
370
  ref={flatListRef}
353
371
  data={data}
372
+ decelerationRate={0.88}
354
373
  getItemLayout={getItemLayout}
355
374
  initialScrollIndex={initialScrollIndex}
356
- windowSize={numberOfItemsToShow}
375
+ keyExtractor={(_, index) => index.toString()}
376
+ onMomentumScrollEnd={onMomentumScrollEnd}
377
+ onScroll={onScroll}
357
378
  renderItem={renderItem}
358
- keyExtractor={KEY_EXTRACTOR}
359
- showsVerticalScrollIndicator={false}
360
- decelerationRate={0.88}
379
+ scrollEnabled={!isDisabled}
361
380
  scrollEventThrottle={16}
381
+ showsVerticalScrollIndicator={false}
362
382
  snapToAlignment="start"
363
- scrollEnabled={!isDisabled}
364
383
  // used in place of snapToOffset due to bug on Android
365
384
  snapToOffsets={[...Array(data.length)].map(
366
385
  (_, i) => i * styles.pickerItemContainer.height
367
386
  )}
387
+ testID="duration-scroll-flatlist"
368
388
  viewabilityConfigCallbackPairs={
369
389
  !disableInfiniteScroll
370
390
  ? viewabilityConfigCallbackPairs?.current
371
391
  : undefined
372
392
  }
373
- onMomentumScrollEnd={onMomentumScrollEnd}
374
- onScroll={
375
- aggressivelyGetLatestDuration ? onScroll : undefined
376
- }
377
- testID="duration-scroll-flatlist"
393
+ windowSize={numberOfItemsToShow}
378
394
  />
379
- <View style={styles.pickerLabelContainer} pointerEvents="none">
395
+ <View pointerEvents="none" style={styles.pickerLabelContainer}>
380
396
  {typeof label === "string" ? (
381
397
  <Text
382
398
  allowFontScaling={allowFontScaling}
@@ -400,9 +416,9 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
400
416
  opacity: 0,
401
417
  }),
402
418
  ]}
403
- start={{ x: 1, y: 0.3 }}
404
419
  end={{ x: 1, y: 1 }}
405
420
  pointerEvents="none"
421
+ start={{ x: 1, y: 0.3 }}
406
422
  {...pickerGradientOverlayProps}
407
423
  {...topPickerGradientOverlayProps}
408
424
  style={[styles.pickerGradientOverlay, { top: 0 }]}
@@ -418,9 +434,9 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>(
418
434
  styles.pickerContainer.backgroundColor ??
419
435
  "white",
420
436
  ]}
421
- start={{ x: 1, y: 0 }}
422
437
  end={{ x: 1, y: 0.7 }}
423
438
  pointerEvents="none"
439
+ start={{ x: 1, y: 0 }}
424
440
  {...pickerGradientOverlayProps}
425
441
  {...bottomPickerGradientOverlayProps}
426
442
  style={[
@@ -0,0 +1,63 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { MutableRefObject } from "react";
3
+
4
+ import type { View } from "react-native";
5
+
6
+ import type { generateStyles } from "../TimerPicker/styles";
7
+
8
+ export interface DurationScrollProps {
9
+ Audio?: any;
10
+ Haptics?: any;
11
+ LinearGradient?: any;
12
+ aggressivelyGetLatestDuration: boolean;
13
+ allowFontScaling?: boolean;
14
+ amLabel?: string;
15
+ bottomPickerGradientOverlayProps?: Partial<LinearGradientProps>;
16
+ clickSoundAsset?: SoundAssetType;
17
+ disableInfiniteScroll?: boolean;
18
+ initialValue?: number;
19
+ is12HourPicker?: boolean;
20
+ isDisabled?: boolean;
21
+ label?: string | React.ReactElement;
22
+ limit?: LimitType;
23
+ numberOfItems: number;
24
+ onDurationChange: (duration: number) => void;
25
+ padNumbersWithZero?: boolean;
26
+ padWithNItems: number;
27
+ pickerGradientOverlayProps?: Partial<LinearGradientProps>;
28
+ pmLabel?: string;
29
+ styles: ReturnType<typeof generateStyles>;
30
+ testID?: string;
31
+ topPickerGradientOverlayProps?: Partial<LinearGradientProps>;
32
+ }
33
+
34
+ export interface DurationScrollRef {
35
+ latestDuration: MutableRefObject<number>;
36
+ reset: (options?: { animated?: boolean }) => void;
37
+ setValue: (value: number, options?: { animated?: boolean }) => void;
38
+ }
39
+
40
+ type LinearGradientPoint = {
41
+ x: number;
42
+ y: number;
43
+ };
44
+
45
+ export type LinearGradientProps = React.ComponentProps<typeof View> & {
46
+ colors: string[];
47
+ end?: LinearGradientPoint | null;
48
+ locations?: number[] | null;
49
+ start?: LinearGradientPoint | null;
50
+ };
51
+
52
+ export type LimitType = {
53
+ max?: number;
54
+ min?: number;
55
+ };
56
+
57
+ export type SoundAssetType =
58
+ | number
59
+ | {
60
+ headers?: Record<string, string>;
61
+ overrideFileExtensionAndroid?: string;
62
+ uri: string;
63
+ };
@@ -1,5 +1,5 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import React, { useCallback, useEffect, useRef } from "react";
2
+
3
3
  import {
4
4
  Animated,
5
5
  Easing,
@@ -8,34 +8,24 @@ import {
8
8
  useWindowDimensions,
9
9
  } from "react-native";
10
10
 
11
- import { styles } from "./Modal.styles";
11
+ import { styles } from "./styles";
12
+ import type { ModalProps } from "./types";
12
13
 
13
- interface ModalProps {
14
- children?: React.ReactElement;
15
- onOverlayPress?: () => void;
16
- onHide?: () => void;
17
- isVisible?: boolean;
18
- animationDuration?: number;
19
- overlayOpacity?: number;
20
- modalProps?: any;
21
- contentStyle?: any;
22
- overlayStyle?: any;
23
- testID?: string;
24
- }
14
+ export const Modal = (props: ModalProps) => {
15
+ const {
16
+ animationDuration = 300,
17
+ children,
18
+ contentStyle,
19
+ isVisible = false,
20
+ modalProps,
21
+ onHide,
22
+ onOverlayPress,
23
+ overlayOpacity = 0.4,
24
+ overlayStyle,
25
+ testID = "modal",
26
+ } = props;
25
27
 
26
- export const Modal = ({
27
- children,
28
- onOverlayPress,
29
- onHide,
30
- isVisible = false,
31
- animationDuration = 300,
32
- overlayOpacity = 0.4,
33
- modalProps,
34
- contentStyle,
35
- overlayStyle,
36
- testID = "modal",
37
- }: ModalProps): React.ReactElement => {
38
- const { width: screenWidth, height: screenHeight } = useWindowDimensions();
28
+ const { height: screenHeight, width: screenWidth } = useWindowDimensions();
39
29
 
40
30
  const isMounted = useRef(false);
41
31
  const animatedOpacity = useRef(new Animated.Value(0));
@@ -105,8 +95,8 @@ export const Modal = ({
105
95
 
106
96
  return (
107
97
  <ReactNativeModal
108
- transparent
109
98
  animationType="fade"
99
+ transparent
110
100
  visible={isVisible}
111
101
  {...modalProps}
112
102
  testID={testID}>
@@ -123,8 +113,8 @@ export const Modal = ({
123
113
  />
124
114
  </TouchableWithoutFeedback>
125
115
  <Animated.View
126
- style={[styles.content, contentAnimatedStyle, contentStyle]}
127
- pointerEvents="box-none">
116
+ pointerEvents="box-none"
117
+ style={[styles.content, contentAnimatedStyle, contentStyle]}>
128
118
  {children}
129
119
  </Animated.View>
130
120
  </ReactNativeModal>