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/List.tsx ADDED
@@ -0,0 +1,115 @@
1
+ import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
2
+
3
+ import {
4
+ FlatListProps,
5
+ LayoutChangeEvent,
6
+ StyleSheet,
7
+ View,
8
+ } from "react-native";
9
+
10
+ import { GestureDetector } from "react-native-gesture-handler";
11
+ import Animated, {
12
+ scrollTo,
13
+ useAnimatedReaction,
14
+ useAnimatedRef,
15
+ useAnimatedScrollHandler,
16
+ useComposedEventHandler,
17
+ useSharedValue,
18
+ } from "react-native-reanimated";
19
+
20
+ import { ListScroller, useCollapsibleTabsContext } from "./Context";
21
+ import { useTabSelfContext } from "./Tab";
22
+ import { useStableCallback } from "./useStableCallback";
23
+
24
+ export type ListProps<T> = Omit<FlatListProps<T>, "CellRendererComponent">;
25
+
26
+ const List = <T,>({
27
+ onLayout,
28
+ onContentSizeChange,
29
+ ...props
30
+ }: ListProps<T>) => {
31
+ const {
32
+ listGestures,
33
+ activeTabIndex,
34
+ activeListOffset,
35
+ registerListScroller,
36
+ } = useCollapsibleTabsContext();
37
+ const { index } = useTabSelfContext();
38
+ const selfOffset = useSharedValue(0);
39
+ const listRef = useAnimatedRef<Animated.FlatList<T>>();
40
+
41
+ const onScroll = useAnimatedScrollHandler((event) => {
42
+ selfOffset.value = event.contentOffset.y;
43
+ if (activeTabIndex.value === index)
44
+ activeListOffset.value = event.contentOffset.y;
45
+ });
46
+
47
+ const composedScrollEvent = useComposedEventHandler(
48
+ props.onScroll ? [onScroll, props.onScroll] : [onScroll],
49
+ );
50
+
51
+ useAnimatedReaction(
52
+ () => activeTabIndex.value,
53
+ (value) => {
54
+ if (value === index) activeListOffset.value = selfOffset.value;
55
+ },
56
+ [index],
57
+ );
58
+
59
+ const scroller = useMemo(
60
+ (): ListScroller =>
61
+ (animated = true) => {
62
+ "worklet";
63
+ scrollTo(listRef, 0, 0, animated);
64
+ },
65
+ [listRef],
66
+ );
67
+
68
+ useEffect(() => {
69
+ registerListScroller(index, scroller);
70
+ return () => registerListScroller(index, null);
71
+ }, [index, registerListScroller, scroller]);
72
+
73
+ const stableLayout = useStableCallback(onLayout);
74
+ const stableContentSizeChange = useStableCallback(onContentSizeChange);
75
+
76
+ const handleLayout = useCallback(
77
+ (event: LayoutChangeEvent) => {
78
+ stableLayout?.(event);
79
+ },
80
+ [stableLayout],
81
+ );
82
+
83
+ const handleContentSizeChange = useCallback(
84
+ (width: number, height: number) => {
85
+ stableContentSizeChange?.(width, height);
86
+ },
87
+ [stableContentSizeChange],
88
+ );
89
+
90
+ return (
91
+ <View style={styles.view} collapsable={false}>
92
+ <GestureDetector gesture={listGestures[index]}>
93
+ <Animated.FlatList
94
+ ref={listRef}
95
+ scrollEventThrottle={16}
96
+ showsVerticalScrollIndicator
97
+ directionalLockEnabled
98
+ keyboardShouldPersistTaps="handled"
99
+ {...props}
100
+ onScroll={composedScrollEvent}
101
+ onLayout={handleLayout}
102
+ onContentSizeChange={handleContentSizeChange}
103
+ />
104
+ </GestureDetector>
105
+ </View>
106
+ );
107
+ };
108
+
109
+ const styles = StyleSheet.create({
110
+ view: { position: "relative" },
111
+ });
112
+
113
+ List.displayName = "CollapsibleTabs.List";
114
+
115
+ export default memo(List) as <T>(props: ListProps<T>) => ReactElement;
package/src/Pager.tsx ADDED
@@ -0,0 +1,134 @@
1
+ import { ReactNode, memo, useCallback, useMemo } from "react";
2
+
3
+ import { StyleProp, StyleSheet, ViewStyle } from "react-native";
4
+
5
+ import { GestureDetector } from "react-native-gesture-handler";
6
+ import PagerView, {
7
+ PagerViewOnPageScrollEventData,
8
+ PagerViewOnPageSelectedEvent,
9
+ PagerViewProps,
10
+ } from "react-native-pager-view";
11
+ import Animated, {
12
+ useAnimatedStyle,
13
+ useEvent,
14
+ useHandler,
15
+ } from "react-native-reanimated";
16
+
17
+ import { useCollapsibleTabsContext } from "./Context";
18
+
19
+ const AnimatedPagerView = Animated.createAnimatedComponent(PagerView);
20
+
21
+ type OnPageScrollWorklet = (
22
+ event: PagerViewOnPageScrollEventData,
23
+ context: Record<string, unknown>,
24
+ ) => void;
25
+
26
+ type PageScrollHandlers = {
27
+ onPageScroll?: OnPageScrollWorklet;
28
+ };
29
+
30
+ function usePageScrollHandler(
31
+ handlers: PageScrollHandlers,
32
+ dependencies?: unknown[],
33
+ ) {
34
+ const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
35
+ return useEvent<PagerViewOnPageScrollEventData>(
36
+ (event) => {
37
+ "worklet";
38
+ const { onPageScroll } = handlers;
39
+ if (onPageScroll && event.eventName.endsWith("onPageScroll"))
40
+ onPageScroll(event, context);
41
+ },
42
+ ["onPageScroll"],
43
+ doDependenciesDiffer,
44
+ );
45
+ }
46
+
47
+ export type PagerProps = {
48
+ children?: ReactNode;
49
+ style?: StyleProp<ViewStyle>;
50
+ height?: number;
51
+ getHeight?: (
52
+ staticHeaderHeight: number,
53
+ stickyHeaderHeight: number,
54
+ ) => number;
55
+ } & PagerViewProps;
56
+
57
+ const Pager = ({
58
+ children,
59
+ style,
60
+ height,
61
+ getHeight,
62
+ ...pagerProps
63
+ }: PagerProps) => {
64
+ const {
65
+ headerOffset,
66
+ listPanGesture,
67
+ activeTabIndex,
68
+ activeListOffset,
69
+ pageDecimal,
70
+ pagerRef,
71
+ staticHeightValue,
72
+ stickyHeightValue,
73
+ } = useCollapsibleTabsContext();
74
+
75
+ const listHeight =
76
+ height ?? getHeight?.(staticHeightValue, stickyHeightValue) ?? null;
77
+
78
+ const pageScrollHandlers = useMemo<PageScrollHandlers>(
79
+ () => ({
80
+ onPageScroll: (event) => {
81
+ "worklet";
82
+ pageDecimal.value = event.position + event.offset;
83
+ },
84
+ }),
85
+ [pageDecimal],
86
+ );
87
+ const onPageScroll = usePageScrollHandler(pageScrollHandlers);
88
+
89
+ const onPageSelected = useCallback(
90
+ (event: PagerViewOnPageSelectedEvent) => {
91
+ activeTabIndex.value = event.nativeEvent.position;
92
+ activeListOffset.value = 0;
93
+ },
94
+ [activeListOffset, activeTabIndex],
95
+ );
96
+
97
+ const animatedStyle = useAnimatedStyle(
98
+ () => ({
99
+ ...(listHeight != null ? { height: listHeight } : null),
100
+ transform: [{ translateY: headerOffset.value }],
101
+ }),
102
+ [listHeight],
103
+ );
104
+
105
+ return (
106
+ <GestureDetector gesture={listPanGesture}>
107
+ <Animated.View
108
+ style={[listHeight == null && styles.flex1, animatedStyle]}
109
+ >
110
+ <AnimatedPagerView
111
+ ref={pagerRef}
112
+ orientation="horizontal"
113
+ overScrollMode="never"
114
+ style={[styles.flex1, style]}
115
+ onPageScroll={
116
+ onPageScroll as unknown as PagerViewProps["onPageScroll"]
117
+ }
118
+ onPageSelected={onPageSelected}
119
+ {...pagerProps}
120
+ >
121
+ {children}
122
+ </AnimatedPagerView>
123
+ </Animated.View>
124
+ </GestureDetector>
125
+ );
126
+ };
127
+
128
+ Pager.displayName = "CollapsibleTabs.Pager";
129
+
130
+ const styles = StyleSheet.create({
131
+ flex1: { flex: 1 },
132
+ });
133
+
134
+ export default memo(Pager);
package/src/Root.tsx ADDED
@@ -0,0 +1,194 @@
1
+ import { ReactNode, forwardRef, memo, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
2
+
3
+ import { Platform } from 'react-native';
4
+
5
+ import { Gesture } from 'react-native-gesture-handler';
6
+ import PagerView from 'react-native-pager-view';
7
+ import { clamp, useAnimatedReaction, useDerivedValue, useSharedValue, withDecay, withSpring } from 'react-native-reanimated';
8
+ import { scheduleOnRN } from 'react-native-worklets';
9
+
10
+ import { CollapsibleTabsContextProvider, CollapsibleTabsContextValue, ItemLayout, ListScroller } from './Context';
11
+
12
+ export type CollapsibleTabsRootRef = {
13
+ scrollToViewTop: (animated?: boolean) => void;
14
+ };
15
+
16
+ export type RootProps = {
17
+ children?: ReactNode;
18
+ pageLength: number;
19
+ initialStaticHeight?: number;
20
+ initialStickyHeight?: number;
21
+ offsetAdjustment?: number;
22
+ };
23
+
24
+ const DECELERATION = Platform.OS === 'android' ? 0.985 : 0.998;
25
+ const HEIGHT_EPSILON = 0.5;
26
+
27
+ type RootInnerProps = Required<Pick<RootProps, 'initialStaticHeight' | 'initialStickyHeight' | 'offsetAdjustment'>> & Pick<RootProps, 'children' | 'pageLength'>;
28
+
29
+ const RootInner = memo(
30
+ forwardRef<CollapsibleTabsRootRef, RootInnerProps>(({ initialStaticHeight, initialStickyHeight, pageLength, offsetAdjustment, children }, ref) => {
31
+ const headerOffset = useSharedValue(0);
32
+ const staticHeight = useSharedValue(initialStaticHeight);
33
+ const stickyHeight = useSharedValue(initialStickyHeight);
34
+ const offsetAdjustmentShared = useDerivedValue(() => offsetAdjustment, [offsetAdjustment]);
35
+
36
+ const activeTabIndex = useSharedValue(0);
37
+ const [activeTabIndexValue, setActiveTabIndexValue] = useState(0);
38
+ useAnimatedReaction(
39
+ () => activeTabIndex.value,
40
+ (next, prev) => {
41
+ if (next !== prev) scheduleOnRN(setActiveTabIndexValue, next);
42
+ },
43
+ []
44
+ );
45
+
46
+ const pageDecimal = useSharedValue(0);
47
+ const pagerRef = useRef<PagerView | null>(null);
48
+ const [itemLayout, setItemLayout] = useState<ItemLayout[]>([]);
49
+ const listScrollersRef = useRef<Map<number, ListScroller>>(new Map());
50
+
51
+ const registerButton = useCallback((config: ItemLayout) => {
52
+ setItemLayout((prev) => {
53
+ const next = prev.slice();
54
+ next[config.index] = config;
55
+ return next;
56
+ });
57
+ }, []);
58
+
59
+ const registerListScroller = useCallback((index: number, scroller: ListScroller | null) => {
60
+ if (scroller) listScrollersRef.current.set(index, scroller);
61
+ else listScrollersRef.current.delete(index);
62
+ }, []);
63
+
64
+ useImperativeHandle(
65
+ ref,
66
+ () => ({
67
+ scrollToViewTop: (animated = true) => {
68
+ const scroller = listScrollersRef.current.get(activeTabIndex.value);
69
+ scroller?.(animated);
70
+ headerOffset.value = withSpring(0, { duration: 250, dampingRatio: 1, mass: 4 });
71
+ },
72
+ }),
73
+ [activeTabIndex, headerOffset]
74
+ );
75
+
76
+ const [staticHeightValue, setStaticHeightValue] = useState(initialStaticHeight);
77
+ const [stickyHeightValue, setStickyHeightValue] = useState(initialStickyHeight);
78
+
79
+ const updateStaticHeight = useCallback(
80
+ (height: number) => {
81
+ staticHeight.value = height;
82
+ setStaticHeightValue((prev) => {
83
+ if (Math.abs(prev - height) < HEIGHT_EPSILON) return prev;
84
+ return height;
85
+ });
86
+ },
87
+ [staticHeight]
88
+ );
89
+
90
+ const updateStickyHeight = useCallback(
91
+ (height: number) => {
92
+ stickyHeight.value = height;
93
+ setStickyHeightValue((prev) => {
94
+ if (Math.abs(prev - height) < HEIGHT_EPSILON) return prev;
95
+ return height;
96
+ });
97
+ },
98
+ [stickyHeight]
99
+ );
100
+
101
+ const touchX = useSharedValue(0);
102
+ const touchY = useSharedValue(0);
103
+ const isVertical = useSharedValue(false);
104
+ const activeListOffset = useSharedValue(0);
105
+ const listGestures = useMemo(() => Array.from({ length: pageLength }, () => Gesture.Native().cancelsTouchesInView(true)), [pageLength]);
106
+
107
+ const listPanGesture = useMemo(() => {
108
+ return Gesture.Pan()
109
+ .manualActivation(true)
110
+ .maxPointers(1)
111
+ .minPointers(1)
112
+ .onTouchesDown((evt) => {
113
+ const touch = evt.allTouches[0];
114
+ touchX.value = touch.x;
115
+ touchY.value = touch.y;
116
+ isVertical.value = false;
117
+ })
118
+ .onTouchesMove((evt, state) => {
119
+ const touch = evt.allTouches[0];
120
+ const toTop = touch.y > touchY.value;
121
+ const isHorizontal = Math.abs(touch.x - touchX.value) > 5;
122
+ const vertical = Math.abs(touch.y - touchY.value) > 5;
123
+
124
+ if (vertical) isVertical.value = true;
125
+ if (isHorizontal && !isVertical.value) return state.fail();
126
+ if (!vertical) return;
127
+
128
+ const minOffset = -(staticHeight.value - offsetAdjustmentShared.value);
129
+ if (toTop && activeListOffset.value === 0) state.activate();
130
+ else if (!toTop && headerOffset.value > minOffset) state.activate();
131
+ })
132
+ .onChange((evt) => {
133
+ isVertical.value = true;
134
+ const minOffset = -(staticHeight.value - offsetAdjustmentShared.value);
135
+ headerOffset.value = clamp(headerOffset.value + evt.changeY, minOffset, 0);
136
+ })
137
+ .onEnd((evt) => {
138
+ const toTop = evt.translationY > 0;
139
+ const isHeaderPartialShown = headerOffset.value !== 0;
140
+ const minOffset = -(staticHeight.value - offsetAdjustmentShared.value);
141
+ const isFast = Math.abs(evt.velocityY) > 800;
142
+
143
+ if (isFast) {
144
+ if (toTop && isHeaderPartialShown) {
145
+ headerOffset.value = withSpring(0, { duration: 250, dampingRatio: 1, mass: 4, overshootClamping: false, velocity: evt.velocityY });
146
+ } else if (!toTop && isHeaderPartialShown) {
147
+ headerOffset.value = withSpring(minOffset, { duration: 250, dampingRatio: 1, mass: 4, overshootClamping: false, velocity: evt.velocityY });
148
+ }
149
+ } else {
150
+ headerOffset.value = withDecay({ velocity: evt.velocityY, rubberBandEffect: false, clamp: [minOffset, 0], deceleration: DECELERATION });
151
+ }
152
+ })
153
+ .simultaneousWithExternalGesture(...listGestures);
154
+ }, [activeListOffset, headerOffset, isVertical, listGestures, offsetAdjustmentShared, staticHeight, touchX, touchY]);
155
+
156
+ const ctxValue = useMemo(
157
+ (): CollapsibleTabsContextValue => ({
158
+ headerOffset,
159
+ staticHeight,
160
+ stickyHeight,
161
+ offsetAdjustment: offsetAdjustmentShared,
162
+ activeTabIndex,
163
+ activeTabIndexValue,
164
+ pageDecimal,
165
+ listPanGesture,
166
+ listGestures,
167
+ pagerRef,
168
+ itemLayout,
169
+ registerButton,
170
+ registerListScroller,
171
+ staticHeightValue,
172
+ stickyHeightValue,
173
+ activeListOffset,
174
+ updateStaticHeight,
175
+ updateStickyHeight,
176
+ }),
177
+ [activeListOffset, activeTabIndex, activeTabIndexValue, headerOffset, itemLayout, listGestures, listPanGesture, offsetAdjustmentShared, pageDecimal, registerButton, registerListScroller, staticHeight, staticHeightValue, stickyHeight, stickyHeightValue, updateStaticHeight, updateStickyHeight]
178
+ );
179
+
180
+ return <CollapsibleTabsContextProvider {...ctxValue}>{children}</CollapsibleTabsContextProvider>;
181
+ })
182
+ );
183
+
184
+ RootInner.displayName = 'CollapsibleTabs.RootInner';
185
+
186
+ const Root = forwardRef<CollapsibleTabsRootRef, RootProps>((props, ref) => (
187
+ <RootInner ref={ref} initialStaticHeight={props.initialStaticHeight ?? 0} initialStickyHeight={props.initialStickyHeight ?? 0} pageLength={props.pageLength} offsetAdjustment={props.offsetAdjustment ?? 0}>
188
+ {props.children}
189
+ </RootInner>
190
+ ));
191
+
192
+ Root.displayName = 'CollapsibleTabs.Root';
193
+
194
+ export default memo(Root);
@@ -0,0 +1,116 @@
1
+ import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
2
+
3
+ import {
4
+ LayoutChangeEvent,
5
+ ScrollViewProps as RNScrollViewProps,
6
+ StyleSheet,
7
+ View,
8
+ } from "react-native";
9
+
10
+ import { GestureDetector } from "react-native-gesture-handler";
11
+ import Animated, {
12
+ scrollTo,
13
+ useAnimatedReaction,
14
+ useAnimatedRef,
15
+ useAnimatedScrollHandler,
16
+ useComposedEventHandler,
17
+ useSharedValue,
18
+ } from "react-native-reanimated";
19
+
20
+ import { ListScroller, useCollapsibleTabsContext } from "./Context";
21
+ import { useTabSelfContext } from "./Tab";
22
+ import { useStableCallback } from "./useStableCallback";
23
+
24
+ export type ScrollViewProps = RNScrollViewProps;
25
+
26
+ const ScrollView = ({
27
+ onLayout,
28
+ onContentSizeChange,
29
+ ...props
30
+ }: ScrollViewProps) => {
31
+ const {
32
+ listGestures,
33
+ activeTabIndex,
34
+ activeListOffset,
35
+ registerListScroller,
36
+ } = useCollapsibleTabsContext();
37
+ const { index } = useTabSelfContext();
38
+ const selfOffset = useSharedValue(0);
39
+ const scrollRef = useAnimatedRef<Animated.ScrollView>();
40
+
41
+ const onScroll = useAnimatedScrollHandler((event) => {
42
+ selfOffset.value = event.contentOffset.y;
43
+ if (activeTabIndex.value === index)
44
+ activeListOffset.value = event.contentOffset.y;
45
+ });
46
+
47
+ const composedScrollEvent = useComposedEventHandler(
48
+ props.onScroll ? [onScroll, props.onScroll] : [onScroll],
49
+ );
50
+
51
+ useAnimatedReaction(
52
+ () => activeTabIndex.value,
53
+ (value) => {
54
+ if (value === index) activeListOffset.value = selfOffset.value;
55
+ },
56
+ [index],
57
+ );
58
+
59
+ const scroller = useMemo(
60
+ (): ListScroller =>
61
+ (animated = true) => {
62
+ "worklet";
63
+ scrollTo(scrollRef, 0, 0, animated);
64
+ },
65
+ [scrollRef],
66
+ );
67
+
68
+ useEffect(() => {
69
+ registerListScroller(index, scroller);
70
+ return () => registerListScroller(index, null);
71
+ }, [index, registerListScroller, scroller]);
72
+
73
+ const stableLayout = useStableCallback(onLayout);
74
+ const stableContentSizeChange = useStableCallback(onContentSizeChange);
75
+
76
+ const handleLayout = useCallback(
77
+ (event: LayoutChangeEvent) => {
78
+ stableLayout?.(event);
79
+ },
80
+ [stableLayout],
81
+ );
82
+
83
+ const handleContentSizeChange = useCallback(
84
+ (width: number, height: number) => {
85
+ stableContentSizeChange?.(width, height);
86
+ },
87
+ [stableContentSizeChange],
88
+ );
89
+
90
+ return (
91
+ <View style={styles.view} collapsable={false}>
92
+ <GestureDetector gesture={listGestures[index]}>
93
+ <Animated.ScrollView
94
+ ref={scrollRef}
95
+ scrollEventThrottle={16}
96
+ showsVerticalScrollIndicator
97
+ directionalLockEnabled
98
+ keyboardShouldPersistTaps="handled"
99
+ {...props}
100
+ // onScroll={onScroll as RNScrollViewProps["onScroll"]}
101
+ onScroll={composedScrollEvent}
102
+ onLayout={handleLayout}
103
+ onContentSizeChange={handleContentSizeChange}
104
+ />
105
+ </GestureDetector>
106
+ </View>
107
+ );
108
+ };
109
+
110
+ const styles = StyleSheet.create({
111
+ view: { position: "relative", flex: 1 },
112
+ });
113
+
114
+ ScrollView.displayName = "CollapsibleTabs.ScrollView";
115
+
116
+ export default memo(ScrollView) as (props: ScrollViewProps) => ReactElement;
@@ -0,0 +1,30 @@
1
+ import { memo } from 'react';
2
+
3
+ import { LayoutChangeEvent, ViewProps } from 'react-native';
4
+
5
+ import Animated from 'react-native-reanimated';
6
+
7
+ import { useCollapsibleTabsContext } from './Context';
8
+
9
+ const StaticHeader = ({ children, onLayout, ...props }: ViewProps) => {
10
+ const { updateStaticHeight, staticHeightValue } = useCollapsibleTabsContext();
11
+
12
+ const handleLayout = (evt: LayoutChangeEvent) => {
13
+ const height = evt.nativeEvent.layout.height;
14
+ if (__DEV__ && (!staticHeightValue || Math.abs(staticHeightValue - height) > 10)) {
15
+ console.info(`Set initialStaticHeight=${height} to reduce first-render flicker.`);
16
+ }
17
+ updateStaticHeight(height);
18
+ onLayout?.(evt);
19
+ };
20
+
21
+ return (
22
+ <Animated.View {...props} onLayout={handleLayout}>
23
+ {children}
24
+ </Animated.View>
25
+ );
26
+ };
27
+
28
+ StaticHeader.displayName = 'CollapsibleTabs.StaticHeader';
29
+
30
+ export default memo(StaticHeader);
@@ -0,0 +1,30 @@
1
+ import { memo } from 'react';
2
+
3
+ import { LayoutChangeEvent, ViewProps } from 'react-native';
4
+
5
+ import Animated from 'react-native-reanimated';
6
+
7
+ import { useCollapsibleTabsContext } from './Context';
8
+
9
+ const StickyHeader = ({ children, onLayout, ...props }: ViewProps) => {
10
+ const { updateStickyHeight, stickyHeightValue } = useCollapsibleTabsContext();
11
+
12
+ const handleLayout = (evt: LayoutChangeEvent) => {
13
+ const height = evt.nativeEvent.layout.height;
14
+ if (__DEV__ && (!stickyHeightValue || Math.abs(stickyHeightValue - height) > 10)) {
15
+ console.info(`Set initialStickyHeight=${height} to reduce first-render flicker.`);
16
+ }
17
+ updateStickyHeight(height);
18
+ onLayout?.(evt);
19
+ };
20
+
21
+ return (
22
+ <Animated.View {...props} onLayout={handleLayout}>
23
+ {children}
24
+ </Animated.View>
25
+ );
26
+ };
27
+
28
+ StickyHeader.displayName = 'CollapsibleTabs.StickyHeader';
29
+
30
+ export default memo(StickyHeader);