react-native-collapsible-header-tab-view 1.0.0 → 1.0.1

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.
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import { FlatList, FlatListProps, NativeScrollEvent, NativeSyntheticEvent } from "react-native";
3
+ export type TabFlatListProps<T> = Omit<FlatListProps<T>, "onScroll"> & {
4
+ onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
5
+ };
6
+ export declare const TabFlatList: <T>(props: TabFlatListProps<T> & {
7
+ ref?: React.Ref<FlatList<T>>;
8
+ }) => React.ReactElement;
9
+ //# sourceMappingURL=TabFlatList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabFlatList.d.ts","sourceRoot":"","sources":["../../src/TabFlatList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AACf,OAAO,EAEL,QAAQ,EACR,aAAa,EAEb,iBAAiB,EACjB,oBAAoB,EAErB,MAAM,cAAc,CAAC;AAUtB,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG;IACrE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;CACjE,CAAC;AAkGF,eAAO,MAAM,WAAW,EAAmC,CAAC,CAAC,EAC3D,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,KAC1D,KAAK,CAAC,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps } from "react-native";
3
+ export type TabScrollViewProps = Omit<ScrollViewProps, "onScroll"> & {
4
+ onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
5
+ };
6
+ declare const TabScrollView: React.ForwardRefExoticComponent<Omit<ScrollViewProps, "onScroll"> & {
7
+ onScroll?: (e: NativeSyntheticEvent<NativeScrollEvent>) => void;
8
+ } & React.RefAttributes<ScrollView>>;
9
+ export { TabScrollView };
10
+ //# sourceMappingURL=TabScrollView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TabScrollView.d.ts","sourceRoot":"","sources":["../../src/TabScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAA;AACd,OAAO,EAGL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,eAAe,EAChB,MAAM,cAAc,CAAA;AAIrB,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,GAAG;IACnE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAA;CAChE,CAAA;AAED,QAAA,MAAM,aAAa;eAHN,CAAC,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,KAAK,IAAI;oCA+DhE,CAAA;AAED,OAAO,EAAE,aAAa,EAAE,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { CollapsibleContextValue } from "./types";
2
+ export declare const CollapsibleContext: import("react").Context<CollapsibleContextValue | null>;
3
+ export declare const TabIndexContext: import("react").Context<number>;
4
+ export declare const useCollapsible: () => CollapsibleContextValue;
5
+ export declare const useTabIndex: () => number;
6
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEjD,eAAO,MAAM,kBAAkB,yDAAsD,CAAA;AAErF,eAAO,MAAM,eAAe,iCAA2B,CAAA;AAEvD,eAAO,MAAM,cAAc,+BAM1B,CAAA;AAED,eAAO,MAAM,WAAW,cAAoC,CAAA"}
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import { CollapsibleTabViewProps, CollapsibleTabViewRef } from "./types";
3
+ export { TabFlatList } from "./TabFlatList";
4
+ export { TabScrollView } from "./TabScrollView";
5
+ export type { CollapsibleTabViewProps, CollapsibleTabViewRef, TabBarProps, } from "./types";
6
+ declare const CollapsibleTabView: React.ForwardRefExoticComponent<CollapsibleTabViewProps & React.RefAttributes<CollapsibleTabViewRef>>;
7
+ export default CollapsibleTabView;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,EAEL,uBAAuB,EACvB,qBAAqB,EAEtB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EACV,uBAAuB,EACvB,qBAAqB,EACrB,WAAW,GACZ,MAAM,SAAS,CAAC;AAEjB,QAAA,MAAM,kBAAkB,uGAySvB,CAAC;AAwBF,eAAe,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,446 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.tsx
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ TabFlatList: () => TabFlatList,
34
+ TabScrollView: () => TabScrollView,
35
+ default: () => index_default
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+ var import_react4 = __toESM(require("react"));
39
+ var import_react_native3 = require("react-native");
40
+ var import_react_native_pager_view = __toESM(require("react-native-pager-view"));
41
+
42
+ // src/context.ts
43
+ var import_react = require("react");
44
+ var CollapsibleContext = (0, import_react.createContext)(null);
45
+ var TabIndexContext = (0, import_react.createContext)(0);
46
+ var useCollapsible = () => {
47
+ const ctx = (0, import_react.useContext)(CollapsibleContext);
48
+ if (!ctx) {
49
+ throw new Error("useCollapsible must be used within CollapsibleTabView");
50
+ }
51
+ return ctx;
52
+ };
53
+ var useTabIndex = () => (0, import_react.useContext)(TabIndexContext);
54
+
55
+ // src/TabFlatList.tsx
56
+ var import_react2 = __toESM(require("react"));
57
+ var import_react_native = require("react-native");
58
+ var AnimatedFlatListComponent = import_react_native.Animated.createAnimatedComponent(import_react_native.FlatList);
59
+ var AnimatedFlatList = AnimatedFlatListComponent;
60
+ var TabFlatListInner = ({
61
+ contentContainerStyle,
62
+ onScroll,
63
+ ListHeaderComponent,
64
+ ...props
65
+ }, ref) => {
66
+ const index = useTabIndex();
67
+ const {
68
+ scrollY,
69
+ activeIndex,
70
+ stickyEnabled,
71
+ headerHeight,
72
+ tabBarHeight,
73
+ renderHeader,
74
+ renderTabBar,
75
+ registerRef,
76
+ syncScrollY
77
+ } = useCollapsible();
78
+ const innerRef = (0, import_react2.useRef)(null);
79
+ const isActive = activeIndex === index;
80
+ (0, import_react2.useEffect)(() => {
81
+ registerRef(index, innerRef.current);
82
+ return () => registerRef(index, null);
83
+ }, [index]);
84
+ (0, import_react2.useImperativeHandle)(ref, () => innerRef.current, []);
85
+ const handleScroll = (0, import_react2.useCallback)(
86
+ (e) => {
87
+ const y = e.nativeEvent.contentOffset.y;
88
+ syncScrollY(index, y);
89
+ onScroll?.(e);
90
+ },
91
+ [index, onScroll, syncScrollY]
92
+ );
93
+ const mergedHeader = (0, import_react2.useMemo)(() => {
94
+ if (stickyEnabled) return ListHeaderComponent;
95
+ const OriginalHeader = typeof ListHeaderComponent === "function" ? /* @__PURE__ */ import_react2.default.createElement(ListHeaderComponent, null) : ListHeaderComponent ?? null;
96
+ return /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, null, renderHeader?.(), renderTabBar?.(), OriginalHeader);
97
+ }, [stickyEnabled, ListHeaderComponent, renderHeader, renderTabBar]);
98
+ const paddingTop = stickyEnabled ? headerHeight + tabBarHeight : 0;
99
+ const collapseRange = stickyEnabled ? headerHeight : 0;
100
+ const [containerHeight, setContainerHeight] = (0, import_react2.useState)(0);
101
+ const handleLayout = (0, import_react2.useCallback)((e) => {
102
+ setContainerHeight(e.nativeEvent.layout.height);
103
+ }, []);
104
+ const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
105
+ return /* @__PURE__ */ import_react2.default.createElement(
106
+ AnimatedFlatList,
107
+ {
108
+ ...props,
109
+ ref: innerRef,
110
+ onLayout: handleLayout,
111
+ ListHeaderComponent: mergedHeader,
112
+ contentContainerStyle: [
113
+ paddingTop > 0 && { paddingTop },
114
+ minHeight > 0 && { minHeight },
115
+ contentContainerStyle
116
+ ],
117
+ onScroll: isActive && stickyEnabled ? import_react_native.Animated.event(
118
+ [{ nativeEvent: { contentOffset: { y: scrollY } } }],
119
+ {
120
+ useNativeDriver: true,
121
+ listener: handleScroll
122
+ }
123
+ ) : handleScroll,
124
+ scrollEventThrottle: 16,
125
+ showsVerticalScrollIndicator: false
126
+ }
127
+ );
128
+ };
129
+ var TabFlatList = (0, import_react2.forwardRef)(TabFlatListInner);
130
+
131
+ // src/TabScrollView.tsx
132
+ var import_react3 = __toESM(require("react"));
133
+ var import_react_native2 = require("react-native");
134
+ var TabScrollView = (0, import_react3.forwardRef)(
135
+ ({ contentContainerStyle, onScroll, children, ...props }, ref) => {
136
+ const index = useTabIndex();
137
+ const { scrollY, activeIndex, headerHeight, tabBarHeight, registerRef, syncScrollY } = useCollapsible();
138
+ const innerRef = (0, import_react3.useRef)(null);
139
+ const isActive = activeIndex === index;
140
+ (0, import_react3.useEffect)(() => {
141
+ registerRef(index, innerRef.current);
142
+ return () => registerRef(index, null);
143
+ }, [index]);
144
+ (0, import_react3.useImperativeHandle)(ref, () => innerRef.current, []);
145
+ const handleScroll = (0, import_react3.useCallback)(
146
+ (e) => {
147
+ const y = e.nativeEvent.contentOffset.y;
148
+ syncScrollY(index, y);
149
+ onScroll?.(e);
150
+ },
151
+ [index, onScroll, syncScrollY]
152
+ );
153
+ const paddingTop = headerHeight + tabBarHeight;
154
+ const collapseRange = headerHeight;
155
+ const [containerHeight, setContainerHeight] = (0, import_react3.useState)(0);
156
+ const handleLayout = (0, import_react3.useCallback)((e) => {
157
+ setContainerHeight(e.nativeEvent.layout.height);
158
+ }, []);
159
+ const minHeight = containerHeight > 0 ? containerHeight + collapseRange : 0;
160
+ return /* @__PURE__ */ import_react3.default.createElement(
161
+ import_react_native2.Animated.ScrollView,
162
+ {
163
+ ref: innerRef,
164
+ ...props,
165
+ onLayout: handleLayout,
166
+ contentContainerStyle: [
167
+ { paddingTop },
168
+ minHeight > 0 && { minHeight },
169
+ contentContainerStyle
170
+ ],
171
+ onScroll: isActive ? import_react_native2.Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
172
+ useNativeDriver: true,
173
+ listener: handleScroll
174
+ }) : handleScroll,
175
+ scrollEventThrottle: 16,
176
+ showsVerticalScrollIndicator: false
177
+ },
178
+ children
179
+ );
180
+ }
181
+ );
182
+
183
+ // src/index.tsx
184
+ var CollapsibleTabView = (0, import_react4.forwardRef)(
185
+ ({
186
+ children,
187
+ renderHeader,
188
+ estimatedHeaderHeight = 0,
189
+ estimatedTabBarHeight = 0,
190
+ stickyEnabled = true,
191
+ stickyTop = 0,
192
+ renderTabBar,
193
+ initialTabIndex = 0,
194
+ onTabChange,
195
+ onScroll: onScrollProp,
196
+ swipeEnabled = true,
197
+ style
198
+ }, ref) => {
199
+ const [activeIndex, setActiveIndex] = (0, import_react4.useState)(initialTabIndex);
200
+ const pagerRef = (0, import_react4.useRef)(null);
201
+ const pages = (0, import_react4.useMemo)(
202
+ () => import_react4.default.Children.toArray(children).filter(import_react4.default.isValidElement),
203
+ [children]
204
+ );
205
+ (0, import_react4.useImperativeHandle)(ref, () => ({
206
+ scrollToTab: (index, animated = true) => {
207
+ if (index === activeIndex || index < 0 || index >= pages.length) return;
208
+ syncTabOnSwitch(index);
209
+ setActiveIndex(index);
210
+ if (animated) {
211
+ pagerRef.current?.setPage(index);
212
+ } else {
213
+ pagerRef.current?.setPageWithoutAnimation(index);
214
+ }
215
+ onTabChange?.(index);
216
+ },
217
+ getActiveIndex: () => activeIndex
218
+ }));
219
+ const hasEstimate = estimatedHeaderHeight > 0;
220
+ const headerHeightRef = (0, import_react4.useRef)(0);
221
+ const tabBarHeightRef = (0, import_react4.useRef)(0);
222
+ const [layout, setLayout] = (0, import_react4.useState)({
223
+ headerHeight: estimatedHeaderHeight,
224
+ tabBarHeight: estimatedTabBarHeight,
225
+ ready: hasEstimate
226
+ });
227
+ const { headerHeight, tabBarHeight } = layout;
228
+ const visible = layout.ready;
229
+ const adjustY = (0, import_react4.useRef)(new import_react_native3.Animated.Value(0)).current;
230
+ const tryCommitLayout = (0, import_react4.useCallback)(() => {
231
+ const h = headerHeightRef.current;
232
+ const t = tabBarHeightRef.current;
233
+ if (h > 0 && t > 0) {
234
+ setLayout((prev) => {
235
+ if (Math.abs(prev.headerHeight - h) <= 1 && Math.abs(prev.tabBarHeight - t) <= 1) {
236
+ return prev;
237
+ }
238
+ const diff = h + t - (prev.headerHeight + prev.tabBarHeight);
239
+ if (prev.ready && Math.abs(diff) > 1) {
240
+ adjustY.setValue(-diff);
241
+ import_react_native3.Animated.timing(adjustY, {
242
+ toValue: 0,
243
+ duration: 200,
244
+ useNativeDriver: true
245
+ }).start();
246
+ }
247
+ return { headerHeight: h, tabBarHeight: t, ready: true };
248
+ });
249
+ }
250
+ }, []);
251
+ const handleHeaderLayout = (0, import_react4.useCallback)(
252
+ (e) => {
253
+ headerHeightRef.current = e.nativeEvent.layout.height;
254
+ tryCommitLayout();
255
+ },
256
+ [tryCommitLayout]
257
+ );
258
+ const handleTabBarLayout = (0, import_react4.useCallback)(
259
+ (e) => {
260
+ tabBarHeightRef.current = e.nativeEvent.layout.height;
261
+ tryCommitLayout();
262
+ },
263
+ [tryCommitLayout]
264
+ );
265
+ const scrollY = (0, import_react4.useRef)(new import_react_native3.Animated.Value(0)).current;
266
+ const tabScrollYMap = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
267
+ const tabRefs = (0, import_react4.useRef)(
268
+ /* @__PURE__ */ new Map()
269
+ );
270
+ const collapseRange = stickyEnabled ? Math.max(headerHeight - stickyTop, 0) : 0;
271
+ const headerTranslateY = (0, import_react4.useMemo)(
272
+ () => collapseRange > 0 ? scrollY.interpolate({
273
+ inputRange: [0, collapseRange],
274
+ outputRange: [0, -collapseRange],
275
+ extrapolate: "clamp"
276
+ }) : new import_react_native3.Animated.Value(0),
277
+ [scrollY, collapseRange]
278
+ );
279
+ const registerRef = (0, import_react4.useCallback)(
280
+ (index, ref2) => {
281
+ if (ref2) {
282
+ tabRefs.current.set(index, ref2);
283
+ } else {
284
+ tabRefs.current.delete(index);
285
+ }
286
+ },
287
+ []
288
+ );
289
+ const syncScrollY = (0, import_react4.useCallback)(
290
+ (index, y) => {
291
+ tabScrollYMap.current.set(index, y);
292
+ onScrollProp?.(y);
293
+ },
294
+ [onScrollProp]
295
+ );
296
+ const scrollTabTo = (0, import_react4.useCallback)((index, offset) => {
297
+ const ref2 = tabRefs.current.get(index);
298
+ if (!ref2) return;
299
+ if (ref2.scrollToOffset) {
300
+ ref2.scrollToOffset({ offset, animated: false });
301
+ } else if (ref2.scrollTo) {
302
+ ref2.scrollTo({ y: offset, animated: false });
303
+ } else if (ref2.getNode) {
304
+ const node = ref2.getNode();
305
+ if (node?.scrollToOffset) {
306
+ node.scrollToOffset({ offset, animated: false });
307
+ } else if (node?.scrollTo) {
308
+ node.scrollTo({ y: offset, animated: false });
309
+ }
310
+ }
311
+ }, []);
312
+ const syncTabOnSwitch = (0, import_react4.useCallback)(
313
+ (newIndex) => {
314
+ const currentY = tabScrollYMap.current.get(activeIndex) ?? 0;
315
+ const newTabSavedY = tabScrollYMap.current.get(newIndex) ?? 0;
316
+ const isCollapsed = currentY >= collapseRange - 1;
317
+ let targetY;
318
+ if (isCollapsed) {
319
+ targetY = Math.max(newTabSavedY, collapseRange);
320
+ } else {
321
+ targetY = Math.min(currentY, collapseRange);
322
+ }
323
+ import_react_native3.InteractionManager.runAfterInteractions(() => {
324
+ scrollTabTo(newIndex, targetY);
325
+ tabScrollYMap.current.set(newIndex, targetY);
326
+ scrollY.setValue(Math.min(targetY, collapseRange));
327
+ });
328
+ },
329
+ [activeIndex, collapseRange, scrollTabTo, scrollY]
330
+ );
331
+ const handleTabPress = (0, import_react4.useCallback)(
332
+ (index) => {
333
+ if (index === activeIndex) return;
334
+ syncTabOnSwitch(index);
335
+ setActiveIndex(index);
336
+ pagerRef.current?.setPageWithoutAnimation(index);
337
+ onTabChange?.(index);
338
+ },
339
+ [activeIndex, onTabChange, syncTabOnSwitch]
340
+ );
341
+ const handlePageSelected = (0, import_react4.useCallback)(
342
+ (e) => {
343
+ const newIndex = e.nativeEvent.position;
344
+ if (newIndex === activeIndex) return;
345
+ syncTabOnSwitch(newIndex);
346
+ setActiveIndex(newIndex);
347
+ onTabChange?.(newIndex);
348
+ },
349
+ [activeIndex, onTabChange, syncTabOnSwitch]
350
+ );
351
+ const tabBarProps = (0, import_react4.useMemo)(
352
+ () => ({
353
+ activeIndex,
354
+ onTabPress: handleTabPress
355
+ }),
356
+ [activeIndex, handleTabPress]
357
+ );
358
+ const renderTabBarNode = (0, import_react4.useCallback)(
359
+ () => renderTabBar(tabBarProps),
360
+ [renderTabBar, tabBarProps]
361
+ );
362
+ const contextValue = (0, import_react4.useMemo)(
363
+ () => ({
364
+ scrollY,
365
+ activeIndex,
366
+ stickyEnabled,
367
+ headerHeight,
368
+ tabBarHeight,
369
+ renderHeader: stickyEnabled ? void 0 : renderHeader,
370
+ renderTabBar: stickyEnabled ? void 0 : renderTabBarNode,
371
+ registerRef,
372
+ syncScrollY
373
+ }),
374
+ [
375
+ scrollY,
376
+ activeIndex,
377
+ stickyEnabled,
378
+ headerHeight,
379
+ tabBarHeight,
380
+ renderHeader,
381
+ renderTabBarNode,
382
+ registerRef,
383
+ syncScrollY
384
+ ]
385
+ );
386
+ return /* @__PURE__ */ import_react4.default.createElement(CollapsibleContext.Provider, { value: contextValue }, /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { style: [styles.container, style] }, /* @__PURE__ */ import_react4.default.createElement(
387
+ import_react_native3.Animated.View,
388
+ {
389
+ style: [
390
+ styles.pager,
391
+ !visible && styles.hidden,
392
+ { transform: [{ translateY: adjustY }] }
393
+ ]
394
+ },
395
+ /* @__PURE__ */ import_react4.default.createElement(
396
+ import_react_native_pager_view.default,
397
+ {
398
+ ref: pagerRef,
399
+ style: styles.pager,
400
+ initialPage: initialTabIndex,
401
+ onPageSelected: handlePageSelected,
402
+ scrollEnabled: swipeEnabled
403
+ },
404
+ pages.map((page, i) => /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { key: page?.key ?? i, style: styles.page }, /* @__PURE__ */ import_react4.default.createElement(TabIndexContext.Provider, { value: i }, page)))
405
+ )
406
+ ), stickyEnabled && /* @__PURE__ */ import_react4.default.createElement(
407
+ import_react_native3.Animated.View,
408
+ {
409
+ style: [
410
+ styles.overlay,
411
+ { transform: [{ translateY: headerTranslateY }] }
412
+ ],
413
+ pointerEvents: "box-none"
414
+ },
415
+ /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { pointerEvents: "box-none", onLayout: handleHeaderLayout }, renderHeader()),
416
+ /* @__PURE__ */ import_react4.default.createElement(import_react_native3.View, { onLayout: handleTabBarLayout }, renderTabBarNode())
417
+ )));
418
+ }
419
+ );
420
+ var styles = import_react_native3.StyleSheet.create({
421
+ container: {
422
+ flex: 1
423
+ },
424
+ pager: {
425
+ flex: 1
426
+ },
427
+ hidden: {
428
+ opacity: 0
429
+ },
430
+ page: {
431
+ flex: 1
432
+ },
433
+ overlay: {
434
+ position: "absolute",
435
+ top: 0,
436
+ left: 0,
437
+ right: 0,
438
+ zIndex: 10
439
+ }
440
+ });
441
+ var index_default = CollapsibleTabView;
442
+ // Annotate the CommonJS export names for ESM import in node:
443
+ 0 && (module.exports = {
444
+ TabFlatList,
445
+ TabScrollView
446
+ });
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.1",
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"
@@ -23,11 +24,17 @@
23
24
  "react-native-pager-view": "^6.0.0"
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
package/src/index.tsx DELETED
@@ -1,357 +0,0 @@
1
- import React, {
2
- forwardRef,
3
- useCallback,
4
- useImperativeHandle,
5
- useMemo,
6
- useRef,
7
- useState,
8
- } from "react";
9
- import {
10
- Animated,
11
- FlatList,
12
- InteractionManager,
13
- LayoutChangeEvent,
14
- ScrollView,
15
- StyleSheet,
16
- View,
17
- } from "react-native";
18
- import PagerView from "react-native-pager-view";
19
-
20
- import { CollapsibleContext, TabIndexContext } from "./context";
21
- import {
22
- CollapsibleContextValue,
23
- CollapsibleTabViewProps,
24
- CollapsibleTabViewRef,
25
- TabBarProps,
26
- } from "./types";
27
-
28
- export { TabFlatList } from "./TabFlatList";
29
- export { TabScrollView } from "./TabScrollView";
30
- export type {
31
- CollapsibleTabViewProps,
32
- CollapsibleTabViewRef,
33
- TabBarProps,
34
- } from "./types";
35
-
36
- const CollapsibleTabView = forwardRef<
37
- CollapsibleTabViewRef,
38
- CollapsibleTabViewProps
39
- >(
40
- (
41
- {
42
- children,
43
- renderHeader,
44
- estimatedHeaderHeight = 0,
45
- estimatedTabBarHeight = 0,
46
- stickyEnabled = true,
47
- stickyTop = 0,
48
- renderTabBar,
49
- initialTabIndex = 0,
50
- onTabChange,
51
- onScroll: onScrollProp,
52
- swipeEnabled = true,
53
- style,
54
- },
55
- ref,
56
- ) => {
57
- const [activeIndex, setActiveIndex] = useState(initialTabIndex);
58
- const pagerRef = useRef<PagerView>(null);
59
-
60
- const pages = useMemo(
61
- () => React.Children.toArray(children).filter(React.isValidElement),
62
- [children],
63
- );
64
-
65
- useImperativeHandle(ref, () => ({
66
- scrollToTab: (index: number, animated = true) => {
67
- if (index === activeIndex || index < 0 || index >= pages.length) return;
68
- syncTabOnSwitch(index);
69
- setActiveIndex(index);
70
- if (animated) {
71
- pagerRef.current?.setPage(index);
72
- } else {
73
- pagerRef.current?.setPageWithoutAnimation(index);
74
- }
75
- onTabChange?.(index);
76
- },
77
- getActiveIndex: () => activeIndex,
78
- }));
79
-
80
- // 解决预估高度与实际高度差的问题
81
- const hasEstimate = estimatedHeaderHeight > 0;
82
- const headerHeightRef = useRef(0);
83
- const tabBarHeightRef = useRef(0);
84
- const [layout, setLayout] = useState({
85
- headerHeight: estimatedHeaderHeight,
86
- tabBarHeight: estimatedTabBarHeight,
87
- ready: hasEstimate,
88
- });
89
- const { headerHeight, tabBarHeight } = layout;
90
- const visible = layout.ready;
91
-
92
- const adjustY = useRef(new Animated.Value(0)).current;
93
-
94
- // 真实高度与预估高度如果存在高度差,则以动画形式让UI组件补偿这个高度差
95
- const tryCommitLayout = useCallback(() => {
96
- const h = headerHeightRef.current;
97
- const t = tabBarHeightRef.current;
98
- if (h > 0 && t > 0) {
99
- setLayout((prev) => {
100
- if (
101
- Math.abs(prev.headerHeight - h) <= 1 &&
102
- Math.abs(prev.tabBarHeight - t) <= 1
103
- ) {
104
- return prev;
105
- }
106
- const diff = h + t - (prev.headerHeight + prev.tabBarHeight);
107
- if (prev.ready && Math.abs(diff) > 1) {
108
- adjustY.setValue(-diff);
109
- Animated.timing(adjustY, {
110
- toValue: 0,
111
- duration: 200,
112
- useNativeDriver: true,
113
- }).start();
114
- }
115
- return { headerHeight: h, tabBarHeight: t, ready: true };
116
- });
117
- }
118
- }, []);
119
-
120
- const handleHeaderLayout = useCallback(
121
- (e: LayoutChangeEvent) => {
122
- headerHeightRef.current = e.nativeEvent.layout.height;
123
- tryCommitLayout();
124
- },
125
- [tryCommitLayout],
126
- );
127
-
128
- const handleTabBarLayout = useCallback(
129
- (e: LayoutChangeEvent) => {
130
- tabBarHeightRef.current = e.nativeEvent.layout.height;
131
- tryCommitLayout();
132
- },
133
- [tryCommitLayout],
134
- );
135
-
136
- const scrollY = useRef(new Animated.Value(0)).current;
137
- const tabScrollYMap = useRef(new Map<number, number>());
138
- const tabRefs = useRef(
139
- new Map<number, FlatList<any> | ScrollView | null>(),
140
- );
141
- // header浮层 在Y轴 偏移的最大距离
142
- const collapseRange = stickyEnabled
143
- ? Math.max(headerHeight - stickyTop, 0)
144
- : 0;
145
-
146
- // header浮层 垂直偏移动画值,驱动 header + tabBar 整体上移实现折叠效果。
147
- const headerTranslateY = useMemo(
148
- () =>
149
- collapseRange > 0
150
- ? scrollY.interpolate({
151
- inputRange: [0, collapseRange],
152
- outputRange: [0, -collapseRange],
153
- extrapolate: "clamp",
154
- })
155
- : new Animated.Value(0),
156
- [scrollY, collapseRange],
157
- );
158
-
159
- const registerRef = useCallback(
160
- (index: number, ref: FlatList<any> | ScrollView | null) => {
161
- if (ref) {
162
- tabRefs.current.set(index, ref);
163
- } else {
164
- tabRefs.current.delete(index);
165
- }
166
- },
167
- [],
168
- );
169
-
170
- // 记录每个 tab 的当前滚动位置
171
- const syncScrollY = useCallback(
172
- (index: number, y: number) => {
173
- tabScrollYMap.current.set(index, y);
174
- onScrollProp?.(y);
175
- },
176
- [onScrollProp],
177
- );
178
-
179
- // 切换 tab 时,把新 tab 的 FlatList/ScrollView 滚动到计算好的 targetY,确保 header 吸顶状态和列表位置一致。
180
- const scrollTabTo = useCallback((index: number, offset: number) => {
181
- const ref: any = tabRefs.current.get(index);
182
- if (!ref) return;
183
- if (ref.scrollToOffset) {
184
- ref.scrollToOffset({ offset, animated: false });
185
- } else if (ref.scrollTo) {
186
- ref.scrollTo({ y: offset, animated: false });
187
- } else if (ref.getNode) {
188
- const node = ref.getNode();
189
- if (node?.scrollToOffset) {
190
- node.scrollToOffset({ offset, animated: false });
191
- } else if (node?.scrollTo) {
192
- node.scrollTo({ y: offset, animated: false });
193
- }
194
- }
195
- }, []);
196
-
197
- //核心代码: 切换Tab时 计算页面滚动的位置
198
- const syncTabOnSwitch = useCallback(
199
- (newIndex: number) => {
200
- // 读取当前 Tab 和目标 Tab 的滚动位置
201
- const currentY = tabScrollYMap.current.get(activeIndex) ?? 0;
202
- const newTabSavedY = tabScrollYMap.current.get(newIndex) ?? 0;
203
-
204
- // 判断当前 Tab 的头部是否已折叠
205
- const isCollapsed = currentY >= collapseRange - 1;
206
-
207
- // 计算目标 Tab 应该滚动到的位置
208
- let targetY: number;
209
- if (isCollapsed) {
210
- targetY = Math.max(newTabSavedY, collapseRange);
211
- } else {
212
- targetY = Math.min(currentY, collapseRange);
213
- }
214
-
215
- // 在交互完成后执行滚动并更新状态
216
- // runAfterInteractions 会等待所有正在进行的动画和触摸交互完成后再执行回调,确保:
217
- // 1. 新 Tab 页已经渲染就绪,scrollTo 能生效
218
- // 2. 不与切换动画争抢帧,体验更流畅
219
- InteractionManager.runAfterInteractions(() => {
220
- scrollTabTo(newIndex, targetY);
221
- tabScrollYMap.current.set(newIndex, targetY);
222
- scrollY.setValue(Math.min(targetY, collapseRange));
223
- });
224
- },
225
- [activeIndex, collapseRange, scrollTabTo, scrollY],
226
- );
227
-
228
- const handleTabPress = useCallback(
229
- (index: number) => {
230
- if (index === activeIndex) return;
231
- syncTabOnSwitch(index);
232
- setActiveIndex(index);
233
- pagerRef.current?.setPageWithoutAnimation(index);
234
- onTabChange?.(index);
235
- },
236
- [activeIndex, onTabChange, syncTabOnSwitch],
237
- );
238
-
239
- const handlePageSelected = useCallback(
240
- (e: { nativeEvent: { position: number } }) => {
241
- const newIndex = e.nativeEvent.position;
242
- if (newIndex === activeIndex) return;
243
- syncTabOnSwitch(newIndex);
244
- setActiveIndex(newIndex);
245
- onTabChange?.(newIndex);
246
- },
247
- [activeIndex, onTabChange, syncTabOnSwitch],
248
- );
249
-
250
- const tabBarProps: TabBarProps = useMemo(
251
- () => ({
252
- activeIndex,
253
- onTabPress: handleTabPress,
254
- }),
255
- [activeIndex, handleTabPress],
256
- );
257
-
258
- const renderTabBarNode = useCallback(
259
- () => renderTabBar(tabBarProps),
260
- [renderTabBar, tabBarProps],
261
- );
262
-
263
- const contextValue: CollapsibleContextValue = useMemo(
264
- () => ({
265
- scrollY,
266
- activeIndex,
267
- stickyEnabled,
268
- headerHeight,
269
- tabBarHeight,
270
- renderHeader: stickyEnabled ? undefined : renderHeader,
271
- renderTabBar: stickyEnabled ? undefined : renderTabBarNode,
272
- registerRef,
273
- syncScrollY,
274
- }),
275
- [
276
- scrollY,
277
- activeIndex,
278
- stickyEnabled,
279
- headerHeight,
280
- tabBarHeight,
281
- renderHeader,
282
- renderTabBarNode,
283
- registerRef,
284
- syncScrollY,
285
- ],
286
- );
287
-
288
- return (
289
- <CollapsibleContext.Provider value={contextValue}>
290
- <View style={[styles.container, style]}>
291
- <Animated.View
292
- style={[
293
- styles.pager,
294
- !visible && styles.hidden,
295
- { transform: [{ translateY: adjustY }] },
296
- ]}
297
- >
298
- <PagerView
299
- ref={pagerRef}
300
- style={styles.pager}
301
- initialPage={initialTabIndex}
302
- onPageSelected={handlePageSelected}
303
- scrollEnabled={swipeEnabled}
304
- >
305
- {pages.map((page: React.ReactElement, i) => (
306
- <View key={page?.key ?? i} style={styles.page}>
307
- <TabIndexContext.Provider value={i}>
308
- {page}
309
- </TabIndexContext.Provider>
310
- </View>
311
- ))}
312
- </PagerView>
313
- </Animated.View>
314
-
315
- {stickyEnabled && (
316
- <Animated.View
317
- style={[
318
- styles.overlay,
319
- { transform: [{ translateY: headerTranslateY }] },
320
- ]}
321
- pointerEvents="box-none"
322
- >
323
- <View pointerEvents="box-none" onLayout={handleHeaderLayout}>
324
- {renderHeader()}
325
- </View>
326
- <View onLayout={handleTabBarLayout}>{renderTabBarNode()}</View>
327
- </Animated.View>
328
- )}
329
- </View>
330
- </CollapsibleContext.Provider>
331
- );
332
- },
333
- );
334
-
335
- const styles = StyleSheet.create({
336
- container: {
337
- flex: 1,
338
- },
339
- pager: {
340
- flex: 1,
341
- },
342
- hidden: {
343
- opacity: 0,
344
- },
345
- page: {
346
- flex: 1,
347
- },
348
- overlay: {
349
- position: "absolute",
350
- top: 0,
351
- left: 0,
352
- right: 0,
353
- zIndex: 10,
354
- },
355
- });
356
-
357
- export default CollapsibleTabView;