react-native-collapsible-tabs-reanimated 0.1.0-beta

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/lib/commonjs/Bar.js +247 -0
  4. package/lib/commonjs/Bar.js.map +1 -0
  5. package/lib/commonjs/Button.js +150 -0
  6. package/lib/commonjs/Button.js.map +1 -0
  7. package/lib/commonjs/Context.js +21 -0
  8. package/lib/commonjs/Context.js.map +1 -0
  9. package/lib/commonjs/FlashList.js +91 -0
  10. package/lib/commonjs/FlashList.js.map +1 -0
  11. package/lib/commonjs/Header.js +54 -0
  12. package/lib/commonjs/Header.js.map +1 -0
  13. package/lib/commonjs/Indicator.js +156 -0
  14. package/lib/commonjs/Indicator.js.map +1 -0
  15. package/lib/commonjs/Lazy.js +87 -0
  16. package/lib/commonjs/Lazy.js.map +1 -0
  17. package/lib/commonjs/LegendList.js +86 -0
  18. package/lib/commonjs/LegendList.js.map +1 -0
  19. package/lib/commonjs/List.js +83 -0
  20. package/lib/commonjs/List.js.map +1 -0
  21. package/lib/commonjs/Pager.js +93 -0
  22. package/lib/commonjs/Pager.js.map +1 -0
  23. package/lib/commonjs/Root.js +169 -0
  24. package/lib/commonjs/Root.js.map +1 -0
  25. package/lib/commonjs/ScrollView.js +85 -0
  26. package/lib/commonjs/ScrollView.js.map +1 -0
  27. package/lib/commonjs/StaticHeader.js +37 -0
  28. package/lib/commonjs/StaticHeader.js.map +1 -0
  29. package/lib/commonjs/StickyHeader.js +37 -0
  30. package/lib/commonjs/StickyHeader.js.map +1 -0
  31. package/lib/commonjs/Tab.js +86 -0
  32. package/lib/commonjs/Tab.js.map +1 -0
  33. package/lib/commonjs/flash-list.js +14 -0
  34. package/lib/commonjs/flash-list.js.map +1 -0
  35. package/lib/commonjs/index.js +128 -0
  36. package/lib/commonjs/index.js.map +1 -0
  37. package/lib/commonjs/legend-list.js +14 -0
  38. package/lib/commonjs/legend-list.js.map +1 -0
  39. package/lib/commonjs/package.json +1 -0
  40. package/lib/commonjs/useStableCallback.js +15 -0
  41. package/lib/commonjs/useStableCallback.js.map +1 -0
  42. package/lib/module/Bar.js +242 -0
  43. package/lib/module/Bar.js.map +1 -0
  44. package/lib/module/Button.js +145 -0
  45. package/lib/module/Button.js.map +1 -0
  46. package/lib/module/Context.js +16 -0
  47. package/lib/module/Context.js.map +1 -0
  48. package/lib/module/FlashList.js +86 -0
  49. package/lib/module/FlashList.js.map +1 -0
  50. package/lib/module/Header.js +49 -0
  51. package/lib/module/Header.js.map +1 -0
  52. package/lib/module/Indicator.js +151 -0
  53. package/lib/module/Indicator.js.map +1 -0
  54. package/lib/module/Lazy.js +82 -0
  55. package/lib/module/Lazy.js.map +1 -0
  56. package/lib/module/LegendList.js +81 -0
  57. package/lib/module/LegendList.js.map +1 -0
  58. package/lib/module/List.js +78 -0
  59. package/lib/module/List.js.map +1 -0
  60. package/lib/module/Pager.js +87 -0
  61. package/lib/module/Pager.js.map +1 -0
  62. package/lib/module/Root.js +165 -0
  63. package/lib/module/Root.js.map +1 -0
  64. package/lib/module/ScrollView.js +80 -0
  65. package/lib/module/ScrollView.js.map +1 -0
  66. package/lib/module/StaticHeader.js +32 -0
  67. package/lib/module/StaticHeader.js.map +1 -0
  68. package/lib/module/StickyHeader.js +32 -0
  69. package/lib/module/StickyHeader.js.map +1 -0
  70. package/lib/module/Tab.js +81 -0
  71. package/lib/module/Tab.js.map +1 -0
  72. package/lib/module/flash-list.js +4 -0
  73. package/lib/module/flash-list.js.map +1 -0
  74. package/lib/module/index.js +44 -0
  75. package/lib/module/index.js.map +1 -0
  76. package/lib/module/legend-list.js +4 -0
  77. package/lib/module/legend-list.js.map +1 -0
  78. package/lib/module/useStableCallback.js +11 -0
  79. package/lib/module/useStableCallback.js.map +1 -0
  80. package/lib/typescript/Bar.d.ts +22 -0
  81. package/lib/typescript/Bar.d.ts.map +1 -0
  82. package/lib/typescript/Button.d.ts +32 -0
  83. package/lib/typescript/Button.d.ts.map +1 -0
  84. package/lib/typescript/Context.d.ts +37 -0
  85. package/lib/typescript/Context.d.ts.map +1 -0
  86. package/lib/typescript/FlashList.d.ts +6 -0
  87. package/lib/typescript/FlashList.d.ts.map +1 -0
  88. package/lib/typescript/Header.d.ts +7 -0
  89. package/lib/typescript/Header.d.ts.map +1 -0
  90. package/lib/typescript/Indicator.d.ts +11 -0
  91. package/lib/typescript/Indicator.d.ts.map +1 -0
  92. package/lib/typescript/Lazy.d.ts +36 -0
  93. package/lib/typescript/Lazy.d.ts.map +1 -0
  94. package/lib/typescript/LegendList.d.ts +6 -0
  95. package/lib/typescript/LegendList.d.ts.map +1 -0
  96. package/lib/typescript/List.d.ts +6 -0
  97. package/lib/typescript/List.d.ts.map +1 -0
  98. package/lib/typescript/Pager.d.ts +15 -0
  99. package/lib/typescript/Pager.d.ts.map +1 -0
  100. package/lib/typescript/Root.d.ts +14 -0
  101. package/lib/typescript/Root.d.ts.map +1 -0
  102. package/lib/typescript/ScrollView.d.ts +6 -0
  103. package/lib/typescript/ScrollView.d.ts.map +1 -0
  104. package/lib/typescript/StaticHeader.d.ts +7 -0
  105. package/lib/typescript/StaticHeader.d.ts.map +1 -0
  106. package/lib/typescript/StickyHeader.d.ts +7 -0
  107. package/lib/typescript/StickyHeader.d.ts.map +1 -0
  108. package/lib/typescript/Tab.d.ts +31 -0
  109. package/lib/typescript/Tab.d.ts.map +1 -0
  110. package/lib/typescript/flash-list.d.ts +3 -0
  111. package/lib/typescript/flash-list.d.ts.map +1 -0
  112. package/lib/typescript/index.d.ts +69 -0
  113. package/lib/typescript/index.d.ts.map +1 -0
  114. package/lib/typescript/legend-list.d.ts +3 -0
  115. package/lib/typescript/legend-list.d.ts.map +1 -0
  116. package/lib/typescript/useStableCallback.d.ts +2 -0
  117. package/lib/typescript/useStableCallback.d.ts.map +1 -0
  118. package/package.json +112 -0
  119. package/src/Bar.tsx +359 -0
  120. package/src/Button.tsx +219 -0
  121. package/src/Context.tsx +44 -0
  122. package/src/FlashList.tsx +150 -0
  123. package/src/Header.tsx +45 -0
  124. package/src/Indicator.tsx +193 -0
  125. package/src/Lazy.tsx +110 -0
  126. package/src/LegendList.tsx +130 -0
  127. package/src/List.tsx +115 -0
  128. package/src/Pager.tsx +134 -0
  129. package/src/Root.tsx +194 -0
  130. package/src/ScrollView.tsx +116 -0
  131. package/src/StaticHeader.tsx +30 -0
  132. package/src/StickyHeader.tsx +30 -0
  133. package/src/Tab.tsx +89 -0
  134. package/src/flash-list.ts +2 -0
  135. package/src/index.ts +54 -0
  136. package/src/legend-list.ts +2 -0
  137. package/src/useStableCallback.ts +11 -0
package/src/Bar.tsx ADDED
@@ -0,0 +1,359 @@
1
+ import { ReactNode, memo, useCallback, useMemo, useState } from "react";
2
+
3
+ import {
4
+ LayoutChangeEvent,
5
+ ScrollViewProps,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ ViewProps,
10
+ ViewStyle,
11
+ } from "react-native";
12
+
13
+ import { Pressable, ScrollView } from "react-native-gesture-handler";
14
+ import Animated, {
15
+ SharedValue,
16
+ interpolate,
17
+ scrollTo,
18
+ useAnimatedReaction,
19
+ useAnimatedRef,
20
+ useAnimatedScrollHandler,
21
+ useAnimatedStyle,
22
+ useDerivedValue,
23
+ useSharedValue,
24
+ withTiming,
25
+ } from "react-native-reanimated";
26
+
27
+ import { useCollapsibleTabsContext } from "./Context";
28
+ import { scheduleOnUI } from "react-native-worklets";
29
+
30
+ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
31
+
32
+ export type BarProps = Omit<
33
+ ScrollViewProps,
34
+ "contentContainerStyle" | "style"
35
+ > & {
36
+ fullWidth?: boolean;
37
+ scrollButtons?: boolean;
38
+ left?: ReactNode;
39
+ right?: ReactNode;
40
+ scrollContainerStyle?: ScrollViewProps["style"];
41
+ tabButtonsGap?: number;
42
+ children?: ReactNode;
43
+ containerProps?: ViewProps;
44
+ backgroundColor?: string;
45
+ scrollButtonBackgroundColor?: string;
46
+ scrollButtonIconColor?: string;
47
+ renderScrollButtonIcon?: (dir: "left" | "right") => ReactNode;
48
+ };
49
+
50
+ const Bar = ({
51
+ scrollEnabled = true,
52
+ fullWidth,
53
+ scrollButtons = true,
54
+ left,
55
+ right,
56
+ containerProps,
57
+ children,
58
+ scrollContainerStyle,
59
+ tabButtonsGap = 0,
60
+ backgroundColor = "#ffffff",
61
+ scrollButtonBackgroundColor = "rgba(255, 255, 255, 0.95)",
62
+ scrollButtonIconColor = "rgba(48, 48, 48, 0.7)",
63
+ renderScrollButtonIcon,
64
+ ...props
65
+ }: BarProps) => {
66
+ const { pageDecimal, itemLayout } = useCollapsibleTabsContext();
67
+ const [barSize, setBarSize] = useState({
68
+ width: 0,
69
+ height: 0,
70
+ measured: false,
71
+ });
72
+
73
+ const contentWidth = useSharedValue(0);
74
+ const layoutWidth = useSharedValue(0);
75
+ const scrollRef = useAnimatedRef<Animated.ScrollView>();
76
+ const scrollX = useSharedValue(0);
77
+ const leftButtonOpacity = useSharedValue(0);
78
+ const rightButtonOpacity = useSharedValue(0);
79
+
80
+ useAnimatedReaction(
81
+ () => {
82
+ if (!scrollButtons || layoutWidth.value === 0 || contentWidth.value === 0)
83
+ return { showLeft: false, showRight: false };
84
+ const maxScroll = contentWidth.value - layoutWidth.value;
85
+ const isScrollable = maxScroll > 1;
86
+ return {
87
+ showLeft: isScrollable && scrollX.value > 10,
88
+ showRight: isScrollable && scrollX.value < maxScroll - 10,
89
+ };
90
+ },
91
+ (curr, prev) => {
92
+ if (curr.showLeft !== prev?.showLeft)
93
+ leftButtonOpacity.value = withTiming(curr.showLeft ? 1 : 0);
94
+ if (curr.showRight !== prev?.showRight)
95
+ rightButtonOpacity.value = withTiming(curr.showRight ? 1 : 0);
96
+ },
97
+ [scrollButtons],
98
+ );
99
+
100
+ const onScroll = useAnimatedScrollHandler((evt) => {
101
+ scrollX.value = evt.contentOffset.x;
102
+ });
103
+
104
+ const scrollByButton = useCallback(
105
+ (dir: "left" | "right") => {
106
+ "worklet";
107
+ const step = layoutWidth.value / 2;
108
+ const current = scrollX.value;
109
+ const target = dir === "left" ? current - step : current + step;
110
+ const maxScroll = contentWidth.value - layoutWidth.value;
111
+ scrollTo(
112
+ scrollRef,
113
+ Math.max(0, Math.min(target, maxScroll > 0 ? maxScroll : 0)),
114
+ 0,
115
+ true,
116
+ );
117
+ },
118
+ [contentWidth, layoutWidth, scrollRef, scrollX],
119
+ );
120
+
121
+ const handlePress = useCallback(
122
+ (dir: "left" | "right") => {
123
+ scheduleOnUI(scrollByButton, dir);
124
+ },
125
+ [scrollByButton],
126
+ );
127
+
128
+ const interpolationTable = useDerivedValue(() => {
129
+ if (itemLayout.length <= 1) return null;
130
+ const inputRange = new Array<number>(itemLayout.length);
131
+ const outputRange = new Array<number>(itemLayout.length);
132
+ const halfBar = barSize.width / 2;
133
+ for (let index = 0; index < itemLayout.length; index += 1) {
134
+ inputRange[index] = index;
135
+ const item = itemLayout[index];
136
+ outputRange[index] = +(item.x + item.width / 2 - halfBar).toFixed(2);
137
+ }
138
+ return { inputRange, outputRange };
139
+ }, [barSize.width, itemLayout]);
140
+
141
+ const centerOffset = useDerivedValue(() => {
142
+ if (!interpolationTable.value) return 0;
143
+ return interpolate(
144
+ +pageDecimal.value.toFixed(3),
145
+ interpolationTable.value.inputRange,
146
+ interpolationTable.value.outputRange,
147
+ "identity",
148
+ );
149
+ }, [interpolationTable]);
150
+
151
+ useAnimatedReaction(
152
+ () => Math.round(centerOffset.value),
153
+ (value, prev) => {
154
+ if (value !== prev) scrollTo(scrollRef, value, 0, true);
155
+ },
156
+ );
157
+
158
+ const contentContainerStyle = useMemo(
159
+ (): ViewStyle => ({ flex: fullWidth ? 1 : 0, gap: tabButtonsGap }),
160
+ [fullWidth, tabButtonsGap],
161
+ );
162
+
163
+ const onContentSizeChange = useCallback(
164
+ (width: number) => {
165
+ contentWidth.value = width;
166
+ },
167
+ [contentWidth],
168
+ );
169
+
170
+ const onLayout = useCallback(
171
+ (evt: LayoutChangeEvent) => {
172
+ layoutWidth.value = evt.nativeEvent.layout.width;
173
+ },
174
+ [layoutWidth],
175
+ );
176
+
177
+ const containerOnLayout = containerProps?.onLayout;
178
+ const onContainerLayout = useCallback(
179
+ (evt: LayoutChangeEvent) => {
180
+ const { width, height } = evt.nativeEvent.layout;
181
+ setBarSize((prev) =>
182
+ prev.width === width && prev.height === height
183
+ ? prev
184
+ : { width, height, measured: true },
185
+ );
186
+ containerOnLayout?.(evt);
187
+ },
188
+ [containerOnLayout],
189
+ );
190
+
191
+ return (
192
+ <View
193
+ {...containerProps}
194
+ style={[styles.containerRow, { backgroundColor }, containerProps?.style]}
195
+ onLayout={onContainerLayout}
196
+ collapsable={false}
197
+ >
198
+ {left}
199
+ <View style={[styles.scrollContainer, scrollContainerStyle]}>
200
+ {!!scrollButtons && (
201
+ <ScrollButton
202
+ dir="left"
203
+ buttonProgress={leftButtonOpacity}
204
+ handlePress={handlePress}
205
+ backgroundColor={scrollButtonBackgroundColor}
206
+ iconColor={scrollButtonIconColor}
207
+ renderIcon={renderScrollButtonIcon}
208
+ />
209
+ )}
210
+ <AnimatedScrollView
211
+ ref={scrollRef}
212
+ horizontal
213
+ showsHorizontalScrollIndicator={false}
214
+ scrollEventThrottle={16}
215
+ keyboardShouldPersistTaps="handled"
216
+ scrollEnabled={scrollEnabled}
217
+ contentContainerStyle={contentContainerStyle}
218
+ directionalLockEnabled
219
+ onContentSizeChange={onContentSizeChange}
220
+ onLayout={onLayout}
221
+ onScroll={onScroll}
222
+ bounces={false}
223
+ collapsable={false}
224
+ {...props}
225
+ >
226
+ {children}
227
+ </AnimatedScrollView>
228
+ {!!scrollButtons && (
229
+ <ScrollButton
230
+ dir="right"
231
+ buttonProgress={rightButtonOpacity}
232
+ handlePress={handlePress}
233
+ backgroundColor={scrollButtonBackgroundColor}
234
+ iconColor={scrollButtonIconColor}
235
+ renderIcon={renderScrollButtonIcon}
236
+ />
237
+ )}
238
+ </View>
239
+ {right}
240
+ </View>
241
+ );
242
+ };
243
+
244
+ type ScrollButtonProps = {
245
+ dir: "left" | "right";
246
+ buttonProgress: SharedValue<number>;
247
+ handlePress: (dir: "left" | "right") => void;
248
+ backgroundColor: string;
249
+ iconColor: string;
250
+ renderIcon?: (dir: "left" | "right") => ReactNode;
251
+ };
252
+
253
+ const ScrollButton = memo(
254
+ ({
255
+ dir,
256
+ buttonProgress,
257
+ handlePress,
258
+ backgroundColor,
259
+ iconColor,
260
+ renderIcon,
261
+ }: ScrollButtonProps) => {
262
+ const isLeft = dir === "left";
263
+ const width = useSharedValue(0);
264
+
265
+ const animatedStyle = useAnimatedStyle(() => {
266
+ const opacity = buttonProgress.value;
267
+ return {
268
+ opacity,
269
+ transform: [
270
+ {
271
+ translateX: interpolate(
272
+ opacity,
273
+ [0, 1],
274
+ [isLeft ? -width.value : width.value, 0],
275
+ ),
276
+ },
277
+ ],
278
+ pointerEvents: opacity === 0 ? "none" : "auto",
279
+ };
280
+ }, [isLeft]);
281
+
282
+ const onLayoutButton = useCallback(
283
+ (evt: LayoutChangeEvent) => {
284
+ width.value = evt.nativeEvent.layout.width;
285
+ },
286
+ [width],
287
+ );
288
+
289
+ const onPress = useCallback(() => handlePress(dir), [dir, handlePress]);
290
+
291
+ return (
292
+ <Animated.View
293
+ style={[
294
+ isLeft
295
+ ? scrollButtonStyles.containerLeft
296
+ : scrollButtonStyles.containerRight,
297
+ { backgroundColor },
298
+ animatedStyle,
299
+ ]}
300
+ onLayout={onLayoutButton}
301
+ >
302
+ <Pressable style={scrollButtonStyles.button} onPress={onPress}>
303
+ {renderIcon ? (
304
+ renderIcon(dir)
305
+ ) : (
306
+ <Text style={[scrollButtonStyles.icon, { color: iconColor }]}>
307
+ {isLeft ? "<" : ">"}
308
+ </Text>
309
+ )}
310
+ </Pressable>
311
+ </Animated.View>
312
+ );
313
+ },
314
+ );
315
+
316
+ const styles = StyleSheet.create({
317
+ containerRow: {
318
+ position: "relative",
319
+ flexDirection: "row",
320
+ alignItems: "center",
321
+ },
322
+ scrollContainer: {
323
+ flex: 1,
324
+ flexDirection: "row",
325
+ alignItems: "center",
326
+ justifyContent: "center",
327
+ position: "relative",
328
+ paddingHorizontal: 20,
329
+ },
330
+ });
331
+
332
+ const scrollButtonStyles = StyleSheet.create({
333
+ containerLeft: {
334
+ position: "absolute",
335
+ left: 0,
336
+ zIndex: 2,
337
+ height: "100%",
338
+ },
339
+ containerRight: {
340
+ position: "absolute",
341
+ right: 0,
342
+ zIndex: 2,
343
+ height: "100%",
344
+ },
345
+ button: {
346
+ paddingHorizontal: 12,
347
+ paddingVertical: 4,
348
+ height: "100%",
349
+ justifyContent: "center",
350
+ },
351
+ icon: {
352
+ fontSize: 16,
353
+ fontWeight: "700",
354
+ },
355
+ });
356
+
357
+ Bar.displayName = "CollapsibleTabs.Bar";
358
+
359
+ export default memo(Bar);
package/src/Button.tsx ADDED
@@ -0,0 +1,219 @@
1
+ import { ReactNode, memo, useCallback, useMemo, useRef } from "react";
2
+
3
+ import {
4
+ LayoutChangeEvent,
5
+ StyleProp,
6
+ StyleSheet,
7
+ TextProps,
8
+ TextStyle,
9
+ View,
10
+ ViewStyle,
11
+ } from "react-native";
12
+
13
+ import { Pressable, PressableProps } from "react-native-gesture-handler";
14
+ import Animated, {
15
+ AnimatedStyle,
16
+ SharedValue,
17
+ interpolateColor,
18
+ useAnimatedStyle,
19
+ withTiming,
20
+ } from "react-native-reanimated";
21
+
22
+ import { useCollapsibleTabsContext } from "./Context";
23
+
24
+ type PressableEvent = Parameters<NonNullable<PressableProps["onPress"]>>[0];
25
+
26
+ export type RenderTabLabelProps = {
27
+ index: number;
28
+ name: string;
29
+ isActive: boolean;
30
+ style: StyleProp<TextStyle>;
31
+ animatedStyle: AnimatedStyle<StyleProp<TextStyle>>;
32
+ pageDecimal: SharedValue<number>;
33
+ };
34
+
35
+ export type ButtonProps = {
36
+ index: number;
37
+ name: string;
38
+ style?: StyleProp<ViewStyle>;
39
+ fullWidth?: boolean;
40
+ activeStyle?: StyleProp<ViewStyle>;
41
+ labelStyle?: StyleProp<TextStyle>;
42
+ activeLabelStyle?: StyleProp<TextStyle>;
43
+ labelProps?: Omit<TextProps, "style">;
44
+ contentWrapperStyle?: StyleProp<ViewStyle>;
45
+ activeLabelColor?: string;
46
+ inactiveLabelColor?: string;
47
+ children?: string | ((props: RenderTabLabelProps) => ReactNode);
48
+ } & Omit<PressableProps, "style" | "children">;
49
+
50
+ const Button = ({
51
+ index,
52
+ name,
53
+ onPress,
54
+ style,
55
+ fullWidth,
56
+ labelStyle,
57
+ labelProps,
58
+ activeLabelStyle,
59
+ activeStyle,
60
+ children,
61
+ contentWrapperStyle,
62
+ activeLabelColor = "#111827",
63
+ inactiveLabelColor = "#9ca3af",
64
+ ...props
65
+ }: ButtonProps) => {
66
+ const {
67
+ activeTabIndex,
68
+ activeTabIndexValue,
69
+ pageDecimal,
70
+ pagerRef,
71
+ registerButton,
72
+ itemLayout,
73
+ } = useCollapsibleTabsContext();
74
+ const isActive = activeTabIndexValue === index;
75
+
76
+ const combinedStaticLabelStyle = useMemo(
77
+ (): StyleProp<TextStyle> => [
78
+ styles.label,
79
+ labelStyle,
80
+ activeLabelStyle && isActive && activeLabelStyle,
81
+ ],
82
+ [activeLabelStyle, isActive, labelStyle],
83
+ );
84
+
85
+ const animatedLabelStyle = useAnimatedStyle(() => {
86
+ const color = interpolateColor(
87
+ pageDecimal.value,
88
+ [index - 1, index, index + 1],
89
+ [inactiveLabelColor, activeLabelColor, inactiveLabelColor],
90
+ );
91
+ return { color };
92
+ }, [activeLabelColor, inactiveLabelColor, index, pageDecimal]);
93
+
94
+ const handleOnPress = useCallback(
95
+ (event: PressableEvent) => {
96
+ onPress?.(event);
97
+ if (pagerRef.current) {
98
+ pagerRef.current.setPage(index);
99
+ return;
100
+ }
101
+ activeTabIndex.value = index;
102
+ pageDecimal.value = withTiming(index);
103
+ },
104
+ [activeTabIndex, index, onPress, pageDecimal, pagerRef],
105
+ );
106
+
107
+ const pressableLayoutRef = useRef<{ x: number; y: number } | null>(null);
108
+ const contentLayoutRef = useRef<{
109
+ x: number;
110
+ y: number;
111
+ width: number;
112
+ height: number;
113
+ } | null>(null);
114
+
115
+ const commitLayout = useCallback(() => {
116
+ const pressable = pressableLayoutRef.current;
117
+ const content = contentLayoutRef.current;
118
+ if (!pressable || !content) return;
119
+ registerButton({
120
+ width: content.width,
121
+ height: content.height,
122
+ x: pressable.x + content.x,
123
+ y: pressable.y + content.y,
124
+ name,
125
+ index,
126
+ });
127
+ }, [index, name, registerButton]);
128
+
129
+ const onLayout = useCallback(
130
+ (event: LayoutChangeEvent) => {
131
+ props.onLayout?.(event);
132
+ const { x, y } = event.nativeEvent.layout;
133
+ pressableLayoutRef.current = { x, y };
134
+ commitLayout();
135
+ },
136
+ [commitLayout, props],
137
+ );
138
+
139
+ const onContentLayout = useCallback(
140
+ (event: LayoutChangeEvent) => {
141
+ const { x, y, width, height } = event.nativeEvent.layout;
142
+ contentLayoutRef.current = { x, y, width, height };
143
+ commitLayout();
144
+ },
145
+ [commitLayout],
146
+ );
147
+
148
+ const isLast = itemLayout.length - 1 === index;
149
+ const isFirst = index === 0;
150
+ const mergedStyle = useMemo(
151
+ (): StyleProp<ViewStyle> => [
152
+ styles.pressable,
153
+ isFirst && { paddingLeft: 0 },
154
+ isLast && { paddingRight: 0 },
155
+ fullWidth && styles.fullWidth,
156
+ style,
157
+ isActive && activeStyle,
158
+ ],
159
+ [activeStyle, fullWidth, isActive, isFirst, isLast, style],
160
+ );
161
+
162
+ return (
163
+ <Pressable
164
+ onPress={handleOnPress}
165
+ {...props}
166
+ onLayout={onLayout}
167
+ style={mergedStyle}
168
+ >
169
+ <View
170
+ onLayout={onContentLayout}
171
+ style={[styles.contentWrapper, contentWrapperStyle]}
172
+ >
173
+ {typeof children === "function" ? (
174
+ children({
175
+ isActive,
176
+ index,
177
+ name,
178
+ style: combinedStaticLabelStyle,
179
+ animatedStyle: animatedLabelStyle,
180
+ pageDecimal,
181
+ })
182
+ ) : (
183
+ <Animated.Text
184
+ style={[combinedStaticLabelStyle, animatedLabelStyle]}
185
+ numberOfLines={1}
186
+ {...labelProps}
187
+ >
188
+ {children}
189
+ </Animated.Text>
190
+ )}
191
+ </View>
192
+ </Pressable>
193
+ );
194
+ };
195
+
196
+ const styles = StyleSheet.create({
197
+ pressable: {
198
+ paddingVertical: 16,
199
+ paddingHorizontal: 10,
200
+ },
201
+ fullWidth: {
202
+ flex: 1,
203
+ },
204
+ contentWrapper: {
205
+ position: "relative",
206
+ alignSelf: "center",
207
+ },
208
+ label: {
209
+ fontSize: 14,
210
+ lineHeight: 18,
211
+ fontWeight: "700",
212
+ fontVariant: ["tabular-nums"],
213
+ textAlign: "center",
214
+ },
215
+ });
216
+
217
+ Button.displayName = "CollapsibleTabs.Button";
218
+
219
+ export default memo(Button);
@@ -0,0 +1,44 @@
1
+ import { ReactNode, RefObject, createContext, memo, useContext } from 'react';
2
+
3
+ import PagerView from 'react-native-pager-view';
4
+ import { SharedValue } from 'react-native-reanimated';
5
+
6
+ export type ItemLayout = {
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ index: number;
12
+ name: string;
13
+ };
14
+
15
+ export type ListScroller = (animated?: boolean) => void;
16
+
17
+ export type CollapsibleTabsContextValue = {
18
+ headerOffset: SharedValue<number>;
19
+ staticHeight: SharedValue<number>;
20
+ stickyHeight: SharedValue<number>;
21
+ offsetAdjustment: SharedValue<number>;
22
+ activeTabIndex: SharedValue<number>;
23
+ activeTabIndexValue: number;
24
+ activeListOffset: SharedValue<number>;
25
+ pageDecimal: SharedValue<number>;
26
+ listPanGesture: any;
27
+ listGestures: any[];
28
+ pagerRef: RefObject<PagerView | null>;
29
+ itemLayout: ItemLayout[];
30
+ registerButton: (config: ItemLayout) => void;
31
+ registerListScroller: (index: number, scroller: ListScroller | null) => void;
32
+ staticHeightValue: number;
33
+ stickyHeightValue: number;
34
+ updateStaticHeight: (height: number) => void;
35
+ updateStickyHeight: (height: number) => void;
36
+ };
37
+
38
+ const CollapsibleTabsContext = createContext({} as CollapsibleTabsContextValue);
39
+
40
+ export const useCollapsibleTabsContext = () => useContext(CollapsibleTabsContext);
41
+
42
+ export const CollapsibleTabsContextProvider = memo(({ children, ...props }: CollapsibleTabsContextValue & { children: ReactNode }) => {
43
+ return <CollapsibleTabsContext.Provider value={props}>{children}</CollapsibleTabsContext.Provider>;
44
+ });