UNPKG

react-native-ui-lib

Version:

<p align="center"> <img src="https://user-images.githubusercontent.com/1780255/105469025-56759000-5ca0-11eb-993d-3568c1fd54f4.png" height="250px" style="display:block"/> </p> <p align="center">UI Toolset & Components Library for React Native</p> <p a

308 lines (273 loc) • 8.25 kB
import _pt from "prop-types"; import _ from 'lodash'; import React, { Component } from 'react'; import { Platform, StyleSheet } from 'react-native'; import { Colors } from "../../style"; import { Constants, asBaseComponent } from "../../commons/new"; import { LogService } from "../../services"; import View from "../view"; import ScrollBar from "../scrollBar"; import TabBarItem from "./TabBarItem"; const MIN_TABS_FOR_SCROLL = 1; const DEFAULT_BACKGROUND_COLOR = Colors.white; const DEFAULT_HEIGHT = 48; /** * @description: TabBar Component * @modifiers: alignment, flex, padding, margin, background, typography, color (list of supported modifiers) * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TabBarScreen.tsx * @extends: ScrollBar * @notes: This is screen width component. */ class TabBar extends Component { static propTypes = { /** * Show Tab Bar bottom shadow */ enableShadow: _pt.bool, /** * The minimum number of tabs to render in scroll mode */ minTabsForScroll: _pt.number, /** * current selected tab index */ selectedIndex: _pt.number, /** * callback for when index has change (will not be called on ignored items) */ onChangeIndex: _pt.func, /** * callback for when tab selected */ onTabSelected: _pt.func, /** * Tab Bar height */ height: _pt.number, /** * Pass when container width is different than the screen width */ containerWidth: _pt.number, /** * The background color */ backgroundColor: _pt.string, /** * set darkTheme style */ darkTheme: _pt.bool, children: _pt.node, testID: _pt.string }; static displayName = 'TabBar'; static defaultProps = { selectedIndex: 0 }; static Item = TabBarItem; constructor(props) { super(props); this.state = { scrollEnabled: false, currentIndex: props.selectedIndex || 0 }; this.contentOffset = { x: 0, y: 0 }; this.scrollBar = React.createRef(); this.itemsRefs = []; LogService.componentDeprecationWarn({ oldComponent: 'TabBar', newComponent: 'TabController' }); } componentDidUpdate(prevProps, prevState) { const prevChildrenCount = React.Children.count(prevProps.children); if (this.childrenCount !== prevChildrenCount) { this.updateIndicator(0); } // TODO: since we're implementing an uncontrolled component here, we should verify the selectedIndex has changed // between this.props and nextProps (basically the meaning of selectedIndex should be initialIndex) const isIndexManuallyChanged = this.props.selectedIndex !== prevState.currentIndex && prevProps.selectedIndex !== this.props.selectedIndex; if (isIndexManuallyChanged) { this.updateIndicator(this.props.selectedIndex); } } // generateStyles() { // this.styles = createStyles(this.props); // } get childrenCount() { return React.Children.count(this.props.children); } get scrollContainerWidth() { return this.props.containerWidth || Constants.screenWidth; } isIgnored(index) { const child = React.Children.toArray(this.props.children)[index]; return _.get(child, 'props.ignore'); } hasOverflow() { return this.scrollContentWidth && this.scrollContentWidth > this.scrollContainerWidth; } shouldBeMarked = index => { return this.state.currentIndex === index && !this.isIgnored(index) && this.childrenCount > 1; }; updateIndicator(index) { if (index !== undefined && !this.isIgnored(index)) { this.setState({ currentIndex: index }, () => { this.scrollToSelected(); }); } } scrollToSelected(animated = true) { const childRef = this.itemsRefs[this.state.currentIndex]; const childLayout = childRef.getLayout(); if (childLayout && this.hasOverflow()) { if (childLayout.x + childLayout.width - this.contentOffset.x > this.scrollContainerWidth) { this.scrollBar?.current?.scrollTo?.({ x: childLayout.x - this.scrollContainerWidth + childLayout.width, y: 0, animated }); } else if (childLayout.x - this.contentOffset.x < 0) { this.scrollBar?.current?.scrollTo?.({ x: childLayout.x, y: 0, animated }); } } } onChangeIndex(index) { this.props.onChangeIndex?.(index); } onTabSelected(index) { this.props.onTabSelected?.(index); } onItemPress = (index, props) => { this.updateIndicator(index); setTimeout(() => { if (!props.ignore) { this.onChangeIndex(index); } this.onTabSelected(index); props.onPress?.(); }, 0); }; onScroll = event => { const { contentOffset } = event.nativeEvent; this.contentOffset = contentOffset; }; onContentSizeChange = width => { if (this.scrollContentWidth !== width) { this.scrollContentWidth = width; const { minTabsForScroll } = this.props; const minChildrenCount = minTabsForScroll || MIN_TABS_FOR_SCROLL; if (this.hasOverflow() && this.childrenCount > minChildrenCount) { this.setState({ scrollEnabled: true }); } } }; renderTabBar() { const { height, backgroundColor = DEFAULT_BACKGROUND_COLOR, containerView, containerProps, gradientMargins } = this.props; const { scrollEnabled } = this.state; const containerHeight = height || DEFAULT_HEIGHT; return <ScrollBar // @ts-ignore ref={this.scrollBar} contentContainerStyle={styles.scrollBarContainer} scrollEnabled={scrollEnabled} scrollEventThrottle={16} onScroll={this.onScroll} onContentSizeChange={this.onContentSizeChange} height={containerHeight} gradientColor={backgroundColor} containerView={containerView} containerProps={containerProps} gradientMargins={gradientMargins}> <View row style={[styles.tabBar, { height: containerHeight, backgroundColor }]}> {this.renderChildren()} </View> </ScrollBar>; } renderChildren() { this.itemsRefs = []; const { indicatorStyle, darkTheme } = this.props; const children = React.Children.map(this.props.children, (child, index) => { // @ts-ignore const accessLabel = child?.props.accessibilityLabel || child.props.label || ''; //TODO: review it again, all types here should be correct. As from React.Children.map it gets definitely child: React.ReactNode, and React.cloneElement does not accept it. // But seems it's work in a real life, so maybe it is just trouble with types compatibility //@ts-ignore return React.cloneElement(child, { indicatorStyle, darkTheme, selected: this.shouldBeMarked(index), onPress: () => { // @ts-ignore this.onItemPress(index, child.props); }, ref: r => { this.itemsRefs[index] = r; }, accessibilityLabel: `${accessLabel} ${index + 1} out of ${this.childrenCount}` }); }); return children; } render() { const { enableShadow, style, backgroundColor = DEFAULT_BACKGROUND_COLOR } = this.props; return (// @ts-ignore <View useSafeArea style={[styles.container, enableShadow && styles.containerShadow, style, { height: undefined, width: this.scrollContainerWidth, backgroundColor }]}> {this.renderTabBar()} </View> ); } } export default asBaseComponent(TabBar); const styles = StyleSheet.create({ container: { zIndex: 100 }, containerShadow: { ...Platform.select({ ios: { shadowColor: Colors.grey10, shadowOpacity: 0.05, shadowRadius: 2, shadowOffset: { height: 6, width: 0 } }, android: { elevation: 5, backgroundColor: Colors.white } }) }, tabBar: { flex: 1 }, shadowImage: { width: '100%' }, scrollBarContainer: { minWidth: '100%' } });