UNPKG

react-native-ui-lib

Version:

[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://stand-with-ukraine.pp.ua)

172 lines (171 loc) • 6 kB
import _toUpper from "lodash/toUpper"; import _isEmpty from "lodash/isEmpty"; import _isUndefined from "lodash/isUndefined"; import _cloneDeep from "lodash/cloneDeep"; // TODO: support commented props import React, { useCallback, useContext, useEffect, useRef, useMemo, useState } from 'react'; import { StyleSheet } from 'react-native'; import Reanimated, { runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { Colors, Typography, Spacings } from "../../style"; import Badge from "../badge"; import View from "../view"; import TabBarContext from "./TabBarContext"; import Constants from "../../commons/Constants"; const DEFAULT_LABEL_COLOR = Colors.$textDefault; const DEFAULT_SELECTED_LABEL_COLOR = Colors.$textPrimary; /** * @description: TabController's TabBarItem * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TabControllerScreen/index.tsx * @notes: Must be rendered as a direct child of TabController.TabBar. */ export default function TabBarItem({ index, label, labelColor = DEFAULT_LABEL_COLOR, selectedLabelColor = DEFAULT_SELECTED_LABEL_COLOR, labelStyle, labelProps, selectedLabelStyle, icon, badge, leadingAccessory, trailingAccessory, uppercase, activeOpacity = 0.9, backgroundColor = Colors.$backgroundElevated, activeBackgroundColor, testID, ignore, style, spreadItems, onPress, ...props }) { const { currentPage, setCurrentIndex } = useContext(TabBarContext); const itemRef = useRef(); const itemWidth = useRef(props.width); const isPressed = useSharedValue(false); // JSON.parse(JSON.stringify is due to an issue with reanimated const sharedLabelStyle = useSharedValue(JSON.parse(JSON.stringify(StyleSheet.flatten(labelStyle)))); const sharedSelectedLabelStyle = useSharedValue(JSON.parse(JSON.stringify(StyleSheet.flatten(selectedLabelStyle)))); const [isSelected, setIsSelected] = useState(currentPage.value === index); // NOTE: We clone these color values in refs because they might contain a PlatformColor value // which throws an error (see https://github.com/software-mansion/react-native-reanimated/issues/3164) const inactiveColor = useRef(_cloneDeep(labelColor)); const activeColor = useRef(_cloneDeep(!ignore ? selectedLabelColor : inactiveColor.current)); useEffect(() => { if (props.width) { props.onLayout?.({ nativeEvent: { layout: { x: 0, y: 0, width: itemWidth.current, height: 0 } } }, index); } }, []); useAnimatedReaction(() => currentPage.value === index, (isSelected, prevIsSelected) => { if (isSelected !== prevIsSelected) { runOnJS(setIsSelected)(isSelected); } }); const onLayout = useCallback(event => { const { width } = event.nativeEvent.layout; if (!itemWidth.current && itemRef?.current) { itemWidth.current = width; // @ts-ignore itemRef.current?.setNativeProps?.({ style: { width, paddingHorizontal: null, flex: null } }); props.onLayout?.(event, index); } }, [index, props.onLayout]); const animatedLabelStyle = useAnimatedStyle(() => { const isActive = currentPage.value === index; return isActive ? sharedSelectedLabelStyle.value : sharedLabelStyle.value; }, [currentPage]); const animatedLabelColorStyle = useAnimatedStyle(() => { const isActive = currentPage.value === index; return { color: isActive ? activeColor.current : inactiveColor.current }; }); const animatedIconStyle = useAnimatedStyle(() => { const isActive = currentPage.value === index; return { tintColor: isActive ? activeColor.current : inactiveColor.current }; }); const pressStyle = useAnimatedStyle(() => { return { backgroundColor: isPressed.value ? activeBackgroundColor : backgroundColor, opacity: isPressed.value ? activeOpacity : 1 }; }); const _style = useMemo(() => { const flex = Constants.isWeb ? spreadItems ? 1 : undefined : 1; const constantWidthStyle = itemWidth.current ? { flex: 0, width: itemWidth.current } : undefined; return [styles.tabItem, { flex }, style, constantWidthStyle, pressStyle]; }, [style, spreadItems]); const accessibilityState = useMemo(() => ({ selected: isSelected }), [isSelected]); const gesture = Gesture.Tap().maxDuration(60000).onEnd(() => { if (!ignore) { setCurrentIndex(index); } onPress && runOnJS(onPress)(index); }).onFinalize(() => { isPressed.value = false; }).onTouchesDown(() => { isPressed.value = true; }); return <GestureDetector gesture={gesture}> <View reanimated // @ts-expect-error ref={itemRef} style={_style} onLayout={onLayout} testID={testID} accessible accessibilityRole="tab" accessibilityState={accessibilityState}> {leadingAccessory} {icon && <Reanimated.Image source={icon} style={[!_isUndefined(label) && styles.tabItemIconWithLabel, animatedIconStyle]} />} {!_isEmpty(label) && <Reanimated.Text {...labelProps} fsTagName={'unmask'} style={[styles.tabItemLabel, labelStyle, animatedLabelStyle, animatedLabelColorStyle]}> {uppercase ? _toUpper(label) : label} </Reanimated.Text>} {badge && <Badge backgroundColor={Colors.$backgroundDangerHeavy} size={20} {...badge} containerStyle={styles.badge} />} {trailingAccessory} </View> </GestureDetector>; } const styles = StyleSheet.create({ tabItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: Spacings.s4 }, tabItemLabel: { ...Typography.text80 }, tabItemIconWithLabel: { marginRight: 10 }, badge: { marginLeft: Spacings.s1 } });