zkit-ui 1.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 (267) hide show
  1. package/README.md +267 -0
  2. package/dist/ComponentLibProvider.d.ts +15 -0
  3. package/dist/ComponentLibProvider.d.ts.map +1 -0
  4. package/dist/ComponentLibProvider.js +18 -0
  5. package/dist/assets/animations/spinner.json +1 -0
  6. package/dist/assets/icons/check.svg +4 -0
  7. package/dist/assets/icons/toast/error.webp +0 -0
  8. package/dist/assets/icons/toast/success.webp +0 -0
  9. package/dist/assets/icons/toast/warning.webp +0 -0
  10. package/dist/config.d.ts +16 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +42 -0
  13. package/dist/i18n/I18nProvider.d.ts +11 -0
  14. package/dist/i18n/I18nProvider.d.ts.map +1 -0
  15. package/dist/i18n/I18nProvider.js +84 -0
  16. package/dist/i18n/locale.d.ts +7 -0
  17. package/dist/i18n/locale.d.ts.map +1 -0
  18. package/dist/i18n/locale.js +87 -0
  19. package/dist/i18n/locales/de.d.ts +51 -0
  20. package/dist/i18n/locales/de.d.ts.map +1 -0
  21. package/dist/i18n/locales/de.js +62 -0
  22. package/dist/i18n/locales/en-US.d.ts +51 -0
  23. package/dist/i18n/locales/en-US.d.ts.map +1 -0
  24. package/dist/i18n/locales/en-US.js +62 -0
  25. package/dist/i18n/locales/index.d.ts +9 -0
  26. package/dist/i18n/locales/index.d.ts.map +1 -0
  27. package/dist/i18n/locales/index.js +23 -0
  28. package/dist/i18n/locales/ja.d.ts +51 -0
  29. package/dist/i18n/locales/ja.d.ts.map +1 -0
  30. package/dist/i18n/locales/ja.js +62 -0
  31. package/dist/i18n/locales/zh-CN.d.ts +51 -0
  32. package/dist/i18n/locales/zh-CN.d.ts.map +1 -0
  33. package/dist/i18n/locales/zh-CN.js +62 -0
  34. package/dist/i18n/locales/zh-TW.d.ts +51 -0
  35. package/dist/i18n/locales/zh-TW.d.ts.map +1 -0
  36. package/dist/i18n/locales/zh-TW.js +62 -0
  37. package/dist/i18n/types.d.ts +11 -0
  38. package/dist/i18n/types.d.ts.map +1 -0
  39. package/dist/i18n/types.js +2 -0
  40. package/dist/i18n/useI18n.d.ts +2 -0
  41. package/dist/i18n/useI18n.d.ts.map +1 -0
  42. package/dist/i18n/useI18n.js +41 -0
  43. package/dist/index.d.ts +57 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +90 -0
  46. package/dist/services/ActionDialogService/ActionDialog.d.ts +4 -0
  47. package/dist/services/ActionDialogService/ActionDialog.d.ts.map +1 -0
  48. package/dist/services/ActionDialogService/ActionDialog.js +531 -0
  49. package/dist/services/ActionDialogService/index.d.ts +4 -0
  50. package/dist/services/ActionDialogService/index.d.ts.map +1 -0
  51. package/dist/services/ActionDialogService/index.js +8 -0
  52. package/dist/services/ActionDialogService/service.d.ts +7 -0
  53. package/dist/services/ActionDialogService/service.d.ts.map +1 -0
  54. package/dist/services/ActionDialogService/service.js +309 -0
  55. package/dist/services/ActionDialogService/shared.d.ts +17 -0
  56. package/dist/services/ActionDialogService/shared.d.ts.map +1 -0
  57. package/dist/services/ActionDialogService/shared.js +167 -0
  58. package/dist/services/ActionDialogService/types.d.ts +160 -0
  59. package/dist/services/ActionDialogService/types.d.ts.map +1 -0
  60. package/dist/services/ActionDialogService/types.js +2 -0
  61. package/dist/services/CardToastService/index.d.ts +115 -0
  62. package/dist/services/CardToastService/index.d.ts.map +1 -0
  63. package/dist/services/CardToastService/index.js +899 -0
  64. package/dist/services/ImagePreviewService/index.d.ts +179 -0
  65. package/dist/services/ImagePreviewService/index.d.ts.map +1 -0
  66. package/dist/services/ImagePreviewService/index.js +1383 -0
  67. package/dist/services/LoadingService/index.d.ts +146 -0
  68. package/dist/services/LoadingService/index.d.ts.map +1 -0
  69. package/dist/services/LoadingService/index.js +827 -0
  70. package/dist/services/PermissionPurposeDialogService/index.d.ts +99 -0
  71. package/dist/services/PermissionPurposeDialogService/index.d.ts.map +1 -0
  72. package/dist/services/PermissionPurposeDialogService/index.js +360 -0
  73. package/dist/services/PickerService/index.d.ts +128 -0
  74. package/dist/services/PickerService/index.d.ts.map +1 -0
  75. package/dist/services/PickerService/index.js +250 -0
  76. package/dist/theme/ThemeProvider.d.ts +15 -0
  77. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  78. package/dist/theme/ThemeProvider.js +61 -0
  79. package/dist/theme/defaultTheme.d.ts +3 -0
  80. package/dist/theme/defaultTheme.d.ts.map +1 -0
  81. package/dist/theme/defaultTheme.js +28 -0
  82. package/dist/theme/mergeTheme.d.ts +3 -0
  83. package/dist/theme/mergeTheme.d.ts.map +1 -0
  84. package/dist/theme/mergeTheme.js +19 -0
  85. package/dist/theme/types.d.ts +17 -0
  86. package/dist/theme/types.d.ts.map +1 -0
  87. package/dist/theme/types.js +2 -0
  88. package/dist/theme/useTheme.d.ts +2 -0
  89. package/dist/theme/useTheme.d.ts.map +1 -0
  90. package/dist/theme/useTheme.js +46 -0
  91. package/dist/ui/Accordion/index.d.ts +99 -0
  92. package/dist/ui/Accordion/index.d.ts.map +1 -0
  93. package/dist/ui/Accordion/index.js +777 -0
  94. package/dist/ui/AddressCascader/index.d.ts +58 -0
  95. package/dist/ui/AddressCascader/index.d.ts.map +1 -0
  96. package/dist/ui/AddressCascader/index.js +710 -0
  97. package/dist/ui/BetweenTime/index.d.ts +82 -0
  98. package/dist/ui/BetweenTime/index.d.ts.map +1 -0
  99. package/dist/ui/BetweenTime/index.js +702 -0
  100. package/dist/ui/Button/index.d.ts +93 -0
  101. package/dist/ui/Button/index.d.ts.map +1 -0
  102. package/dist/ui/Button/index.js +832 -0
  103. package/dist/ui/Checkbox/index.d.ts +103 -0
  104. package/dist/ui/Checkbox/index.d.ts.map +1 -0
  105. package/dist/ui/Checkbox/index.js +694 -0
  106. package/dist/ui/DatePicker/index.d.ts +5 -0
  107. package/dist/ui/DatePicker/index.d.ts.map +1 -0
  108. package/dist/ui/DatePicker/index.js +157 -0
  109. package/dist/ui/DatePicker/types.d.ts +99 -0
  110. package/dist/ui/DatePicker/types.d.ts.map +1 -0
  111. package/dist/ui/DatePicker/types.js +2 -0
  112. package/dist/ui/DatePicker/utils.d.ts +32 -0
  113. package/dist/ui/DatePicker/utils.d.ts.map +1 -0
  114. package/dist/ui/DatePicker/utils.js +316 -0
  115. package/dist/ui/LinkedScroll/index.d.ts +78 -0
  116. package/dist/ui/LinkedScroll/index.d.ts.map +1 -0
  117. package/dist/ui/LinkedScroll/index.js +470 -0
  118. package/dist/ui/LoadingSpinner/index.d.ts +8 -0
  119. package/dist/ui/LoadingSpinner/index.d.ts.map +1 -0
  120. package/dist/ui/LoadingSpinner/index.js +122 -0
  121. package/dist/ui/Picker/actionBar.d.ts +14 -0
  122. package/dist/ui/Picker/actionBar.d.ts.map +1 -0
  123. package/dist/ui/Picker/actionBar.js +28 -0
  124. package/dist/ui/Picker/index.d.ts +5 -0
  125. package/dist/ui/Picker/index.d.ts.map +1 -0
  126. package/dist/ui/Picker/index.js +412 -0
  127. package/dist/ui/Picker/types.d.ts +80 -0
  128. package/dist/ui/Picker/types.d.ts.map +1 -0
  129. package/dist/ui/Picker/types.js +2 -0
  130. package/dist/ui/Picker/utils.d.ts +27 -0
  131. package/dist/ui/Picker/utils.d.ts.map +1 -0
  132. package/dist/ui/Picker/utils.js +182 -0
  133. package/dist/ui/Radio/index.d.ts +95 -0
  134. package/dist/ui/Radio/index.d.ts.map +1 -0
  135. package/dist/ui/Radio/index.js +633 -0
  136. package/dist/ui/Sheet/index.d.ts +12 -0
  137. package/dist/ui/Sheet/index.d.ts.map +1 -0
  138. package/dist/ui/Sheet/index.js +832 -0
  139. package/dist/ui/Sheet/nativeBottom.d.ts +52 -0
  140. package/dist/ui/Sheet/nativeBottom.d.ts.map +1 -0
  141. package/dist/ui/Sheet/nativeBottom.js +442 -0
  142. package/dist/ui/Sheet/nativeBottom.web.d.ts +18 -0
  143. package/dist/ui/Sheet/nativeBottom.web.d.ts.map +1 -0
  144. package/dist/ui/Sheet/nativeBottom.web.js +46 -0
  145. package/dist/ui/Sheet/slots.d.ts +5 -0
  146. package/dist/ui/Sheet/slots.d.ts.map +1 -0
  147. package/dist/ui/Sheet/slots.js +40 -0
  148. package/dist/ui/Sheet/types.d.ts +131 -0
  149. package/dist/ui/Sheet/types.d.ts.map +1 -0
  150. package/dist/ui/Sheet/types.js +2 -0
  151. package/dist/ui/SliderCaptcha/index.d.ts +57 -0
  152. package/dist/ui/SliderCaptcha/index.d.ts.map +1 -0
  153. package/dist/ui/SliderCaptcha/index.js +586 -0
  154. package/dist/ui/Switch/index.d.ts +72 -0
  155. package/dist/ui/Switch/index.d.ts.map +1 -0
  156. package/dist/ui/Switch/index.js +671 -0
  157. package/dist/ui/Text/index.d.ts +41 -0
  158. package/dist/ui/Text/index.d.ts.map +1 -0
  159. package/dist/ui/Text/index.js +325 -0
  160. package/dist/ui/TextInput/TextInputPrimitive.d.ts +8 -0
  161. package/dist/ui/TextInput/TextInputPrimitive.d.ts.map +1 -0
  162. package/dist/ui/TextInput/TextInputPrimitive.js +54 -0
  163. package/dist/ui/TextInput/index.d.ts +89 -0
  164. package/dist/ui/TextInput/index.d.ts.map +1 -0
  165. package/dist/ui/TextInput/index.js +545 -0
  166. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.d.ts +39 -0
  167. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.d.ts.map +1 -0
  168. package/dist/ui/WheelColumn/ZKitWheelPickerNativeComponent.js +60 -0
  169. package/dist/ui/WheelColumn/constants.d.ts +2 -0
  170. package/dist/ui/WheelColumn/constants.d.ts.map +1 -0
  171. package/dist/ui/WheelColumn/constants.js +4 -0
  172. package/dist/ui/WheelColumn/index.d.ts +59 -0
  173. package/dist/ui/WheelColumn/index.d.ts.map +1 -0
  174. package/dist/ui/WheelColumn/index.js +547 -0
  175. package/ios/ZKitWheelPicker.h +18 -0
  176. package/ios/ZKitWheelPicker.mm +230 -0
  177. package/ios/ZKitWheelPickerLabel.h +10 -0
  178. package/ios/ZKitWheelPickerLabel.mm +31 -0
  179. package/ios/ZKitWheelPickerManager.h +5 -0
  180. package/ios/ZKitWheelPickerManager.mm +53 -0
  181. package/package.json +75 -0
  182. package/react-native.config.js +10 -0
  183. package/src/ComponentLibProvider.tsx +56 -0
  184. package/src/assets/animations/spinner.json +1 -0
  185. package/src/assets/icons/check.svg +4 -0
  186. package/src/assets/icons/toast/error.webp +0 -0
  187. package/src/assets/icons/toast/success.webp +0 -0
  188. package/src/assets/icons/toast/warning.webp +0 -0
  189. package/src/config.ts +65 -0
  190. package/src/i18n/I18nProvider.tsx +69 -0
  191. package/src/i18n/locale.ts +108 -0
  192. package/src/i18n/locales/de.ts +67 -0
  193. package/src/i18n/locales/en-US.ts +67 -0
  194. package/src/i18n/locales/index.ts +19 -0
  195. package/src/i18n/locales/ja.ts +67 -0
  196. package/src/i18n/locales/zh-CN.ts +67 -0
  197. package/src/i18n/locales/zh-TW.ts +67 -0
  198. package/src/i18n/types.ts +21 -0
  199. package/src/i18n/useI18n.ts +7 -0
  200. package/src/index.ts +395 -0
  201. package/src/services/ActionDialogService/ActionDialog.tsx +756 -0
  202. package/src/services/ActionDialogService/README.md +146 -0
  203. package/src/services/ActionDialogService/index.tsx +39 -0
  204. package/src/services/ActionDialogService/service.tsx +355 -0
  205. package/src/services/ActionDialogService/shared.ts +204 -0
  206. package/src/services/ActionDialogService/types.ts +197 -0
  207. package/src/services/CardToastService/README.md +128 -0
  208. package/src/services/CardToastService/index.tsx +1284 -0
  209. package/src/services/ImagePreviewService/README.md +67 -0
  210. package/src/services/ImagePreviewService/index.tsx +2148 -0
  211. package/src/services/LoadingService/README.md +128 -0
  212. package/src/services/LoadingService/index.tsx +1113 -0
  213. package/src/services/PermissionPurposeDialogService/README.md +80 -0
  214. package/src/services/PermissionPurposeDialogService/index.tsx +449 -0
  215. package/src/services/PickerService/README.md +154 -0
  216. package/src/services/PickerService/index.tsx +410 -0
  217. package/src/theme/ThemeProvider.tsx +40 -0
  218. package/src/theme/defaultTheme.ts +30 -0
  219. package/src/theme/mergeTheme.ts +16 -0
  220. package/src/theme/types.ts +36 -0
  221. package/src/theme/useTheme.ts +10 -0
  222. package/src/types/svg.d.ts +9 -0
  223. package/src/types/vant-area-data.d.ts +19 -0
  224. package/src/ui/Accordion/README.md +67 -0
  225. package/src/ui/Accordion/index.tsx +1221 -0
  226. package/src/ui/AddressCascader/README.md +144 -0
  227. package/src/ui/AddressCascader/index.tsx +1084 -0
  228. package/src/ui/BetweenTime/README.md +490 -0
  229. package/src/ui/BetweenTime/index.tsx +1001 -0
  230. package/src/ui/Button/README.md +186 -0
  231. package/src/ui/Button/index.tsx +1356 -0
  232. package/src/ui/Checkbox/README.md +211 -0
  233. package/src/ui/Checkbox/index.tsx +1231 -0
  234. package/src/ui/DatePicker/README.md +117 -0
  235. package/src/ui/DatePicker/index.tsx +295 -0
  236. package/src/ui/DatePicker/types.ts +115 -0
  237. package/src/ui/DatePicker/utils.ts +405 -0
  238. package/src/ui/LinkedScroll/README.md +55 -0
  239. package/src/ui/LinkedScroll/index.tsx +809 -0
  240. package/src/ui/LoadingSpinner/index.tsx +104 -0
  241. package/src/ui/Picker/README.md +190 -0
  242. package/src/ui/Picker/actionBar.tsx +70 -0
  243. package/src/ui/Picker/index.tsx +694 -0
  244. package/src/ui/Picker/types.ts +94 -0
  245. package/src/ui/Picker/utils.ts +239 -0
  246. package/src/ui/Radio/README.md +207 -0
  247. package/src/ui/Radio/index.tsx +1117 -0
  248. package/src/ui/Sheet/README.md +82 -0
  249. package/src/ui/Sheet/index.tsx +1163 -0
  250. package/src/ui/Sheet/nativeBottom.tsx +683 -0
  251. package/src/ui/Sheet/nativeBottom.web.tsx +40 -0
  252. package/src/ui/Sheet/slots.tsx +75 -0
  253. package/src/ui/Sheet/types.ts +195 -0
  254. package/src/ui/SliderCaptcha/README.md +61 -0
  255. package/src/ui/SliderCaptcha/index.tsx +816 -0
  256. package/src/ui/Switch/README.md +143 -0
  257. package/src/ui/Switch/index.tsx +1092 -0
  258. package/src/ui/Text/README.md +157 -0
  259. package/src/ui/Text/index.tsx +450 -0
  260. package/src/ui/TextInput/README.md +197 -0
  261. package/src/ui/TextInput/TextInputPrimitive.tsx +44 -0
  262. package/src/ui/TextInput/index.tsx +942 -0
  263. package/src/ui/WheelColumn/README.md +104 -0
  264. package/src/ui/WheelColumn/ZKitWheelPickerNativeComponent.tsx +68 -0
  265. package/src/ui/WheelColumn/constants.ts +1 -0
  266. package/src/ui/WheelColumn/index.tsx +826 -0
  267. package/zkit-ui.podspec +19 -0
@@ -0,0 +1,809 @@
1
+ import * as React from 'react';
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+ import type {
4
+ NativeScrollEvent,
5
+ NativeSyntheticEvent,
6
+ StyleProp,
7
+ TextStyle,
8
+ ViewStyle,
9
+ } from 'react-native';
10
+ import { FlashList } from '@shopify/flash-list';
11
+ import type {
12
+ FlashListProps,
13
+ FlashListRef,
14
+ ListRenderItemInfo,
15
+ } from '@shopify/flash-list';
16
+ import { wp } from 'zkit-tools';
17
+ import { useTheme } from '../../theme/useTheme';
18
+ import { Text } from '../Text';
19
+
20
+ export type LinkedScrollValue = string | number;
21
+
22
+ export type LinkedScrollItem<
23
+ Value extends LinkedScrollValue = LinkedScrollValue,
24
+ Data = unknown,
25
+ > = {
26
+ value: Value;
27
+ label: React.ReactNode;
28
+ disabled?: boolean;
29
+ data?: Data;
30
+ accessibilityLabel?: string;
31
+ };
32
+
33
+ export type LinkedScrollChangeSource = 'menu' | 'content';
34
+
35
+ export type LinkedScrollChangeMeta<
36
+ Value extends LinkedScrollValue = LinkedScrollValue,
37
+ Data = unknown,
38
+ > = {
39
+ source: LinkedScrollChangeSource;
40
+ item: LinkedScrollItem<Value, Data>;
41
+ index: number;
42
+ };
43
+
44
+ export type LinkedScrollMenuItemRenderContext<
45
+ Value extends LinkedScrollValue = LinkedScrollValue,
46
+ Data = unknown,
47
+ > = {
48
+ item: LinkedScrollItem<Value, Data>;
49
+ index: number;
50
+ selected: boolean;
51
+ disabled: boolean;
52
+ press: () => void;
53
+ };
54
+
55
+ export type LinkedScrollSectionRenderContext<
56
+ Value extends LinkedScrollValue = LinkedScrollValue,
57
+ Data = unknown,
58
+ > = {
59
+ item: LinkedScrollItem<Value, Data>;
60
+ index: number;
61
+ selected: boolean;
62
+ };
63
+
64
+ type ContentListReservedProps =
65
+ | 'data'
66
+ | 'extraData'
67
+ | 'horizontal'
68
+ | 'keyExtractor'
69
+ | 'renderItem'
70
+ | 'onViewableItemsChanged'
71
+ | 'viewabilityConfig'
72
+ | 'viewabilityConfigCallbackPairs';
73
+
74
+ type MenuListReservedProps =
75
+ | 'data'
76
+ | 'extraData'
77
+ | 'horizontal'
78
+ | 'keyExtractor'
79
+ | 'renderItem';
80
+
81
+ export type LinkedScrollProps<
82
+ Value extends LinkedScrollValue = LinkedScrollValue,
83
+ Data = unknown,
84
+ > = {
85
+ items: ReadonlyArray<LinkedScrollItem<Value, Data>>;
86
+ renderSection: (context: LinkedScrollSectionRenderContext<Value, Data>) => React.ReactNode;
87
+
88
+ value?: Value;
89
+ defaultValue?: Value;
90
+ onChange?: (
91
+ value: Value,
92
+ item: LinkedScrollItem<Value, Data>,
93
+ meta: LinkedScrollChangeMeta<Value, Data>
94
+ ) => void;
95
+
96
+ disabled?: boolean;
97
+ menuPosition?: 'left' | 'right';
98
+
99
+ menuWidth?: number;
100
+ menuItemHeight?: number;
101
+ menuScrollViewPosition?: number;
102
+ contentScrollViewPosition?: number;
103
+ contentScrollViewOffset?: number;
104
+ sectionGap?: number;
105
+ contentPaddingHorizontal?: number;
106
+ contentPaddingVertical?: number;
107
+ activeViewAreaCoveragePercentThreshold?: number;
108
+ programmaticScrollGuardDuration?: number;
109
+
110
+ activeColor?: string;
111
+ inactiveColor?: string;
112
+ disabledColor?: string;
113
+ activeBackgroundColor?: string;
114
+ menuBackgroundColor?: string;
115
+ contentBackgroundColor?: string;
116
+
117
+ keyExtractor?: (item: LinkedScrollItem<Value, Data>, index: number) => string;
118
+ renderMenuItem?: (context: LinkedScrollMenuItemRenderContext<Value, Data>) => React.ReactNode;
119
+ getMenuItemType?: FlashListProps<LinkedScrollItem<Value, Data>>['getItemType'];
120
+ getSectionType?: FlashListProps<LinkedScrollItem<Value, Data>>['getItemType'];
121
+ viewabilityConfig?: FlashListProps<LinkedScrollItem<Value, Data>>['viewabilityConfig'];
122
+
123
+ style?: StyleProp<ViewStyle>;
124
+ menuStyle?: StyleProp<ViewStyle>;
125
+ contentStyle?: StyleProp<ViewStyle>;
126
+ menuItemStyle?: StyleProp<ViewStyle>;
127
+ activeMenuItemStyle?: StyleProp<ViewStyle>;
128
+ disabledMenuItemStyle?: StyleProp<ViewStyle>;
129
+ menuItemTextStyle?: StyleProp<TextStyle>;
130
+ activeMenuItemTextStyle?: StyleProp<TextStyle>;
131
+ disabledMenuItemTextStyle?: StyleProp<TextStyle>;
132
+ sectionStyle?: StyleProp<ViewStyle>;
133
+ activeSectionStyle?: StyleProp<ViewStyle>;
134
+
135
+ menuListProps?: Omit<FlashListProps<LinkedScrollItem<Value, Data>>, MenuListReservedProps>;
136
+ contentListProps?: Omit<FlashListProps<LinkedScrollItem<Value, Data>>, ContentListReservedProps>;
137
+ testID?: string;
138
+ };
139
+
140
+ const DEFAULT_MENU_WIDTH = wp(96);
141
+ const DEFAULT_MENU_ITEM_HEIGHT = wp(52);
142
+ const DEFAULT_SECTION_GAP = wp(12);
143
+ const DEFAULT_CONTENT_PADDING_HORIZONTAL = wp(12);
144
+ const DEFAULT_CONTENT_PADDING_VERTICAL = wp(12);
145
+ const MIN_MENU_WIDTH = wp(64);
146
+ const MIN_TOUCH_TARGET = wp(44);
147
+ const ONE_PX = wp(1);
148
+ const DEFAULT_VIEW_AREA_THRESHOLD = 1;
149
+ const DEFAULT_PROGRAMMATIC_GUARD_DURATION = 2200;
150
+ const PROGRAMMATIC_SETTLE_DURATION = 120;
151
+ const DEFAULT_MINIMUM_VIEW_TIME = 32;
152
+ const DISABLED_OPACITY = 0.45;
153
+ const PRESSED_OPACITY = 0.72;
154
+ const MENU_VISIBLE_INDEX_BUFFER = 1;
155
+ const DISABLED_MAINTAIN_VISIBLE_CONTENT_POSITION = { disabled: true };
156
+
157
+ function clampNumber(value: number, min: number, max: number) {
158
+ if (!Number.isFinite(value)) return min;
159
+ return Math.min(Math.max(value, min), max);
160
+ }
161
+
162
+ function resolveNonNegative(value: number | undefined, fallback: number) {
163
+ if (value == null || !Number.isFinite(value)) return fallback;
164
+ return Math.max(0, value);
165
+ }
166
+
167
+ function resolvePositive(value: number | undefined, fallback: number, min: number) {
168
+ if (value == null || !Number.isFinite(value)) return fallback;
169
+ return Math.max(min, value);
170
+ }
171
+
172
+ function findIndexByValue<Value extends LinkedScrollValue, Data>(
173
+ items: ReadonlyArray<LinkedScrollItem<Value, Data>>,
174
+ value: Value | undefined
175
+ ) {
176
+ if (value === undefined) return -1;
177
+ return items.findIndex((item) => Object.is(item.value, value));
178
+ }
179
+
180
+ function findInitialIndex<Value extends LinkedScrollValue, Data>(
181
+ items: ReadonlyArray<LinkedScrollItem<Value, Data>>
182
+ ) {
183
+ const enabledIndex = items.findIndex((item) => !item.disabled);
184
+ if (enabledIndex >= 0) return enabledIndex;
185
+ return items.length > 0 ? 0 : -1;
186
+ }
187
+
188
+ function renderDefaultMenuLabel(label: React.ReactNode, style: StyleProp<TextStyle>) {
189
+ if (typeof label === 'string' || typeof label === 'number') {
190
+ return (
191
+ <Text numberOfLines={1} style={style}>
192
+ {label}
193
+ </Text>
194
+ );
195
+ }
196
+
197
+ return <View style={styles.menuLabelNode}>{label}</View>;
198
+ }
199
+
200
+ export function LinkedScroll<
201
+ Value extends LinkedScrollValue = LinkedScrollValue,
202
+ Data = unknown,
203
+ >({
204
+ items,
205
+ renderSection,
206
+ value: valueProp,
207
+ defaultValue,
208
+ onChange,
209
+ disabled = false,
210
+ menuPosition = 'left',
211
+ menuWidth,
212
+ menuItemHeight,
213
+ menuScrollViewPosition = 0.5,
214
+ contentScrollViewPosition = 0,
215
+ contentScrollViewOffset,
216
+ sectionGap,
217
+ contentPaddingHorizontal,
218
+ contentPaddingVertical,
219
+ activeViewAreaCoveragePercentThreshold,
220
+ programmaticScrollGuardDuration = DEFAULT_PROGRAMMATIC_GUARD_DURATION,
221
+ activeColor,
222
+ inactiveColor,
223
+ disabledColor,
224
+ activeBackgroundColor,
225
+ menuBackgroundColor,
226
+ contentBackgroundColor,
227
+ keyExtractor,
228
+ renderMenuItem,
229
+ getMenuItemType,
230
+ getSectionType,
231
+ viewabilityConfig,
232
+ style,
233
+ menuStyle,
234
+ contentStyle,
235
+ menuItemStyle,
236
+ activeMenuItemStyle,
237
+ disabledMenuItemStyle,
238
+ menuItemTextStyle,
239
+ activeMenuItemTextStyle,
240
+ disabledMenuItemTextStyle,
241
+ sectionStyle,
242
+ activeSectionStyle,
243
+ menuListProps,
244
+ contentListProps,
245
+ testID,
246
+ }: LinkedScrollProps<Value, Data>) {
247
+ const theme = useTheme();
248
+
249
+ const isControlled = valueProp !== undefined;
250
+ const [uncontrolledValue, setUncontrolledValue] = React.useState<Value | undefined>(() => {
251
+ if (defaultValue !== undefined) return defaultValue;
252
+ const initialIndex = findInitialIndex(items);
253
+ return initialIndex >= 0 ? items[initialIndex]?.value : undefined;
254
+ });
255
+
256
+ const currentValue = isControlled ? valueProp : uncontrolledValue;
257
+ const rawActiveIndex = findIndexByValue(items, currentValue);
258
+ const fallbackActiveIndex = findInitialIndex(items);
259
+ const activeIndex = rawActiveIndex >= 0 ? rawActiveIndex : fallbackActiveIndex;
260
+ const activeItem = activeIndex >= 0 ? items[activeIndex] : undefined;
261
+ const activeValue = activeItem?.value;
262
+
263
+ const resolvedMenuWidth = resolvePositive(menuWidth, DEFAULT_MENU_WIDTH, MIN_MENU_WIDTH);
264
+ const resolvedMenuItemHeight = resolvePositive(
265
+ menuItemHeight,
266
+ DEFAULT_MENU_ITEM_HEIGHT,
267
+ MIN_TOUCH_TARGET
268
+ );
269
+ const resolvedSectionGap = resolveNonNegative(sectionGap, DEFAULT_SECTION_GAP);
270
+ const resolvedContentPaddingHorizontal = resolveNonNegative(
271
+ contentPaddingHorizontal,
272
+ DEFAULT_CONTENT_PADDING_HORIZONTAL
273
+ );
274
+ const resolvedContentPaddingVertical = resolveNonNegative(
275
+ contentPaddingVertical,
276
+ DEFAULT_CONTENT_PADDING_VERTICAL
277
+ );
278
+ const resolvedContentScrollViewOffset = resolveNonNegative(contentScrollViewOffset, 0);
279
+ const resolvedMenuScrollViewPosition = clampNumber(menuScrollViewPosition, 0, 1);
280
+ const resolvedContentScrollViewPosition = clampNumber(contentScrollViewPosition, 0, 1);
281
+ const resolvedViewAreaThreshold = clampNumber(
282
+ activeViewAreaCoveragePercentThreshold ?? DEFAULT_VIEW_AREA_THRESHOLD,
283
+ 0,
284
+ 100
285
+ );
286
+ const resolvedProgrammaticGuardDuration = Math.max(
287
+ 0,
288
+ Number.isFinite(programmaticScrollGuardDuration) ? programmaticScrollGuardDuration : 0
289
+ );
290
+ const resolvedActiveColor = activeColor ?? theme.colors.primary;
291
+ const resolvedInactiveColor = inactiveColor ?? theme.colors.muted;
292
+ const resolvedDisabledColor = disabledColor ?? theme.colors.disabled;
293
+ const resolvedActiveBackgroundColor = activeBackgroundColor ?? theme.colors.secondary;
294
+ const resolvedMenuBackgroundColor = menuBackgroundColor ?? '#F7F8FA';
295
+ const resolvedContentBackgroundColor = contentBackgroundColor ?? theme.colors.surface;
296
+
297
+ const menuListRef = React.useRef<FlashListRef<LinkedScrollItem<Value, Data>>>(null);
298
+ const contentListRef = React.useRef<FlashListRef<LinkedScrollItem<Value, Data>>>(null);
299
+ const pendingProgrammaticValueRef = React.useRef<Value | null>(null);
300
+ const programmaticTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
301
+ const programmaticSettleTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
302
+ const programmaticScrollIdRef = React.useRef(0);
303
+ const activeValueRef = React.useRef<Value | undefined>(activeValue);
304
+ const disabledRef = React.useRef(disabled);
305
+ const lastInternalCommitValueRef = React.useRef<Value | null>(null);
306
+ const lastChangeSourceRef = React.useRef<LinkedScrollChangeSource | 'external'>('external');
307
+ const didInitialContentSyncRef = React.useRef(false);
308
+ const didInitialMenuSyncRef = React.useRef(false);
309
+ const itemsRef = React.useRef(items);
310
+
311
+ itemsRef.current = items;
312
+ activeValueRef.current = activeValue;
313
+ disabledRef.current = disabled;
314
+
315
+ const clearProgrammaticTimer = React.useCallback(() => {
316
+ if (programmaticTimerRef.current == null) return;
317
+ clearTimeout(programmaticTimerRef.current);
318
+ programmaticTimerRef.current = null;
319
+ }, []);
320
+
321
+ const clearProgrammaticSettleTimer = React.useCallback(() => {
322
+ if (programmaticSettleTimerRef.current == null) return;
323
+ clearTimeout(programmaticSettleTimerRef.current);
324
+ programmaticSettleTimerRef.current = null;
325
+ }, []);
326
+
327
+ const releaseProgrammaticSync = React.useCallback((scrollId?: number) => {
328
+ if (scrollId != null && scrollId !== programmaticScrollIdRef.current) return;
329
+ pendingProgrammaticValueRef.current = null;
330
+ clearProgrammaticTimer();
331
+ clearProgrammaticSettleTimer();
332
+ }, [clearProgrammaticSettleTimer, clearProgrammaticTimer]);
333
+
334
+ const releaseProgrammaticSyncRef = React.useRef(releaseProgrammaticSync);
335
+ releaseProgrammaticSyncRef.current = releaseProgrammaticSync;
336
+
337
+ const setProgrammaticTarget = React.useCallback(
338
+ (nextValue: Value) => {
339
+ const scrollId = programmaticScrollIdRef.current + 1;
340
+ programmaticScrollIdRef.current = scrollId;
341
+ pendingProgrammaticValueRef.current = nextValue;
342
+ clearProgrammaticTimer();
343
+ if (resolvedProgrammaticGuardDuration <= 0) return scrollId;
344
+ programmaticTimerRef.current = setTimeout(() => {
345
+ if (scrollId !== programmaticScrollIdRef.current) return;
346
+ pendingProgrammaticValueRef.current = null;
347
+ programmaticTimerRef.current = null;
348
+ }, resolvedProgrammaticGuardDuration);
349
+
350
+ return scrollId;
351
+ },
352
+ [clearProgrammaticTimer, resolvedProgrammaticGuardDuration]
353
+ );
354
+
355
+ const finishProgrammaticSync = React.useCallback(
356
+ (scrollId: number | undefined) => {
357
+ if (scrollId == null) return;
358
+ clearProgrammaticSettleTimer();
359
+ programmaticSettleTimerRef.current = setTimeout(() => {
360
+ programmaticSettleTimerRef.current = null;
361
+ releaseProgrammaticSyncRef.current(scrollId);
362
+ }, PROGRAMMATIC_SETTLE_DURATION);
363
+ },
364
+ [clearProgrammaticSettleTimer]
365
+ );
366
+
367
+ React.useEffect(() => {
368
+ return () => {
369
+ clearProgrammaticTimer();
370
+ clearProgrammaticSettleTimer();
371
+ };
372
+ }, [clearProgrammaticSettleTimer, clearProgrammaticTimer]);
373
+
374
+ React.useEffect(() => {
375
+ if (isControlled || rawActiveIndex >= 0 || activeItem == null) return;
376
+ setUncontrolledValue(activeItem.value);
377
+ }, [activeItem, isControlled, rawActiveIndex]);
378
+
379
+ const commitValue = React.useCallback(
380
+ (
381
+ nextValue: Value,
382
+ item: LinkedScrollItem<Value, Data>,
383
+ index: number,
384
+ source: LinkedScrollChangeSource
385
+ ) => {
386
+ if (Object.is(activeValueRef.current, nextValue)) return;
387
+
388
+ lastInternalCommitValueRef.current = nextValue;
389
+ lastChangeSourceRef.current = source;
390
+ if (!isControlled) {
391
+ activeValueRef.current = nextValue;
392
+ setUncontrolledValue(nextValue);
393
+ }
394
+
395
+ onChange?.(nextValue, item, { source, item, index });
396
+ },
397
+ [isControlled, onChange]
398
+ );
399
+
400
+ const commitValueRef = React.useRef(commitValue);
401
+ commitValueRef.current = commitValue;
402
+
403
+ const resolvedKeyExtractor = React.useCallback(
404
+ (item: LinkedScrollItem<Value, Data>, index: number) => {
405
+ return keyExtractor?.(item, index) ?? String(item.value);
406
+ },
407
+ [keyExtractor]
408
+ );
409
+
410
+ const scrollMenuToIndex = React.useCallback(
411
+ (index: number, animated: boolean) => {
412
+ if (index < 0) return Promise.resolve();
413
+ return menuListRef.current?.scrollToIndex({
414
+ index,
415
+ animated,
416
+ viewPosition: resolvedMenuScrollViewPosition,
417
+ }) ?? Promise.resolve();
418
+ },
419
+ [resolvedMenuScrollViewPosition]
420
+ );
421
+
422
+ const isMenuIndexComfortablyVisible = React.useCallback((index: number) => {
423
+ if (index < 0) return true;
424
+
425
+ const visibleRange = menuListRef.current?.computeVisibleIndices();
426
+ if (!visibleRange) return false;
427
+
428
+ const { startIndex, endIndex } = visibleRange;
429
+ if (!Number.isFinite(startIndex) || !Number.isFinite(endIndex)) return false;
430
+ if (startIndex < 0 || endIndex < 0) return false;
431
+
432
+ return (
433
+ index >= startIndex + MENU_VISIBLE_INDEX_BUFFER &&
434
+ index <= endIndex - MENU_VISIBLE_INDEX_BUFFER
435
+ );
436
+ }, []);
437
+
438
+ const scrollContentToIndex = React.useCallback(
439
+ (index: number, animated: boolean) => {
440
+ if (index < 0) return Promise.resolve();
441
+ return contentListRef.current?.scrollToIndex({
442
+ index,
443
+ animated,
444
+ viewPosition: resolvedContentScrollViewPosition,
445
+ viewOffset: resolvedContentScrollViewOffset,
446
+ }) ?? Promise.resolve();
447
+ },
448
+ [resolvedContentScrollViewOffset, resolvedContentScrollViewPosition]
449
+ );
450
+
451
+ React.useEffect(() => {
452
+ if (activeIndex < 0) return;
453
+ const didInitialMenuSync = didInitialMenuSyncRef.current;
454
+ didInitialMenuSyncRef.current = true;
455
+
456
+ const source =
457
+ activeValue !== undefined && Object.is(lastInternalCommitValueRef.current, activeValue)
458
+ ? lastChangeSourceRef.current
459
+ : 'external';
460
+
461
+ const frame = requestAnimationFrame(() => {
462
+ if (!didInitialMenuSync || source === 'external' || source === 'menu') {
463
+ void scrollMenuToIndex(activeIndex, didInitialMenuSync);
464
+ return;
465
+ }
466
+
467
+ if (!isMenuIndexComfortablyVisible(activeIndex)) {
468
+ void scrollMenuToIndex(activeIndex, false);
469
+ }
470
+ });
471
+
472
+ return () => cancelAnimationFrame(frame);
473
+ }, [activeIndex, activeValue, isMenuIndexComfortablyVisible, scrollMenuToIndex]);
474
+
475
+ React.useEffect(() => {
476
+ if (activeIndex < 0 || activeValue === undefined) return;
477
+
478
+ if (!didInitialContentSyncRef.current) {
479
+ didInitialContentSyncRef.current = true;
480
+ if (activeIndex <= 0) return;
481
+ const frame = requestAnimationFrame(() => scrollContentToIndex(activeIndex, false));
482
+ return () => cancelAnimationFrame(frame);
483
+ }
484
+
485
+ if (!isControlled || valueProp === undefined || rawActiveIndex < 0) return;
486
+ if (Object.is(lastInternalCommitValueRef.current, valueProp)) return;
487
+
488
+ const scrollId = setProgrammaticTarget(valueProp);
489
+ const frame = requestAnimationFrame(() => {
490
+ void scrollContentToIndex(rawActiveIndex, true).then(() => finishProgrammaticSync(scrollId));
491
+ });
492
+ return () => cancelAnimationFrame(frame);
493
+ }, [
494
+ activeIndex,
495
+ activeValue,
496
+ isControlled,
497
+ rawActiveIndex,
498
+ finishProgrammaticSync,
499
+ scrollContentToIndex,
500
+ setProgrammaticTarget,
501
+ valueProp,
502
+ ]);
503
+
504
+ const handleMenuPress = React.useCallback(
505
+ (item: LinkedScrollItem<Value, Data>, index: number) => {
506
+ if (disabled || item.disabled) return;
507
+ const scrollId = setProgrammaticTarget(item.value);
508
+ commitValue(item.value, item, index, 'menu');
509
+ void scrollMenuToIndex(index, true);
510
+ void scrollContentToIndex(index, true).then(() => finishProgrammaticSync(scrollId));
511
+ },
512
+ [
513
+ commitValue,
514
+ disabled,
515
+ finishProgrammaticSync,
516
+ scrollContentToIndex,
517
+ scrollMenuToIndex,
518
+ setProgrammaticTarget,
519
+ ]
520
+ );
521
+
522
+ const viewabilityConfigRef = React.useRef<
523
+ FlashListProps<LinkedScrollItem<Value, Data>>['viewabilityConfig']
524
+ >(
525
+ viewabilityConfig ?? {
526
+ minimumViewTime: DEFAULT_MINIMUM_VIEW_TIME,
527
+ viewAreaCoveragePercentThreshold: resolvedViewAreaThreshold,
528
+ }
529
+ );
530
+
531
+ const handleViewableItemsChanged = React.useRef<
532
+ NonNullable<FlashListProps<LinkedScrollItem<Value, Data>>['onViewableItemsChanged']>
533
+ >(({ viewableItems }) => {
534
+ if (disabledRef.current || viewableItems.length === 0) return;
535
+
536
+ const candidates = viewableItems
537
+ .filter((token) => token.isViewable && token.index != null)
538
+ .sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
539
+ const candidate = candidates[0];
540
+ if (candidate?.item == null || candidate.index == null) return;
541
+
542
+ if (pendingProgrammaticValueRef.current != null) return;
543
+
544
+ commitValueRef.current(candidate.item.value, candidate.item, candidate.index, 'content');
545
+ }).current;
546
+
547
+ const {
548
+ contentContainerStyle: menuContentContainerStyle,
549
+ style: menuListStyle,
550
+ onScrollBeginDrag: onMenuScrollBeginDrag,
551
+ drawDistance: menuDrawDistance = wp(240),
552
+ maintainVisibleContentPosition: menuMaintainVisibleContentPosition =
553
+ DISABLED_MAINTAIN_VISIBLE_CONTENT_POSITION,
554
+ scrollEnabled: menuScrollEnabled = true,
555
+ showsVerticalScrollIndicator: menuShowsVerticalScrollIndicator = false,
556
+ ...restMenuListProps
557
+ } = menuListProps ?? {};
558
+
559
+ const {
560
+ contentContainerStyle: contentListContentContainerStyle,
561
+ style: contentListStyle,
562
+ onScrollBeginDrag: onContentScrollBeginDrag,
563
+ onScrollEndDrag: onContentScrollEndDrag,
564
+ onMomentumScrollEnd: onContentMomentumScrollEnd,
565
+ drawDistance: contentDrawDistance = wp(640),
566
+ maintainVisibleContentPosition: contentMaintainVisibleContentPosition =
567
+ DISABLED_MAINTAIN_VISIBLE_CONTENT_POSITION,
568
+ scrollEnabled: contentScrollEnabled = true,
569
+ showsVerticalScrollIndicator: contentShowsVerticalScrollIndicator = false,
570
+ ...restContentListProps
571
+ } = contentListProps ?? {};
572
+
573
+ const handleMenuScrollBeginDrag = React.useCallback(
574
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
575
+ releaseProgrammaticSync();
576
+ onMenuScrollBeginDrag?.(event);
577
+ },
578
+ [onMenuScrollBeginDrag, releaseProgrammaticSync]
579
+ );
580
+
581
+ const handleContentScrollBeginDrag = React.useCallback(
582
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
583
+ releaseProgrammaticSync();
584
+ onContentScrollBeginDrag?.(event);
585
+ },
586
+ [onContentScrollBeginDrag, releaseProgrammaticSync]
587
+ );
588
+
589
+ const handleContentScrollEndDrag = React.useCallback(
590
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
591
+ onContentScrollEndDrag?.(event);
592
+ },
593
+ [onContentScrollEndDrag]
594
+ );
595
+
596
+ const handleContentMomentumScrollEnd = React.useCallback(
597
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
598
+ onContentMomentumScrollEnd?.(event);
599
+ },
600
+ [onContentMomentumScrollEnd]
601
+ );
602
+
603
+ const renderMenuListItem = React.useCallback(
604
+ ({ item, index }: ListRenderItemInfo<LinkedScrollItem<Value, Data>>) => {
605
+ const selected = activeValue !== undefined && Object.is(item.value, activeValue);
606
+ const itemDisabled = disabled || !!item.disabled;
607
+ const press = () => handleMenuPress(item, index);
608
+
609
+ if (renderMenuItem) {
610
+ return (
611
+ <>
612
+ {renderMenuItem({
613
+ item,
614
+ index,
615
+ selected,
616
+ disabled: itemDisabled,
617
+ press,
618
+ })}
619
+ </>
620
+ );
621
+ }
622
+
623
+ return (
624
+ <Pressable
625
+ accessibilityLabel={item.accessibilityLabel}
626
+ accessibilityRole="button"
627
+ accessibilityState={{ selected, disabled: itemDisabled }}
628
+ disabled={itemDisabled}
629
+ onPress={press}
630
+ style={({ pressed }) => [
631
+ styles.menuItem,
632
+ {
633
+ height: resolvedMenuItemHeight,
634
+ backgroundColor: selected ? resolvedActiveBackgroundColor : 'transparent',
635
+ opacity: itemDisabled ? DISABLED_OPACITY : pressed ? PRESSED_OPACITY : 1,
636
+ },
637
+ menuItemStyle,
638
+ selected ? activeMenuItemStyle : null,
639
+ itemDisabled ? disabledMenuItemStyle : null,
640
+ ]}
641
+ >
642
+ {renderDefaultMenuLabel(item.label, [
643
+ styles.menuItemText,
644
+ {
645
+ color: itemDisabled
646
+ ? resolvedDisabledColor
647
+ : selected
648
+ ? resolvedActiveColor
649
+ : resolvedInactiveColor,
650
+ },
651
+ menuItemTextStyle,
652
+ selected ? activeMenuItemTextStyle : null,
653
+ itemDisabled ? disabledMenuItemTextStyle : null,
654
+ ])}
655
+ </Pressable>
656
+ );
657
+ },
658
+ [
659
+ activeMenuItemStyle,
660
+ activeMenuItemTextStyle,
661
+ activeValue,
662
+ disabled,
663
+ disabledMenuItemStyle,
664
+ disabledMenuItemTextStyle,
665
+ handleMenuPress,
666
+ menuItemStyle,
667
+ menuItemTextStyle,
668
+ renderMenuItem,
669
+ resolvedActiveBackgroundColor,
670
+ resolvedActiveColor,
671
+ resolvedDisabledColor,
672
+ resolvedInactiveColor,
673
+ resolvedMenuItemHeight,
674
+ ]
675
+ );
676
+
677
+ const renderContentListItem = React.useCallback(
678
+ ({ item, index }: ListRenderItemInfo<LinkedScrollItem<Value, Data>>) => {
679
+ const selected = activeValue !== undefined && Object.is(item.value, activeValue);
680
+ const isLast = index === itemsRef.current.length - 1;
681
+ return (
682
+ <View
683
+ style={[
684
+ styles.section,
685
+ !isLast && resolvedSectionGap > 0 ? { marginBottom: resolvedSectionGap } : null,
686
+ sectionStyle,
687
+ selected ? activeSectionStyle : null,
688
+ ]}
689
+ >
690
+ {renderSection({ item, index, selected })}
691
+ </View>
692
+ );
693
+ },
694
+ [activeSectionStyle, activeValue, renderSection, resolvedSectionGap, sectionStyle]
695
+ );
696
+
697
+ return (
698
+ <View
699
+ testID={testID}
700
+ style={[
701
+ styles.root,
702
+ menuPosition === 'right' ? styles.rootReverse : null,
703
+ { backgroundColor: resolvedContentBackgroundColor },
704
+ style,
705
+ ]}
706
+ >
707
+ <View
708
+ style={[
709
+ styles.menuFrame,
710
+ {
711
+ width: resolvedMenuWidth,
712
+ backgroundColor: resolvedMenuBackgroundColor,
713
+ borderColor: theme.colors.border,
714
+ borderRightWidth: menuPosition === 'left' ? ONE_PX : 0,
715
+ borderLeftWidth: menuPosition === 'right' ? ONE_PX : 0,
716
+ },
717
+ menuStyle,
718
+ ]}
719
+ >
720
+ <FlashList
721
+ {...restMenuListProps}
722
+ ref={menuListRef}
723
+ data={items}
724
+ drawDistance={menuDrawDistance}
725
+ extraData={activeValue}
726
+ getItemType={getMenuItemType}
727
+ keyExtractor={resolvedKeyExtractor}
728
+ maintainVisibleContentPosition={menuMaintainVisibleContentPosition}
729
+ onScrollBeginDrag={handleMenuScrollBeginDrag}
730
+ renderItem={renderMenuListItem}
731
+ scrollEnabled={!disabled && menuScrollEnabled}
732
+ showsVerticalScrollIndicator={menuShowsVerticalScrollIndicator}
733
+ style={[styles.list, menuListStyle]}
734
+ contentContainerStyle={menuContentContainerStyle}
735
+ />
736
+ </View>
737
+
738
+ <View style={[styles.contentFrame, contentStyle]}>
739
+ <FlashList
740
+ {...restContentListProps}
741
+ ref={contentListRef}
742
+ data={items}
743
+ drawDistance={contentDrawDistance}
744
+ extraData={activeValue}
745
+ getItemType={getSectionType}
746
+ keyExtractor={resolvedKeyExtractor}
747
+ maintainVisibleContentPosition={contentMaintainVisibleContentPosition}
748
+ onMomentumScrollEnd={handleContentMomentumScrollEnd}
749
+ onScrollBeginDrag={handleContentScrollBeginDrag}
750
+ onScrollEndDrag={handleContentScrollEndDrag}
751
+ onViewableItemsChanged={handleViewableItemsChanged}
752
+ renderItem={renderContentListItem}
753
+ scrollEnabled={!disabled && contentScrollEnabled}
754
+ showsVerticalScrollIndicator={contentShowsVerticalScrollIndicator}
755
+ style={[styles.list, contentListStyle]}
756
+ contentContainerStyle={[
757
+ {
758
+ paddingHorizontal: resolvedContentPaddingHorizontal,
759
+ paddingVertical: resolvedContentPaddingVertical,
760
+ },
761
+ contentListContentContainerStyle,
762
+ ]}
763
+ viewabilityConfig={viewabilityConfigRef.current}
764
+ />
765
+ </View>
766
+ </View>
767
+ );
768
+ }
769
+
770
+ const styles = StyleSheet.create({
771
+ root: {
772
+ flex: 1,
773
+ minHeight: 0,
774
+ flexDirection: 'row',
775
+ },
776
+ rootReverse: {
777
+ flexDirection: 'row-reverse',
778
+ },
779
+ menuFrame: {
780
+ overflow: 'hidden',
781
+ },
782
+ contentFrame: {
783
+ flex: 1,
784
+ minWidth: 0,
785
+ },
786
+ list: {
787
+ flex: 1,
788
+ },
789
+ menuItem: {
790
+ position: 'relative',
791
+ alignItems: 'center',
792
+ justifyContent: 'center',
793
+ paddingHorizontal: wp(10),
794
+ },
795
+ menuItemText: {
796
+ fontSize: wp(15),
797
+ lineHeight: wp(20),
798
+ fontWeight: '500',
799
+ includeFontPadding: false,
800
+ },
801
+ menuLabelNode: {
802
+ alignItems: 'center',
803
+ justifyContent: 'center',
804
+ minWidth: 0,
805
+ },
806
+ section: {
807
+ width: '100%',
808
+ },
809
+ });