@orionarm/react-native-collapse-tabs 1.0.1 → 1.0.3
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 +245 -1
- package/lib/CollapseTabs.d.ts +1 -5
- package/lib/CollapseTabs.d.ts.map +1 -1
- package/lib/CollapseTabs.js +124 -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/lib/CollapseTabs.d.ts +4 -0
- package/lib/lib/CollapseTabs.d.ts.map +1 -0
- package/lib/lib/CollapseTabs.js +156 -0
- package/lib/lib/CollapseTabs.js.map +1 -0
- package/lib/lib/FlatList.d.ts +8 -0
- package/lib/lib/FlatList.d.ts.map +1 -0
- package/lib/lib/FlatList.js +59 -0
- package/lib/lib/ScrollView.d.ts +9 -0
- package/lib/lib/ScrollView.d.ts.map +1 -0
- package/lib/lib/ScrollView.js +61 -0
- package/lib/lib/Tab.d.ts +8 -0
- package/lib/lib/Tab.d.ts.map +1 -0
- package/lib/lib/Tab.js +19 -0
- package/lib/lib/TabBar.d.ts +4 -0
- package/lib/lib/TabBar.d.ts.map +1 -0
- package/lib/lib/TabBar.js +89 -0
- package/lib/lib/context/index.d.ts +5 -0
- package/lib/lib/context/index.d.ts.map +1 -0
- package/lib/lib/context/index.js +8 -0
- package/lib/lib/hooks/index.d.ts +3 -0
- package/lib/lib/hooks/index.d.ts.map +1 -0
- package/lib/lib/hooks/index.js +11 -0
- package/lib/lib/index.d.ts +8 -0
- package/lib/lib/index.d.ts.map +1 -0
- package/lib/lib/index.js +15 -0
- package/lib/lib/index.js.map +1 -0
- package/lib/lib/types/types.d.ts +57 -0
- package/lib/lib/types/types.d.ts.map +1 -0
- package/lib/lib/types/types.js +2 -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 +219 -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,3 +1,247 @@
|
|
|
1
1
|
# @orionarm/react-native-collapse-tabs
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> A lightweight, performant collapsible-tabs component for React Native, built on top of [`react-native-pager-view`](https://github.com/callstack/react-native-pager-view) and [`react-native-reanimated`](https://github.com/software-mansion/react-native-reanimated) v3.
|
|
4
|
+
|
|
5
|
+
> ⚠️ **Work in Progress** — APIs may still change before `1.x` is considered stable.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 📜 **Collapsible header** — the header smoothly collapses while the inner list is scrolled.
|
|
12
|
+
- 🗂 **Swipeable tabs** — horizontal paging powered by `react-native-pager-view`.
|
|
13
|
+
- 🎚 **Per-tab scroll state** — each tab keeps its own scroll position.
|
|
14
|
+
- 🪄 **Animated tab switching** — header smoothly tweens between tabs (no sudden jumps).
|
|
15
|
+
- 🎨 **Custom header & tab bar** — bring your own UI, or use the built-in `DefaultTabBar`.
|
|
16
|
+
- ⚡ **Reanimated 3 worklets** — animations run on the UI thread for 60fps.
|
|
17
|
+
- 🧩 **Drop-in scrollables** — wrapped `FlatList` / `ScrollView` handle the plumbing for you.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
| Peer dependency | Version |
|
|
24
|
+
| ----------------------------- | ---------- |
|
|
25
|
+
| `react` | `>=16.8.0` |
|
|
26
|
+
| `react-native` | `>=0.60.0` |
|
|
27
|
+
| `react-native-gesture-handler`| `>=2.0.0` |
|
|
28
|
+
| `react-native-pager-view` | `>=6.0.0` |
|
|
29
|
+
| `react-native-reanimated` | `>=3.0.0` |
|
|
30
|
+
|
|
31
|
+
Make sure Reanimated is set up correctly (Babel plugin + `react-native-reanimated/plugin` at the **end** of your `babel.config.js` plugins array).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# npm
|
|
39
|
+
npm install @orionarm/react-native-collapse-tabs
|
|
40
|
+
|
|
41
|
+
# yarn
|
|
42
|
+
yarn add @orionarm/react-native-collapse-tabs
|
|
43
|
+
|
|
44
|
+
# pnpm
|
|
45
|
+
pnpm add @orionarm/react-native-collapse-tabs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then install the peer dependencies if you haven't already:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install react-native-pager-view react-native-reanimated react-native-gesture-handler
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For iOS, don't forget to install pods:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd ios && pod install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import React from "react";
|
|
66
|
+
import { Text, View, StyleSheet } from "react-native";
|
|
67
|
+
import {
|
|
68
|
+
CollapseTabs,
|
|
69
|
+
Tab,
|
|
70
|
+
FlatList,
|
|
71
|
+
} from "@orionarm/react-native-collapse-tabs";
|
|
72
|
+
|
|
73
|
+
const HEADER_HEIGHT = 200;
|
|
74
|
+
const TAB_BAR_HEIGHT = 44;
|
|
75
|
+
|
|
76
|
+
export default function Example() {
|
|
77
|
+
return (
|
|
78
|
+
<CollapseTabs
|
|
79
|
+
headerHeight={HEADER_HEIGHT}
|
|
80
|
+
tabBarHeight={TAB_BAR_HEIGHT}
|
|
81
|
+
renderHeader={() => (
|
|
82
|
+
<View style={styles.header}>
|
|
83
|
+
<Text style={styles.headerText}>My Profile</Text>
|
|
84
|
+
</View>
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<Tab name="Posts">
|
|
88
|
+
<FlatList
|
|
89
|
+
name="Posts"
|
|
90
|
+
data={Array.from({ length: 30 }).map((_, i) => `Post ${i}`)}
|
|
91
|
+
keyExtractor={(item) => item}
|
|
92
|
+
renderItem={({ item }) => (
|
|
93
|
+
<View style={styles.row}>
|
|
94
|
+
<Text>{item}</Text>
|
|
95
|
+
</View>
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
</Tab>
|
|
99
|
+
|
|
100
|
+
<Tab name="Likes">
|
|
101
|
+
<FlatList
|
|
102
|
+
name="Likes"
|
|
103
|
+
data={Array.from({ length: 30 }).map((_, i) => `Like ${i}`)}
|
|
104
|
+
keyExtractor={(item) => item}
|
|
105
|
+
renderItem={({ item }) => (
|
|
106
|
+
<View style={styles.row}>
|
|
107
|
+
<Text>{item}</Text>
|
|
108
|
+
</View>
|
|
109
|
+
)}
|
|
110
|
+
/>
|
|
111
|
+
</Tab>
|
|
112
|
+
</CollapseTabs>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
header: { flex: 1, backgroundColor: "#2D8CFF", justifyContent: "center", alignItems: "center" },
|
|
118
|
+
headerText: { color: "#fff", fontSize: 22, fontWeight: "600" },
|
|
119
|
+
row: { padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: "#eee" },
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> ⚠️ Important: the `name` prop of `<Tab>` and the `name` prop of the wrapped `<FlatList>` / `<ScrollView>` **must match** — that's how each tab's scroll position is tracked.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## API
|
|
128
|
+
|
|
129
|
+
### `<CollapseTabs />`
|
|
130
|
+
|
|
131
|
+
The root container.
|
|
132
|
+
|
|
133
|
+
| Prop | Type | Required | Description |
|
|
134
|
+
| ---------------------- | ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ |
|
|
135
|
+
| `headerHeight` | `number` | ✅ | Fixed height of the header content (above the tab bar). |
|
|
136
|
+
| `tabBarHeight` | `number` | ✅ | Fixed height of the tab bar. |
|
|
137
|
+
| `children` | `TabReactElement \| TabReactElement[]` | ✅ | One or more `<Tab>` children. |
|
|
138
|
+
| `initialTabName` | `string` | | Tab to focus on mount. Defaults to the first child. |
|
|
139
|
+
| `minHeaderHeight` | `number` | | Minimum visible header height once collapsed. Default `0`. |
|
|
140
|
+
| `renderHeader` | `(props: HeaderProps) => ReactElement \| null` | | Render function for the header content. |
|
|
141
|
+
| `renderTabBar` | `(props: TabBarProps) => ReactElement \| null` | | Render function for a custom tab bar. Falls back to `<DefaultTabBar>` if not provided. |
|
|
142
|
+
| `containerStyle` | `StyleProp<ViewStyle>` | | Style for the outer container. |
|
|
143
|
+
| `headerContainerStyle` | `StyleProp<ViewStyle>` | | Style for the absolutely-positioned header wrapper. |
|
|
144
|
+
| `pagerProps` | `Omit<PagerViewProps, "onPageScroll" \| "initialPage">` | | Extra props forwarded to `PagerView`. |
|
|
145
|
+
| `onIndexChange` | `(index: number) => void` | | Fired after the focused tab changes. |
|
|
146
|
+
|
|
147
|
+
### `<Tab />`
|
|
148
|
+
|
|
149
|
+
Wraps a single tab's content. Should be a direct child of `<CollapseTabs>`.
|
|
150
|
+
|
|
151
|
+
| Prop | Type | Required | Description |
|
|
152
|
+
| ---------- | ----------------- | -------- | ------------------------------------------------- |
|
|
153
|
+
| `name` | `string` | ✅ | Unique identifier — must match the inner list's `name`. |
|
|
154
|
+
| `label` | `string` | | Optional display label (currently used by `DefaultTabBar` via `name`). |
|
|
155
|
+
| `children` | `React.ReactNode` | ✅ | Tab content — usually a wrapped `<FlatList>` or `<ScrollView>`. |
|
|
156
|
+
|
|
157
|
+
### `<FlatList />` and `<ScrollView />`
|
|
158
|
+
|
|
159
|
+
Drop-in replacements for the standard components, pre-wired to the collapse-tabs scroll system.
|
|
160
|
+
|
|
161
|
+
| Prop | Type | Required | Description |
|
|
162
|
+
| ------ | -------- | -------- | ------------------------------------------------------ |
|
|
163
|
+
| `name` | `string` | ✅ | Must match the parent `<Tab name="..." />`. |
|
|
164
|
+
| ... | — | | All other standard `FlatList` / `ScrollView` props. |
|
|
165
|
+
|
|
166
|
+
> They automatically apply `paddingTop: headerHeight + tabBarHeight` to the content so your first item starts below the header.
|
|
167
|
+
|
|
168
|
+
### `<DefaultTabBar />`
|
|
169
|
+
|
|
170
|
+
A minimal built-in tab bar with a sliding underline indicator. Pass any custom UI through `renderTabBar` if you need something different.
|
|
171
|
+
|
|
172
|
+
### `useTabsContext()`
|
|
173
|
+
|
|
174
|
+
Hook for advanced use cases (e.g. building a fully custom header/tab bar that needs access to the shared scroll state).
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const {
|
|
178
|
+
headerHeight,
|
|
179
|
+
tabBarHeight,
|
|
180
|
+
headerScrollDistance,
|
|
181
|
+
tabNames,
|
|
182
|
+
index, // SharedValue<number>
|
|
183
|
+
indexDecimal, // SharedValue<number>
|
|
184
|
+
focusedTab, // SharedValue<string>
|
|
185
|
+
scrollY, // SharedValue<Record<string, number>>
|
|
186
|
+
headerTranslateY,// SharedValue<number>
|
|
187
|
+
containerRef,
|
|
188
|
+
} = useTabsContext();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Customization
|
|
194
|
+
|
|
195
|
+
### Custom tab bar
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<CollapseTabs
|
|
199
|
+
headerHeight={200}
|
|
200
|
+
tabBarHeight={44}
|
|
201
|
+
renderTabBar={({ tabNames, indexDecimal, onTabPress }) => (
|
|
202
|
+
<MyFancyTabBar
|
|
203
|
+
tabs={tabNames}
|
|
204
|
+
indexDecimal={indexDecimal}
|
|
205
|
+
onPress={onTabPress}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
{/* ... */}
|
|
210
|
+
</CollapseTabs>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Animated header
|
|
214
|
+
|
|
215
|
+
`renderHeader` receives the same `HeaderProps` so you can animate things based on `indexDecimal` or read `headerTranslateY` from `useTabsContext()` inside a child component.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Types
|
|
220
|
+
|
|
221
|
+
All public types are exported from the package root:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import type {
|
|
225
|
+
CollapseTabsProps,
|
|
226
|
+
TabProps,
|
|
227
|
+
TabBarProps,
|
|
228
|
+
HeaderProps,
|
|
229
|
+
TabName,
|
|
230
|
+
} from "@orionarm/react-native-collapse-tabs";
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Roadmap
|
|
236
|
+
|
|
237
|
+
- [ ] Pull-to-refresh integration
|
|
238
|
+
- [ ] Dynamic header height
|
|
239
|
+
- [ ] SectionList / horizontal-list support
|
|
240
|
+
- [ ] Snap-to-collapse behavior
|
|
241
|
+
- [ ] Example app & screenshots
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
ISC © yanan_orionarm
|
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;AAkBf,OAAO,EACL,iBAAiB,EAOlB,MAAM,eAAe,CAAC;AAIvB,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgLpD,CAAC"}
|
package/lib/CollapseTabs.js
CHANGED
|
@@ -1,12 +1,125 @@
|
|
|
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, withTiming, } 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 tab = focusedTab.value;
|
|
25
|
+
const y = scrollY.value[tab] ?? 0;
|
|
26
|
+
return {
|
|
27
|
+
tab,
|
|
28
|
+
translateY: -Math.min(y, headerScrollDistance),
|
|
29
|
+
};
|
|
30
|
+
}, (next, prev) => {
|
|
31
|
+
if (prev && prev.tab !== next.tab) {
|
|
32
|
+
headerTranslateY.value = withTiming(next.translateY, { duration: 250 });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
headerTranslateY.value = next.translateY;
|
|
36
|
+
}
|
|
37
|
+
}, [headerScrollDistance]);
|
|
38
|
+
const refMap = useRef({}).current;
|
|
39
|
+
const setRef = useCallback((key, ref) => {
|
|
40
|
+
refMap[key] = ref;
|
|
41
|
+
return ref;
|
|
42
|
+
}, [refMap]);
|
|
43
|
+
const containerRef = useRef(null);
|
|
44
|
+
const onTabPress = useCallback((name) => {
|
|
45
|
+
const i = tabNames.indexOf(name);
|
|
46
|
+
if (i < 0)
|
|
47
|
+
return;
|
|
48
|
+
containerRef.current?.setPage(i);
|
|
49
|
+
}, [tabNames]);
|
|
50
|
+
const [renderIndex, setRenderIndex] = useState(initialIndex);
|
|
51
|
+
const { doDependenciesDiffer } = useHandler({});
|
|
52
|
+
const pageScrollHandler = useEvent((e) => {
|
|
53
|
+
"worklet";
|
|
54
|
+
indexDecimal.value = e.position + e.offset;
|
|
55
|
+
}, ["onPageScroll"], doDependenciesDiffer);
|
|
56
|
+
const onPageSelected = useCallback((e) => {
|
|
57
|
+
const i = e.nativeEvent.position;
|
|
58
|
+
index.value = i;
|
|
59
|
+
focusedTab.value = tabNames[i] ?? "";
|
|
60
|
+
setRenderIndex(i);
|
|
61
|
+
onIndexChange?.(i);
|
|
62
|
+
}, [tabNames, index, focusedTab, onIndexChange]);
|
|
63
|
+
const headerAnimStyle = useAnimatedStyle(() => ({
|
|
64
|
+
transform: [{ translateY: headerTranslateY.value }],
|
|
65
|
+
}));
|
|
66
|
+
const ctxValue = {
|
|
67
|
+
headerHeight,
|
|
68
|
+
tabBarHeight,
|
|
69
|
+
minHeaderHeight,
|
|
70
|
+
headerScrollDistance,
|
|
71
|
+
tabNames,
|
|
72
|
+
index,
|
|
73
|
+
indexDecimal,
|
|
74
|
+
focusedTab,
|
|
75
|
+
scrollY,
|
|
76
|
+
headerTranslateY,
|
|
77
|
+
setRef,
|
|
78
|
+
refMap,
|
|
79
|
+
containerRef,
|
|
80
|
+
};
|
|
81
|
+
const headerProps = {
|
|
82
|
+
tabNames,
|
|
83
|
+
focusedTab,
|
|
84
|
+
index,
|
|
85
|
+
indexDecimal,
|
|
86
|
+
onTabPress,
|
|
87
|
+
};
|
|
88
|
+
return (<Context.Provider value={ctxValue}>
|
|
89
|
+
<View style={[styles.container, containerStyle]}>
|
|
90
|
+
<AnimatedPagerView ref={containerRef} style={StyleSheet.absoluteFill} initialPage={initialIndex} onPageScroll={pageScrollHandler} onPageSelected={onPageSelected} {...pagerProps}>
|
|
91
|
+
{tabs.map((tab, i) => (<View key={tab.props.name} style={styles.page} collapsable={false}>
|
|
92
|
+
{tab}
|
|
93
|
+
</View>))}
|
|
94
|
+
</AnimatedPagerView>
|
|
95
|
+
|
|
96
|
+
<Animated.View pointerEvents="box-none" style={[
|
|
97
|
+
styles.headerContainer,
|
|
98
|
+
{ height: headerHeight + tabBarHeight },
|
|
99
|
+
headerContainerStyle,
|
|
100
|
+
headerAnimStyle,
|
|
101
|
+
]}>
|
|
102
|
+
<View pointerEvents="box-none" style={{ height: headerHeight }}>
|
|
103
|
+
{renderHeader?.(headerProps)}
|
|
104
|
+
</View>
|
|
105
|
+
<View style={{ height: tabBarHeight }}>
|
|
106
|
+
{renderTabBar
|
|
107
|
+
? renderTabBar(headerProps)
|
|
108
|
+
: <DefaultTabBar {...headerProps}/>}
|
|
109
|
+
</View>
|
|
110
|
+
</Animated.View>
|
|
111
|
+
</View>
|
|
112
|
+
</Context.Provider>);
|
|
4
113
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
114
|
+
const styles = StyleSheet.create({
|
|
115
|
+
container: { flex: 1, overflow: "hidden" },
|
|
116
|
+
page: { flex: 1 },
|
|
117
|
+
headerContainer: {
|
|
118
|
+
position: "absolute",
|
|
119
|
+
top: 0,
|
|
120
|
+
left: 0,
|
|
121
|
+
right: 0,
|
|
122
|
+
zIndex: 10,
|
|
123
|
+
backgroundColor: "#fff",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -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 @@
|
|
|
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"}
|