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
@@ -0,0 +1,150 @@
1
+ import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
2
+
3
+ import {
4
+ FlashList as ShopifyFlashList,
5
+ FlashListProps as ShopifyFlashListProps,
6
+ FlashListRef as ShopifyFlashListRef,
7
+ } from "@shopify/flash-list";
8
+ import {
9
+ LayoutChangeEvent,
10
+ ScrollViewProps,
11
+ StyleSheet,
12
+ View,
13
+ } from "react-native";
14
+
15
+ import {
16
+ GestureDetector,
17
+ ScrollView as RNGHScrollView,
18
+ } from "react-native-gesture-handler";
19
+ import Animated, {
20
+ AnimatedProps,
21
+ scrollTo,
22
+ useAnimatedReaction,
23
+ useAnimatedRef,
24
+ useAnimatedScrollHandler,
25
+ useComposedEventHandler,
26
+ useSharedValue,
27
+ } from "react-native-reanimated";
28
+
29
+ import { ListScroller, useCollapsibleTabsContext } from "./Context";
30
+ import { useTabSelfContext } from "./Tab";
31
+ import { useStableCallback } from "./useStableCallback";
32
+
33
+ const AnimatedFlashList = Animated.createAnimatedComponent(
34
+ ShopifyFlashList,
35
+ ) as <T>(
36
+ props: AnimatedProps<
37
+ ShopifyFlashListProps<T> & {
38
+ ref?: React.Ref<ShopifyFlashListRef<T>> | undefined;
39
+ }
40
+ >,
41
+ ) => ReactElement;
42
+
43
+ export type CollapsibleFlashListProps<T> = Omit<
44
+ ShopifyFlashListProps<T>,
45
+ "renderScrollComponent"
46
+ >;
47
+
48
+ const CollapsibleFlashList = <T,>({
49
+ onLayout,
50
+ onContentSizeChange,
51
+
52
+ ...props
53
+ }: CollapsibleFlashListProps<T>) => {
54
+ const {
55
+ listGestures,
56
+ activeTabIndex,
57
+ activeListOffset,
58
+ registerListScroller,
59
+ } = useCollapsibleTabsContext();
60
+ const { index } = useTabSelfContext();
61
+ const selfOffset = useSharedValue(0);
62
+ const listRef = useAnimatedRef<any>();
63
+
64
+ const onScroll = useAnimatedScrollHandler((event) => {
65
+ selfOffset.value = event.contentOffset.y;
66
+ if (activeTabIndex.value === index)
67
+ activeListOffset.value = event.contentOffset.y;
68
+ });
69
+
70
+ const composedScrollEvent = useComposedEventHandler(
71
+ props.onScroll ? [onScroll, props.onScroll] : [onScroll],
72
+ );
73
+
74
+ const myListGesture = listGestures[index];
75
+
76
+ const renderScrollComponent = useCallback(
77
+ (scrollProps: ScrollViewProps) => (
78
+ <GestureDetector gesture={myListGesture}>
79
+ <Animated.ScrollView {...scrollProps} />
80
+ </GestureDetector>
81
+ ),
82
+ [myListGesture],
83
+ );
84
+
85
+ useAnimatedReaction(
86
+ () => activeTabIndex.value,
87
+ (value) => {
88
+ if (value === index) activeListOffset.value = selfOffset.value;
89
+ },
90
+ [index],
91
+ );
92
+
93
+ const scroller = useMemo(
94
+ (): ListScroller =>
95
+ (animated = true) => {
96
+ "worklet";
97
+ scrollTo(listRef, 0, 0, animated);
98
+ },
99
+ [listRef],
100
+ );
101
+
102
+ useEffect(() => {
103
+ registerListScroller(index, scroller);
104
+ return () => registerListScroller(index, null);
105
+ }, [index, registerListScroller, scroller]);
106
+
107
+ const stableLayout = useStableCallback(onLayout);
108
+ const stableContentSizeChange = useStableCallback(onContentSizeChange);
109
+
110
+ const handleLayout = useCallback(
111
+ (event: LayoutChangeEvent) => {
112
+ stableLayout?.(event);
113
+ },
114
+ [stableLayout],
115
+ );
116
+
117
+ const handleContentSizeChange = useCallback(
118
+ (width: number, height: number) => {
119
+ stableContentSizeChange?.(width, height);
120
+ },
121
+ [stableContentSizeChange],
122
+ );
123
+
124
+ return (
125
+ <View style={styles.view} collapsable={false}>
126
+ <AnimatedFlashList<T>
127
+ ref={listRef}
128
+ scrollEventThrottle={16}
129
+ showsVerticalScrollIndicator
130
+ directionalLockEnabled
131
+ keyboardShouldPersistTaps="handled"
132
+ {...props}
133
+ renderScrollComponent={renderScrollComponent}
134
+ onScroll={composedScrollEvent}
135
+ onLayout={handleLayout}
136
+ onContentSizeChange={handleContentSizeChange}
137
+ />
138
+ </View>
139
+ );
140
+ };
141
+
142
+ const styles = StyleSheet.create({
143
+ view: { position: "relative", flex: 1 },
144
+ });
145
+
146
+ CollapsibleFlashList.displayName = "CollapsibleTabs.CollapsibleFlashList";
147
+
148
+ export default memo(CollapsibleFlashList) as <T>(
149
+ props: CollapsibleFlashListProps<T>,
150
+ ) => ReactElement;
package/src/Header.tsx ADDED
@@ -0,0 +1,45 @@
1
+ import { memo, useMemo } from 'react';
2
+
3
+ import { Platform, ViewProps } from 'react-native';
4
+
5
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
6
+ import Animated, { clamp, useAnimatedStyle, withDecay } from 'react-native-reanimated';
7
+
8
+ import { useCollapsibleTabsContext } from './Context';
9
+
10
+ const DECELERATION = Platform.OS === 'android' ? 0.985 : 0.998;
11
+
12
+ const Header = ({ children, style, ...props }: ViewProps) => {
13
+ const { headerOffset, staticHeight, offsetAdjustment } = useCollapsibleTabsContext();
14
+
15
+ const panGesture = useMemo(
16
+ () =>
17
+ Gesture.Pan()
18
+ .activeOffsetY([-5, 5])
19
+ .failOffsetX([-5, 5])
20
+ .maxPointers(1)
21
+ .onChange((evt) => {
22
+ const minOffset = -(staticHeight.value - offsetAdjustment.value);
23
+ headerOffset.value = clamp(headerOffset.value + evt.changeY, minOffset, 0);
24
+ })
25
+ .onEnd((evt) => {
26
+ const minOffset = -(staticHeight.value - offsetAdjustment.value);
27
+ headerOffset.value = withDecay({ velocity: evt.velocityY, rubberBandEffect: false, clamp: [minOffset, 0], deceleration: DECELERATION });
28
+ }),
29
+ [headerOffset, offsetAdjustment, staticHeight]
30
+ );
31
+
32
+ const animatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: headerOffset.value }] }), []);
33
+
34
+ return (
35
+ <GestureDetector gesture={panGesture}>
36
+ <Animated.View {...props} style={[style, animatedStyle]} collapsable={false}>
37
+ {children}
38
+ </Animated.View>
39
+ </GestureDetector>
40
+ );
41
+ };
42
+
43
+ Header.displayName = 'CollapsibleTabs.Header';
44
+
45
+ export default memo(Header);
@@ -0,0 +1,193 @@
1
+ import { memo } from "react";
2
+
3
+ import {
4
+ PixelRatio,
5
+ StyleProp,
6
+ StyleSheet,
7
+ ViewProps,
8
+ ViewStyle,
9
+ } from "react-native";
10
+
11
+ import Animated, {
12
+ AnimatedStyle,
13
+ interpolate,
14
+ useAnimatedStyle,
15
+ useDerivedValue,
16
+ } from "react-native-reanimated";
17
+
18
+ import { useCollapsibleTabsContext } from "./Context";
19
+
20
+ type CommonIndicatorProps = {
21
+ style?: AnimatedStyle<StyleProp<ViewStyle>>;
22
+ color?: string;
23
+ borderRadius?: number;
24
+ } & ViewProps;
25
+
26
+ export const MaterialIndicator = memo(
27
+ ({
28
+ style,
29
+ color = "#111827",
30
+ borderRadius = 999,
31
+ ...props
32
+ }: CommonIndicatorProps) => {
33
+ const { itemLayout, pageDecimal } = useCollapsibleTabsContext();
34
+
35
+ const data = useDerivedValue(() => {
36
+ if (!itemLayout || itemLayout.length === 0)
37
+ return { input: [], width: [], translateX: [] };
38
+ const input = new Array<number>(itemLayout.length);
39
+ const translateX = new Array<number>(itemLayout.length);
40
+ const width = new Array<number>(itemLayout.length);
41
+ for (let index = 0; index < itemLayout.length; index += 1) {
42
+ const item = itemLayout[index];
43
+ input[index] = index;
44
+ translateX[index] = item.x;
45
+ width[index] = item.width;
46
+ }
47
+ return {
48
+ input,
49
+ translateX,
50
+ width,
51
+ };
52
+ }, [itemLayout]);
53
+
54
+ const animatedStyles = useAnimatedStyle(() => {
55
+ if (
56
+ data.value.input.length === 0 ||
57
+ data.value.width.length === 0 ||
58
+ data.value.translateX.length === 0
59
+ )
60
+ return { opacity: 0, width: 0 };
61
+ if (data.value.input.length === 1)
62
+ return {
63
+ opacity: 1,
64
+ width: data.value.width[0],
65
+ transform: [{ translateX: data.value.translateX[0] }],
66
+ };
67
+ return {
68
+ opacity: 1,
69
+ width: interpolate(
70
+ pageDecimal.value,
71
+ data.value.input,
72
+ data.value.width,
73
+ "clamp",
74
+ ),
75
+ transform: [
76
+ {
77
+ translateX: interpolate(
78
+ pageDecimal.value,
79
+ data.value.input,
80
+ data.value.translateX,
81
+ "clamp",
82
+ ),
83
+ },
84
+ ],
85
+ };
86
+ }, []);
87
+
88
+ return (
89
+ <Animated.View
90
+ style={[
91
+ styles.indicator,
92
+ { backgroundColor: color, borderRadius },
93
+ style,
94
+ animatedStyles,
95
+ ]}
96
+ {...props}
97
+ />
98
+ );
99
+ },
100
+ );
101
+
102
+ export const SegmentIndicator = memo(
103
+ ({
104
+ style,
105
+ color = "#ffffff",
106
+ borderRadius = 999,
107
+ ...props
108
+ }: CommonIndicatorProps) => {
109
+ const { itemLayout, pageDecimal } = useCollapsibleTabsContext();
110
+
111
+ const data = useDerivedValue(() => {
112
+ if (!itemLayout || itemLayout.length === 0)
113
+ return { input: [], width: [], translateX: [], height: [] };
114
+ const input = new Array<number>(itemLayout.length);
115
+ const translateX = new Array<number>(itemLayout.length);
116
+ const width = new Array<number>(itemLayout.length);
117
+ const height = new Array<number>(itemLayout.length);
118
+ for (let index = 0; index < itemLayout.length; index += 1) {
119
+ const item = itemLayout[index];
120
+ input[index] = index;
121
+ translateX[index] = item.x;
122
+ width[index] = item.width;
123
+ height[index] = item.height;
124
+ }
125
+ return {
126
+ input,
127
+ translateX,
128
+ width,
129
+ height,
130
+ };
131
+ }, [itemLayout]);
132
+
133
+ const animatedStyles = useAnimatedStyle(() => {
134
+ const input = data.value.input;
135
+ const width = data.value.width;
136
+ const translateX = data.value.translateX;
137
+ const height = data.value.height;
138
+ if (input.length === 0 || width.length === 0 || translateX.length === 0)
139
+ return { opacity: 0, width: 0 };
140
+ if (input.length === 1)
141
+ return {
142
+ opacity: 1,
143
+ width: width[0],
144
+ transform: [{ translateX: translateX[0] }],
145
+ height: height[0],
146
+ };
147
+ return {
148
+ opacity: 1,
149
+ width: interpolate(pageDecimal.value, input, width, "clamp"),
150
+ transform: [
151
+ {
152
+ translateX: interpolate(
153
+ pageDecimal.value,
154
+ input,
155
+ translateX,
156
+ "clamp",
157
+ ),
158
+ },
159
+ ],
160
+ height: height[0],
161
+ };
162
+ }, []);
163
+
164
+ return (
165
+ <Animated.View
166
+ style={[
167
+ styles.segment,
168
+ { backgroundColor: color, borderRadius },
169
+ style,
170
+ animatedStyles,
171
+ ]}
172
+ {...props}
173
+ />
174
+ );
175
+ },
176
+ );
177
+
178
+ MaterialIndicator.displayName = "CollapsibleTabs.MaterialIndicator";
179
+ SegmentIndicator.displayName = "CollapsibleTabs.SegmentIndicator";
180
+
181
+ const styles = StyleSheet.create({
182
+ indicator: {
183
+ height: PixelRatio.roundToNearestPixel(3),
184
+ position: "absolute",
185
+ left: 0,
186
+ bottom: 0,
187
+ },
188
+ segment: {
189
+ position: "absolute",
190
+ left: 0,
191
+ zIndex: 0,
192
+ },
193
+ });
package/src/Lazy.tsx ADDED
@@ -0,0 +1,110 @@
1
+ import { ComponentProps, ReactNode, useEffect, useRef, useState } from 'react';
2
+
3
+ import { StyleProp, StyleSheet, ViewStyle } from 'react-native';
4
+
5
+ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
6
+
7
+ import { useStableCallback } from './useStableCallback';
8
+
9
+ type AnimatedViewProps = ComponentProps<typeof Animated.View>;
10
+ type LazyViewProps = Omit<AnimatedViewProps, 'children' | 'entering' | 'exiting' | 'style'>;
11
+
12
+ export type LazyPlaceholderInfo = {
13
+ focused: boolean;
14
+ canMount: boolean;
15
+ };
16
+
17
+ export type LazyProps = {
18
+ focused: boolean;
19
+ children?: ReactNode;
20
+ placeholder?: ReactNode;
21
+ renderPlaceholder?: (info: LazyPlaceholderInfo) => ReactNode;
22
+ placeholderStyle?: StyleProp<ViewStyle>;
23
+ placeholderProps?: LazyViewProps & { style?: StyleProp<ViewStyle> };
24
+ style?: StyleProp<ViewStyle>;
25
+ containerProps?: LazyViewProps & { style?: StyleProp<ViewStyle> };
26
+ disableEntering?: boolean;
27
+ disableExiting?: boolean;
28
+ entering?: AnimatedViewProps['entering'] | null;
29
+ exiting?: AnimatedViewProps['exiting'] | null;
30
+ enteringDuration?: number;
31
+ enteringDelay?: number;
32
+ exitingDuration?: number;
33
+ duration?: number;
34
+ delay?: number;
35
+ onMount?: () => void;
36
+ };
37
+
38
+ export function Lazy({
39
+ placeholder,
40
+ renderPlaceholder,
41
+ placeholderStyle,
42
+ placeholderProps,
43
+ containerProps,
44
+ disableEntering = false,
45
+ disableExiting = false,
46
+ entering,
47
+ exiting,
48
+ enteringDuration,
49
+ enteringDelay,
50
+ exitingDuration = 300,
51
+ focused,
52
+ duration = 200,
53
+ delay = 50,
54
+ onMount,
55
+ children,
56
+ style,
57
+ }: LazyProps) {
58
+ const [canMount, setCanMount] = useState(false);
59
+ const mountNotifiedRef = useRef(false);
60
+ const stableOnMount = useStableCallback(onMount);
61
+
62
+ useEffect(() => {
63
+ if (focused) {
64
+ setCanMount(true);
65
+ if (!mountNotifiedRef.current) {
66
+ mountNotifiedRef.current = true;
67
+ stableOnMount();
68
+ }
69
+ }
70
+ }, [focused, stableOnMount]);
71
+
72
+ const placeholderPointerEvents = placeholderProps?.pointerEvents ?? 'box-none';
73
+ const { style: placeholderContainerStyle, pointerEvents: _placeholderPointerEvents, ...restPlaceholderProps } = placeholderProps ?? {};
74
+ const { style: containerStyle, ...restContainerProps } = containerProps ?? {};
75
+
76
+ const enteringAnimation =
77
+ disableEntering || entering === null ? undefined : entering ?? FadeIn.duration(enteringDuration ?? duration).delay(enteringDelay ?? delay);
78
+ const exitingAnimation = disableExiting || exiting === null ? undefined : exiting ?? FadeOut.duration(exitingDuration);
79
+
80
+ if (!canMount) {
81
+ return (
82
+ <Animated.View
83
+ {...restPlaceholderProps}
84
+ exiting={exitingAnimation}
85
+ style={[styles.placeholder, placeholderStyle, placeholderContainerStyle]}
86
+ pointerEvents={placeholderPointerEvents}
87
+ >
88
+ {renderPlaceholder ? renderPlaceholder({ focused, canMount }) : placeholder}
89
+ </Animated.View>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <Animated.View {...restContainerProps} entering={enteringAnimation} style={[styles.container, style, containerStyle]}>
95
+ {children}
96
+ </Animated.View>
97
+ );
98
+ }
99
+
100
+ const styles = StyleSheet.create({
101
+ container: {
102
+ width: '100%',
103
+ flex: 1,
104
+ height: '100%',
105
+ },
106
+ placeholder: {
107
+ paddingVertical: 12,
108
+ width: '100%',
109
+ },
110
+ });
@@ -0,0 +1,130 @@
1
+ import { ReactElement, memo, useCallback, useEffect, useMemo } from "react";
2
+
3
+ import {
4
+ LegendList,
5
+ LegendListProps as LegendListLibProps,
6
+ LegendListRef,
7
+ } from "@legendapp/list/react-native";
8
+ import { LayoutChangeEvent, StyleSheet, View } from "react-native";
9
+
10
+ import {
11
+ GestureDetector,
12
+ ScrollView as RNGHScrollView,
13
+ } from "react-native-gesture-handler";
14
+ import Animated, {
15
+ AnimatedProps,
16
+ useAnimatedReaction,
17
+ useAnimatedRef,
18
+ useAnimatedScrollHandler,
19
+ useComposedEventHandler,
20
+ useSharedValue,
21
+ } from "react-native-reanimated";
22
+
23
+ import { ListScroller, useCollapsibleTabsContext } from "./Context";
24
+ import { useTabSelfContext } from "./Tab";
25
+ import { useStableCallback } from "./useStableCallback";
26
+
27
+ const AnimatedLegendList = Animated.createAnimatedComponent(LegendList) as <T>(
28
+ props: AnimatedProps<
29
+ LegendListLibProps<T> & {
30
+ ref?: React.Ref<LegendListRef> | undefined;
31
+ }
32
+ >,
33
+ ) => ReactElement;
34
+
35
+ export type CollapsibleLegendListProps<T> = LegendListLibProps<T>;
36
+
37
+ const CollapsibleLegendList = <T,>({
38
+ onLayout,
39
+ onContentSizeChange,
40
+ ...props
41
+ }: CollapsibleLegendListProps<T>) => {
42
+ const {
43
+ listGestures,
44
+ activeTabIndex,
45
+ activeListOffset,
46
+ registerListScroller,
47
+ } = useCollapsibleTabsContext();
48
+ const { index } = useTabSelfContext();
49
+ const selfOffset = useSharedValue(0);
50
+ const listRef = useAnimatedRef<any>();
51
+
52
+ const onScroll = useAnimatedScrollHandler((event) => {
53
+ selfOffset.value = event.contentOffset.y;
54
+ if (activeTabIndex.value === index)
55
+ activeListOffset.value = event.contentOffset.y;
56
+ });
57
+
58
+ const composedScrollEvent = useComposedEventHandler(
59
+ props.onScroll ? [onScroll, props.onScroll] : [onScroll],
60
+ );
61
+
62
+ useAnimatedReaction(
63
+ () => activeTabIndex.value,
64
+ (value) => {
65
+ if (value === index) activeListOffset.value = selfOffset.value;
66
+ },
67
+ [index],
68
+ );
69
+
70
+ const scroller = useMemo(
71
+ (): ListScroller =>
72
+ (animated = true) => {
73
+ (listRef.current as unknown as LegendListRef)?.scrollToOffset({
74
+ offset: 0,
75
+ animated,
76
+ });
77
+ },
78
+ [listRef],
79
+ );
80
+
81
+ useEffect(() => {
82
+ registerListScroller(index, scroller);
83
+ return () => registerListScroller(index, null);
84
+ }, [index, registerListScroller, scroller]);
85
+
86
+ const stableLayout = useStableCallback(onLayout);
87
+ const stableContentSizeChange = useStableCallback(onContentSizeChange);
88
+
89
+ const handleLayout = useCallback(
90
+ (event: LayoutChangeEvent) => {
91
+ stableLayout?.(event);
92
+ },
93
+ [stableLayout],
94
+ );
95
+
96
+ const handleContentSizeChange = useCallback(
97
+ (width: number, height: number) => {
98
+ stableContentSizeChange?.(width, height);
99
+ },
100
+ [stableContentSizeChange],
101
+ );
102
+
103
+ return (
104
+ <View style={styles.view} collapsable={false}>
105
+ <GestureDetector gesture={listGestures[index]}>
106
+ <AnimatedLegendList<T>
107
+ ref={listRef}
108
+ scrollEventThrottle={16}
109
+ showsVerticalScrollIndicator
110
+ directionalLockEnabled
111
+ keyboardShouldPersistTaps="handled"
112
+ {...props}
113
+ onScroll={composedScrollEvent}
114
+ onLayout={handleLayout}
115
+ onContentSizeChange={handleContentSizeChange}
116
+ />
117
+ </GestureDetector>
118
+ </View>
119
+ );
120
+ };
121
+
122
+ const styles = StyleSheet.create({
123
+ view: { position: "relative" },
124
+ });
125
+
126
+ CollapsibleLegendList.displayName = "CollapsibleTabs.CollapsibleLegendList";
127
+
128
+ export default memo(CollapsibleLegendList) as <T>(
129
+ props: CollapsibleLegendListProps<T>,
130
+ ) => ReactElement;