react-native-collapsible-tab-view
Version:
Collapsible tab view component for React Native
180 lines (177 loc) • 5.72 kB
JavaScript
"use strict";
import React from 'react';
import { StyleSheet, useWindowDimensions } from 'react-native';
import Animated, { cancelAnimation, scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, useSharedValue, withTiming } from 'react-native-reanimated';
import { Indicator } from './Indicator';
import { MaterialTabItem } from './TabItem';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export const TABBAR_HEIGHT = 48;
/**
* Basic usage looks like this:
*
* ```tsx
* <Tabs.Container
* ...
* TabBarComponent={(props) => (
* <MaterialTabBar
* {...props}
* activeColor="red"
* inactiveColor="yellow"
* inactiveOpacity={1}
* labelStyle={{ fontSize: 14 }}
* />
* )}
* >
* {...}
* </Tabs.Container>
* ```
*/
const MaterialTabBar = ({
tabNames,
indexDecimal,
scrollEnabled = false,
indicatorStyle,
index,
TabItemComponent = MaterialTabItem,
getLabelText = name => String(name).toUpperCase(),
onTabPress,
style,
tabProps,
contentContainerStyle,
labelStyle,
inactiveColor,
activeColor,
tabStyle,
width: customWidth,
keepActiveTabCentered
}) => {
const tabBarRef = useAnimatedRef();
const windowWidth = useWindowDimensions().width;
const width = customWidth ?? windowWidth;
const isFirstRender = React.useRef(true);
const itemLayoutGathering = React.useRef(new Map());
const tabsOffset = useSharedValue(0);
const isScrolling = useSharedValue(false);
const nTabs = tabNames.length;
const [itemsLayout, setItemsLayout] = React.useState(scrollEnabled ? [] : tabNames.map((_, i) => {
const tabWidth = width / nTabs;
return {
width: tabWidth,
x: i * tabWidth
};
}));
React.useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else if (!scrollEnabled) {
// update items width on window resizing
const tabWidth = width / nTabs;
setItemsLayout(tabNames.map((_, i) => {
return {
width: tabWidth,
x: i * tabWidth
};
}));
}
}, [scrollEnabled, nTabs, tabNames, width]);
const onTabItemLayout = React.useCallback((event, name) => {
if (scrollEnabled) {
if (!event.nativeEvent?.layout) return;
const {
width,
x
} = event.nativeEvent.layout;
itemLayoutGathering.current.set(name, {
width,
x
});
// pick out the layouts for the tabs we know about (in case they changed dynamically)
const layout = Array.from(itemLayoutGathering.current.entries()).filter(([tabName]) => tabNames.includes(tabName)).map(([, layout]) => layout).sort((a, b) => a.x - b.x);
if (layout.length === tabNames.length) {
setItemsLayout(layout);
}
}
}, [scrollEnabled, tabNames]);
const cancelNextScrollSync = useSharedValue(index.value);
const onScroll = useAnimatedScrollHandler({
onScroll: event => {
tabsOffset.value = event.contentOffset.x;
},
onBeginDrag: () => {
isScrolling.value = true;
cancelNextScrollSync.value = index.value;
},
onMomentumEnd: () => {
isScrolling.value = false;
}
}, []);
const currentIndexToSync = useSharedValue(index.value);
const targetIndexToSync = useSharedValue(index.value);
useAnimatedReaction(() => {
return index.value;
}, nextIndex => {
if (scrollEnabled) {
cancelAnimation(currentIndexToSync);
targetIndexToSync.value = nextIndex;
currentIndexToSync.value = withTiming(nextIndex);
}
}, [scrollEnabled]);
useAnimatedReaction(() => {
return currentIndexToSync.value === targetIndexToSync.value;
}, canSync => {
if (canSync && scrollEnabled && itemsLayout.length === nTabs && itemsLayout[index.value]) {
const halfTab = itemsLayout[index.value].width / 2;
const offset = itemsLayout[index.value].x;
if (keepActiveTabCentered || offset < tabsOffset.value || offset > tabsOffset.value + width - 2 * halfTab) {
scrollTo(tabBarRef, offset - width / 2 + halfTab, 0, true);
}
}
}, [scrollEnabled, itemsLayout, nTabs]);
return /*#__PURE__*/_jsxs(Animated.ScrollView, {
ref: tabBarRef,
horizontal: true,
style: style,
contentContainerStyle: [styles.contentContainer, !scrollEnabled && {
width
}, contentContainerStyle],
keyboardShouldPersistTaps: "handled",
bounces: false,
alwaysBounceHorizontal: false,
scrollsToTop: false,
showsHorizontalScrollIndicator: false,
automaticallyAdjustContentInsets: false,
overScrollMode: "never",
scrollEnabled: scrollEnabled,
onScroll: scrollEnabled ? onScroll : undefined,
scrollEventThrottle: 16,
children: [tabNames.map((name, i) => {
return /*#__PURE__*/_jsx(TabItemComponent, {
index: i,
name: name,
label: tabProps.get(name)?.label || getLabelText(name),
onPress: onTabPress,
onLayout: scrollEnabled ? event => onTabItemLayout(event, name) : undefined,
scrollEnabled: scrollEnabled,
indexDecimal: indexDecimal,
labelStyle: labelStyle,
activeColor: activeColor,
inactiveColor: inactiveColor,
style: tabStyle
}, name);
}), itemsLayout.length === nTabs && /*#__PURE__*/_jsx(Indicator, {
indexDecimal: indexDecimal,
itemsLayout: itemsLayout,
fadeIn: scrollEnabled,
style: indicatorStyle
})]
});
};
const MemoizedTabBar = /*#__PURE__*/React.memo(MaterialTabBar);
export { MemoizedTabBar as MaterialTabBar };
const styles = StyleSheet.create({
contentContainer: {
flexDirection: 'row',
flexWrap: 'nowrap'
}
});
//# sourceMappingURL=TabBar.js.map