react-native-collapsible-header-tab-view 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/dist/index.mjs ADDED
@@ -0,0 +1,443 @@
1
+ // src/index.tsx
2
+ import React3, {
3
+ forwardRef as forwardRef3,
4
+ useCallback as useCallback3,
5
+ useImperativeHandle as useImperativeHandle3,
6
+ useMemo as useMemo2,
7
+ useRef as useRef3,
8
+ useState as useState3
9
+ } from "react";
10
+ import {
11
+ Animated as Animated3,
12
+ InteractionManager,
13
+ StyleSheet,
14
+ View as View2
15
+ } from "react-native";
16
+ import PagerView from "react-native-pager-view";
17
+
18
+ // src/context.ts
19
+ import { createContext, useContext } from "react";
20
+ var CollapsibleContext = createContext(null);
21
+ var TabIndexContext = createContext(0);
22
+ var useCollapsible = () => {
23
+ const ctx = useContext(CollapsibleContext);
24
+ if (!ctx) {
25
+ throw new Error("useCollapsible must be used within CollapsibleTabView");
26
+ }
27
+ return ctx;
28
+ };
29
+ var useTabIndex = () => useContext(TabIndexContext);
30
+
31
+ // src/TabFlatList.tsx
32
+ import React, {
33
+ forwardRef,
34
+ useCallback,
35
+ useEffect,
36
+ useImperativeHandle,
37
+ useMemo,
38
+ useRef,
39
+ useState
40
+ } from "react";
41
+ import {
42
+ Animated,
43
+ FlatList,
44
+ View
45
+ } from "react-native";
46
+ var AnimatedFlatListComponent = Animated.createAnimatedComponent(FlatList);
47
+ var AnimatedFlatList = AnimatedFlatListComponent;
48
+ var TabFlatListInner = ({
49
+ contentContainerStyle,
50
+ onScroll,
51
+ ListHeaderComponent,
52
+ ...props
53
+ }, ref) => {
54
+ const index = useTabIndex();
55
+ const {
56
+ scrollY,
57
+ activeIndex,
58
+ stickyEnabled,
59
+ headerHeight,
60
+ tabBarHeight,
61
+ renderHeader,
62
+ renderTabBar,
63
+ registerRef,
64
+ syncScrollY
65
+ } = useCollapsible();
66
+ const innerRef = useRef(null);
67
+ const isActive = activeIndex === index;
68
+ useEffect(() => {
69
+ registerRef(index, innerRef.current);
70
+ return () => registerRef(index, null);
71
+ }, [index]);
72
+ useImperativeHandle(ref, () => innerRef.current, []);
73
+ const handleScroll = useCallback(
74
+ (e) => {
75
+ const y = e.nativeEvent.contentOffset.y;
76
+ syncScrollY(index, y);
77
+ onScroll?.(e);
78
+ },
79
+ [index, onScroll, syncScrollY]
80
+ );
81
+ const mergedHeader = useMemo(() => {
82
+ if (stickyEnabled) return ListHeaderComponent;
83
+ const OriginalHeader = typeof ListHeaderComponent === "function" ? /* @__PURE__ */ React.createElement(ListHeaderComponent, null) : ListHeaderComponent ?? null;
84
+ return /* @__PURE__ */ React.createElement(View, null, renderHeader?.(), renderTabBar?.(), OriginalHeader);
85
+ }, [stickyEnabled, ListHeaderComponent, renderHeader, renderTabBar]);
86
+ const paddingTop = stickyEnabled ? headerHeight + tabBarHeight : 0;
87
+ const collapseRange = stickyEnabled ? headerHeight : 0;
88
+ const [containerHeight, setContainerHeight] = useState(0);
89
+ const handleLayout = useCallback((e) => {
90
+ setContainerHeight(e.nativeEvent.layout.height);
91
+ }, []);
92
+ const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
93
+ return /* @__PURE__ */ React.createElement(
94
+ AnimatedFlatList,
95
+ {
96
+ ...props,
97
+ ref: innerRef,
98
+ onLayout: handleLayout,
99
+ ListHeaderComponent: mergedHeader,
100
+ contentContainerStyle: [
101
+ paddingTop > 0 && { paddingTop },
102
+ minHeight > 0 && { minHeight },
103
+ contentContainerStyle
104
+ ],
105
+ onScroll: isActive && stickyEnabled ? Animated.event(
106
+ [{ nativeEvent: { contentOffset: { y: scrollY } } }],
107
+ {
108
+ useNativeDriver: true,
109
+ listener: handleScroll
110
+ }
111
+ ) : handleScroll,
112
+ scrollEventThrottle: 16,
113
+ showsVerticalScrollIndicator: false
114
+ }
115
+ );
116
+ };
117
+ var TabFlatList = forwardRef(TabFlatListInner);
118
+
119
+ // src/TabScrollView.tsx
120
+ import React2, {
121
+ forwardRef as forwardRef2,
122
+ useCallback as useCallback2,
123
+ useEffect as useEffect2,
124
+ useImperativeHandle as useImperativeHandle2,
125
+ useRef as useRef2,
126
+ useState as useState2
127
+ } from "react";
128
+ import {
129
+ Animated as Animated2
130
+ } from "react-native";
131
+ var TabScrollView = forwardRef2(
132
+ ({ contentContainerStyle, onScroll, children, ...props }, ref) => {
133
+ const index = useTabIndex();
134
+ const { scrollY, activeIndex, headerHeight, tabBarHeight, registerRef, syncScrollY } = useCollapsible();
135
+ const innerRef = useRef2(null);
136
+ const isActive = activeIndex === index;
137
+ useEffect2(() => {
138
+ registerRef(index, innerRef.current);
139
+ return () => registerRef(index, null);
140
+ }, [index]);
141
+ useImperativeHandle2(ref, () => innerRef.current, []);
142
+ const handleScroll = useCallback2(
143
+ (e) => {
144
+ const y = e.nativeEvent.contentOffset.y;
145
+ syncScrollY(index, y);
146
+ onScroll?.(e);
147
+ },
148
+ [index, onScroll, syncScrollY]
149
+ );
150
+ const paddingTop = headerHeight + tabBarHeight;
151
+ const collapseRange = headerHeight;
152
+ const [containerHeight, setContainerHeight] = useState2(0);
153
+ const handleLayout = useCallback2((e) => {
154
+ setContainerHeight(e.nativeEvent.layout.height);
155
+ }, []);
156
+ const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
157
+ return /* @__PURE__ */ React2.createElement(
158
+ Animated2.ScrollView,
159
+ {
160
+ ref: innerRef,
161
+ ...props,
162
+ onLayout: handleLayout,
163
+ contentContainerStyle: [
164
+ { paddingTop },
165
+ minHeight > 0 && { minHeight },
166
+ contentContainerStyle
167
+ ],
168
+ onScroll: isActive ? Animated2.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
169
+ useNativeDriver: true,
170
+ listener: handleScroll
171
+ }) : handleScroll,
172
+ scrollEventThrottle: 16,
173
+ showsVerticalScrollIndicator: false
174
+ },
175
+ children
176
+ );
177
+ }
178
+ );
179
+
180
+ // src/index.tsx
181
+ var CollapsibleTabView = forwardRef3(
182
+ ({
183
+ children,
184
+ renderHeader,
185
+ estimatedHeaderHeight = 0,
186
+ estimatedTabBarHeight = 0,
187
+ stickyEnabled = true,
188
+ stickyTop = 0,
189
+ renderTabBar,
190
+ initialTabIndex = 0,
191
+ onTabChange,
192
+ onScroll: onScrollProp,
193
+ swipeEnabled = true,
194
+ style
195
+ }, ref) => {
196
+ const [activeIndex, setActiveIndex] = useState3(initialTabIndex);
197
+ const pagerRef = useRef3(null);
198
+ const pages = useMemo2(
199
+ () => React3.Children.toArray(children).filter(React3.isValidElement),
200
+ [children]
201
+ );
202
+ useImperativeHandle3(ref, () => ({
203
+ scrollToTab: (index, animated = true) => {
204
+ if (index === activeIndex || index < 0 || index >= pages.length) return;
205
+ syncTabOnSwitch(index);
206
+ setActiveIndex(index);
207
+ if (animated) {
208
+ pagerRef.current?.setPage(index);
209
+ } else {
210
+ pagerRef.current?.setPageWithoutAnimation(index);
211
+ }
212
+ onTabChange?.(index);
213
+ },
214
+ getActiveIndex: () => activeIndex
215
+ }));
216
+ const hasEstimate = estimatedHeaderHeight > 0;
217
+ const headerHeightRef = useRef3(0);
218
+ const tabBarHeightRef = useRef3(0);
219
+ const [layout, setLayout] = useState3({
220
+ headerHeight: estimatedHeaderHeight,
221
+ tabBarHeight: estimatedTabBarHeight,
222
+ ready: hasEstimate
223
+ });
224
+ const { headerHeight, tabBarHeight } = layout;
225
+ const visible = layout.ready;
226
+ const adjustY = useRef3(new Animated3.Value(0)).current;
227
+ const tryCommitLayout = useCallback3(() => {
228
+ const h = headerHeightRef.current;
229
+ const t = tabBarHeightRef.current;
230
+ if (h > 0 && t > 0) {
231
+ setLayout((prev) => {
232
+ if (Math.abs(prev.headerHeight - h) <= 1 && Math.abs(prev.tabBarHeight - t) <= 1) {
233
+ return prev;
234
+ }
235
+ const diff = h + t - (prev.headerHeight + prev.tabBarHeight);
236
+ if (prev.ready && Math.abs(diff) > 1) {
237
+ adjustY.setValue(-diff);
238
+ Animated3.timing(adjustY, {
239
+ toValue: 0,
240
+ duration: 200,
241
+ useNativeDriver: true
242
+ }).start();
243
+ }
244
+ return { headerHeight: h, tabBarHeight: t, ready: true };
245
+ });
246
+ }
247
+ }, []);
248
+ const handleHeaderLayout = useCallback3(
249
+ (e) => {
250
+ headerHeightRef.current = e.nativeEvent.layout.height;
251
+ tryCommitLayout();
252
+ },
253
+ [tryCommitLayout]
254
+ );
255
+ const handleTabBarLayout = useCallback3(
256
+ (e) => {
257
+ tabBarHeightRef.current = e.nativeEvent.layout.height;
258
+ tryCommitLayout();
259
+ },
260
+ [tryCommitLayout]
261
+ );
262
+ const scrollY = useRef3(new Animated3.Value(0)).current;
263
+ const tabScrollYMap = useRef3(/* @__PURE__ */ new Map());
264
+ const tabRefs = useRef3(
265
+ /* @__PURE__ */ new Map()
266
+ );
267
+ const collapseRange = stickyEnabled ? Math.max(headerHeight - stickyTop, 0) : 0;
268
+ const headerTranslateY = useMemo2(
269
+ () => collapseRange > 0 ? scrollY.interpolate({
270
+ inputRange: [0, collapseRange],
271
+ outputRange: [0, -collapseRange],
272
+ extrapolate: "clamp"
273
+ }) : new Animated3.Value(0),
274
+ [scrollY, collapseRange]
275
+ );
276
+ const registerRef = useCallback3(
277
+ (index, ref2) => {
278
+ if (ref2) {
279
+ tabRefs.current.set(index, ref2);
280
+ } else {
281
+ tabRefs.current.delete(index);
282
+ }
283
+ },
284
+ []
285
+ );
286
+ const syncScrollY = useCallback3(
287
+ (index, y) => {
288
+ tabScrollYMap.current.set(index, y);
289
+ onScrollProp?.(y);
290
+ },
291
+ [onScrollProp]
292
+ );
293
+ const scrollTabTo = useCallback3((index, offset) => {
294
+ const ref2 = tabRefs.current.get(index);
295
+ if (!ref2) return;
296
+ if (ref2.scrollToOffset) {
297
+ ref2.scrollToOffset({ offset, animated: false });
298
+ } else if (ref2.scrollTo) {
299
+ ref2.scrollTo({ y: offset, animated: false });
300
+ } else if (ref2.getNode) {
301
+ const node = ref2.getNode();
302
+ if (node?.scrollToOffset) {
303
+ node.scrollToOffset({ offset, animated: false });
304
+ } else if (node?.scrollTo) {
305
+ node.scrollTo({ y: offset, animated: false });
306
+ }
307
+ }
308
+ }, []);
309
+ const syncTabOnSwitch = useCallback3(
310
+ (newIndex) => {
311
+ const currentY = tabScrollYMap.current.get(activeIndex) ?? 0;
312
+ const newTabSavedY = tabScrollYMap.current.get(newIndex) ?? 0;
313
+ const isCollapsed = currentY >= collapseRange - 1;
314
+ let targetY;
315
+ if (isCollapsed) {
316
+ targetY = Math.max(newTabSavedY, collapseRange);
317
+ } else {
318
+ targetY = Math.min(currentY, collapseRange);
319
+ }
320
+ InteractionManager.runAfterInteractions(() => {
321
+ scrollTabTo(newIndex, targetY);
322
+ tabScrollYMap.current.set(newIndex, targetY);
323
+ scrollY.setValue(Math.min(targetY, collapseRange));
324
+ });
325
+ },
326
+ [activeIndex, collapseRange, scrollTabTo, scrollY]
327
+ );
328
+ const handleTabPress = useCallback3(
329
+ (index) => {
330
+ if (index === activeIndex) return;
331
+ syncTabOnSwitch(index);
332
+ setActiveIndex(index);
333
+ pagerRef.current?.setPageWithoutAnimation(index);
334
+ onTabChange?.(index);
335
+ },
336
+ [activeIndex, onTabChange, syncTabOnSwitch]
337
+ );
338
+ const handlePageSelected = useCallback3(
339
+ (e) => {
340
+ const newIndex = e.nativeEvent.position;
341
+ if (newIndex === activeIndex) return;
342
+ syncTabOnSwitch(newIndex);
343
+ setActiveIndex(newIndex);
344
+ onTabChange?.(newIndex);
345
+ },
346
+ [activeIndex, onTabChange, syncTabOnSwitch]
347
+ );
348
+ const tabBarProps = useMemo2(
349
+ () => ({
350
+ activeIndex,
351
+ onTabPress: handleTabPress
352
+ }),
353
+ [activeIndex, handleTabPress]
354
+ );
355
+ const renderTabBarNode = useCallback3(
356
+ () => renderTabBar(tabBarProps),
357
+ [renderTabBar, tabBarProps]
358
+ );
359
+ const contextValue = useMemo2(
360
+ () => ({
361
+ scrollY,
362
+ activeIndex,
363
+ stickyEnabled,
364
+ headerHeight,
365
+ tabBarHeight,
366
+ renderHeader: stickyEnabled ? void 0 : renderHeader,
367
+ renderTabBar: stickyEnabled ? void 0 : renderTabBarNode,
368
+ registerRef,
369
+ syncScrollY
370
+ }),
371
+ [
372
+ scrollY,
373
+ activeIndex,
374
+ stickyEnabled,
375
+ headerHeight,
376
+ tabBarHeight,
377
+ renderHeader,
378
+ renderTabBarNode,
379
+ registerRef,
380
+ syncScrollY
381
+ ]
382
+ );
383
+ return /* @__PURE__ */ React3.createElement(CollapsibleContext.Provider, { value: contextValue }, /* @__PURE__ */ React3.createElement(View2, { style: [styles.container, style] }, /* @__PURE__ */ React3.createElement(
384
+ Animated3.View,
385
+ {
386
+ style: [
387
+ styles.pager,
388
+ !visible && styles.hidden,
389
+ { transform: [{ translateY: adjustY }] }
390
+ ]
391
+ },
392
+ /* @__PURE__ */ React3.createElement(
393
+ PagerView,
394
+ {
395
+ ref: pagerRef,
396
+ style: styles.pager,
397
+ initialPage: initialTabIndex,
398
+ onPageSelected: handlePageSelected,
399
+ scrollEnabled: swipeEnabled
400
+ },
401
+ pages.map((page, i) => /* @__PURE__ */ React3.createElement(View2, { key: page?.key ?? i, style: styles.page }, /* @__PURE__ */ React3.createElement(TabIndexContext.Provider, { value: i }, page)))
402
+ )
403
+ ), stickyEnabled && /* @__PURE__ */ React3.createElement(
404
+ Animated3.View,
405
+ {
406
+ style: [
407
+ styles.overlay,
408
+ { transform: [{ translateY: headerTranslateY }] }
409
+ ],
410
+ pointerEvents: "box-none"
411
+ },
412
+ /* @__PURE__ */ React3.createElement(View2, { pointerEvents: "box-none", onLayout: handleHeaderLayout }, renderHeader()),
413
+ /* @__PURE__ */ React3.createElement(View2, { onLayout: handleTabBarLayout }, renderTabBarNode())
414
+ )));
415
+ }
416
+ );
417
+ var styles = StyleSheet.create({
418
+ container: {
419
+ flex: 1
420
+ },
421
+ pager: {
422
+ flex: 1
423
+ },
424
+ hidden: {
425
+ opacity: 0
426
+ },
427
+ page: {
428
+ flex: 1
429
+ },
430
+ overlay: {
431
+ position: "absolute",
432
+ top: 0,
433
+ left: 0,
434
+ right: 0,
435
+ zIndex: 10
436
+ }
437
+ });
438
+ var index_default = CollapsibleTabView;
439
+ export {
440
+ TabFlatList,
441
+ TabScrollView,
442
+ index_default as default
443
+ };
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { Animated, FlatList, ScrollView, StyleProp, ViewStyle } from "react-native";
3
+ export interface TabBarProps {
4
+ activeIndex: number;
5
+ onTabPress: (index: number) => void;
6
+ }
7
+ export interface CollapsibleTabViewProps {
8
+ children: React.ReactNode;
9
+ renderHeader: () => React.ReactNode;
10
+ estimatedHeaderHeight?: number;
11
+ estimatedTabBarHeight?: number;
12
+ stickyEnabled?: boolean;
13
+ stickyTop?: number;
14
+ renderTabBar: (props: TabBarProps) => React.ReactNode;
15
+ initialTabIndex?: number;
16
+ onTabChange?: (index: number) => void;
17
+ onScroll?: (scrollY: number) => void;
18
+ swipeEnabled?: boolean;
19
+ style?: StyleProp<ViewStyle>;
20
+ }
21
+ export interface CollapsibleTabViewRef {
22
+ scrollToTab: (index: number, animated?: boolean) => void;
23
+ getActiveIndex: () => number;
24
+ }
25
+ export interface CollapsibleContextValue {
26
+ scrollY: Animated.Value;
27
+ activeIndex: number;
28
+ stickyEnabled: boolean;
29
+ headerHeight: number;
30
+ tabBarHeight: number;
31
+ renderHeader?: () => React.ReactNode;
32
+ renderTabBar?: () => React.ReactNode;
33
+ registerRef: (index: number, ref: FlatList<any> | ScrollView | null) => void;
34
+ syncScrollY: (index: number, y: number) => void;
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEnF,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACnC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,KAAK,CAAC,SAAS,CAAA;IACrD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACxD,cAAc,EAAE,MAAM,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,OAAO,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAA;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAC5E,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChD"}
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "react-native-collapsible-header-tab-view",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "不依赖 react-native-reanimated ,可吸顶的tab view",
5
- "main": "src/index.tsx",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/MasterZuom/react-native-collapsible-header-tab-view.git"
@@ -20,14 +21,20 @@
20
21
  "peerDependencies": {
21
22
  "react": "*",
22
23
  "react-native": ">=0.61.0",
23
- "react-native-pager-view": "^6.0.0"
24
+ "react-native-pager-view": "*"
24
25
  },
25
26
  "devDependencies": {
27
+ "@types/react": "^19.2.14",
28
+ "@types/react-native": "^0.72.8",
26
29
  "react": "^18.0.0",
27
30
  "react-native": "^0.71.0",
28
31
  "react-native-pager-view": "^6.7.0",
32
+ "tsup": "^8.5.1",
29
33
  "typescript": "^5.0.0"
30
34
  },
35
+ "scripts": {
36
+ "build": "tsup src/index.tsx --format cjs,esm --clean && tsc --emitDeclarationOnly --outDir dist --skipLibCheck"
37
+ },
31
38
  "files": [
32
39
  "dist"
33
40
  ]
package/dist/a.txt DELETED
File without changes