UNPKG

@exponent/ex-navigation

Version:

Route-centric navigation library for React Native.

382 lines (325 loc) 10.2 kB
/** * @flow */ import React, { Children, } from 'react'; import { StyleSheet, View, } from 'react-native'; import PureComponent from '../utils/PureComponent'; import StaticContainer from 'react-static-container'; import _ from 'lodash'; import invariant from 'invariant'; import cloneReferencedElement from 'react-clone-referenced-element'; import Actions from '../ExNavigationActions'; import ExNavigatorContext from '../ExNavigatorContext'; import ExNavigationTabBar from './ExNavigationTabBar'; import ExNavigationTabItem from './ExNavigationTabItem'; import { createNavigatorComponent } from '../ExNavigationComponents'; import type ExNavigationContext from '../ExNavigationContext'; export class ExNavigationTabContext extends ExNavigatorContext { type = 'tab'; navigatorUID: string; navigatorId: string; dispatch: Function; _navigatorTabMap: Object = {}; _getNavigatorState: () => any; setNavigatorUIDForCurrentTab(navigatorUID: string) { const navigatorState = this._getNavigatorState(); if (!navigatorState) { return; } const currentTab = navigatorState.routes[navigatorState.index]; this._navigatorTabMap[currentTab.key] = navigatorUID; } getNavigatorUIDForTabKey(tabKey: string) { return this._navigatorTabMap[tabKey]; } jumpToTab(tabKey: string) { this.navigationContext.performAction(({ tabs }) => { tabs(this.navigatorUID).jumpToTab(tabKey); }); } } type TabItem = { id: string, renderIcon?: Function, renderBadge?: Function, tabContent?: React.Element<{}>, }; type Props = { id: string, navigatorUID: string, initialTab: string, renderTabBar: (props: Object) => React.Element<{}>, tabBarHeight?: number, tabBarColor?: string, tabBarStyle?: any, children: Array<React.Element<{}>>, navigation: ExNavigationContext, onRegisterNavigatorContext: (navigatorUID: string, navigatorContext: ExNavigationTabContext) => void, onUnregisterNavigatorContext: (navigatorUID: string) => void, onWillChangeTab: (id: string) => bool, navigationState: Object, translucent?: bool, }; type State = { id: string, navigatorUID: string, tabItems: Array<TabItem>, parentNavigatorUID: string, renderedTabKeys: Array<string>, }; class ExNavigationTab extends PureComponent<any, Props, State> { props: Props; state: State; static route = { __isNavigator: true, }; static defaultProps = { renderTabBar(props) { return <ExNavigationTabBar {...props} />; }, }; static contextTypes = { parentNavigatorUID: React.PropTypes.string, }; static childContextTypes = { parentNavigatorUID: React.PropTypes.string, navigator: React.PropTypes.instanceOf(ExNavigationTabContext), }; getChildContext() { return { // Get the navigator actions instance for this navigator navigator: this._getNavigatorContext(), parentNavigatorUID: this.state.navigatorUID, }; } constructor(props, context) { super(props, context); this.state = { tabItems: [], id: props.id, navigatorUID: props.navigatorUID, parentNavigatorUID: context.parentNavigatorUID, renderedTabKeys: [], }; } render() { if (!this.props.children || !this.state.tabItems) { return null; } const navigationState: ?Object = this._getNavigationState(); if (!navigationState) { return null; } const tabBarProps = { selectedTab: navigationState.routes[navigationState.index].key, items: this.state.tabItems, height: this.props.tabBarHeight, translucent: this.props.translucent, style: [ this.props.tabBarStyle, this.props.tabBarColor ? {backgroundColor: this.props.tabBarColor} : {}, ], }; const tabBar = this.props.renderTabBar(tabBarProps); const TabBarComponent = tabBar.type; // Get the tab bar's height from a static property on the class const tabBarHeight = this.props.tabBarHeight || TabBarComponent.defaultHeight || 0; const isTranslucent = this.props.translucent; return ( <View style={styles.container}> <View style={{flex: 1, marginBottom: isTranslucent ? 0 : tabBarHeight}}> {this.renderTabs()} </View> {tabBar} </View> ); } renderTabs() { const tabs = this.state.renderedTabKeys.map(key => this.state.tabItems.find(i => i.id === key) ); return ( <View style={styles.tabContent}> {tabs.map(tab => this.renderTab(tab))} </View> ); } renderTab(tabItem: Object) { if (!tabItem.element) { return null; } const navState = this._getNavigationState(); const selectedChild = navState.routes[navState.index]; const isSelected = tabItem.id === selectedChild.key; return ( <View key={tabItem.id} removeClippedSubviews={!isSelected} style={[styles.tabContentInner, {opacity: isSelected ? 1 : 0}]} pointerEvents={isSelected ? 'auto' : 'none'}> <StaticContainer shouldUpdate={isSelected}> {tabItem.element} </StaticContainer> </View> ); } componentWillMount() { this._parseTabItems(this.props); this._registerNavigatorContext(); this.props.navigation.dispatch(Actions.setCurrentNavigator( this.state.navigatorUID, this.state.parentNavigatorUID, 'tab', {}, [{ key: this.props.initialTab, }], )); } componentWillUnmount() { this.props.navigation.dispatch(Actions.removeNavigator(this.state.navigatorUID)); this.props.onUnregisterNavigatorContext(this.state.navigatorUID); } componentWillReceiveProps(nextProps) { if (nextProps.children && nextProps.children !== this.props.children) { this._parseTabItems(nextProps); } if (nextProps.navigationState !== this.props.navigationState) { this.setState({ renderedTabKeys: this._updateRenderedTabKeys(nextProps, this.state.renderedTabKeys), }); } } componentDidUpdate(prevProps) { if (prevProps.navigation.dispatch !== this.props.navigation.dispatch) { this._registerNavigatorContext(); } // When we're changing tabs, let's make sure we set the current navigator to be the controlled navigator, // if it exists. if (prevProps.navigationState !== this.props.navigationState) { const navigationState = this.props.navigationState; const currentTabKey = navigationState.routes[navigationState.index].key; const navigatorUIDForTabKey = this._getNavigatorContext().getNavigatorUIDForTabKey(currentTabKey); if (navigatorUIDForTabKey) { this.props.navigation.dispatch( Actions.setCurrentNavigator(navigatorUIDForTabKey) ); } } } _updateRenderedTabKeys(props, currentRenderedTabKeys) { const navState = this._getNavigationState(props); const currentTabItems = navState.routes.map(c => c.key); const selectedChild = navState.routes[navState.index]; return [ ..._.uniq(_.without([...currentRenderedTabKeys, ...currentTabItems], selectedChild.key)), selectedChild.key, ]; } _parseTabItems(props) { const tabItems = Children.map(props.children, (child, index) => { invariant( child.type === ExNavigationTabItem, 'All children of TabNavigation must be TabNavigationItems.', ); const tabItemProps = child.props; let tabItem = { ..._.omit(tabItemProps, ['children']), }; if (Children.count(tabItemProps.children) > 0) { let child = Children.only(tabItemProps.children); // NOTE: a bit hacky, identifying navigation component like StackNav // via initialRoute if (child.props.initialRoute && this.props.translucent) { let defaultRouteConfig = child.props.defaultRouteConfig || {}; defaultRouteConfig = {...defaultRouteConfig, __tabBarInset: this.props.tabBarHeight}; tabItem.element = cloneReferencedElement(child, {...child.props, defaultRouteConfig}); } else { tabItem.element = child; } } const tabItemOnPress = () => { this._setActiveTab(tabItemProps.id, index); }; if (typeof tabItemProps.onPress === 'function') { tabItem.onPress = tabItem.onPress.bind(this, tabItemOnPress); } else { tabItem.onPress = tabItemOnPress; } if (typeof tabItemProps.onLongPress === 'function') { tabItem.onLongPress = tabItem.onLongPress.bind(this, tabItemOnPress); } else { tabItem.onLongPress = tabItem.onPress; } return tabItem; }); this.setState({ tabItems, }); } _setActiveTab(id, index) { if (typeof this.props.onWillChangeTab === 'function') { let changeTab = this.props.onWillChangeTab(id); if (!changeTab) { return; } } this._getNavigatorContext().jumpToTab(id); if (typeof this.props.onTabPress === 'function') { this.props.onTabPress(id); } } _getNavigationState(props: ?Props): Object { if (!props) { props = this.props; } const { navigationState } = props; return navigationState; } _registerNavigatorContext() { this.props.onRegisterNavigatorContext( this.state.navigatorUID, new ExNavigationTabContext( this.state.navigatorUID, this.state.parentNavigatorUID, this.state.id, this.props.navigation, ) ); } _getNavigatorContext(): ExNavigationTabContext { const navigatorContext: any = this.props.navigation.getNavigatorByUID(this.state.navigatorUID); return (navigatorContext: ExNavigationTabContext); } } export default createNavigatorComponent(ExNavigationTab); const styles = StyleSheet.create({ container: { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, }, tabContent: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'white', }, tabContentInner: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'white', }, });