Compare commits

...

18 Commits

Author SHA1 Message Date
Brent Vatne
8bd25cf372 Bump to 1.0.0 2018-02-06 17:51:55 -08:00
Brent Vatne
20af8c688e Prevent push from bubbling up (#3454) 2018-02-06 17:49:52 -08:00
Brent Vatne
b0dccd7e88 Prevent pop and popToTop from bubbling up to parent stack (#3453) 2018-02-06 17:35:32 -08:00
Brent Vatne
c69a22f10e Bump version 2018-02-06 15:59:44 -08:00
Eric Vicenti
43a1c5ddbd Fix issue with StackRouter popToTop (#3451)
Previously the state was getting squashed, in this case it would destroy the routeName of the state, which was a route for the parent navigator, who could no longer render properly.
2018-02-06 15:56:39 -08:00
Brent Vatne
e97d41cccf Bump version and update description 2018-02-06 14:53:08 -08:00
Brent Vatne
3e4ddc685a Remove Header.HEIGHT deprecation warning, no good alternative solution available yet 2018-02-06 14:48:44 -08:00
Brent Vatne
f62c728593 Bump version 2018-02-06 14:13:41 -08:00
Eric Vicenti
616f9a56f2 Fix StackRouter Replace Key Behavior (#3450)
Replace should actually provide new keys on the replaced route, use ‘newKey’ on the action if you want to define the new route key
2018-02-06 14:12:59 -08:00
Brent Vatne
3b93faa0fc Bump version 2018-02-06 13:01:46 -08:00
Eric Vicenti
333b2e4b68 StackNavigator Replace Action (#3440)
* Navigation replace action

The long awaited action to replace the a route in StackNavigator

* Fix flow maybe
2018-02-06 12:59:16 -08:00
Brent Vatne
7f50173cf1 Bump version 2018-02-06 12:49:43 -08:00
Brent Vatne
8432f67529 Add adaptive to TabBarBottom flow interface 2018-02-06 12:49:37 -08:00
Eric Vicenti
c72b44ce10 Fix focus event for initial screen (#3448) 2018-02-06 12:49:23 -08:00
Peter Steffey
149f5f5c66 Fix #2795 - Not reducing header height in landscape on iPad (#3251)
* Adding tablet check for small header

* Switching to Platform.isPad

* Moving fully to Platform.isPad, no isHeightConstrained
2018-02-06 10:40:03 -08:00
Peter Steffey
85f77fe903 Issue #2794 - Tab bar fix - Continued from #3041 (#3267)
* Fixing iPad iOS 11 Tab Bar Bottom Behavior with changing widths via Multitasking

* Adding width constrained check

* Moving to only Platform.isPad (no more layout-based tests)

* Remove type import
2018-02-06 10:39:41 -08:00
Jay Phelps
d4b759606d remove $FlowFixMe comments left over, as flow is no longer used (#3439) 2018-02-06 14:59:38 +01:00
Brent Vatne
4d2609f4a8 Use a stack inside of drawer 2018-02-05 14:48:30 -08:00
14 changed files with 513 additions and 170 deletions

View File

@@ -4,7 +4,7 @@
import React from 'react';
import { Button, Platform, ScrollView, StatusBar } from 'react-native';
import { DrawerNavigator, SafeAreaView } from 'react-navigation';
import { StackNavigator, DrawerNavigator, SafeAreaView } from 'react-navigation';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import SampleText from './SampleText';
@@ -16,6 +16,10 @@ const MyNavScreen = ({ navigation, banner }) => (
onPress={() => navigation.navigate('DrawerOpen')}
title="Open drawer"
/>
<Button
onPress={() => navigation.navigate('Email')}
title="Open other screen"
/>
<Button onPress={() => navigation.goBack(null)} title="Go back" />
</SafeAreaView>
<StatusBar barStyle="default" />
@@ -36,6 +40,10 @@ InboxScreen.navigationOptions = {
),
};
const EmailScreen = ({ navigation }) => (
<MyNavScreen banner={'Email Screen'} navigation={navigation} />
);
const DraftsScreen = ({ navigation }) => (
<MyNavScreen banner={'Drafts Screen'} navigation={navigation} />
);
@@ -46,15 +54,25 @@ DraftsScreen.navigationOptions = {
),
};
const InboxStack = StackNavigator({
Inbox: { screen: InboxScreen },
Email: { screen: EmailScreen },
});
const DraftsStack = StackNavigator({
Drafts: { screen: DraftsScreen },
Email: { screen: EmailScreen },
});
const DrawerExample = DrawerNavigator(
{
Inbox: {
path: '/',
screen: InboxScreen,
screen: InboxStack,
},
Drafts: {
path: '/sent',
screen: DraftsScreen,
screen: DraftsStack,
},
},
{

View File

@@ -24,22 +24,19 @@ class MyNavScreen extends React.Component<MyNavScreenProps> {
<SafeAreaView>
<SampleText>{banner}</SampleText>
<Button
onPress={() => navigation.navigate('Profile', { name: 'Jane' })}
title="Go to a profile screen"
onPress={() => navigation.push('Profile', { name: 'Jane' })}
title="Push a profile screen"
/>
<Button
onPress={() => navigation.navigate('Photos', { name: 'Jane' })}
title="Go to a photos screen"
title="Navigate to a photos screen"
/>
<Button
onPress={() =>
navigation.navigate('Profile', {
name: 'Dog',
headerBackImage: require('./assets/dog-back.png'),
})
}
title="Custom back button"
onPress={() => navigation.replace('Profile', { name: 'Lucy' })}
title="Replace with profile"
/>
<Button onPress={() => navigation.popToTop()} title="Pop to top" />
<Button onPress={() => navigation.pop()} title="Pop" />
<Button onPress={() => navigation.goBack(null)} title="Go back" />
<StatusBar barStyle="default" />
</SafeAreaView>

View File

@@ -1,7 +1,6 @@
// @flow
declare module 'react-navigation' {
/**
* First, a bunch of things we would love to import but instead must
* reconstruct (mostly copy-pasted).
@@ -68,6 +67,8 @@ declare module 'react-navigation' {
// The action to run inside the sub-router
action?: NavigationNavigateAction,
key?: string,
|};
declare type DeprecatedNavigationNavigateAction = {|
@@ -130,8 +131,9 @@ declare module 'react-navigation' {
type: 'Reset',
index: number,
key?: ?string,
actions:
Array<NavigationNavigateAction | DeprecatedNavigationNavigateAction>,
actions: Array<
NavigationNavigateAction | DeprecatedNavigationNavigateAction
>,
|};
declare export type NavigationUriAction = {|
@@ -144,9 +146,37 @@ declare module 'react-navigation' {
uri: string,
|};
declare export type NavigationReplaceAction = {|
+type: 'Navigation/REPLACE',
+key: string,
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
|};
declare export type NavigationPopAction = {|
+type: 'Navigation/POP',
+n?: number,
+immediate?: boolean,
|};
declare export type NavigationPopToTopAction = {|
+type: 'Navigation/POP_TO_TOP',
+immediate?: boolean,
|};
declare export type NavigationPushAction = {|
+type: 'Navigation/PUSH',
+routeName: string,
+params?: NavigationParams,
+action?: NavigationNavigateAction,
+key?: string,
|};
declare export type NavigationAction =
| NavigationInitAction
| NavigationNavigateAction
| NavigationReplaceAction
| NavigationPopAction
| NavigationPopToTopAction
| NavigationPushAction
| NavigationBackAction
| NavigationSetParamsAction
| NavigationResetAction;
@@ -209,9 +239,8 @@ declare module 'react-navigation' {
params?: NavigationParams,
};
declare export type NavigationStateRoute =
& NavigationLeafRoute
& NavigationState;
declare export type NavigationStateRoute = NavigationLeafRoute &
NavigationState;
/**
* Router
@@ -291,12 +320,8 @@ declare module 'react-navigation' {
Route: NavigationRoute,
Options: {},
Props: {}
> =
& React$ComponentType<NavigationNavigatorProps<Options, Route> & Props>
& (
| {}
| { navigationOptions: NavigationScreenConfig<Options> }
);
> = React$ComponentType<NavigationNavigatorProps<Options, Route> & Props> &
({} | { navigationOptions: NavigationScreenConfig<Options> });
declare export type NavigationNavigator<
State: NavigationState,
@@ -334,16 +359,18 @@ declare module 'react-navigation' {
declare export type HeaderMode = 'float' | 'screen' | 'none';
declare export type HeaderProps = $Shape<NavigationSceneRendererProps & {
mode: HeaderMode,
router: NavigationRouter<NavigationState, NavigationStackScreenOptions>,
getScreenDetails: NavigationScene => NavigationScreenDetails<
NavigationStackScreenOptions
>,
leftInterpolator: (props: NavigationSceneRendererProps) => {},
titleInterpolator: (props: NavigationSceneRendererProps) => {},
rightInterpolator: (props: NavigationSceneRendererProps) => {},
}>;
declare export type HeaderProps = $Shape<
NavigationSceneRendererProps & {
mode: HeaderMode,
router: NavigationRouter<NavigationState, NavigationStackScreenOptions>,
getScreenDetails: NavigationScene => NavigationScreenDetails<
NavigationStackScreenOptions
>,
leftInterpolator: (props: NavigationSceneRendererProps) => {},
titleInterpolator: (props: NavigationSceneRendererProps) => {},
rightInterpolator: (props: NavigationSceneRendererProps) => {},
}
>;
/**
* Stack Navigator
@@ -493,6 +520,18 @@ declare module 'react-navigation' {
eventName: string,
callback: NavigationEventCallback
) => NavigationEventSubscription,
push: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
replace: (
routeName: string,
params?: NavigationParams,
action?: NavigationNavigateAction
) => boolean,
pop: (n?: number, params?: { immediate?: boolean }) => boolean,
popToTop: (params?: { immediate?: boolean }) => boolean,
};
declare export type NavigationNavigatorProps<O: {}, S: {}> = $Shape<{
@@ -767,7 +806,7 @@ declare module 'react-navigation' {
>(
router: NavigationRouter<S, O>,
routeConfigs?: NavigationRouteConfigMap,
navigatorConfig?: NavigatorConfig,
navigatorConfig?: NavigatorConfig
): _NavigatorCreator<NavigationViewProps, S, O>;
declare export function StackNavigator(
@@ -1026,6 +1065,7 @@ declare module 'react-navigation' {
declare type _TabBarBottomProps = {
activeTintColor: string,
activeBackgroundColor: string,
adaptive?: boolean,
inactiveTintColor: string,
inactiveBackgroundColor: string,
showLabel: boolean,

View File

@@ -1,7 +1,7 @@
{
"name": "react-navigation",
"version": "1.0.0-beta.30",
"description": "React Navigation",
"version": "1.0.0",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
"url": "git@github.com:react-navigation/react-navigation.git",

View File

@@ -5,6 +5,7 @@ const POP = 'Navigation/POP';
const POP_TO_TOP = 'Navigation/POP_TO_TOP';
const PUSH = 'Navigation/PUSH';
const RESET = 'Navigation/RESET';
const REPLACE = 'Navigation/REPLACE';
const SET_PARAMS = 'Navigation/SET_PARAMS';
const URI = 'Navigation/URI';
const COMPLETE_TRANSITION = 'Navigation/COMPLETE_TRANSITION';
@@ -79,6 +80,16 @@ const reset = createAction(RESET, payload => ({
actions: payload.actions,
}));
const replace = createAction(REPLACE, payload => ({
type: REPLACE,
key: payload.key,
newKey: payload.newKey,
params: payload.params,
action: payload.action,
routeName: payload.routeName,
immediate: payload.immediate,
}));
const setParams = createAction(SET_PARAMS, payload => ({
type: SET_PARAMS,
key: payload.key,
@@ -157,6 +168,7 @@ export default {
POP_TO_TOP,
PUSH,
RESET,
REPLACE,
SET_PARAMS,
URI,
COMPLETE_TRANSITION,
@@ -169,6 +181,7 @@ export default {
popToTop,
push,
reset,
replace,
setParams,
uri,
completeTransition,

View File

@@ -65,5 +65,15 @@ export default function(navigation) {
navigation.dispatch(
NavigationActions.push({ routeName, params, action })
),
replace: (routeName, params, action) =>
navigation.dispatch(
NavigationActions.replace({
routeName,
params,
action,
key: navigation.state.key,
})
),
};
}

View File

@@ -24,9 +24,10 @@ export default function createNavigationContainer(Component) {
this._validateProps(props);
this._initialAction = NavigationActions.init();
this.state = {
nav: this._isStateful()
? Component.router.getStateForAction(NavigationActions.init())
? Component.router.getStateForAction(this._initialAction)
: null,
};
}
@@ -134,6 +135,15 @@ export default function createNavigationContainer(Component) {
Linking.addEventListener('url', this._handleOpenURL);
Linking.getInitialURL().then(url => url && this._handleOpenURL({ url }));
this._actionEventSubscribers.forEach(subscriber =>
subscriber({
type: 'action',
action: this._initialAction,
state: this.state.nav,
lastState: null,
})
);
}
componentWillUnmount() {
@@ -154,7 +164,6 @@ export default function createNavigationContainer(Component) {
const nav = Component.router.getStateForAction(action, oldNav);
const dispatchActionEvents = () => {
this._actionEventSubscribers.forEach(subscriber =>
// $FlowFixMe - Payload should probably understand generic state type
subscriber({
type: 'action',
action,

View File

@@ -100,6 +100,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,
@@ -337,6 +338,7 @@ exports[`StackNavigator renders successfully 1`] = `
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"index": 0,

View File

@@ -105,68 +105,81 @@ exports[`TabNavigator renders successfully 1`] = `
"alignItems": "center",
"backgroundColor": "rgba(0, 0, 0, 0)",
"flex": 1,
"justifyContent": "flex-end",
}
}
testID={undefined}
>
<View
style={
Object {
"flexGrow": 1,
}
Array [
Object {
"alignItems": "center",
"flex": 1,
},
Object {
"flexDirection": "column",
"justifyContent": "flex-end",
},
undefined,
]
}
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"flexGrow": 1,
}
}
/>
<View
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"textAlign": "center",
}
}
/>
>
Welcome anonymous
</Text>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"marginLeft": 0,
"marginTop": 0,
"textAlign": "center",
}
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>

View File

@@ -81,7 +81,6 @@ export default (routeConfigs, stackConfig = {}) => {
});
paths = Object.entries(pathsByRouteNames);
/* $FlowFixMe */
paths.sort((a: [string, *], b: [string, *]) => b[1].priority - a[1].priority);
return {
@@ -172,10 +171,15 @@ export default (routeConfigs, stackConfig = {}) => {
}
}
//Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
// Handle pop-to-top behavior. Make sure this happens after children have had a chance to handle the action, so that the inner stack pops to top first.
if (action.type === NavigationActions.POP_TO_TOP) {
if (state.index !== 0) {
if (state.index === 0) {
return {
...state,
};
} else {
return {
...state,
isTransitioning: action.immediate !== true,
index: 0,
routes: [state.routes[0]],
@@ -184,6 +188,31 @@ export default (routeConfigs, stackConfig = {}) => {
return state;
}
// Handle replace action
if (action.type === NavigationActions.REPLACE) {
const routeIndex = state.routes.findIndex(r => r.key === action.key);
// Only replace if the key matches one of our routes
if (routeIndex !== -1) {
const childRouter = childRouters[action.routeName];
let childState = {};
if (childRouter) {
const childAction =
action.action ||
NavigationActions.init({ params: action.params });
childState = childRouter.getStateForAction(childAction);
}
const routes = [...state.routes];
routes[routeIndex] = {
params: action.params,
// merge the child state in this order to allow params override
...childState,
key: action.newKey || generateKey(),
routeName: action.routeName,
};
return { ...state, routes };
}
}
// Handle explicit push navigation action. Make sure this happens after children have had a chance to handle the action
if (
behavesLikePushAction(action) &&
@@ -236,6 +265,7 @@ export default (routeConfigs, stackConfig = {}) => {
action.action || NavigationActions.init({ params: action.params });
route = {
params: action.params,
// merge the child state in this order to allow params override
...childRouter.getStateForAction(childAction),
key,
routeName: action.routeName,
@@ -251,6 +281,13 @@ export default (routeConfigs, stackConfig = {}) => {
...StateUtils.push(state, route),
isTransitioning: action.immediate !== true,
};
} else if (
action.type === NavigationActions.PUSH &&
childRouters[action.routeName] === undefined
) {
return {
...state,
};
}
if (
@@ -365,6 +402,7 @@ export default (routeConfigs, stackConfig = {}) => {
const backRoute = state.routes.find(route => route.key === key);
backRouteIndex = state.routes.indexOf(backRoute);
}
if (backRouteIndex > 0) {
return {
...state,
@@ -372,6 +410,13 @@ export default (routeConfigs, stackConfig = {}) => {
index: backRouteIndex - 1,
isTransitioning: immediate !== true,
};
} else if (
backRouteIndex === 0 &&
action.type === NavigationActions.POP
) {
return {
...state,
};
}
}
return state;

View File

@@ -366,6 +366,97 @@ describe('StackRouter', () => {
expect(pushedState.routes[1].routes[1].routeName).toEqual('qux');
});
test('pop does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
key: 'StackRouterRoot',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.POP,
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('push does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
Bad: { screen: () => <div /> },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.PUSH,
routeName: 'Bad',
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes.length).toEqual(2);
});
test('popToTop does not bubble up', () => {
const ChildNavigator = () => <div />;
ChildNavigator.router = StackRouter({
Baz: { screen: () => <div /> },
Qux: { screen: () => <div /> },
});
const router = StackRouter({
Foo: { screen: () => <div /> },
Bar: { screen: ChildNavigator },
});
const state = router.getStateForAction({ type: NavigationActions.INIT });
const state2 = router.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state
);
const barKey = state2.routes[1].routes[0].key;
const state3 = router.getStateForAction(
{
type: NavigationActions.POP_TO_TOP,
},
state2
);
expect(state3 && state3.index).toEqual(1);
expect(state3 && state3.routes[1].index).toEqual(0);
});
test('popToTop works as expected', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
@@ -515,6 +606,41 @@ describe('StackRouter', () => {
});
});
test('Replace action works', () => {
const TestRouter = StackRouter({
foo: { screen: () => <div /> },
bar: { screen: () => <div /> },
});
const initState = TestRouter.getStateForAction(
NavigationActions.navigate({ routeName: 'foo' })
);
const replacedState = TestRouter.getStateForAction(
NavigationActions.replace({
routeName: 'bar',
params: { meaning: 42 },
key: initState.routes[0].key,
}),
initState
);
expect(replacedState.index).toEqual(0);
expect(replacedState.routes.length).toEqual(1);
expect(replacedState.routes[0].key).not.toEqual(initState.routes[0].key);
expect(replacedState.routes[0].routeName).toEqual('bar');
expect(replacedState.routes[0].params.meaning).toEqual(42);
const replacedState2 = TestRouter.getStateForAction(
NavigationActions.replace({
routeName: 'bar',
key: initState.routes[0].key,
newKey: 'wow',
}),
initState
);
expect(replacedState2.index).toEqual(0);
expect(replacedState2.routes.length).toEqual(1);
expect(replacedState2.routes[0].key).toEqual('wow');
expect(replacedState2.routes[0].routeName).toEqual('bar');
});
test('Handles push transition logic with completion action', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
@@ -1161,9 +1287,7 @@ describe('StackRouter', () => {
};
const { path, params } = router.getPathAndParamsForState(state);
expect(path).toEqual('baz/321');
/* $FlowFixMe: params.id has to exist */
expect(params.id).toEqual('123');
/* $FlowFixMe: params.bazId has to exist */
expect(params.bazId).toEqual('321');
}
@@ -1323,9 +1447,7 @@ test('Handles deep navigate completion action', () => {
);
expect(state2 && state2.index).toEqual(0);
expect(state2 && state2.isTransitioning).toEqual(false);
/* $FlowFixMe */
expect(state2 && state2.routes[0].index).toEqual(1);
/* $FlowFixMe */
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
expect(!!key).toEqual(true);
const state3 = router.getStateForAction(
@@ -1336,8 +1458,6 @@ test('Handles deep navigate completion action', () => {
);
expect(state3 && state3.index).toEqual(0);
expect(state3 && state3.isTransitioning).toEqual(false);
/* $FlowFixMe */
expect(state3 && state3.routes[0].index).toEqual(1);
/* $FlowFixMe */
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
});

View File

@@ -27,9 +27,6 @@ class Header extends React.PureComponent {
};
static get HEIGHT() {
console.warn(
'Header.HEIGHT is deprecated and will be removed before react-navigation comes out of beta.'
);
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
}
@@ -292,7 +289,8 @@ class Header extends React.PureComponent {
const { options } = this.props.getScreenDetails(scene);
const { headerStyle } = options;
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
const appBarHeight =
Platform.OS === 'ios' ? (isLandscape && !Platform.isPad ? 32 : 44) : 56;
const containerStyles = [
styles.container,
{

View File

@@ -13,7 +13,8 @@ import withOrientation from '../withOrientation';
const majorVersion = parseInt(Platform.Version, 10);
const isIos = Platform.OS === 'ios';
const useHorizontalTabs = majorVersion >= 11 && isIos;
const isIOS11 = majorVersion >= 11 && isIos;
const defaultMaxTabBarItemWidth = 125;
class TabBarBottom extends React.PureComponent {
// See https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/UITabBar.html
@@ -25,6 +26,7 @@ class TabBarBottom extends React.PureComponent {
showLabel: true,
showIcon: true,
allowFontScaling: true,
adaptive: isIOS11,
};
_renderLabel = scene => {
@@ -56,19 +58,18 @@ class TabBarBottom extends React.PureComponent {
const tintColor = scene.focused ? activeTintColor : inactiveTintColor;
const label = this.props.getLabel({ ...scene, tintColor });
let marginLeft = 0;
if (isLandscape && showIcon && useHorizontalTabs) {
marginLeft = LABEL_LEFT_MARGIN;
}
let marginTop = 0;
if (!isLandscape && showIcon && useHorizontalTabs) {
marginTop = LABEL_TOP_MARGIN;
}
if (typeof label === 'string') {
return (
<Animated.Text
style={[styles.label, { color, marginLeft, marginTop }, labelStyle]}
style={[
styles.label,
{ color },
showIcon && this._shouldUseHorizontalTabs()
? styles.labelBeside
: styles.labelBeneath,
labelStyle,
]}
allowFontScaling={allowFontScaling}
>
{label}
@@ -104,7 +105,7 @@ class TabBarBottom extends React.PureComponent {
inactiveTintColor={inactiveTintColor}
renderIcon={renderIcon}
scene={scene}
style={showLabel && useHorizontalTabs ? {} : styles.icon}
style={showLabel && this._shouldUseHorizontalTabs() ? {} : styles.icon}
/>
);
};
@@ -115,6 +116,64 @@ class TabBarBottom extends React.PureComponent {
return testIDProps;
};
_tabItemMaxWidth() {
const { tabStyle, layout } = this.props;
let maxTabBarItemWidth;
const flattenedTabStyle = StyleSheet.flatten(tabStyle);
if (flattenedTabStyle) {
if (typeof flattenedTabStyle.width === 'number') {
maxTabBarItemWidth = flattenedTabStyle.width;
} else if (
typeof flattenedTabStyle.width === 'string' &&
flattenedTabStyle.endsWith('%')
) {
const width = parseFloat(flattenedTabStyle.width);
if (Number.isFinite(width)) {
maxTabBarItemWidth = layout.width * (width / 100);
}
} else if (typeof flattenedTabStyle.maxWidth === 'number') {
maxTabBarItemWidth = flattenedTabStyle.maxWidth;
} else if (
typeof flattenedTabStyle.maxWidth === 'string' &&
flattenedTabStyle.endsWith('%')
) {
const width = parseFloat(flattenedTabStyle.maxWidth);
if (Number.isFinite(width)) {
maxTabBarItemWidth = layout.width * (width / 100);
}
}
}
if (!maxTabBarItemWidth) {
maxTabBarItemWidth = defaultMaxTabBarItemWidth;
}
return maxTabBarItemWidth;
}
_shouldUseHorizontalTabs() {
const { routes } = this.props.navigation.state;
const { isLandscape, layout, adaptive, tabStyle } = this.props;
if (!adaptive) {
return false;
}
let tabBarWidth = layout.width;
if (tabBarWidth === 0) {
return Platform.isPad;
}
if (!Platform.isPad) {
return isLandscape;
} else {
const maxTabBarItemWidth = this._tabItemMaxWidth();
return routes.length * maxTabBarItemWidth <= tabBarWidth;
}
}
render() {
const {
position,
@@ -136,9 +195,9 @@ class TabBarBottom extends React.PureComponent {
const tabBarStyle = [
styles.tabBar,
isLandscape && useHorizontalTabs
? styles.tabBarLandscape
: styles.tabBarPortrait,
this._shouldUseHorizontalTabs() && !Platform.isPad
? styles.tabBarCompact
: styles.tabBarRegular,
style,
];
@@ -178,17 +237,19 @@ class TabBarBottom extends React.PureComponent {
: jumpToIndex(index)
}
>
<Animated.View
style={[
styles.tab,
isLandscape && useHorizontalTabs && styles.tabLandscape,
!isLandscape && useHorizontalTabs && styles.tabPortrait,
{ backgroundColor },
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
<Animated.View style={[styles.tab, { backgroundColor }]}>
<View
style={[
styles.tab,
this._shouldUseHorizontalTabs()
? styles.tabLandscape
: styles.tabPortrait,
tabStyle,
]}
>
{this._renderIcon(scene)}
{this._renderLabel(scene)}
</View>
</Animated.View>
</TouchableWithoutFeedback>
);
@@ -199,8 +260,6 @@ class TabBarBottom extends React.PureComponent {
}
}
const LABEL_LEFT_MARGIN = 20;
const LABEL_TOP_MARGIN = 15;
const styles = StyleSheet.create({
tabBar: {
backgroundColor: '#F7F7F7', // Default background color in iOS 10
@@ -208,16 +267,15 @@ const styles = StyleSheet.create({
borderTopColor: 'rgba(0, 0, 0, .3)',
flexDirection: 'row',
},
tabBarLandscape: {
tabBarCompact: {
height: 29,
},
tabBarPortrait: {
tabBarRegular: {
height: 49,
},
tab: {
flex: 1,
alignItems: isIos ? 'center' : 'stretch',
justifyContent: 'flex-end',
},
tabPortrait: {
justifyContent: 'flex-end',
@@ -232,9 +290,15 @@ const styles = StyleSheet.create({
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',
},
labelBeneath: {
fontSize: 10,
marginBottom: 1.5,
backgroundColor: 'transparent',
},
labelBeside: {
fontSize: 13,
marginLeft: 20,
},
});

View File

@@ -61,68 +61,81 @@ exports[`TabBarBottom renders successfully 1`] = `
"alignItems": "center",
"backgroundColor": "rgba(0, 0, 0, 0)",
"flex": 1,
"justifyContent": "flex-end",
}
}
testID={undefined}
>
<View
style={
Object {
"flexGrow": 1,
}
Array [
Object {
"alignItems": "center",
"flex": 1,
},
Object {
"flexDirection": "column",
"justifyContent": "flex-end",
},
undefined,
]
}
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
"flexGrow": 1,
}
}
/>
<View
>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 1,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
<View
collapsable={undefined}
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
}
}
/>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"alignItems": "center",
"bottom": 0,
"justifyContent": "center",
"left": 0,
"opacity": 0,
"position": "absolute",
"right": 0,
"top": 0,
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"textAlign": "center",
}
}
/>
>
s1
</Text>
</View>
<Text
accessible={true}
allowFontScaling={true}
collapsable={undefined}
ellipsizeMode="tail"
style={
Object {
"backgroundColor": "transparent",
"color": "rgba(52, 120, 246, 1)",
"fontSize": 10,
"marginBottom": 1.5,
"marginLeft": 0,
"marginTop": 0,
"textAlign": "center",
}
}
>
s1
</Text>
</View>
</View>
</View>
@@ -227,6 +240,7 @@ exports[`TabBarBottom renders successfully 1`] = `
"pop": [Function],
"popToTop": [Function],
"push": [Function],
"replace": [Function],
"setParams": [Function],
"state": Object {
"key": "s1",