react-native-tab-view
Version:
Tab view component for React Native
422 lines (418 loc) • 15.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.TabBar = TabBar;
var React = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _useLatestCallback = _interopRequireDefault(require("use-latest-callback"));
var _TabBarIndicator = require("./TabBarIndicator");
var _TabBarItem = require("./TabBarItem");
var _useAnimatedValue = require("./useAnimatedValue");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
const Separator = _ref => {
let {
width
} = _ref;
return /*#__PURE__*/React.createElement(_reactNative.View, {
style: {
width
}
});
};
const getFlattenedTabWidth = style => {
const tabStyle = _reactNative.StyleSheet.flatten(style);
return tabStyle === null || tabStyle === void 0 ? void 0 : tabStyle.width;
};
const getComputedTabWidth = (index, layout, routes, scrollEnabled, tabWidths, flattenedWidth) => {
if (flattenedWidth === 'auto') {
return tabWidths[routes[index].key] || 0;
}
switch (typeof flattenedWidth) {
case 'number':
return flattenedWidth;
case 'string':
if (flattenedWidth.endsWith('%')) {
const width = parseFloat(flattenedWidth);
if (Number.isFinite(width)) {
return layout.width * (width / 100);
}
}
}
if (scrollEnabled) {
return layout.width / 5 * 2;
}
return layout.width / routes.length;
};
const getMaxScrollDistance = (tabBarWidth, layoutWidth) => tabBarWidth - layoutWidth;
const getTranslateX = (scrollAmount, maxScrollDistance) => _reactNative.Animated.multiply(_reactNative.Platform.OS === 'android' && _reactNative.I18nManager.isRTL ? _reactNative.Animated.add(maxScrollDistance, _reactNative.Animated.multiply(scrollAmount, -1)) : scrollAmount, _reactNative.I18nManager.isRTL ? 1 : -1);
const getTabBarWidth = _ref2 => {
let {
navigationState,
layout,
gap,
scrollEnabled,
flattenedTabWidth,
tabWidths
} = _ref2;
const {
routes
} = navigationState;
return routes.reduce((acc, _, i) => acc + (i > 0 ? gap ?? 0 : 0) + getComputedTabWidth(i, layout, routes, scrollEnabled, tabWidths, flattenedTabWidth), 0);
};
const normalizeScrollValue = _ref3 => {
let {
layout,
navigationState,
gap,
scrollEnabled,
tabWidths,
value,
flattenedTabWidth
} = _ref3;
const tabBarWidth = getTabBarWidth({
layout,
navigationState,
tabWidths,
gap,
scrollEnabled,
flattenedTabWidth
});
const maxDistance = getMaxScrollDistance(tabBarWidth, layout.width);
const scrollValue = Math.max(Math.min(value, maxDistance), 0);
if (_reactNative.Platform.OS === 'android' && _reactNative.I18nManager.isRTL) {
// On Android, scroll value is not applied in reverse in RTL
// so we need to manually adjust it to apply correct value
return maxDistance - scrollValue;
}
return scrollValue;
};
const getScrollAmount = _ref4 => {
let {
layout,
navigationState,
gap,
scrollEnabled,
flattenedTabWidth,
tabWidths
} = _ref4;
const centerDistance = Array.from({
length: navigationState.index + 1
}).reduce((total, _, i) => {
const tabWidth = getComputedTabWidth(i, layout, navigationState.routes, scrollEnabled, tabWidths, flattenedTabWidth);
// To get the current index centered we adjust scroll amount by width of indexes
// 0 through (i - 1) and add half the width of current index i
return total + (navigationState.index === i ? (tabWidth + (gap ?? 0)) / 2 : tabWidth + (gap ?? 0));
}, 0);
const scrollAmount = centerDistance - layout.width / 2;
return normalizeScrollValue({
layout,
navigationState,
tabWidths,
value: scrollAmount,
gap,
scrollEnabled,
flattenedTabWidth
});
};
const getLabelTextDefault = _ref5 => {
let {
route
} = _ref5;
return route.title;
};
const getAccessibleDefault = _ref6 => {
let {
route
} = _ref6;
return typeof route.accessible !== 'undefined' ? route.accessible : true;
};
const getAccessibilityLabelDefault = _ref7 => {
let {
route
} = _ref7;
return typeof route.accessibilityLabel === 'string' ? route.accessibilityLabel : typeof route.title === 'string' ? route.title : undefined;
};
const renderIndicatorDefault = props => /*#__PURE__*/React.createElement(_TabBarIndicator.TabBarIndicator, props);
const getTestIdDefault = _ref8 => {
let {
route
} = _ref8;
return route.testID;
};
// How many items measurements should we update per batch.
// Defaults to 10, since that's whats FlatList is using in initialNumToRender.
const MEASURE_PER_BATCH = 10;
function TabBar(_ref9) {
let {
getLabelText = getLabelTextDefault,
getAccessible = getAccessibleDefault,
getAccessibilityLabel = getAccessibilityLabelDefault,
getTestID = getTestIdDefault,
renderIndicator = renderIndicatorDefault,
gap = 0,
scrollEnabled,
jumpTo,
navigationState,
position,
activeColor,
bounces,
contentContainerStyle,
inactiveColor,
indicatorContainerStyle,
indicatorStyle,
labelStyle,
onTabLongPress,
onTabPress,
pressColor,
pressOpacity,
renderBadge,
renderIcon,
renderLabel,
renderTabBarItem,
style,
tabStyle,
testID,
android_ripple
} = _ref9;
const [layout, setLayout] = React.useState({
width: 0,
height: 0
});
const [tabWidths, setTabWidths] = React.useState({});
const flatListRef = React.useRef(null);
const isFirst = React.useRef(true);
const scrollAmount = (0, _useAnimatedValue.useAnimatedValue)(0);
const measuredTabWidths = React.useRef({});
const {
routes
} = navigationState;
const flattenedTabWidth = getFlattenedTabWidth(tabStyle);
const isWidthDynamic = flattenedTabWidth === 'auto';
const scrollOffset = getScrollAmount({
layout,
navigationState,
tabWidths,
gap,
scrollEnabled,
flattenedTabWidth
});
const hasMeasuredTabWidths = Boolean(layout.width) && routes.slice(0, navigationState.index).every(r => typeof tabWidths[r.key] === 'number');
React.useEffect(() => {
if (isFirst.current) {
isFirst.current = false;
return;
}
if (isWidthDynamic && !hasMeasuredTabWidths) {
return;
}
if (scrollEnabled) {
var _flatListRef$current;
(_flatListRef$current = flatListRef.current) === null || _flatListRef$current === void 0 ? void 0 : _flatListRef$current.scrollToOffset({
offset: scrollOffset,
animated: true
});
}
}, [hasMeasuredTabWidths, isWidthDynamic, scrollEnabled, scrollOffset]);
const handleLayout = e => {
const {
height,
width
} = e.nativeEvent.layout;
setLayout(layout => layout.width === width && layout.height === height ? layout : {
width,
height
});
};
const tabBarWidth = getTabBarWidth({
layout,
navigationState,
tabWidths,
gap,
scrollEnabled,
flattenedTabWidth
});
const separatorsWidth = Math.max(0, routes.length - 1) * gap;
const separatorPercent = separatorsWidth / tabBarWidth * 100;
const tabBarWidthPercent = `${routes.length * 40}%`;
const translateX = React.useMemo(() => getTranslateX(scrollAmount, getMaxScrollDistance(tabBarWidth, layout.width)), [layout.width, scrollAmount, tabBarWidth]);
const renderItem = React.useCallback(_ref10 => {
let {
item: route,
index
} = _ref10;
const props = {
key: route.key,
position: position,
route: route,
navigationState: navigationState,
getAccessibilityLabel: getAccessibilityLabel,
getAccessible: getAccessible,
getLabelText: getLabelText,
getTestID: getTestID,
renderBadge: renderBadge,
renderIcon: renderIcon,
renderLabel: renderLabel,
activeColor: activeColor,
inactiveColor: inactiveColor,
pressColor: pressColor,
pressOpacity: pressOpacity,
onLayout: isWidthDynamic ? e => {
measuredTabWidths.current[route.key] = e.nativeEvent.layout.width;
// When we have measured widths for all of the tabs, we should updates the state
// We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
// If we have more than 10 routes divide updating tabWidths into multiple batches. Here we update only first batch of 10 items.
if (routes.length > MEASURE_PER_BATCH && index === MEASURE_PER_BATCH && routes.slice(0, MEASURE_PER_BATCH).every(r => typeof measuredTabWidths.current[r.key] === 'number')) {
setTabWidths({
...measuredTabWidths.current
});
} else if (routes.every(r => typeof measuredTabWidths.current[r.key] === 'number')) {
// When we have measured widths for all of the tabs, we should updates the state
// We avoid doing separate setState for each layout since it triggers multiple renders and slows down app
setTabWidths({
...measuredTabWidths.current
});
}
} : undefined,
onPress: () => {
const event = {
route,
defaultPrevented: false,
preventDefault: () => {
event.defaultPrevented = true;
}
};
onTabPress === null || onTabPress === void 0 ? void 0 : onTabPress(event);
if (event.defaultPrevented) {
return;
}
jumpTo(route.key);
},
onLongPress: () => onTabLongPress === null || onTabLongPress === void 0 ? void 0 : onTabLongPress({
route
}),
labelStyle: labelStyle,
style: tabStyle,
// Calculate the deafult width for tab for FlatList to work
defaultTabWidth: !isWidthDynamic ? getComputedTabWidth(index, layout, routes, scrollEnabled, tabWidths, getFlattenedTabWidth(tabStyle)) : undefined,
android_ripple
};
return /*#__PURE__*/React.createElement(React.Fragment, null, gap > 0 && index > 0 ? /*#__PURE__*/React.createElement(Separator, {
width: gap
}) : null, renderTabBarItem ? renderTabBarItem(props) : /*#__PURE__*/React.createElement(_TabBarItem.TabBarItem, props));
}, [activeColor, android_ripple, gap, getAccessibilityLabel, getAccessible, getLabelText, getTestID, inactiveColor, isWidthDynamic, jumpTo, labelStyle, layout, navigationState, onTabLongPress, onTabPress, position, pressColor, pressOpacity, renderBadge, renderIcon, renderLabel, renderTabBarItem, routes, scrollEnabled, tabStyle, tabWidths]);
const keyExtractor = React.useCallback(item => item.key, []);
const contentContainerStyleMemoized = React.useMemo(() => [styles.tabContent, scrollEnabled ? {
width: tabBarWidth > separatorsWidth ? tabBarWidth : tabBarWidthPercent
} : styles.container, contentContainerStyle], [contentContainerStyle, scrollEnabled, separatorsWidth, tabBarWidth, tabBarWidthPercent]);
const handleScroll = React.useMemo(() => _reactNative.Animated.event([{
nativeEvent: {
contentOffset: {
x: scrollAmount
}
}
}], {
useNativeDriver: true
}), [scrollAmount]);
const handleViewableItemsChanged = (0, _useLatestCallback.default)(_ref11 => {
let {
changed
} = _ref11;
if (routes.length <= MEASURE_PER_BATCH) {
return;
}
// Get next vievable item
const item = changed[changed.length - 1];
const index = (item === null || item === void 0 ? void 0 : item.index) || 0;
if (item.isViewable && (index % 10 === 0 || index === navigationState.index || index === routes.length - 1)) {
setTabWidths({
...measuredTabWidths.current
});
}
});
return /*#__PURE__*/React.createElement(_reactNative.Animated.View, {
onLayout: handleLayout,
style: [styles.tabBar, style]
}, /*#__PURE__*/React.createElement(_reactNative.Animated.View, {
pointerEvents: "none",
style: [styles.indicatorContainer, scrollEnabled ? {
transform: [{
translateX
}]
} : null, tabBarWidth > separatorsWidth ? {
width: tabBarWidth - separatorsWidth
} : scrollEnabled ? {
width: tabBarWidthPercent
} : null, indicatorContainerStyle]
}, renderIndicator({
position,
layout,
navigationState,
jumpTo,
width: isWidthDynamic ? 'auto' : `${(100 - separatorPercent) / routes.length}%`,
style: indicatorStyle,
getTabWidth: i => getComputedTabWidth(i, layout, routes, scrollEnabled, tabWidths, flattenedTabWidth),
gap
})), /*#__PURE__*/React.createElement(_reactNative.View, {
style: styles.scroll
}, /*#__PURE__*/React.createElement(_reactNative.Animated.FlatList, {
data: routes,
keyExtractor: keyExtractor,
horizontal: true,
accessibilityRole: "tablist",
keyboardShouldPersistTaps: "handled",
scrollEnabled: scrollEnabled,
bounces: bounces,
initialNumToRender: MEASURE_PER_BATCH,
onViewableItemsChanged: handleViewableItemsChanged,
alwaysBounceHorizontal: false,
scrollsToTop: false,
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false,
automaticallyAdjustContentInsets: false,
overScrollMode: "never",
contentContainerStyle: contentContainerStyleMemoized,
scrollEventThrottle: 16,
renderItem: renderItem,
onScroll: handleScroll,
ref: flatListRef,
testID: testID
})));
}
const styles = _reactNative.StyleSheet.create({
container: {
flex: 1
},
scroll: {
overflow: _reactNative.Platform.select({
default: 'scroll',
web: undefined
})
},
tabBar: {
backgroundColor: '#2196f3',
elevation: 4,
shadowColor: 'black',
shadowOpacity: 0.1,
shadowRadius: _reactNative.StyleSheet.hairlineWidth,
shadowOffset: {
height: _reactNative.StyleSheet.hairlineWidth,
width: 0
},
zIndex: 1
},
tabContent: {
flexDirection: 'row',
flexWrap: 'nowrap'
},
indicatorContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0
}
});
//# sourceMappingURL=TabBar.js.map