@orionarm/react-native-collapse-tabs 1.0.0 → 1.0.2
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.
- package/README.md +1 -31
- package/lib/CollapseTabs.d.ts +1 -5
- package/lib/CollapseTabs.d.ts.map +1 -1
- package/lib/CollapseTabs.js +115 -11
- package/lib/CollapseTabs.js.map +1 -0
- package/lib/FlatList.d.ts +8 -0
- package/lib/FlatList.d.ts.map +1 -0
- package/lib/FlatList.js +23 -0
- package/lib/ScrollView.d.ts +9 -0
- package/lib/ScrollView.d.ts.map +1 -0
- package/lib/ScrollView.js +25 -0
- package/lib/Tab.d.ts +8 -0
- package/lib/Tab.d.ts.map +1 -0
- package/lib/Tab.js +12 -0
- package/lib/TabBar.d.ts +4 -0
- package/lib/TabBar.d.ts.map +1 -0
- package/lib/TabBar.js +49 -0
- package/lib/context/index.d.ts +5 -0
- package/lib/context/index.d.ts.map +1 -0
- package/lib/context/index.js +2 -0
- package/lib/hooks/index.d.ts +3 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +8 -0
- package/lib/index.d.ts +7 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -5
- package/lib/index.js.map +1 -0
- package/lib/types/types.d.ts +57 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +1 -0
- package/package.json +9 -5
- package/src/CollapseTabs.tsx +210 -8
- package/src/FlatList.tsx +47 -0
- package/src/ScrollView.tsx +54 -0
- package/src/Tab.tsx +15 -0
- package/src/TabBar.tsx +86 -0
- package/src/context/index.tsx +8 -0
- package/src/hooks/index.ts +9 -0
- package/src/index.tsx +14 -2
- package/src/types/types.ts +85 -0
package/README.md
CHANGED
|
@@ -1,33 +1,3 @@
|
|
|
1
1
|
# @orionarm/react-native-collapse-tabs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @orionarm/react-native-collapse-tabs
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
or
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
yarn add @orionarm/react-native-collapse-tabs
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## Usage
|
|
18
|
-
|
|
19
|
-
```tsx
|
|
20
|
-
import { CollapseTabs } from '@orionarm/react-native-collapse-tabs';
|
|
21
|
-
|
|
22
|
-
function App() {
|
|
23
|
-
return (
|
|
24
|
-
<CollapseTabs>
|
|
25
|
-
{/* Your content here */}
|
|
26
|
-
</CollapseTabs>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## License
|
|
32
|
-
|
|
33
|
-
ISC
|
|
3
|
+
> ⚠️ **Work in Progress** - This package is currently under active development and not yet ready for production use.
|
package/lib/CollapseTabs.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
export interface CollapseTabsProps {
|
|
4
|
-
children?: React.ReactNode;
|
|
5
|
-
style?: ViewStyle;
|
|
6
|
-
}
|
|
2
|
+
import { CollapseTabsProps } from "./types/types";
|
|
7
3
|
export declare const CollapseTabs: React.FC<CollapseTabsProps>;
|
|
8
4
|
//# sourceMappingURL=CollapseTabs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CollapseTabs.d.ts","sourceRoot":"","sources":["../src/CollapseTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"CollapseTabs.d.ts","sourceRoot":"","sources":["../src/CollapseTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAiBf,OAAO,EACL,iBAAiB,EAOlB,MAAM,eAAe,CAAC;AAIvB,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAwKpD,CAAC"}
|
package/lib/CollapseTabs.js
CHANGED
|
@@ -1,12 +1,116 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import React, { useCallback, useMemo, useRef, useState, } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import PagerView from "react-native-pager-view";
|
|
4
|
+
import Animated, { useAnimatedReaction, useAnimatedStyle, useEvent, useHandler, useSharedValue, } from "react-native-reanimated";
|
|
5
|
+
import { Context } from "./context";
|
|
6
|
+
import { DefaultTabBar } from "./TabBar";
|
|
7
|
+
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView);
|
|
8
|
+
export const CollapseTabs = ({ children, initialTabName, headerHeight, tabBarHeight, minHeaderHeight = 0, renderHeader, renderTabBar, containerStyle, headerContainerStyle, pagerProps, onIndexChange, }) => {
|
|
9
|
+
const tabs = useMemo(() => React.Children.toArray(children).filter((c) => React.isValidElement(c)), [children]);
|
|
10
|
+
const tabNames = useMemo(() => tabs.map((t) => t.props.name), [tabs]);
|
|
11
|
+
const initialIndex = Math.max(0, initialTabName ? tabNames.indexOf(initialTabName) : 0);
|
|
12
|
+
const headerScrollDistance = headerHeight - minHeaderHeight;
|
|
13
|
+
const index = useSharedValue(initialIndex);
|
|
14
|
+
const indexDecimal = useSharedValue(initialIndex);
|
|
15
|
+
const focusedTab = useSharedValue(tabNames[initialIndex] ?? "");
|
|
16
|
+
const initialScrollY = useMemo(() => {
|
|
17
|
+
const m = {};
|
|
18
|
+
tabNames.forEach((n) => (m[n] = 0));
|
|
19
|
+
return m;
|
|
20
|
+
}, [tabNames]);
|
|
21
|
+
const scrollY = useSharedValue(initialScrollY);
|
|
22
|
+
const headerTranslateY = useSharedValue(0);
|
|
23
|
+
useAnimatedReaction(() => {
|
|
24
|
+
const y = scrollY.value[focusedTab.value] ?? 0;
|
|
25
|
+
return -Math.min(y, headerScrollDistance);
|
|
26
|
+
}, (next) => {
|
|
27
|
+
headerTranslateY.value = next;
|
|
28
|
+
}, [headerScrollDistance]);
|
|
29
|
+
const refMap = useRef({}).current;
|
|
30
|
+
const setRef = useCallback((key, ref) => {
|
|
31
|
+
refMap[key] = ref;
|
|
32
|
+
return ref;
|
|
33
|
+
}, [refMap]);
|
|
34
|
+
const containerRef = useRef(null);
|
|
35
|
+
const onTabPress = useCallback((name) => {
|
|
36
|
+
const i = tabNames.indexOf(name);
|
|
37
|
+
if (i < 0)
|
|
38
|
+
return;
|
|
39
|
+
containerRef.current?.setPage(i);
|
|
40
|
+
}, [tabNames]);
|
|
41
|
+
const [renderIndex, setRenderIndex] = useState(initialIndex);
|
|
42
|
+
const { doDependenciesDiffer } = useHandler({});
|
|
43
|
+
const pageScrollHandler = useEvent((e) => {
|
|
44
|
+
"worklet";
|
|
45
|
+
indexDecimal.value = e.position + e.offset;
|
|
46
|
+
}, ["onPageScroll"], doDependenciesDiffer);
|
|
47
|
+
const onPageSelected = useCallback((e) => {
|
|
48
|
+
const i = e.nativeEvent.position;
|
|
49
|
+
index.value = i;
|
|
50
|
+
focusedTab.value = tabNames[i] ?? "";
|
|
51
|
+
setRenderIndex(i);
|
|
52
|
+
onIndexChange?.(i);
|
|
53
|
+
}, [tabNames, index, focusedTab, onIndexChange]);
|
|
54
|
+
const headerAnimStyle = useAnimatedStyle(() => ({
|
|
55
|
+
transform: [{ translateY: headerTranslateY.value }],
|
|
56
|
+
}));
|
|
57
|
+
const ctxValue = {
|
|
58
|
+
headerHeight,
|
|
59
|
+
tabBarHeight,
|
|
60
|
+
minHeaderHeight,
|
|
61
|
+
headerScrollDistance,
|
|
62
|
+
tabNames,
|
|
63
|
+
index,
|
|
64
|
+
indexDecimal,
|
|
65
|
+
focusedTab,
|
|
66
|
+
scrollY,
|
|
67
|
+
headerTranslateY,
|
|
68
|
+
setRef,
|
|
69
|
+
refMap,
|
|
70
|
+
containerRef,
|
|
71
|
+
};
|
|
72
|
+
const headerProps = {
|
|
73
|
+
tabNames,
|
|
74
|
+
focusedTab,
|
|
75
|
+
index,
|
|
76
|
+
indexDecimal,
|
|
77
|
+
onTabPress,
|
|
78
|
+
};
|
|
79
|
+
return (<Context.Provider value={ctxValue}>
|
|
80
|
+
<View style={[styles.container, containerStyle]}>
|
|
81
|
+
<AnimatedPagerView ref={containerRef} style={StyleSheet.absoluteFill} initialPage={initialIndex} onPageScroll={pageScrollHandler} onPageSelected={onPageSelected} {...pagerProps}>
|
|
82
|
+
{tabs.map((tab, i) => (<View key={tab.props.name} style={styles.page} collapsable={false}>
|
|
83
|
+
{tab}
|
|
84
|
+
</View>))}
|
|
85
|
+
</AnimatedPagerView>
|
|
86
|
+
|
|
87
|
+
<Animated.View pointerEvents="box-none" style={[
|
|
88
|
+
styles.headerContainer,
|
|
89
|
+
{ height: headerHeight + tabBarHeight },
|
|
90
|
+
headerContainerStyle,
|
|
91
|
+
headerAnimStyle,
|
|
92
|
+
]}>
|
|
93
|
+
<View pointerEvents="box-none" style={{ height: headerHeight }}>
|
|
94
|
+
{renderHeader?.(headerProps)}
|
|
95
|
+
</View>
|
|
96
|
+
<View style={{ height: tabBarHeight }}>
|
|
97
|
+
{renderTabBar
|
|
98
|
+
? renderTabBar(headerProps)
|
|
99
|
+
: <DefaultTabBar {...headerProps}/>}
|
|
100
|
+
</View>
|
|
101
|
+
</Animated.View>
|
|
102
|
+
</View>
|
|
103
|
+
</Context.Provider>);
|
|
4
104
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
105
|
+
const styles = StyleSheet.create({
|
|
106
|
+
container: { flex: 1, overflow: "hidden" },
|
|
107
|
+
page: { flex: 1 },
|
|
108
|
+
headerContainer: {
|
|
109
|
+
position: "absolute",
|
|
110
|
+
top: 0,
|
|
111
|
+
left: 0,
|
|
112
|
+
right: 0,
|
|
113
|
+
zIndex: 10,
|
|
114
|
+
backgroundColor: "#fff",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CollapseTabs.js","sourceRoot":"","sources":["../src/CollapseTabs.tsx"],"names":[],"mappings":";;;AAAA,iCAA0B;AAC1B,+CAAiE;AAO1D,MAAM,YAAY,GAAgC,CAAC,EACxD,QAAQ,EACR,KAAK,GACN,EAAE,EAAE;IACH,OAAO,CAAC,mBAAI,CAAE,CAAA,CAAC,QAAQ,CAAC,EAAE,mBAAI,CAAC,CAAC;AAClC,CAAC,CAAC;AALW,QAAA,YAAY,gBAKvB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FlatListProps } from "react-native";
|
|
3
|
+
type Props<T> = FlatListProps<T> & {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function FlatList<T>({ name, contentContainerStyle, ...rest }: Props<T>): React.JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=FlatList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlatList.d.ts","sourceRoot":"","sources":["../src/FlatList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,EAA0B,aAAa,EAAE,MAAM,cAAc,CAAC;AAOrE,KAAK,KAAK,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,GAAG,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,qBAoC7E"}
|
package/lib/FlatList.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import Animated, { useAnimatedRef, useAnimatedScrollHandler, } from "react-native-reanimated";
|
|
3
|
+
import { useTabsContext } from "./hooks";
|
|
4
|
+
export function FlatList({ name, contentContainerStyle, ...rest }) {
|
|
5
|
+
const { headerHeight, tabBarHeight, scrollY, focusedTab, setRef, } = useTabsContext();
|
|
6
|
+
const ref = useAnimatedRef();
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setRef(name, ref);
|
|
9
|
+
}, [name, ref, setRef]);
|
|
10
|
+
const handler = useAnimatedScrollHandler({
|
|
11
|
+
onScroll: (event) => {
|
|
12
|
+
if (focusedTab.value !== name)
|
|
13
|
+
return;
|
|
14
|
+
const map = { ...scrollY.value };
|
|
15
|
+
map[name] = event.contentOffset.y;
|
|
16
|
+
scrollY.value = map;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
return (<Animated.FlatList {...rest} ref={ref} onScroll={handler} scrollEventThrottle={16} contentContainerStyle={[
|
|
20
|
+
{ paddingTop: headerHeight + tabBarHeight },
|
|
21
|
+
contentContainerStyle,
|
|
22
|
+
]}/>);
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ScrollViewProps } from "react-native";
|
|
3
|
+
type Props = ScrollViewProps & {
|
|
4
|
+
name: string;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
export declare function ScrollView({ name, contentContainerStyle, children, ...rest }: Props): React.JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ScrollView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScrollView.d.ts","sourceRoot":"","sources":["../src/ScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAC;AACzC,OAAO,EAAE,eAAe,EAA8B,MAAM,cAAc,CAAC;AAO3E,KAAK,KAAK,GAAG,eAAe,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,CAAC;AAE5E,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,qBAAqB,EACrB,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,KAAK,qBAsCP"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import Animated, { useAnimatedRef, useAnimatedScrollHandler, } from "react-native-reanimated";
|
|
3
|
+
import { useTabsContext } from "./hooks";
|
|
4
|
+
export function ScrollView({ name, contentContainerStyle, children, ...rest }) {
|
|
5
|
+
const { headerHeight, tabBarHeight, scrollY, focusedTab, setRef, } = useTabsContext();
|
|
6
|
+
const ref = useAnimatedRef();
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setRef(name, ref);
|
|
9
|
+
}, [name, ref, setRef]);
|
|
10
|
+
const handler = useAnimatedScrollHandler({
|
|
11
|
+
onScroll: (event) => {
|
|
12
|
+
if (focusedTab.value !== name)
|
|
13
|
+
return;
|
|
14
|
+
const map = { ...scrollY.value };
|
|
15
|
+
map[name] = event.contentOffset.y;
|
|
16
|
+
scrollY.value = map;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
return (<Animated.ScrollView {...rest} ref={ref} onScroll={handler} scrollEventThrottle={16} contentContainerStyle={[
|
|
20
|
+
{ paddingTop: headerHeight + tabBarHeight },
|
|
21
|
+
contentContainerStyle,
|
|
22
|
+
]}>
|
|
23
|
+
{children}
|
|
24
|
+
</Animated.ScrollView>);
|
|
25
|
+
}
|
package/lib/Tab.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { TabProps } from "./types/types";
|
|
3
|
+
/**
|
|
4
|
+
* Container for a single tab's content. Should be a direct child of
|
|
5
|
+
* <CollapseTabs>. Place a wrapped <FlatList> or <ScrollView> inside.
|
|
6
|
+
*/
|
|
7
|
+
export declare const Tab: <T extends string>({ children }: TabProps<T>) => React.JSX.Element;
|
|
8
|
+
//# sourceMappingURL=Tab.d.ts.map
|
package/lib/Tab.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tab.d.ts","sourceRoot":"","sources":["../src/Tab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,GAAG,GAAI,CAAC,SAAS,MAAM,EAAE,cAAc,QAAQ,CAAC,CAAC,CAAC,sBAE9D,CAAC"}
|
package/lib/Tab.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
/**
|
|
4
|
+
* Container for a single tab's content. Should be a direct child of
|
|
5
|
+
* <CollapseTabs>. Place a wrapped <FlatList> or <ScrollView> inside.
|
|
6
|
+
*/
|
|
7
|
+
export const Tab = ({ children }) => {
|
|
8
|
+
return <View style={styles.container}>{children}</View>;
|
|
9
|
+
};
|
|
10
|
+
const styles = StyleSheet.create({
|
|
11
|
+
container: { flex: 1 },
|
|
12
|
+
});
|
package/lib/TabBar.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabBar.d.ts","sourceRoot":"","sources":["../src/TabBar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAmB/C,CAAC"}
|
package/lib/TabBar.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, StyleSheet, View } from "react-native";
|
|
3
|
+
import Animated, { interpolate, useAnimatedStyle, } from "react-native-reanimated";
|
|
4
|
+
export const DefaultTabBar = ({ tabNames, indexDecimal, onTabPress, }) => {
|
|
5
|
+
return (<View style={styles.container}>
|
|
6
|
+
{tabNames.map((name, i) => (<TabBarItem key={name} name={name} index={i} indexDecimal={indexDecimal} onPress={() => onTabPress(name)}/>))}
|
|
7
|
+
<Indicator count={tabNames.length} indexDecimal={indexDecimal}/>
|
|
8
|
+
</View>);
|
|
9
|
+
};
|
|
10
|
+
const TabBarItem = ({ name, index, indexDecimal, onPress }) => {
|
|
11
|
+
const labelStyle = useAnimatedStyle(() => ({
|
|
12
|
+
opacity: interpolate(indexDecimal.value, [index - 1, index, index + 1], [0.6, 1, 0.6], "clamp"),
|
|
13
|
+
}));
|
|
14
|
+
return (<Pressable style={styles.item} onPress={onPress}>
|
|
15
|
+
<Animated.Text style={[styles.label, labelStyle]}>{name}</Animated.Text>
|
|
16
|
+
</Pressable>);
|
|
17
|
+
};
|
|
18
|
+
const Indicator = ({ count, indexDecimal }) => {
|
|
19
|
+
const style = useAnimatedStyle(() => ({
|
|
20
|
+
transform: [
|
|
21
|
+
{ translateX: (indexDecimal.value * 100) / count + "%" },
|
|
22
|
+
],
|
|
23
|
+
width: `${100 / count}%`,
|
|
24
|
+
}));
|
|
25
|
+
return <Animated.View style={[styles.indicator, style]}/>;
|
|
26
|
+
};
|
|
27
|
+
const styles = StyleSheet.create({
|
|
28
|
+
container: {
|
|
29
|
+
flexDirection: "row",
|
|
30
|
+
backgroundColor: "#fff",
|
|
31
|
+
},
|
|
32
|
+
item: {
|
|
33
|
+
flex: 1,
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
justifyContent: "center",
|
|
36
|
+
},
|
|
37
|
+
label: {
|
|
38
|
+
fontSize: 14,
|
|
39
|
+
fontWeight: "500",
|
|
40
|
+
color: "#333",
|
|
41
|
+
},
|
|
42
|
+
indicator: {
|
|
43
|
+
position: "absolute",
|
|
44
|
+
bottom: 0,
|
|
45
|
+
left: 0,
|
|
46
|
+
height: 2,
|
|
47
|
+
backgroundColor: "#2D8CFF",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAW,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEzE,eAAO,MAAM,OAAO,gDAEnB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,wBAAgB,cAAc,IAAI,WAAW,CAAC,MAAM,CAAC,CAIpD"}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
export { CollapseTabs } from
|
|
2
|
-
export
|
|
1
|
+
export { CollapseTabs } from "./CollapseTabs";
|
|
2
|
+
export { Tab } from "./Tab";
|
|
3
|
+
export { FlatList } from "./FlatList";
|
|
4
|
+
export { ScrollView } from "./ScrollView";
|
|
5
|
+
export { DefaultTabBar } from "./TabBar";
|
|
6
|
+
export { useTabsContext } from "./hooks";
|
|
7
|
+
export type { CollapseTabsProps, TabProps, TabBarProps, HeaderProps, TabName, } from "./types/types";
|
|
3
8
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,YAAY,EACV,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,WAAW,EACX,OAAO,GACR,MAAM,eAAe,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export { CollapseTabs } from "./CollapseTabs";
|
|
2
|
+
export { Tab } from "./Tab";
|
|
3
|
+
export { FlatList } from "./FlatList";
|
|
4
|
+
export { ScrollView } from "./ScrollView";
|
|
5
|
+
export { DefaultTabBar } from "./TabBar";
|
|
6
|
+
export { useTabsContext } from "./hooks";
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":";;;AAAA,+CAA8C;AAArC,4GAAA,YAAY,OAAA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FlatList, ScrollView, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import PagerView, { PagerViewProps } from "react-native-pager-view";
|
|
4
|
+
import Animated, { AnimatedRef, SharedValue } from "react-native-reanimated";
|
|
5
|
+
export type TabName = string;
|
|
6
|
+
export type ContainerRef = PagerView;
|
|
7
|
+
export type RefComponent = FlatList<any> | ScrollView | Animated.ScrollView;
|
|
8
|
+
export type TabBarProps<T extends TabName = TabName> = {
|
|
9
|
+
tabNames: T[];
|
|
10
|
+
focusedTab: SharedValue<T>;
|
|
11
|
+
index: SharedValue<number>;
|
|
12
|
+
indexDecimal: SharedValue<number>;
|
|
13
|
+
onTabPress: (name: T) => void;
|
|
14
|
+
};
|
|
15
|
+
export type HeaderProps<T extends TabName = TabName> = TabBarProps<T>;
|
|
16
|
+
export type TabProps<T extends TabName = TabName> = {
|
|
17
|
+
readonly name: T;
|
|
18
|
+
label?: string;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
};
|
|
21
|
+
export type TabReactElement<T extends TabName = TabName> = React.ReactElement<TabProps<T>>;
|
|
22
|
+
export type CollapseTabsProps = {
|
|
23
|
+
children: TabReactElement | TabReactElement[];
|
|
24
|
+
/** Initial tab name. Defaults to the first child. */
|
|
25
|
+
initialTabName?: TabName;
|
|
26
|
+
/** Fixed header height. Required for MVP. */
|
|
27
|
+
headerHeight: number;
|
|
28
|
+
/** Fixed tab bar height. Required for MVP. */
|
|
29
|
+
tabBarHeight: number;
|
|
30
|
+
/** Minimum header height when fully collapsed. @default 0 */
|
|
31
|
+
minHeaderHeight?: number;
|
|
32
|
+
renderHeader?: (props: HeaderProps) => React.ReactElement | null;
|
|
33
|
+
renderTabBar?: (props: TabBarProps) => React.ReactElement | null;
|
|
34
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
35
|
+
headerContainerStyle?: StyleProp<ViewStyle>;
|
|
36
|
+
pagerProps?: Omit<PagerViewProps, "onPageScroll" | "initialPage">;
|
|
37
|
+
onIndexChange?: (index: number) => void;
|
|
38
|
+
};
|
|
39
|
+
/** Internal context shared between CollapseTabs and its children. */
|
|
40
|
+
export type ContextType<T extends TabName = TabName> = {
|
|
41
|
+
headerHeight: number;
|
|
42
|
+
tabBarHeight: number;
|
|
43
|
+
minHeaderHeight: number;
|
|
44
|
+
headerScrollDistance: number;
|
|
45
|
+
tabNames: T[];
|
|
46
|
+
index: SharedValue<number>;
|
|
47
|
+
indexDecimal: SharedValue<number>;
|
|
48
|
+
focusedTab: SharedValue<T>;
|
|
49
|
+
/** Per-tab scroll Y, keyed by tab name. */
|
|
50
|
+
scrollY: SharedValue<Record<T, number>>;
|
|
51
|
+
/** Header translateY (clamped to [-headerScrollDistance, 0]). */
|
|
52
|
+
headerTranslateY: SharedValue<number>;
|
|
53
|
+
setRef: <R extends RefComponent>(key: T, ref: AnimatedRef<R>) => AnimatedRef<R>;
|
|
54
|
+
refMap: Record<T, AnimatedRef<RefComponent>>;
|
|
55
|
+
containerRef: React.RefObject<ContainerRef>;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orionarm/react-native-collapse-tabs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A React Native collapsible tabs component",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
|
-
"
|
|
9
|
+
"watch": "tsc --watch",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
10
11
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
@@ -28,17 +29,20 @@
|
|
|
28
29
|
"license": "ISC",
|
|
29
30
|
"peerDependencies": {
|
|
30
31
|
"react": ">=16.8.0",
|
|
31
|
-
"react-native": ">=0.60.0"
|
|
32
|
+
"react-native": ">=0.60.0",
|
|
33
|
+
"react-native-gesture-handler": ">=2.0.0",
|
|
34
|
+
"react-native-pager-view": ">=6.0.0",
|
|
35
|
+
"react-native-reanimated": ">=3.0.0"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
34
38
|
"@types/react": "^18.0.0",
|
|
35
39
|
"@types/react-native": "^0.72.0",
|
|
36
40
|
"react": "^18.2.0",
|
|
37
41
|
"react-native": "^0.72.0",
|
|
38
|
-
"typescript": "^5.0.0",
|
|
39
42
|
"react-native-gesture-handler": "~2.16.0",
|
|
40
43
|
"react-native-pager-view": "^6.3.0",
|
|
41
|
-
"react-native-reanimated": "3.8.1"
|
|
44
|
+
"react-native-reanimated": "3.8.1",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
42
46
|
},
|
|
43
47
|
"files": [
|
|
44
48
|
"lib",
|
package/src/CollapseTabs.tsx
CHANGED
|
@@ -1,14 +1,216 @@
|
|
|
1
|
-
import React
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { StyleSheet, View } from "react-native";
|
|
8
|
+
import PagerView, {
|
|
9
|
+
PagerViewOnPageScrollEventData,
|
|
10
|
+
PagerViewOnPageSelectedEventData,
|
|
11
|
+
} from "react-native-pager-view";
|
|
12
|
+
import Animated, {
|
|
13
|
+
AnimatedRef,
|
|
14
|
+
runOnJS,
|
|
15
|
+
useAnimatedReaction,
|
|
16
|
+
useAnimatedStyle,
|
|
17
|
+
useEvent,
|
|
18
|
+
useHandler,
|
|
19
|
+
useSharedValue,
|
|
20
|
+
} from "react-native-reanimated";
|
|
21
|
+
import { Context } from "./context";
|
|
22
|
+
import { DefaultTabBar } from "./TabBar";
|
|
23
|
+
import {
|
|
24
|
+
CollapseTabsProps,
|
|
25
|
+
ContainerRef,
|
|
26
|
+
ContextType,
|
|
27
|
+
RefComponent,
|
|
28
|
+
TabName,
|
|
29
|
+
TabProps,
|
|
30
|
+
TabReactElement,
|
|
31
|
+
} from "./types/types";
|
|
3
32
|
|
|
4
|
-
|
|
5
|
-
children?: React.ReactNode;
|
|
6
|
-
style?: ViewStyle;
|
|
7
|
-
}
|
|
33
|
+
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView);
|
|
8
34
|
|
|
9
35
|
export const CollapseTabs: React.FC<CollapseTabsProps> = ({
|
|
10
36
|
children,
|
|
11
|
-
|
|
37
|
+
initialTabName,
|
|
38
|
+
headerHeight,
|
|
39
|
+
tabBarHeight,
|
|
40
|
+
minHeaderHeight = 0,
|
|
41
|
+
renderHeader,
|
|
42
|
+
renderTabBar,
|
|
43
|
+
containerStyle,
|
|
44
|
+
headerContainerStyle,
|
|
45
|
+
pagerProps,
|
|
46
|
+
onIndexChange,
|
|
12
47
|
}) => {
|
|
13
|
-
|
|
48
|
+
const tabs = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
React.Children.toArray(children).filter(
|
|
51
|
+
(c): c is TabReactElement => React.isValidElement(c)
|
|
52
|
+
),
|
|
53
|
+
[children]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const tabNames = useMemo(
|
|
57
|
+
() => tabs.map((t) => (t.props as TabProps).name),
|
|
58
|
+
[tabs]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const initialIndex = Math.max(
|
|
62
|
+
0,
|
|
63
|
+
initialTabName ? tabNames.indexOf(initialTabName) : 0
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const headerScrollDistance = headerHeight - minHeaderHeight;
|
|
67
|
+
|
|
68
|
+
const index = useSharedValue(initialIndex);
|
|
69
|
+
const indexDecimal = useSharedValue(initialIndex);
|
|
70
|
+
const focusedTab = useSharedValue<TabName>(tabNames[initialIndex] ?? "");
|
|
71
|
+
|
|
72
|
+
const initialScrollY = useMemo(() => {
|
|
73
|
+
const m: Record<string, number> = {};
|
|
74
|
+
tabNames.forEach((n) => (m[n] = 0));
|
|
75
|
+
return m;
|
|
76
|
+
}, [tabNames]);
|
|
77
|
+
const scrollY = useSharedValue<Record<TabName, number>>(initialScrollY);
|
|
78
|
+
|
|
79
|
+
const headerTranslateY = useSharedValue(0);
|
|
80
|
+
|
|
81
|
+
useAnimatedReaction(
|
|
82
|
+
() => {
|
|
83
|
+
const y = scrollY.value[focusedTab.value] ?? 0;
|
|
84
|
+
return -Math.min(y, headerScrollDistance);
|
|
85
|
+
},
|
|
86
|
+
(next) => {
|
|
87
|
+
headerTranslateY.value = next;
|
|
88
|
+
},
|
|
89
|
+
[headerScrollDistance]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const refMap = useRef<Record<TabName, AnimatedRef<RefComponent>>>({}).current;
|
|
93
|
+
|
|
94
|
+
const setRef = useCallback(
|
|
95
|
+
<R extends RefComponent>(key: TabName, ref: AnimatedRef<R>) => {
|
|
96
|
+
refMap[key] = ref as unknown as AnimatedRef<RefComponent>;
|
|
97
|
+
return ref;
|
|
98
|
+
},
|
|
99
|
+
[refMap]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const containerRef = useRef<ContainerRef>(null);
|
|
103
|
+
|
|
104
|
+
const onTabPress = useCallback((name: TabName) => {
|
|
105
|
+
const i = tabNames.indexOf(name);
|
|
106
|
+
if (i < 0) return;
|
|
107
|
+
containerRef.current?.setPage(i);
|
|
108
|
+
}, [tabNames]);
|
|
109
|
+
|
|
110
|
+
const [renderIndex, setRenderIndex] = useState(initialIndex);
|
|
111
|
+
|
|
112
|
+
const { doDependenciesDiffer } = useHandler({});
|
|
113
|
+
const pageScrollHandler = useEvent<PagerViewOnPageScrollEventData>(
|
|
114
|
+
(e) => {
|
|
115
|
+
"worklet";
|
|
116
|
+
indexDecimal.value = e.position + e.offset;
|
|
117
|
+
},
|
|
118
|
+
["onPageScroll"],
|
|
119
|
+
doDependenciesDiffer
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const onPageSelected = useCallback(
|
|
123
|
+
(e: { nativeEvent: PagerViewOnPageSelectedEventData }) => {
|
|
124
|
+
const i = e.nativeEvent.position;
|
|
125
|
+
index.value = i;
|
|
126
|
+
focusedTab.value = tabNames[i] ?? "";
|
|
127
|
+
setRenderIndex(i);
|
|
128
|
+
onIndexChange?.(i);
|
|
129
|
+
},
|
|
130
|
+
[tabNames, index, focusedTab, onIndexChange]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const headerAnimStyle = useAnimatedStyle(() => ({
|
|
134
|
+
transform: [{ translateY: headerTranslateY.value }],
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
const ctxValue: ContextType = {
|
|
138
|
+
headerHeight,
|
|
139
|
+
tabBarHeight,
|
|
140
|
+
minHeaderHeight,
|
|
141
|
+
headerScrollDistance,
|
|
142
|
+
tabNames,
|
|
143
|
+
index,
|
|
144
|
+
indexDecimal,
|
|
145
|
+
focusedTab,
|
|
146
|
+
scrollY,
|
|
147
|
+
headerTranslateY,
|
|
148
|
+
setRef,
|
|
149
|
+
refMap,
|
|
150
|
+
containerRef,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const headerProps = {
|
|
154
|
+
tabNames,
|
|
155
|
+
focusedTab,
|
|
156
|
+
index,
|
|
157
|
+
indexDecimal,
|
|
158
|
+
onTabPress,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Context.Provider value={ctxValue}>
|
|
163
|
+
<View style={[styles.container, containerStyle]}>
|
|
164
|
+
<AnimatedPagerView
|
|
165
|
+
ref={containerRef as any}
|
|
166
|
+
style={StyleSheet.absoluteFill}
|
|
167
|
+
initialPage={initialIndex}
|
|
168
|
+
onPageScroll={pageScrollHandler as any}
|
|
169
|
+
onPageSelected={onPageSelected}
|
|
170
|
+
{...pagerProps}
|
|
171
|
+
>
|
|
172
|
+
{tabs.map((tab, i) => (
|
|
173
|
+
<View key={tab.props.name} style={styles.page} collapsable={false}>
|
|
174
|
+
{tab}
|
|
175
|
+
</View>
|
|
176
|
+
))}
|
|
177
|
+
</AnimatedPagerView>
|
|
178
|
+
|
|
179
|
+
<Animated.View
|
|
180
|
+
pointerEvents="box-none"
|
|
181
|
+
style={[
|
|
182
|
+
styles.headerContainer,
|
|
183
|
+
{ height: headerHeight + tabBarHeight },
|
|
184
|
+
headerContainerStyle,
|
|
185
|
+
headerAnimStyle,
|
|
186
|
+
]}
|
|
187
|
+
>
|
|
188
|
+
<View
|
|
189
|
+
pointerEvents="box-none"
|
|
190
|
+
style={{ height: headerHeight }}
|
|
191
|
+
>
|
|
192
|
+
{renderHeader?.(headerProps)}
|
|
193
|
+
</View>
|
|
194
|
+
<View style={{ height: tabBarHeight }}>
|
|
195
|
+
{renderTabBar
|
|
196
|
+
? renderTabBar(headerProps)
|
|
197
|
+
: <DefaultTabBar {...headerProps} />}
|
|
198
|
+
</View>
|
|
199
|
+
</Animated.View>
|
|
200
|
+
</View>
|
|
201
|
+
</Context.Provider>
|
|
202
|
+
);
|
|
14
203
|
};
|
|
204
|
+
|
|
205
|
+
const styles = StyleSheet.create({
|
|
206
|
+
container: { flex: 1, overflow: "hidden" },
|
|
207
|
+
page: { flex: 1 },
|
|
208
|
+
headerContainer: {
|
|
209
|
+
position: "absolute",
|
|
210
|
+
top: 0,
|
|
211
|
+
left: 0,
|
|
212
|
+
right: 0,
|
|
213
|
+
zIndex: 10,
|
|
214
|
+
backgroundColor: "#fff",
|
|
215
|
+
},
|
|
216
|
+
});
|
package/src/FlatList.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { FlatList as RNFlatList, FlatListProps } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedRef,
|
|
5
|
+
useAnimatedScrollHandler,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
import { useTabsContext } from "./hooks";
|
|
8
|
+
|
|
9
|
+
type Props<T> = FlatListProps<T> & { name: string };
|
|
10
|
+
|
|
11
|
+
export function FlatList<T>({ name, contentContainerStyle, ...rest }: Props<T>) {
|
|
12
|
+
const {
|
|
13
|
+
headerHeight,
|
|
14
|
+
tabBarHeight,
|
|
15
|
+
scrollY,
|
|
16
|
+
focusedTab,
|
|
17
|
+
setRef,
|
|
18
|
+
} = useTabsContext();
|
|
19
|
+
|
|
20
|
+
const ref = useAnimatedRef<RNFlatList<T>>();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setRef(name, ref);
|
|
24
|
+
}, [name, ref, setRef]);
|
|
25
|
+
|
|
26
|
+
const handler = useAnimatedScrollHandler({
|
|
27
|
+
onScroll: (event) => {
|
|
28
|
+
if (focusedTab.value !== name) return;
|
|
29
|
+
const map = { ...scrollY.value };
|
|
30
|
+
map[name] = event.contentOffset.y;
|
|
31
|
+
scrollY.value = map;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Animated.FlatList
|
|
37
|
+
{...(rest as FlatListProps<T>)}
|
|
38
|
+
ref={ref as any}
|
|
39
|
+
onScroll={handler}
|
|
40
|
+
scrollEventThrottle={16}
|
|
41
|
+
contentContainerStyle={[
|
|
42
|
+
{ paddingTop: headerHeight + tabBarHeight },
|
|
43
|
+
contentContainerStyle,
|
|
44
|
+
]}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { ScrollViewProps, ScrollView as RNScrollView } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedRef,
|
|
5
|
+
useAnimatedScrollHandler,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
import { useTabsContext } from "./hooks";
|
|
8
|
+
|
|
9
|
+
type Props = ScrollViewProps & { name: string; children?: React.ReactNode };
|
|
10
|
+
|
|
11
|
+
export function ScrollView({
|
|
12
|
+
name,
|
|
13
|
+
contentContainerStyle,
|
|
14
|
+
children,
|
|
15
|
+
...rest
|
|
16
|
+
}: Props) {
|
|
17
|
+
const {
|
|
18
|
+
headerHeight,
|
|
19
|
+
tabBarHeight,
|
|
20
|
+
scrollY,
|
|
21
|
+
focusedTab,
|
|
22
|
+
setRef,
|
|
23
|
+
} = useTabsContext();
|
|
24
|
+
|
|
25
|
+
const ref = useAnimatedRef<Animated.ScrollView>();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setRef(name, ref);
|
|
29
|
+
}, [name, ref, setRef]);
|
|
30
|
+
|
|
31
|
+
const handler = useAnimatedScrollHandler({
|
|
32
|
+
onScroll: (event) => {
|
|
33
|
+
if (focusedTab.value !== name) return;
|
|
34
|
+
const map = { ...scrollY.value };
|
|
35
|
+
map[name] = event.contentOffset.y;
|
|
36
|
+
scrollY.value = map;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Animated.ScrollView
|
|
42
|
+
{...rest}
|
|
43
|
+
ref={ref}
|
|
44
|
+
onScroll={handler}
|
|
45
|
+
scrollEventThrottle={16}
|
|
46
|
+
contentContainerStyle={[
|
|
47
|
+
{ paddingTop: headerHeight + tabBarHeight },
|
|
48
|
+
contentContainerStyle,
|
|
49
|
+
]}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</Animated.ScrollView>
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/Tab.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { TabProps } from "./types/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Container for a single tab's content. Should be a direct child of
|
|
7
|
+
* <CollapseTabs>. Place a wrapped <FlatList> or <ScrollView> inside.
|
|
8
|
+
*/
|
|
9
|
+
export const Tab = <T extends string>({ children }: TabProps<T>) => {
|
|
10
|
+
return <View style={styles.container}>{children}</View>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
container: { flex: 1 },
|
|
15
|
+
});
|
package/src/TabBar.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
interpolate,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
import { TabBarProps } from "./types/types";
|
|
8
|
+
|
|
9
|
+
export const DefaultTabBar: React.FC<TabBarProps> = ({
|
|
10
|
+
tabNames,
|
|
11
|
+
indexDecimal,
|
|
12
|
+
onTabPress,
|
|
13
|
+
}) => {
|
|
14
|
+
return (
|
|
15
|
+
<View style={styles.container}>
|
|
16
|
+
{tabNames.map((name, i) => (
|
|
17
|
+
<TabBarItem
|
|
18
|
+
key={name}
|
|
19
|
+
name={name}
|
|
20
|
+
index={i}
|
|
21
|
+
indexDecimal={indexDecimal}
|
|
22
|
+
onPress={() => onTabPress(name)}
|
|
23
|
+
/>
|
|
24
|
+
))}
|
|
25
|
+
<Indicator count={tabNames.length} indexDecimal={indexDecimal} />
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TabBarItem: React.FC<{
|
|
31
|
+
name: string;
|
|
32
|
+
index: number;
|
|
33
|
+
indexDecimal: Animated.SharedValue<number>;
|
|
34
|
+
onPress: () => void;
|
|
35
|
+
}> = ({ name, index, indexDecimal, onPress }) => {
|
|
36
|
+
const labelStyle = useAnimatedStyle(() => ({
|
|
37
|
+
opacity: interpolate(
|
|
38
|
+
indexDecimal.value,
|
|
39
|
+
[index - 1, index, index + 1],
|
|
40
|
+
[0.6, 1, 0.6],
|
|
41
|
+
"clamp"
|
|
42
|
+
),
|
|
43
|
+
}));
|
|
44
|
+
return (
|
|
45
|
+
<Pressable style={styles.item} onPress={onPress}>
|
|
46
|
+
<Animated.Text style={[styles.label, labelStyle]}>{name}</Animated.Text>
|
|
47
|
+
</Pressable>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const Indicator: React.FC<{
|
|
52
|
+
count: number;
|
|
53
|
+
indexDecimal: Animated.SharedValue<number>;
|
|
54
|
+
}> = ({ count, indexDecimal }) => {
|
|
55
|
+
const style = useAnimatedStyle(() => ({
|
|
56
|
+
transform: [
|
|
57
|
+
{ translateX: (indexDecimal.value * 100) / count + "%" } as any,
|
|
58
|
+
],
|
|
59
|
+
width: `${100 / count}%`,
|
|
60
|
+
}));
|
|
61
|
+
return <Animated.View style={[styles.indicator, style]} />;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const styles = StyleSheet.create({
|
|
65
|
+
container: {
|
|
66
|
+
flexDirection: "row",
|
|
67
|
+
backgroundColor: "#fff",
|
|
68
|
+
},
|
|
69
|
+
item: {
|
|
70
|
+
flex: 1,
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
justifyContent: "center",
|
|
73
|
+
},
|
|
74
|
+
label: {
|
|
75
|
+
fontSize: 14,
|
|
76
|
+
fontWeight: "500",
|
|
77
|
+
color: "#333",
|
|
78
|
+
},
|
|
79
|
+
indicator: {
|
|
80
|
+
position: "absolute",
|
|
81
|
+
bottom: 0,
|
|
82
|
+
left: 0,
|
|
83
|
+
height: 2,
|
|
84
|
+
backgroundColor: "#2D8CFF",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { Context } from "../context";
|
|
3
|
+
import { ContextType } from "../types/types";
|
|
4
|
+
|
|
5
|
+
export function useTabsContext(): ContextType<string> {
|
|
6
|
+
const c = useContext(Context);
|
|
7
|
+
if (!c) throw new Error("useTabsContext must be used inside <CollapseTabs>");
|
|
8
|
+
return c;
|
|
9
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
|
-
export { CollapseTabs } from
|
|
2
|
-
export
|
|
1
|
+
export { CollapseTabs } from "./CollapseTabs";
|
|
2
|
+
export { Tab } from "./Tab";
|
|
3
|
+
export { FlatList } from "./FlatList";
|
|
4
|
+
export { ScrollView } from "./ScrollView";
|
|
5
|
+
export { DefaultTabBar } from "./TabBar";
|
|
6
|
+
export { useTabsContext } from "./hooks";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
CollapseTabsProps,
|
|
10
|
+
TabProps,
|
|
11
|
+
TabBarProps,
|
|
12
|
+
HeaderProps,
|
|
13
|
+
TabName,
|
|
14
|
+
} from "./types/types";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FlatList, ScrollView, StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
import PagerView, { PagerViewProps } from "react-native-pager-view";
|
|
4
|
+
import Animated, { AnimatedRef, SharedValue } from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
export type TabName = string;
|
|
7
|
+
|
|
8
|
+
export type ContainerRef = PagerView;
|
|
9
|
+
|
|
10
|
+
export type RefComponent =
|
|
11
|
+
| FlatList<any>
|
|
12
|
+
| ScrollView
|
|
13
|
+
| Animated.ScrollView;
|
|
14
|
+
|
|
15
|
+
export type TabBarProps<T extends TabName = TabName> = {
|
|
16
|
+
tabNames: T[];
|
|
17
|
+
focusedTab: SharedValue<T>;
|
|
18
|
+
index: SharedValue<number>;
|
|
19
|
+
indexDecimal: SharedValue<number>;
|
|
20
|
+
onTabPress: (name: T) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type HeaderProps<T extends TabName = TabName> = TabBarProps<T>;
|
|
24
|
+
|
|
25
|
+
export type TabProps<T extends TabName = TabName> = {
|
|
26
|
+
readonly name: T;
|
|
27
|
+
label?: string;
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TabReactElement<T extends TabName = TabName> =
|
|
32
|
+
React.ReactElement<TabProps<T>>;
|
|
33
|
+
|
|
34
|
+
export type CollapseTabsProps = {
|
|
35
|
+
children: TabReactElement | TabReactElement[];
|
|
36
|
+
|
|
37
|
+
/** Initial tab name. Defaults to the first child. */
|
|
38
|
+
initialTabName?: TabName;
|
|
39
|
+
|
|
40
|
+
/** Fixed header height. Required for MVP. */
|
|
41
|
+
headerHeight: number;
|
|
42
|
+
|
|
43
|
+
/** Fixed tab bar height. Required for MVP. */
|
|
44
|
+
tabBarHeight: number;
|
|
45
|
+
|
|
46
|
+
/** Minimum header height when fully collapsed. @default 0 */
|
|
47
|
+
minHeaderHeight?: number;
|
|
48
|
+
|
|
49
|
+
renderHeader?: (props: HeaderProps) => React.ReactElement | null;
|
|
50
|
+
renderTabBar?: (props: TabBarProps) => React.ReactElement | null;
|
|
51
|
+
|
|
52
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
53
|
+
headerContainerStyle?: StyleProp<ViewStyle>;
|
|
54
|
+
|
|
55
|
+
pagerProps?: Omit<PagerViewProps, "onPageScroll" | "initialPage">;
|
|
56
|
+
|
|
57
|
+
onIndexChange?: (index: number) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Internal context shared between CollapseTabs and its children. */
|
|
61
|
+
export type ContextType<T extends TabName = TabName> = {
|
|
62
|
+
headerHeight: number;
|
|
63
|
+
tabBarHeight: number;
|
|
64
|
+
minHeaderHeight: number;
|
|
65
|
+
headerScrollDistance: number;
|
|
66
|
+
|
|
67
|
+
tabNames: T[];
|
|
68
|
+
index: SharedValue<number>;
|
|
69
|
+
indexDecimal: SharedValue<number>;
|
|
70
|
+
focusedTab: SharedValue<T>;
|
|
71
|
+
|
|
72
|
+
/** Per-tab scroll Y, keyed by tab name. */
|
|
73
|
+
scrollY: SharedValue<Record<T, number>>;
|
|
74
|
+
|
|
75
|
+
/** Header translateY (clamped to [-headerScrollDistance, 0]). */
|
|
76
|
+
headerTranslateY: SharedValue<number>;
|
|
77
|
+
|
|
78
|
+
setRef: <R extends RefComponent>(
|
|
79
|
+
key: T,
|
|
80
|
+
ref: AnimatedRef<R>
|
|
81
|
+
) => AnimatedRef<R>;
|
|
82
|
+
refMap: Record<T, AnimatedRef<RefComponent>>;
|
|
83
|
+
|
|
84
|
+
containerRef: React.RefObject<ContainerRef>;
|
|
85
|
+
};
|