@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 CHANGED
@@ -1,33 +1,3 @@
1
1
  # @orionarm/react-native-collapse-tabs
2
2
 
3
- A React Native collapsible tabs component.
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.
@@ -1,8 +1,4 @@
1
1
  import React from "react";
2
- import { ViewStyle } from "react-native";
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,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAoB,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAKpD,CAAC"}
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"}
@@ -1,12 +1,116 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
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
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CollapseTabs = void 0;
7
- const react_1 = __importDefault(require("react"));
8
- const react_native_1 = require("react-native");
9
- const CollapseTabs = ({ children, style, }) => {
10
- return <react_native_1.View> {children}</react_native_1.View>;
11
- };
12
- exports.CollapseTabs = CollapseTabs;
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"}
@@ -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
@@ -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
+ });
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import { TabBarProps } from "./types/types";
3
+ export declare const DefaultTabBar: React.FC<TabBarProps>;
4
+ //# sourceMappingURL=TabBar.d.ts.map
@@ -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,5 @@
1
+ import React from "react";
2
+ import { ContextType } from "../types/types";
3
+ export type { ContextType, TabName, RefComponent } from "../types/types";
4
+ export declare const Context: React.Context<ContextType<string> | undefined>;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ import React from "react";
2
+ export const Context = React.createContext(undefined);
@@ -0,0 +1,3 @@
1
+ import { ContextType } from "../types/types";
2
+ export declare function useTabsContext(): ContextType<string>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -0,0 +1,8 @@
1
+ import { useContext } from "react";
2
+ import { Context } from "../context";
3
+ export function useTabsContext() {
4
+ const c = useContext(Context);
5
+ if (!c)
6
+ throw new Error("useTabsContext must be used inside <CollapseTabs>");
7
+ return c;
8
+ }
package/lib/index.d.ts CHANGED
@@ -1,3 +1,8 @@
1
- export { CollapseTabs } from './CollapseTabs';
2
- export type { CollapseTabsProps } from './CollapseTabs';
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
@@ -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,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CollapseTabs = void 0;
4
- var CollapseTabs_1 = require("./CollapseTabs");
5
- Object.defineProperty(exports, "CollapseTabs", { enumerable: true, get: function () { return CollapseTabs_1.CollapseTabs; } });
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";
@@ -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.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
- "prepare": "npm run build",
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",
@@ -1,14 +1,216 @@
1
- import React from "react";
2
- import { View, StyleSheet, ViewStyle, Text } from "react-native";
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
- export interface CollapseTabsProps {
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
- style,
37
+ initialTabName,
38
+ headerHeight,
39
+ tabBarHeight,
40
+ minHeaderHeight = 0,
41
+ renderHeader,
42
+ renderTabBar,
43
+ containerStyle,
44
+ headerContainerStyle,
45
+ pagerProps,
46
+ onIndexChange,
12
47
  }) => {
13
- return <View> {children}</View>;
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
+ });
@@ -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,8 @@
1
+ import React from "react";
2
+ import { ContextType, TabName } from "../types/types";
3
+
4
+ export type { ContextType, TabName, RefComponent } from "../types/types";
5
+
6
+ export const Context = React.createContext<ContextType<TabName> | undefined>(
7
+ undefined
8
+ );
@@ -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 './CollapseTabs';
2
- export type { CollapseTabsProps } from './CollapseTabs';
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
+ };