Drawer Router (#3618)

This commit is contained in:
Eric Vicenti 2018-02-27 18:34:05 -08:00 committed by Brent Vatne
parent cd99dc8054
commit 2e47cbb3cb
13 changed files with 178 additions and 138 deletions

View File

@ -4,7 +4,11 @@
import React from 'react';
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, DrawerNavigator, SafeAreaView } from 'react-navigation';
import {
StackNavigator,
DrawerNavigator,
SafeAreaView,
} from 'react-navigation';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import SampleText from './SampleText';
@ -12,10 +16,7 @@ const MyNavScreen = ({ navigation, banner }) => (
<ScrollView>
<SafeAreaView forceInset={{ top: 'always' }}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('DrawerOpen')}
title="Open drawer"
/>
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
<Button
onPress={() => navigation.navigate('Email')}
title="Open other screen"
@ -76,9 +77,6 @@ const DrawerExample = DrawerNavigator(
},
},
{
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
initialRouteName: 'Drafts',
contentOptions: {
activeTintColor: '#e91e63',

View File

@ -11,10 +11,7 @@ import SampleText from './SampleText';
const MyNavScreen = ({ navigation, banner }) => (
<ScrollView style={styles.container}>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('DrawerOpen')}
title="Open drawer"
/>
<Button onPress={() => navigation.openDrawer()} title="Open drawer" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
</ScrollView>
);
@ -55,9 +52,6 @@ const DrawerExample = DrawerNavigator(
},
},
{
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
initialRouteName: 'Drafts',
contentOptions: {
activeTintColor: '#e91e63',
@ -69,10 +63,6 @@ const MainDrawerExample = DrawerNavigator({
Drafts: {
screen: DrawerExample,
},
}, {
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
});
const styles = StyleSheet.create({

View File

@ -766,9 +766,6 @@ declare module 'react-navigation' {
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
drawerWidth?: number | (() => number),
drawerPosition?: 'left' | 'right',
drawerOpenRoute?: string,
drawerCloseRoute?: string,
drawerToggleRoute?: string,
contentComponent?: React$ElementType,
contentOptions?: {},
style?: ViewStyleProp,
@ -901,9 +898,6 @@ declare module 'react-navigation' {
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open',
drawerWidth: number | (() => number),
drawerPosition: 'left' | 'right',
drawerOpenRoute: string,
drawerCloseRoute: string,
drawerToggleRoute: string,
contentComponent: React$ElementType,
contentOptions?: {},
style?: ViewStyleProp,

View File

@ -9,6 +9,9 @@ const REPLACE = 'Navigation/REPLACE';
const SET_PARAMS = 'Navigation/SET_PARAMS';
const URI = 'Navigation/URI';
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
const OPEN_DRAWER = 'Navigation/OPEN_DRAWER';
const CLOSE_DRAWER = 'Navigation/CLOSE_DRAWER';
const TOGGLE_DRAWER = 'Navigation/TOGGLE_DRAWER';
const createAction = (type, fn) => {
fn.toString = () => type;
@ -107,6 +110,16 @@ const completeTransition = createAction(COMPLETE_TRANSITION, payload => ({
key: payload && payload.key,
}));
const openDrawer = createAction(OPEN_DRAWER, payload => ({
type: OPEN_DRAWER,
}));
const closeDrawer = createAction(CLOSE_DRAWER, payload => ({
type: CLOSE_DRAWER,
}));
const toggleDrawer = createAction(TOGGLE_DRAWER, payload => ({
type: TOGGLE_DRAWER,
}));
export default {
// Action constants
BACK,
@ -120,6 +133,9 @@ export default {
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
OPEN_DRAWER,
CLOSE_DRAWER,
TOGGLE_DRAWER,
// Action creators
back,
@ -133,4 +149,7 @@ export default {
setParams,
uri,
completeTransition,
openDrawer,
closeDrawer,
toggleDrawer,
};

View File

@ -85,5 +85,9 @@ export default function(navigation) {
key: navigation.state.key,
})
),
openDrawer: () => navigation.dispatch(NavigationActions.openDrawer()),
closeDrawer: () => navigation.dispatch(NavigationActions.closeDrawer()),
toggleDrawer: () => navigation.dispatch(NavigationActions.toggleDrawer()),
};
}

View File

@ -4,7 +4,7 @@ import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
import TabRouter from '../routers/TabRouter';
import DrawerRouter from '../routers/DrawerRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
@ -38,9 +38,6 @@ const DefaultDrawerConfig = {
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
},
contentComponent: defaultContentComponent,
drawerOpenRoute: 'DrawerOpen',
drawerCloseRoute: 'DrawerClose',
drawerToggleRoute: 'DrawerToggle',
drawerPosition: 'left',
drawerBackgroundColor: 'white',
useNativeAnimations: true,
@ -48,6 +45,7 @@ const DefaultDrawerConfig = {
const DrawerNavigator = (routeConfigs, config = {}) => {
const mergedConfig = { ...DefaultDrawerConfig, ...config };
const {
order,
paths,
@ -55,29 +53,15 @@ const DrawerNavigator = (routeConfigs, config = {}) => {
backBehavior,
...drawerConfig
} = mergedConfig;
const tabsConfig = {
const routerConfig = {
order,
paths,
initialRouteName,
backBehavior,
};
const contentRouter = TabRouter(routeConfigs, tabsConfig);
const drawerRouter = TabRouter(
{
[drawerConfig.drawerCloseRoute]: {
screen: createNavigator(DrawerScreen, contentRouter, config),
},
[drawerConfig.drawerOpenRoute]: {
screen: () => null,
},
[drawerConfig.drawerToggleRoute]: {
screen: () => null,
},
},
{
initialRouteName: drawerConfig.drawerCloseRoute,
}
);
const drawerRouter = DrawerRouter(routeConfigs, routerConfig);
const navigator = createNavigator(DrawerView, drawerRouter, drawerConfig);

View File

@ -0,0 +1,55 @@
import invariant from '../utils/invariant';
import TabRouter from './TabRouter';
import NavigationActions from '../NavigationActions';
export default (routeConfigs, config = {}) => {
const tabRouter = TabRouter(routeConfigs, config);
return {
...tabRouter,
getStateForAction(action, lastState) {
const state = lastState || {
...tabRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
};
// Handle explicit drawer actions
if (
state.isDrawerOpen &&
action.type === NavigationActions.CLOSE_DRAWER
) {
return {
...state,
isDrawerOpen: false,
};
}
if (
!state.isDrawerOpen &&
action.type === NavigationActions.OPEN_DRAWER
) {
return {
...state,
isDrawerOpen: true,
};
}
if (action.type === NavigationActions.TOGGLE_DRAWER) {
return {
...state,
isDrawerOpen: !state.isDrawerOpen,
};
}
// Fall back on tab router for screen switching logic
const tabState = tabRouter.getStateForAction(action, state);
if (tabState !== null && tabState !== state) {
// If the tabs have changed, make sure to close the drawer
return {
...tabState,
isDrawerOpen: false,
};
}
return state;
},
};
};

View File

@ -5,7 +5,6 @@ import createConfigGetter from './createConfigGetter';
import getScreenForRouteName from './getScreenForRouteName';
import StateUtils from '../StateUtils';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
import invariant from '../utils/invariant';
import { generateKey } from './KeyGenerator';
@ -576,7 +575,5 @@ export default (routeConfigs, stackConfig = {}) => {
routeConfigs,
stackConfig.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@ -4,7 +4,6 @@ import createConfigGetter from './createConfigGetter';
import NavigationActions from '../NavigationActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
function childrenUpdateWithoutSwitchingIndex(actionType) {
return [
@ -323,7 +322,5 @@ export default (routeConfigs, config = {}) => {
routeConfigs,
config.navigationOptions
),
getScreenConfig: getScreenConfigDeprecated,
};
};

View File

@ -0,0 +1,72 @@
/* eslint react/display-name:0 */
import React from 'react';
import DrawerRouter from '../DrawerRouter';
import NavigationActions from '../../NavigationActions';
const INIT_ACTION = { type: NavigationActions.INIT };
describe('DrawerRouter', () => {
test('Handles basic tab logic', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
state
);
const expectedState2 = {
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
});
test('Drawer opens closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: NavigationActions.OPEN_DRAWER },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction(
{ type: NavigationActions.CLOSE_DRAWER },
state2
);
expect(state3.isDrawerOpen).toEqual(false);
const state4 = router.getStateForAction(
{ type: NavigationActions.TOGGLE_DRAWER },
state3
);
expect(state4.isDrawerOpen).toEqual(true);
});
});

View File

@ -1,7 +0,0 @@
import invariant from '../utils/invariant';
export default () =>
invariant(
false,
'`getScreenConfig` has been replaced with `getScreenOptions`'
);

View File

@ -26,45 +26,23 @@ export default class DrawerView extends React.PureComponent {
}
componentWillReceiveProps(nextProps) {
if (
this.props.navigation.state.index !== nextProps.navigation.state.index
) {
const {
drawerOpenRoute,
drawerCloseRoute,
drawerToggleRoute,
} = this.props.navigationConfig;
const { routes, index } = nextProps.navigation.state;
if (routes[index].routeName === drawerOpenRoute) {
this._drawer.openDrawer();
} else if (routes[index].routeName === drawerToggleRoute) {
if (this.props.navigation.state.index === 0) {
this.props.navigation.navigate(drawerOpenRoute);
} else {
this.props.navigation.navigate(drawerCloseRoute);
}
} else {
this._drawer.closeDrawer();
}
const { isDrawerOpen } = nextProps.navigation.state;
const wasDrawerOpen = this.props.navigation.state.isDrawerOpen;
if (isDrawerOpen && !wasDrawerOpen) {
this._drawer.openDrawer();
} else if (wasDrawerOpen && !isDrawerOpen) {
this._drawer.closeDrawer();
}
}
_handleDrawerOpen = () => {
const { navigation, navigationConfig } = this.props;
const { drawerOpenRoute } = navigationConfig;
const { routes, index } = navigation.state;
if (routes[index].routeName !== drawerOpenRoute) {
this.props.navigation.navigate(drawerOpenRoute);
}
const { navigation } = this.props;
navigation.dispatch({ type: 'DrawerOpenAction' });
};
_handleDrawerClose = () => {
const { navigation, navigationConfig } = this.props;
const { drawerCloseRoute } = navigationConfig;
const { routes, index } = navigation.state;
if (routes[index].routeName !== drawerCloseRoute) {
this.props.navigation.navigate(drawerCloseRoute);
}
const { navigation } = this.props;
navigation.dispatch({ type: 'DrawerCloseAction' });
};
_updateWidth = () => {
@ -78,52 +56,12 @@ export default class DrawerView extends React.PureComponent {
}
};
_getNavigationState = navigation => {
const { drawerCloseRoute } = this.props.navigationConfig;
const navigationState = navigation.state.routes.find(
route => route.routeName === drawerCloseRoute
);
return navigationState;
};
_renderNavigationView = () => {
const details = Object.values(this.props.descriptors).find(
d => d.state.routeName === this.props.navigationConfig.drawerCloseRoute
);
const router = details.getComponent().router;
const { state, addListener, dispatch } = this.props.navigation;
const { routes } = details.state;
const tabDescriptors = {};
routes.forEach(route => {
const getComponent = () =>
router.getComponentForRouteName(route.routeName);
const childNavigation = addNavigationHelpers({
dispatch,
state: route,
addListener: getChildEventSubscriber(addListener, route.key),
});
const options = router.getScreenOptions(
childNavigation,
this.props.screenProps
);
tabDescriptors[route.key] = {
key: route.key,
getComponent,
options,
state: route,
navigation: childNavigation,
};
});
return (
<DrawerSidebar
screenProps={this.props.screenProps}
navigation={details.navigation}
descriptors={tabDescriptors}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
contentComponent={this.props.navigationConfig.contentComponent}
contentOptions={this.props.navigationConfig.contentOptions}
drawerPosition={this.props.navigationConfig.drawerPosition}
@ -134,9 +72,9 @@ export default class DrawerView extends React.PureComponent {
};
render() {
const descriptor = Object.values(this.props.descriptors).find(
d => d.state.routeName === this.props.navigationConfig.drawerCloseRoute
);
const { state } = this.props.navigation;
const activeKey = state.routes[state.index].key;
const descriptor = this.props.descriptors[activeKey];
const DrawerScreen = descriptor.getComponent();

View File

@ -14,7 +14,6 @@ import {
import Card from './StackViewCard';
import Header from '../Header/Header';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import SceneView from '../SceneView';
import TransitionConfigs from './StackViewTransitionConfigs';