react-native-hold-menu-actions 0.1.6

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 (219) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +38 -0
  3. package/lib/commonjs/components/backdrop/Backdrop.js +104 -0
  4. package/lib/commonjs/components/backdrop/Backdrop.js.map +1 -0
  5. package/lib/commonjs/components/backdrop/constants.js +14 -0
  6. package/lib/commonjs/components/backdrop/constants.js.map +1 -0
  7. package/lib/commonjs/components/backdrop/index.js +16 -0
  8. package/lib/commonjs/components/backdrop/index.js.map +1 -0
  9. package/lib/commonjs/components/backdrop/styles.js +17 -0
  10. package/lib/commonjs/components/backdrop/styles.js.map +1 -0
  11. package/lib/commonjs/components/flatList/FlatList.js +35 -0
  12. package/lib/commonjs/components/flatList/FlatList.js.map +1 -0
  13. package/lib/commonjs/components/flatList/index.js +16 -0
  14. package/lib/commonjs/components/flatList/index.js.map +1 -0
  15. package/lib/commonjs/components/holdItem/HoldItem.js +369 -0
  16. package/lib/commonjs/components/holdItem/HoldItem.js.map +1 -0
  17. package/lib/commonjs/components/holdItem/index.js +16 -0
  18. package/lib/commonjs/components/holdItem/index.js.map +1 -0
  19. package/lib/commonjs/components/holdItem/styles.js +22 -0
  20. package/lib/commonjs/components/holdItem/styles.js.map +1 -0
  21. package/lib/commonjs/components/holdItem/types.d.js +2 -0
  22. package/lib/commonjs/components/holdItem/types.d.js.map +1 -0
  23. package/lib/commonjs/components/icon/Icon.js +43 -0
  24. package/lib/commonjs/components/icon/Icon.js.map +1 -0
  25. package/lib/commonjs/components/icon/index.js +16 -0
  26. package/lib/commonjs/components/icon/index.js.map +1 -0
  27. package/lib/commonjs/components/menu/Menu.js +57 -0
  28. package/lib/commonjs/components/menu/Menu.js.map +1 -0
  29. package/lib/commonjs/components/menu/MenuItem.js +85 -0
  30. package/lib/commonjs/components/menu/MenuItem.js.map +1 -0
  31. package/lib/commonjs/components/menu/MenuItems.js +35 -0
  32. package/lib/commonjs/components/menu/MenuItems.js.map +1 -0
  33. package/lib/commonjs/components/menu/MenuList.js +121 -0
  34. package/lib/commonjs/components/menu/MenuList.js.map +1 -0
  35. package/lib/commonjs/components/menu/Separator.js +47 -0
  36. package/lib/commonjs/components/menu/Separator.js.map +1 -0
  37. package/lib/commonjs/components/menu/calculations.js +31 -0
  38. package/lib/commonjs/components/menu/calculations.js.map +1 -0
  39. package/lib/commonjs/components/menu/constants.js +21 -0
  40. package/lib/commonjs/components/menu/constants.js.map +1 -0
  41. package/lib/commonjs/components/menu/index.js +16 -0
  42. package/lib/commonjs/components/menu/index.js.map +1 -0
  43. package/lib/commonjs/components/menu/styles.js +77 -0
  44. package/lib/commonjs/components/menu/styles.js.map +1 -0
  45. package/lib/commonjs/components/menu/types.d.js +2 -0
  46. package/lib/commonjs/components/menu/types.d.js.map +1 -0
  47. package/lib/commonjs/components/provider/Provider.js +98 -0
  48. package/lib/commonjs/components/provider/Provider.js.map +1 -0
  49. package/lib/commonjs/components/provider/index.js +16 -0
  50. package/lib/commonjs/components/provider/index.js.map +1 -0
  51. package/lib/commonjs/components/provider/reducer.js +50 -0
  52. package/lib/commonjs/components/provider/reducer.js.map +1 -0
  53. package/lib/commonjs/components/provider/types.d.js +2 -0
  54. package/lib/commonjs/components/provider/types.d.js.map +1 -0
  55. package/lib/commonjs/constants.js +60 -0
  56. package/lib/commonjs/constants.js.map +1 -0
  57. package/lib/commonjs/context/index.js +14 -0
  58. package/lib/commonjs/context/index.js.map +1 -0
  59. package/lib/commonjs/context/internal.js +13 -0
  60. package/lib/commonjs/context/internal.js.map +1 -0
  61. package/lib/commonjs/hooks/index.js +24 -0
  62. package/lib/commonjs/hooks/index.js.map +1 -0
  63. package/lib/commonjs/hooks/useDeviceOrientation.js +38 -0
  64. package/lib/commonjs/hooks/useDeviceOrientation.js.map +1 -0
  65. package/lib/commonjs/hooks/useInternal.js +15 -0
  66. package/lib/commonjs/hooks/useInternal.js.map +1 -0
  67. package/lib/commonjs/index.js +40 -0
  68. package/lib/commonjs/index.js.map +1 -0
  69. package/lib/commonjs/styleGuide.js +39 -0
  70. package/lib/commonjs/styleGuide.js.map +1 -0
  71. package/lib/commonjs/utils/calculations.js +73 -0
  72. package/lib/commonjs/utils/calculations.js.map +1 -0
  73. package/lib/commonjs/utils/validations.js +43 -0
  74. package/lib/commonjs/utils/validations.js.map +1 -0
  75. package/lib/module/components/backdrop/Backdrop.js +83 -0
  76. package/lib/module/components/backdrop/Backdrop.js.map +1 -0
  77. package/lib/module/components/backdrop/constants.js +4 -0
  78. package/lib/module/components/backdrop/constants.js.map +1 -0
  79. package/lib/module/components/backdrop/index.js +2 -0
  80. package/lib/module/components/backdrop/index.js.map +1 -0
  81. package/lib/module/components/backdrop/styles.js +7 -0
  82. package/lib/module/components/backdrop/styles.js.map +1 -0
  83. package/lib/module/components/flatList/FlatList.js +17 -0
  84. package/lib/module/components/flatList/FlatList.js.map +1 -0
  85. package/lib/module/components/flatList/index.js +2 -0
  86. package/lib/module/components/flatList/index.js.map +1 -0
  87. package/lib/module/components/holdItem/HoldItem.js +344 -0
  88. package/lib/module/components/holdItem/HoldItem.js.map +1 -0
  89. package/lib/module/components/holdItem/index.js +2 -0
  90. package/lib/module/components/holdItem/index.js.map +1 -0
  91. package/lib/module/components/holdItem/styles.js +12 -0
  92. package/lib/module/components/holdItem/styles.js.map +1 -0
  93. package/lib/module/components/holdItem/types.d.js +2 -0
  94. package/lib/module/components/holdItem/types.d.js.map +1 -0
  95. package/lib/module/components/icon/Icon.js +26 -0
  96. package/lib/module/components/icon/Icon.js.map +1 -0
  97. package/lib/module/components/icon/index.js +2 -0
  98. package/lib/module/components/icon/index.js.map +1 -0
  99. package/lib/module/components/menu/Menu.js +37 -0
  100. package/lib/module/components/menu/Menu.js.map +1 -0
  101. package/lib/module/components/menu/MenuItem.js +59 -0
  102. package/lib/module/components/menu/MenuItem.js.map +1 -0
  103. package/lib/module/components/menu/MenuItems.js +19 -0
  104. package/lib/module/components/menu/MenuItems.js.map +1 -0
  105. package/lib/module/components/menu/MenuList.js +93 -0
  106. package/lib/module/components/menu/MenuList.js.map +1 -0
  107. package/lib/module/components/menu/Separator.js +29 -0
  108. package/lib/module/components/menu/Separator.js.map +1 -0
  109. package/lib/module/components/menu/calculations.js +17 -0
  110. package/lib/module/components/menu/calculations.js.map +1 -0
  111. package/lib/module/components/menu/constants.js +8 -0
  112. package/lib/module/components/menu/constants.js.map +1 -0
  113. package/lib/module/components/menu/index.js +2 -0
  114. package/lib/module/components/menu/index.js.map +1 -0
  115. package/lib/module/components/menu/styles.js +63 -0
  116. package/lib/module/components/menu/styles.js.map +1 -0
  117. package/lib/module/components/menu/types.d.js +2 -0
  118. package/lib/module/components/menu/types.d.js.map +1 -0
  119. package/lib/module/components/provider/Provider.js +75 -0
  120. package/lib/module/components/provider/Provider.js.map +1 -0
  121. package/lib/module/components/provider/index.js +2 -0
  122. package/lib/module/components/provider/index.js.map +1 -0
  123. package/lib/module/components/provider/reducer.js +38 -0
  124. package/lib/module/components/provider/reducer.js.map +1 -0
  125. package/lib/module/components/provider/types.d.js +2 -0
  126. package/lib/module/components/provider/types.d.js.map +1 -0
  127. package/lib/module/constants.js +37 -0
  128. package/lib/module/constants.js.map +1 -0
  129. package/lib/module/context/index.js +2 -0
  130. package/lib/module/context/index.js.map +1 -0
  131. package/lib/module/context/internal.js +4 -0
  132. package/lib/module/context/internal.js.map +1 -0
  133. package/lib/module/hooks/index.js +3 -0
  134. package/lib/module/hooks/index.js.map +1 -0
  135. package/lib/module/hooks/useDeviceOrientation.js +27 -0
  136. package/lib/module/hooks/useDeviceOrientation.js.map +1 -0
  137. package/lib/module/hooks/useInternal.js +4 -0
  138. package/lib/module/hooks/useInternal.js.map +1 -0
  139. package/lib/module/index.js +5 -0
  140. package/lib/module/index.js.map +1 -0
  141. package/lib/module/styleGuide.js +30 -0
  142. package/lib/module/styleGuide.js.map +1 -0
  143. package/lib/module/utils/calculations.js +51 -0
  144. package/lib/module/utils/calculations.js.map +1 -0
  145. package/lib/module/utils/validations.js +38 -0
  146. package/lib/module/utils/validations.js.map +1 -0
  147. package/lib/typescript/components/backdrop/Backdrop.d.ts +3 -0
  148. package/lib/typescript/components/backdrop/constants.d.ts +2 -0
  149. package/lib/typescript/components/backdrop/index.d.ts +1 -0
  150. package/lib/typescript/components/backdrop/styles.d.ts +10 -0
  151. package/lib/typescript/components/flatList/FlatList.d.ts +5 -0
  152. package/lib/typescript/components/flatList/index.d.ts +2 -0
  153. package/lib/typescript/components/holdItem/HoldItem.d.ts +4 -0
  154. package/lib/typescript/components/holdItem/index.d.ts +2 -0
  155. package/lib/typescript/components/holdItem/styles.d.ts +15 -0
  156. package/lib/typescript/components/holdItem/types.d.ts +131 -0
  157. package/lib/typescript/components/icon/Icon.d.ts +7 -0
  158. package/lib/typescript/components/icon/index.d.ts +1 -0
  159. package/lib/typescript/components/menu/Menu.d.ts +3 -0
  160. package/lib/typescript/components/menu/MenuItem.d.ts +8 -0
  161. package/lib/typescript/components/menu/MenuItems.d.ts +6 -0
  162. package/lib/typescript/components/menu/MenuList.d.ts +3 -0
  163. package/lib/typescript/components/menu/Separator.d.ts +3 -0
  164. package/lib/typescript/components/menu/calculations.d.ts +4 -0
  165. package/lib/typescript/components/menu/constants.d.ts +7 -0
  166. package/lib/typescript/components/menu/index.d.ts +1 -0
  167. package/lib/typescript/components/menu/styles.d.ts +59 -0
  168. package/lib/typescript/components/menu/types.d.ts +28 -0
  169. package/lib/typescript/components/provider/Provider.d.ts +10 -0
  170. package/lib/typescript/components/provider/index.d.ts +2 -0
  171. package/lib/typescript/components/provider/reducer.d.ts +20 -0
  172. package/lib/typescript/components/provider/types.d.ts +33 -0
  173. package/lib/typescript/constants.d.ts +29 -0
  174. package/lib/typescript/context/index.d.ts +1 -0
  175. package/lib/typescript/context/internal.d.ts +16 -0
  176. package/lib/typescript/hooks/index.d.ts +2 -0
  177. package/lib/typescript/hooks/useDeviceOrientation.d.ts +3 -0
  178. package/lib/typescript/hooks/useInternal.d.ts +1 -0
  179. package/lib/typescript/index.d.ts +4 -0
  180. package/lib/typescript/styleGuide.d.ts +28 -0
  181. package/lib/typescript/utils/calculations.d.ts +14 -0
  182. package/lib/typescript/utils/validations.d.ts +3 -0
  183. package/package.json +106 -0
  184. package/src/components/backdrop/Backdrop.tsx +138 -0
  185. package/src/components/backdrop/constants.ts +8 -0
  186. package/src/components/backdrop/index.ts +1 -0
  187. package/src/components/backdrop/styles.ts +8 -0
  188. package/src/components/flatList/FlatList.tsx +23 -0
  189. package/src/components/flatList/index.ts +2 -0
  190. package/src/components/holdItem/HoldItem.tsx +449 -0
  191. package/src/components/holdItem/index.ts +2 -0
  192. package/src/components/holdItem/styles.ts +11 -0
  193. package/src/components/holdItem/types.d.ts +131 -0
  194. package/src/components/icon/Icon.tsx +33 -0
  195. package/src/components/icon/index.ts +1 -0
  196. package/src/components/menu/Menu.tsx +57 -0
  197. package/src/components/menu/MenuItem.tsx +79 -0
  198. package/src/components/menu/MenuItems.tsx +26 -0
  199. package/src/components/menu/MenuList.tsx +151 -0
  200. package/src/components/menu/Separator.tsx +28 -0
  201. package/src/components/menu/calculations.ts +49 -0
  202. package/src/components/menu/constants.ts +9 -0
  203. package/src/components/menu/index.ts +1 -0
  204. package/src/components/menu/styles.ts +64 -0
  205. package/src/components/menu/types.d.ts +28 -0
  206. package/src/components/provider/Provider.tsx +105 -0
  207. package/src/components/provider/index.ts +2 -0
  208. package/src/components/provider/reducer.ts +48 -0
  209. package/src/components/provider/types.d.ts +33 -0
  210. package/src/constants.ts +54 -0
  211. package/src/context/index.ts +1 -0
  212. package/src/context/internal.ts +19 -0
  213. package/src/hooks/index.ts +2 -0
  214. package/src/hooks/useDeviceOrientation.ts +28 -0
  215. package/src/hooks/useInternal.ts +4 -0
  216. package/src/index.ts +4 -0
  217. package/src/styleGuide.ts +31 -0
  218. package/src/utils/calculations.ts +110 -0
  219. package/src/utils/validations.ts +42 -0
@@ -0,0 +1,449 @@
1
+ import React, { memo, useMemo } from 'react';
2
+ import { ViewProps } from 'react-native';
3
+
4
+ //#region reanimated & gesture handler
5
+ import {
6
+ TapGestureHandler,
7
+ LongPressGestureHandler,
8
+ TapGestureHandlerGestureEvent,
9
+ LongPressGestureHandlerGestureEvent,
10
+ } from 'react-native-gesture-handler';
11
+ import Animated, {
12
+ measure,
13
+ runOnJS,
14
+ useAnimatedGestureHandler,
15
+ useAnimatedProps,
16
+ useAnimatedRef,
17
+ useAnimatedStyle,
18
+ useSharedValue,
19
+ withDelay,
20
+ withTiming,
21
+ withSequence,
22
+ withSpring,
23
+ useAnimatedReaction,
24
+ } from 'react-native-reanimated';
25
+ //#endregion
26
+
27
+ //#region dependencies
28
+ import { Portal } from '@gorhom/portal';
29
+ import { nanoid } from 'nanoid/non-secure';
30
+ // @ts-ignore
31
+ import * as Haptics from 'expo-haptics';
32
+ //#endregion
33
+
34
+ //#region utils & types
35
+ import {
36
+ TransformOriginAnchorPosition,
37
+ getTransformOrigin,
38
+ calculateMenuHeight,
39
+ } from '../../utils/calculations';
40
+ import {
41
+ HOLD_ITEM_TRANSFORM_DURATION,
42
+ HOLD_ITEM_SCALE_DOWN_DURATION,
43
+ HOLD_ITEM_SCALE_DOWN_VALUE,
44
+ SPRING_CONFIGURATION,
45
+ WINDOW_HEIGHT,
46
+ WINDOW_WIDTH,
47
+ CONTEXT_MENU_STATE,
48
+ } from '../../constants';
49
+ import { useDeviceOrientation } from '../../hooks';
50
+ import styles from './styles';
51
+
52
+ import type { HoldItemProps, GestureHandlerProps } from './types';
53
+ import styleGuide from '../../styleGuide';
54
+ import { useInternal } from '../../hooks';
55
+ //#endregion
56
+
57
+ type Context = { didMeasureLayout: boolean };
58
+
59
+ const HoldItemComponent = ({
60
+ items,
61
+ bottom,
62
+ containerStyles,
63
+ disableMove,
64
+ menuAnchorPosition,
65
+ activateOn,
66
+ hapticFeedback,
67
+ actionParams,
68
+ closeOnTap,
69
+ longPressMinDurationMs = 150,
70
+ children,
71
+ }: HoldItemProps) => {
72
+ //#region hooks
73
+ const { state, menuProps, safeAreaInsets } = useInternal();
74
+ const deviceOrientation = useDeviceOrientation();
75
+ //#endregion
76
+
77
+ //#region variables
78
+ const isActive = useSharedValue(false);
79
+ const isAnimationStarted = useSharedValue(false);
80
+
81
+ const itemRectY = useSharedValue<number>(0);
82
+ const itemRectX = useSharedValue<number>(0);
83
+ const itemRectWidth = useSharedValue<number>(0);
84
+ const itemRectHeight = useSharedValue<number>(0);
85
+ const itemScale = useSharedValue<number>(1);
86
+ const transformValue = useSharedValue<number>(0);
87
+
88
+ const transformOrigin = useSharedValue<TransformOriginAnchorPosition>(
89
+ menuAnchorPosition || 'top-right'
90
+ );
91
+
92
+ const key = useMemo(() => `hold-item-${nanoid()}`, []);
93
+ const menuHeight = useMemo(() => {
94
+ const itemsWithSeparator = items.filter(item => item.withSeparator);
95
+ return calculateMenuHeight(items.length, itemsWithSeparator.length);
96
+ }, [items]);
97
+
98
+ const isHold = !activateOn || activateOn === 'hold';
99
+ //#endregion
100
+
101
+ //#region refs
102
+ const containerRef = useAnimatedRef<Animated.View>();
103
+ //#endregion
104
+
105
+ //#region functions
106
+ const hapticResponse = () => {
107
+ const style = !hapticFeedback ? 'Medium' : hapticFeedback;
108
+ switch (style) {
109
+ case `Selection`:
110
+ Haptics.selectionAsync();
111
+ break;
112
+ case `Light`:
113
+ case `Medium`:
114
+ case `Heavy`:
115
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle[style]);
116
+ break;
117
+ case `Success`:
118
+ case `Warning`:
119
+ case `Error`:
120
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType[style]);
121
+ break;
122
+ default:
123
+ }
124
+ };
125
+ //#endregion
126
+
127
+ //#region worklet functions
128
+ const activateAnimation = (ctx: any) => {
129
+ 'worklet';
130
+ if (!ctx.didMeasureLayout) {
131
+ const measured = measure(containerRef);
132
+
133
+ itemRectY.value = measured.pageY;
134
+ itemRectX.value = measured.pageX;
135
+ itemRectHeight.value = measured.height;
136
+ itemRectWidth.value = measured.width;
137
+
138
+ if (!menuAnchorPosition) {
139
+ const position = getTransformOrigin(
140
+ measured.pageX,
141
+ itemRectWidth.value,
142
+ deviceOrientation === 'portrait' ? WINDOW_WIDTH : WINDOW_HEIGHT,
143
+ bottom
144
+ );
145
+ transformOrigin.value = position;
146
+ }
147
+ }
148
+ };
149
+
150
+ const calculateTransformValue = () => {
151
+ 'worklet';
152
+
153
+ const height =
154
+ deviceOrientation === 'portrait' ? WINDOW_HEIGHT : WINDOW_WIDTH;
155
+
156
+ const isAnchorPointTop = transformOrigin.value.includes('top');
157
+
158
+ let tY = 0;
159
+ if (!disableMove) {
160
+ if (isAnchorPointTop) {
161
+ const topTransform =
162
+ itemRectY.value +
163
+ itemRectHeight.value +
164
+ menuHeight +
165
+ styleGuide.spacing +
166
+ (safeAreaInsets?.bottom || 0);
167
+
168
+ tY = topTransform > height ? height - topTransform : 0;
169
+ } else {
170
+ const bottomTransform =
171
+ itemRectY.value - menuHeight - (safeAreaInsets?.top || 0);
172
+ tY =
173
+ bottomTransform < 0 ? -bottomTransform + styleGuide.spacing * 2 : 0;
174
+ }
175
+ }
176
+ return tY;
177
+ };
178
+
179
+ const setMenuProps = () => {
180
+ 'worklet';
181
+
182
+ menuProps.value = {
183
+ itemHeight: itemRectHeight.value,
184
+ itemWidth: itemRectWidth.value,
185
+ itemY: itemRectY.value,
186
+ itemX: itemRectX.value,
187
+ anchorPosition: transformOrigin.value,
188
+ menuHeight: menuHeight,
189
+ items,
190
+ transformValue: transformValue.value,
191
+ actionParams: actionParams || {},
192
+ };
193
+ };
194
+
195
+ const scaleBack = () => {
196
+ 'worklet';
197
+ itemScale.value = withTiming(1, {
198
+ duration: HOLD_ITEM_TRANSFORM_DURATION / 2,
199
+ });
200
+ };
201
+
202
+ const onCompletion = (isFinised?: boolean) => {
203
+ 'worklet';
204
+ const isListValid = items && items.length > 0;
205
+ if (isFinised && isListValid) {
206
+ state.value = CONTEXT_MENU_STATE.ACTIVE;
207
+ isActive.value = true;
208
+ scaleBack();
209
+ if (hapticFeedback !== 'None') {
210
+ runOnJS(hapticResponse)();
211
+ }
212
+ }
213
+
214
+ isAnimationStarted.value = false;
215
+
216
+ // TODO: Warn user if item list is empty or not given
217
+ };
218
+
219
+ const scaleHold = () => {
220
+ 'worklet';
221
+ itemScale.value = withTiming(
222
+ HOLD_ITEM_SCALE_DOWN_VALUE,
223
+ { duration: HOLD_ITEM_SCALE_DOWN_DURATION },
224
+ onCompletion
225
+ );
226
+ };
227
+
228
+ const scaleTap = () => {
229
+ 'worklet';
230
+ isAnimationStarted.value = true;
231
+
232
+ itemScale.value = withSequence(
233
+ withTiming(HOLD_ITEM_SCALE_DOWN_VALUE, {
234
+ duration: HOLD_ITEM_SCALE_DOWN_DURATION,
235
+ }),
236
+ withTiming(
237
+ 1,
238
+ {
239
+ duration: HOLD_ITEM_TRANSFORM_DURATION / 2,
240
+ },
241
+ onCompletion
242
+ )
243
+ );
244
+ };
245
+
246
+ /**
247
+ * When use tap activation ("tap") and trying to tap multiple times,
248
+ * scale animation is called again despite it is started. This causes a bug.
249
+ * To prevent this, it is better to check is animation already started.
250
+ */
251
+ const canCallActivateFunctions = () => {
252
+ 'worklet';
253
+ const willActivateWithTap =
254
+ activateOn === 'double-tap' || activateOn === 'tap';
255
+
256
+ return (
257
+ (willActivateWithTap && !isAnimationStarted.value) || !willActivateWithTap
258
+ );
259
+ };
260
+ //#endregion
261
+
262
+ //#region gesture events
263
+ const gestureEvent = useAnimatedGestureHandler<
264
+ LongPressGestureHandlerGestureEvent | TapGestureHandlerGestureEvent,
265
+ Context
266
+ >({
267
+ onActive: (_, context) => {
268
+ if (canCallActivateFunctions()) {
269
+ if (!context.didMeasureLayout) {
270
+ activateAnimation(context);
271
+ transformValue.value = calculateTransformValue();
272
+ setMenuProps();
273
+ context.didMeasureLayout = true;
274
+ }
275
+
276
+ if (!isActive.value) {
277
+ if (isHold) {
278
+ scaleHold();
279
+ } else {
280
+ scaleTap();
281
+ }
282
+ }
283
+ }
284
+ },
285
+ onFinish: (_, context) => {
286
+ context.didMeasureLayout = false;
287
+ if (isHold) {
288
+ scaleBack();
289
+ }
290
+ },
291
+ });
292
+
293
+ const overlayGestureEvent = useAnimatedGestureHandler<
294
+ TapGestureHandlerGestureEvent,
295
+ Context
296
+ >({
297
+ onActive: _ => {
298
+ if (closeOnTap) state.value = CONTEXT_MENU_STATE.END;
299
+ },
300
+ });
301
+ //#endregion
302
+
303
+ //#region animated styles & props
304
+ const animatedContainerStyle = useAnimatedStyle(() => {
305
+ const animateOpacity = () =>
306
+ withDelay(HOLD_ITEM_TRANSFORM_DURATION, withTiming(1, { duration: 0 }));
307
+
308
+ return {
309
+ opacity: isActive.value ? 0 : animateOpacity(),
310
+ transform: [
311
+ {
312
+ scale: isActive.value
313
+ ? withTiming(1, { duration: HOLD_ITEM_TRANSFORM_DURATION })
314
+ : itemScale.value,
315
+ },
316
+ ],
317
+ };
318
+ });
319
+ const containerStyle = React.useMemo(
320
+ () => [containerStyles, animatedContainerStyle],
321
+ [containerStyles, animatedContainerStyle]
322
+ );
323
+
324
+ const animatedPortalStyle = useAnimatedStyle(() => {
325
+ const animateOpacity = () =>
326
+ withDelay(HOLD_ITEM_TRANSFORM_DURATION, withTiming(0, { duration: 0 }));
327
+
328
+ let tY = calculateTransformValue();
329
+ const transformAnimation = () =>
330
+ disableMove
331
+ ? 0
332
+ : isActive.value
333
+ ? withSpring(tY, SPRING_CONFIGURATION)
334
+ : withTiming(-0.1, { duration: HOLD_ITEM_TRANSFORM_DURATION });
335
+
336
+ return {
337
+ zIndex: 10,
338
+ position: 'absolute',
339
+ top: itemRectY.value,
340
+ left: itemRectX.value,
341
+ width: itemRectWidth.value,
342
+ height: itemRectHeight.value,
343
+ opacity: isActive.value ? 1 : animateOpacity(),
344
+ transform: [
345
+ {
346
+ translateY: transformAnimation(),
347
+ },
348
+ {
349
+ scale: isActive.value
350
+ ? withTiming(1, { duration: HOLD_ITEM_TRANSFORM_DURATION })
351
+ : itemScale.value,
352
+ },
353
+ ],
354
+ };
355
+ });
356
+ const portalContainerStyle = useMemo(
357
+ () => [styles.holdItem, animatedPortalStyle],
358
+ [animatedPortalStyle]
359
+ );
360
+
361
+ const animatedPortalProps = useAnimatedProps<ViewProps>(() => ({
362
+ pointerEvents: isActive.value ? 'auto' : 'none',
363
+ }));
364
+ //#endregion
365
+
366
+ //#region animated effects
367
+ useAnimatedReaction(
368
+ () => state.value,
369
+ _state => {
370
+ if (_state === CONTEXT_MENU_STATE.END) {
371
+ isActive.value = false;
372
+ }
373
+ }
374
+ );
375
+ //#endregion
376
+
377
+ //#region components
378
+ const GestureHandler = useMemo(() => {
379
+ switch (activateOn) {
380
+ case `double-tap`:
381
+ return ({ children: handlerChildren }: GestureHandlerProps) => (
382
+ <TapGestureHandler
383
+ numberOfTaps={2}
384
+ onHandlerStateChange={gestureEvent}
385
+ >
386
+ {handlerChildren}
387
+ </TapGestureHandler>
388
+ );
389
+ case `tap`:
390
+ return ({ children: handlerChildren }: GestureHandlerProps) => (
391
+ <TapGestureHandler
392
+ numberOfTaps={1}
393
+ onHandlerStateChange={gestureEvent}
394
+ >
395
+ {handlerChildren}
396
+ </TapGestureHandler>
397
+ );
398
+ // default is hold
399
+ default:
400
+ return ({ children: handlerChildren }: GestureHandlerProps) => (
401
+ <LongPressGestureHandler
402
+ minDurationMs={longPressMinDurationMs}
403
+ onHandlerStateChange={gestureEvent}
404
+ >
405
+ {handlerChildren}
406
+ </LongPressGestureHandler>
407
+ );
408
+ }
409
+ }, [activateOn, gestureEvent]);
410
+
411
+ const PortalOverlay = useMemo(() => {
412
+ return () => (
413
+ <TapGestureHandler
414
+ numberOfTaps={1}
415
+ onHandlerStateChange={overlayGestureEvent}
416
+ >
417
+ <Animated.View style={styles.portalOverlay} />
418
+ </TapGestureHandler>
419
+ );
420
+ }, [overlayGestureEvent]);
421
+ //#endregion
422
+
423
+ //#region render
424
+ return (
425
+ <>
426
+ <GestureHandler>
427
+ <Animated.View ref={containerRef} style={containerStyle}>
428
+ {children}
429
+ </Animated.View>
430
+ </GestureHandler>
431
+
432
+ <Portal key={key} name={key}>
433
+ <Animated.View
434
+ key={key}
435
+ style={portalContainerStyle}
436
+ animatedProps={animatedPortalProps}
437
+ >
438
+ <PortalOverlay />
439
+ {children}
440
+ </Animated.View>
441
+ </Portal>
442
+ </>
443
+ );
444
+ //#endregion
445
+ };
446
+
447
+ const HoldItem = memo(HoldItemComponent);
448
+
449
+ export default HoldItem;
@@ -0,0 +1,2 @@
1
+ export { default } from './HoldItem';
2
+ export type { HoldItemProps } from './types';
@@ -0,0 +1,11 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ const styles = StyleSheet.create({
4
+ holdItem: { zIndex: 10, position: 'absolute' },
5
+ portalOverlay: {
6
+ ...StyleSheet.absoluteFillObject,
7
+ zIndex: 15,
8
+ },
9
+ });
10
+
11
+ export default styles;
@@ -0,0 +1,131 @@
1
+ import { ViewStyle } from 'react-native';
2
+ import { MenuItemProps } from '../menu/types';
3
+ import { TransformOriginAnchorPosition } from '../../utils/calculations';
4
+
5
+ export type HoldItemProps = {
6
+ /**
7
+ * List of context menu items.
8
+ * @type MenuItemProps[]
9
+ * @default []
10
+ */
11
+ items: MenuItemProps[];
12
+
13
+ /**
14
+ * Object of keys that same name with items to match parameters to onPress actions.
15
+ * @type { [name: string]: (string | number)[] }
16
+ * @examples
17
+ * ```js
18
+ * const items = [
19
+ * {text: 'Reply', onPress: (messageId) => {}},
20
+ * {text: 'Copy', onPress: (messageText) => {}},
21
+ * ]
22
+ * ...
23
+ * <HoldItem
24
+ * items={items}
25
+ * actionParams={{
26
+ * Reply: ['dd443224-7f43'],
27
+ * Copy: ['Hello World!']
28
+ * }}
29
+ * ><View/></HoldItem>
30
+ * ```
31
+ */
32
+ actionParams?: {
33
+ [name: string]: any[];
34
+ };
35
+
36
+ children: React.ReactElement | React.ReactElement[];
37
+
38
+ /**
39
+ * Menu anchor position is calculated automaticly.
40
+ * But you can override the calculation by passing an anchor position.
41
+ * @type TransformOriginAnchorPosition
42
+ * @examples
43
+ * menuAnchorPosition="top-bottom"
44
+ */
45
+ menuAnchorPosition?: TransformOriginAnchorPosition;
46
+
47
+ /**
48
+ * Disables moving holded item
49
+ * @type boolean
50
+ * @default false
51
+ * @examples
52
+ * disableMove={true}
53
+ */
54
+ disableMove?: boolean;
55
+
56
+ /**
57
+ * HoldItem wrapper component styles.
58
+ * You may need for some examples like dynamic width or hight like message boxes.
59
+ * See Whatsapp example.
60
+ * @type ViewStyles
61
+ * @default {}
62
+ * @examples
63
+ * containerStyles={{ maxWidth: '80%' }}
64
+ */
65
+ containerStyles?: ViewStyle | ViewStyle[];
66
+
67
+ /**
68
+ * Theme for menu background and texts
69
+ * @type string
70
+ * @examples
71
+ * theme="light"
72
+ */
73
+ theme?: 'light' | 'dark';
74
+
75
+ /**
76
+ * Set true if you want to open menu from bottom
77
+ * @type boolean
78
+ * @default false
79
+ * @examples
80
+ * bottom={true}
81
+ */
82
+ bottom?: boolean;
83
+
84
+ /**
85
+ * Set if you'd like a different tap activation
86
+ * @type string
87
+ * @default 'hold'
88
+ * @examples
89
+ * activateOn="hold"
90
+ */
91
+ activateOn?: 'tap' | 'double-tap' | 'hold';
92
+
93
+ /**
94
+ * Set if you'd like to enable haptic feedback on activation
95
+ * @type string
96
+ * @default 'Medium'
97
+ * @examples
98
+ * hapticFeedback="None"
99
+ */
100
+ hapticFeedback?:
101
+ | 'None'
102
+ | 'Selection'
103
+ | 'Light'
104
+ | 'Medium'
105
+ | 'Heavy'
106
+ | 'Success'
107
+ | 'Warning'
108
+ | 'Error';
109
+
110
+ /**
111
+ * Set true if you want to close menu when tap to HoldItem
112
+ * @type boolean
113
+ * @default false
114
+ * @examples
115
+ * closeOnTap={true}
116
+ */
117
+ closeOnTap?: boolean;
118
+
119
+ /**
120
+ * Set delay before long tap will activate gesture. May be useful to increase this value in lists
121
+ * @type number
122
+ * @default 150
123
+ * @examples
124
+ * longPressMinDurationMs={250}
125
+ */
126
+ longPressMinDurationMs?: number;
127
+ };
128
+
129
+ export type GestureHandlerProps = {
130
+ children: React.ReactElement | React.ReactElement[];
131
+ };
@@ -0,0 +1,33 @@
1
+ import React, { memo } from 'react';
2
+
3
+ import Animated, { useAnimatedProps } from 'react-native-reanimated';
4
+ import { useInternal } from '../../hooks';
5
+
6
+ type IconComponentProps = {
7
+ name: string;
8
+ size: number;
9
+ animatedProps: Partial<{ color: string }>;
10
+ };
11
+
12
+ // Update iconComponent type, React.ComponentClass<IconComponentProps, any>
13
+ type IconProps = {
14
+ iconComponent: any;
15
+ name: string;
16
+ };
17
+
18
+ const Icon = ({ iconComponent, name }: IconProps) => {
19
+ const { theme } = useInternal();
20
+ let AnimatedIcon = Animated.createAnimatedComponent<IconComponentProps>(
21
+ iconComponent
22
+ );
23
+
24
+ const iconProps = useAnimatedProps(() => {
25
+ return {
26
+ color: theme.value === 'light' ? 'black' : 'white',
27
+ };
28
+ }, [theme]);
29
+
30
+ return <AnimatedIcon name={name} size={18} animatedProps={iconProps} />;
31
+ };
32
+
33
+ export default memo(Icon);
@@ -0,0 +1 @@
1
+ export { default } from './Icon';
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ withSpring,
6
+ withTiming,
7
+ } from 'react-native-reanimated';
8
+
9
+ import MenuList from './MenuList';
10
+
11
+ import styles from './styles';
12
+ import { useInternal } from '../../hooks';
13
+ import {
14
+ HOLD_ITEM_TRANSFORM_DURATION,
15
+ CONTEXT_MENU_STATE,
16
+ SPRING_CONFIGURATION,
17
+ } from '../../constants';
18
+
19
+ const MenuComponent = () => {
20
+ const { state, menuProps } = useInternal();
21
+
22
+ const wrapperStyles = useAnimatedStyle(() => {
23
+ const anchorPositionVertical = menuProps.value.anchorPosition.split('-')[0];
24
+
25
+ const top =
26
+ anchorPositionVertical === 'top'
27
+ ? menuProps.value.itemHeight + menuProps.value.itemY + 8
28
+ : menuProps.value.itemY - 8;
29
+ const left = menuProps.value.itemX;
30
+ const width = menuProps.value.itemWidth;
31
+ const tY = menuProps.value.transformValue;
32
+
33
+ return {
34
+ top,
35
+ left,
36
+ width,
37
+ transform: [
38
+ {
39
+ translateY:
40
+ state.value === CONTEXT_MENU_STATE.ACTIVE
41
+ ? withSpring(tY, SPRING_CONFIGURATION)
42
+ : withTiming(0, { duration: HOLD_ITEM_TRANSFORM_DURATION }),
43
+ },
44
+ ],
45
+ };
46
+ }, [menuProps]);
47
+
48
+ return (
49
+ <Animated.View style={[styles.menuWrapper, wrapperStyles]}>
50
+ <MenuList />
51
+ </Animated.View>
52
+ );
53
+ };
54
+
55
+ const Menu = React.memo(MenuComponent);
56
+
57
+ export default Menu;