Compare commits

..

40 Commits

Author SHA1 Message Date
Satyajit Sahoo
1e813dfb5b chore: publish
- @react-navigation/bottom-tabs@5.7.3
 - @react-navigation/compat@5.2.4
 - @react-navigation/core@5.12.2
 - @react-navigation/devtools@5.1.4
 - @react-navigation/drawer@5.8.7
 - @react-navigation/material-bottom-tabs@5.2.15
 - @react-navigation/material-top-tabs@5.2.15
 - @react-navigation/native@5.7.2
 - @react-navigation/routers@5.4.10
 - @react-navigation/stack@5.8.0
2020-07-28 14:08:02 +02:00
Simon Arm-Riding
ce4eb7e927 fix: add accessibilityState property (#8548)
React Native 0.62 removes the deprecated `accessibilityStates` property and replaces it with an `accessibilityState` object instead. This PR adds the new property where needed, but leaves the old one in place for backwards compatibility. Without the change, the selected tab in BottomTabNavigator isn't announced properly in VoiceOver.
2020-07-28 11:28:42 +02:00
Madd.is
baea77e332 fix: pass label position flag to label rendering in BottomTabBar (#8557)
In the process of upgrading from v4, I noticed a regression. 
In the past, the function form of `tabBarLabel` did get an `orientation: 'landscape' | 'portrait'`, this is no longer the case.
However, when using a custom Text rendering, we need to apply a margin to the text in horizontal mode.

Since the orientation/horizontal state is decided based on internal heuristics, It is a huge pain with a high bug potential when reimplementing that detection myself.
2020-07-28 11:27:30 +02:00
Martin Treurnicht
15f9b9573e feat: emit gesture navigation events from stack view (#8524)
Allows you to subscribe to gesture navigation events, we have a custom keyboard that we want to hide and show when gesture is being used to navigate (same as native keyboard)
2020-07-28 11:26:45 +02:00
Satyajit Sahoo
b70e3fe618 fix: make sure history is correct after rehydration 2020-07-28 11:20:17 +02:00
Tomasz Krzyżowski
1aa8219021 fix: make sure index is correct when rehydrating state for tabs (#8638)
If the state being rehydrated contained multiple `routes` and had an `index`, we incorrectly kept the same index even if the index of the focused route changed in the state after rehydration. This commit ensures that we also adjust the index appropriately according to the new index of the focused route.
2020-07-28 11:13:01 +02:00
Andrei Barabas
486c3defd2 feat: allow style overrides for HeaderBackButton (#8626) 2020-07-28 11:09:23 +02:00
Satyajit Sahoo
0d6a43f663 chore: publish
- @react-navigation/compat@5.2.3
2020-07-22 03:07:54 +02:00
Satyajit Sahoo
5e358b3aad fix: fix false warning due to change in Object.assign in metro preset
#8584
2020-07-22 03:06:55 +02:00
Satyajit Sahoo
7c2b28ae1e chore: publish
- @react-navigation/bottom-tabs@5.7.2
 - @react-navigation/compat@5.2.1
 - @react-navigation/core@5.12.1
 - @react-navigation/devtools@5.1.3
 - @react-navigation/drawer@5.8.6
 - @react-navigation/material-bottom-tabs@5.2.14
 - @react-navigation/material-top-tabs@5.2.14
 - @react-navigation/native@5.7.1
 - @react-navigation/stack@5.7.1
2020-07-19 14:55:27 +02:00
Satyajit Sahoo
af8b27414c fix: make sure new state events are emitted when new navigators mount 2020-07-19 14:52:43 +02:00
Satyajit Sahoo
b2a99c2a88 chore: publish
- @react-navigation/bottom-tabs@5.7.1
2020-07-14 14:05:18 +02:00
Satyajit Sahoo
2f74541811 fix: don't render badge on bottom tabs if not visible. closes #8577 2020-07-14 14:03:16 +02:00
Satyajit Sahoo
cf09f00472 chore: don't repeat comments for expo preview 2020-07-14 13:44:17 +02:00
Satyajit Sahoo
513482425a chore: publish
- @react-navigation/bottom-tabs@5.7.0
 - @react-navigation/compat@5.2.0
 - @react-navigation/core@5.12.0
 - @react-navigation/devtools@5.1.2
 - @react-navigation/drawer@5.8.5
 - @react-navigation/material-bottom-tabs@5.2.13
 - @react-navigation/material-top-tabs@5.2.13
 - @react-navigation/native@5.7.0
 - @react-navigation/routers@5.4.9
 - @react-navigation/stack@5.7.0
2020-07-10 22:40:45 +02:00
Satyajit Sahoo
f4180295bf feat: add a getComponent prop to lazily specify components 2020-07-10 22:33:13 +02:00
Satyajit Sahoo
c665c027a6 fix: tweak border color to match iOS default 2020-07-10 21:54:29 +02:00
Satyajit Sahoo
849e04ab6a fix: fix bottom tab bar to match iOS defaults 2020-07-10 21:54:29 +02:00
Satyajit Sahoo
374b081b1c fix: only remove non-existed routes from tab history. closes #8567 2020-07-10 21:54:29 +02:00
Satyajit Sahoo
96c7b688ce feat: add support for badges to bottom tab bar 2020-07-10 21:54:29 +02:00
Satyajit Sahoo
e63580edbe fix: improve the warning message for non-serializable values 2020-07-10 15:32:18 +02:00
Satyajit Sahoo
eea9860323 refactor: change format to formatter for documentTitle option 2020-07-10 13:07:47 +02:00
Satyajit Sahoo
13c9d1e281 feat: add a hook to update document title 2020-07-10 13:00:45 +02:00
osdnk
8f5286ef50 fix: ensure correct document title after going back on Chrome 2020-07-10 11:45:03 +02:00
Satyajit Sahoo
a255e350f9 fix: fix options event being emitted incorrectly (#8559) 2020-07-09 15:47:27 +02:00
Satyajit Sahoo
7a74bdb24e test: add test for merging params on navigation 2020-07-09 11:48:48 +02:00
Satyajit Sahoo
7c3a0a0f23 fix: mark some types as read-only 2020-07-09 11:07:14 +02:00
Satyajit Sahoo
bddb1f0046 chore: fix uploading test coverage to codecov 2020-07-08 12:49:03 +02:00
Satyajit Sahoo
c1521e81e8 chore: fix the lint script to be windows compatible 2020-07-02 16:53:35 +02:00
Satyajit Sahoo
bce6c4fc3b chore: tweak types in the example 2020-07-02 16:52:45 +02:00
Satyajit Sahoo
6925e92dc3 feat: add a beforeRemove event
A lot of times, we want to prompt before leaving a screen if we have unsaved changes. Currently, we need to handle multiple cases to prevent this:

- Disable swipe gestures
- Override the back button in header
- Override the hardware back button on Android

This PR adds a new event which is emitted before a screen gets removed, and the developer has a chance to ask the user before closing the screen.

Example:

```js
React.useEffect(
  () =>
    navigation.addListener('beforeRemove', (e) => {
      if (!hasUnsavedChanges) {
        return;
      }

      e.preventDefault();

      Alert.alert(
        'Discard changes?',
        'You have unsaved changes. Are you sure to discard them and leave the screen?',
        [
          { text: "Don't leave", style: 'cancel', onPress: () => {} },
          {
            text: 'Discard',
            style: 'destructive',
            onPress: () => navigation.dispatch(e.data.action),
          },
        ]
      );
    }),
  [navigation, hasUnsavedChanges]
);
```
2020-07-02 14:32:31 +02:00
Satyajit Sahoo
1801a13323 fix: avoid error setting warning for devtools migration. closes #8534 2020-07-02 14:14:56 +02:00
Satyajit Sahoo
9671c76c51 fix: fix bubbling actions to correct target when specified 2020-07-01 21:37:38 +02:00
Satyajit Sahoo
ec840692ec refactor: make state getter hook more generic 2020-07-01 20:41:20 +02:00
Satyajit Sahoo
1cae93331d refactor: consolidate action and focus listeners 2020-07-01 20:10:38 +02:00
Satyajit Sahoo
4edc2a64e2 chore: limit number of jest workers on circle ci 2020-06-30 18:34:43 +02:00
Satyajit Sahoo
75c99b5a12 chore: add missing babel-loader 2020-06-30 18:23:04 +02:00
Satyajit Sahoo
9ba2f84d18 test: add basic unit tests for all navigators 2020-06-30 16:14:52 +02:00
Satyajit Sahoo
2477db47a0 chore: publish
- @react-navigation/bottom-tabs@5.6.1
 - @react-navigation/compat@5.1.28
 - @react-navigation/core@5.11.1
 - @react-navigation/devtools@5.1.1
 - @react-navigation/drawer@5.8.4
 - @react-navigation/material-bottom-tabs@5.2.12
 - @react-navigation/material-top-tabs@5.2.12
 - @react-navigation/native@5.6.1
 - @react-navigation/stack@5.6.2
2020-06-25 17:31:40 +02:00
Satyajit Sahoo
d1210a861b fix: fix error with type definitions. closes #8511 2020-06-25 17:27:48 +02:00
100 changed files with 3432 additions and 716 deletions

View File

@@ -52,10 +52,10 @@ jobs:
- attach_project
- run:
name: Run unit tests
command: yarn test --coverage
command: yarn test --maxWorkers=2 --coverage
- run:
name: Upload test coverage
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
command: yarn codecov
- store_artifacts:
path: coverage
destination: coverage

View File

@@ -1,7 +1,9 @@
{
"extends": "satya164",
"settings": {
"react": { "version": "16" },
"react": {
"version": "16"
},
"import/core-modules": [
"@react-navigation/core",
"@react-navigation/native",
@@ -15,5 +17,11 @@
"@react-navigation/devtools"
]
},
"env": { "browser": true, "node": true }
"env": {
"browser": true,
"node": true
},
"rules": {
"react/no-unused-prop-types": "off"
}
}

View File

@@ -47,9 +47,21 @@ jobs:
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const body = 'The Expo app for the example from this branch is ready!\n\n[expo.io/${{ steps.expo.outputs.path }}](https://expo.io/${{ steps.expo.outputs.path }})\n\n<a href="https://exp.host/${{ steps.expo.outputs.path }}"><img src="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=exp://exp.host/${{ steps.expo.outputs.path }}" height="200px" width="200px"></a>';
const comments = await github.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
if (comments.some(comment => comment.body === body)) {
return;
}
github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'The Expo app for the example from this branch is ready!\n\n[expo.io/${{ steps.expo.outputs.path }}](https://expo.io/${{ steps.expo.outputs.path }})\n\n<a href="https://exp.host/${{ steps.expo.outputs.path }}"><img src="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=exp://exp.host/${{ steps.expo.outputs.path }}" height="200px" width="200px"></a>'
body
})

View File

@@ -1,7 +1,7 @@
name: Check versions
on:
issues:
types: [opened]
types: [opened, edited]
jobs:
check-versions:

View File

@@ -1,22 +1,3 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
targets: {
node: 'current',
},
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/transform-flow-strip-types',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
presets: ['module:metro-react-native-babel-preset'],
};

View File

@@ -44,6 +44,7 @@
"@types/react": "^16.9.36",
"@types/react-dom": "^16.9.8",
"@types/react-native": "^0.62.7",
"babel-loader": "^8.1.0",
"babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "^8.2.1",
"cheerio": "^1.0.0-rc.3",

View File

@@ -5,7 +5,7 @@ import { useTheme, ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
HeaderBackButton,
StackNavigationProp,
StackScreenProps,
} from '@react-navigation/stack';
type AuthStackParams = {
@@ -81,10 +81,6 @@ const HomeScreen = () => {
const SimpleStack = createStackNavigator<AuthStackParams>();
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
type State = {
isLoading: boolean;
isSignout: boolean;
@@ -96,7 +92,9 @@ type Action =
| { type: 'SIGN_IN'; token: string }
| { type: 'SIGN_OUT' };
export default function SimpleStackScreen({ navigation }: Props) {
export default function SimpleStackScreen({
navigation,
}: StackScreenProps<ParamListBase>) {
const [state, dispatch] = React.useReducer<React.Reducer<State, Action>>(
(prevState, action) => {
switch (action.type) {
@@ -135,9 +133,11 @@ export default function SimpleStackScreen({ navigation }: Props) {
return () => clearTimeout(timer);
}, []);
navigation.setOptions({
headerShown: false,
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
const authContext = React.useMemo(
() => ({
@@ -147,6 +147,10 @@ export default function SimpleStackScreen({ navigation }: Props) {
[]
);
if (state.isLoading) {
return <SplashScreen />;
}
return (
<AuthContext.Provider value={authContext}>
<SimpleStack.Navigator
@@ -156,13 +160,7 @@ export default function SimpleStackScreen({ navigation }: Props) {
),
}}
>
{state.isLoading ? (
<SimpleStack.Screen
name="Splash"
component={SplashScreen}
options={{ title: 'Auth Flow' }}
/>
) : state.userToken === undefined ? (
{state.userToken === undefined ? (
<SimpleStack.Screen
name="SignIn"
options={{

View File

@@ -9,7 +9,7 @@ import {
import type { StackScreenProps } from '@react-navigation/stack';
import {
createBottomTabNavigator,
BottomTabNavigationProp,
BottomTabScreenProps,
} from '@react-navigation/bottom-tabs';
import TouchableBounce from '../Shared/TouchableBounce';
import Albums from '../Shared/Albums';
@@ -36,9 +36,7 @@ const scrollEnabled = Platform.select({ web: true, default: false });
const AlbumsScreen = ({
navigation,
}: {
navigation: BottomTabNavigationProp<BottomTabParams>;
}) => {
}: BottomTabScreenProps<BottomTabParams>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -99,6 +97,7 @@ export default function BottomTabsScreen({
options={{
tabBarLabel: 'Chat',
tabBarIcon: getTabBarIcon('message-reply'),
tabBarBadge: 2,
}}
/>
<BottomTabs.Screen

View File

@@ -8,6 +8,7 @@ import {
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
@@ -81,7 +82,6 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
NestedStackParams,
'Article'
>> = ({ navigation }) => {
navigation.dangerouslyGetParent();
return (
<ScrollView>
<View style={styles.buttons}>
@@ -126,8 +126,8 @@ const CompatStack = createCompatStackNavigator<
StackNavigationProp<NestedStackParams>
>(
{
Feed: FeedScreen,
Article: ArticleScreen,
Feed: { getScreen: () => FeedScreen },
Article: { getScreen: () => ArticleScreen },
},
{ navigationOptions: { headerShown: false } }
),
@@ -143,12 +143,12 @@ const CompatStack = createCompatStackNavigator<
export default function CompatStackScreen({
navigation,
}: {
navigation: StackNavigationProp<{}>;
}) {
navigation.setOptions({
headerShown: false,
});
}: StackScreenProps<{}>) {
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return <CompatStack />;
}

View File

@@ -4,13 +4,12 @@ import { Button } from 'react-native-paper';
import {
Link,
StackActions,
RouteProp,
ParamListBase,
useLinkProps,
} from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
@@ -20,8 +19,6 @@ type SimpleStackParams = {
Albums: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const LinkButton = ({
@@ -45,10 +42,7 @@ const LinkButton = ({
const ArticleScreen = ({
navigation,
route,
}: {
navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'Article'>;
}) => {
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -88,11 +82,7 @@ const ArticleScreen = ({
);
};
const AlbumsScreen = ({
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -124,14 +114,15 @@ const AlbumsScreen = ({
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
StackScreenProps<ParamListBase>;
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return (
<SimpleStack.Navigator {...rest}>

View File

@@ -8,12 +8,12 @@ import {
} from '@react-navigation/native';
import {
createDrawerNavigator,
DrawerNavigationProp,
DrawerScreenProps,
DrawerContent,
DrawerContentComponentProps,
DrawerContentOptions,
} from '@react-navigation/drawer';
import type { StackNavigationProp } from '@react-navigation/stack';
import type { StackScreenProps } from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
@@ -24,8 +24,6 @@ type DrawerParams = {
Albums: undefined;
};
type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
const useIsLargeScreen = () => {
const [dimensions, setDimensions] = React.useState(Dimensions.get('window'));
@@ -60,7 +58,9 @@ const Header = ({
);
};
const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
const ArticleScreen = ({
navigation,
}: DrawerScreenProps<DrawerParams, 'Article'>) => {
return (
<>
<Header title="Article" onGoBack={() => navigation.toggleDrawer()} />
@@ -69,7 +69,9 @@ const ArticleScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
);
};
const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
const NewsFeedScreen = ({
navigation,
}: DrawerScreenProps<DrawerParams, 'NewsFeed'>) => {
return (
<>
<Header title="Feed" onGoBack={() => navigation.toggleDrawer()} />
@@ -78,7 +80,9 @@ const NewsFeedScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
);
};
const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
const AlbumsScreen = ({
navigation,
}: DrawerScreenProps<DrawerParams, 'Albums'>) => {
return (
<>
<Header title="Albums" onGoBack={() => navigation.toggleDrawer()} />
@@ -106,15 +110,16 @@ const CustomDrawerContent = (
const Drawer = createDrawerNavigator<DrawerParams>();
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> &
StackScreenProps<ParamListBase>;
export default function DrawerScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
gestureEnabled: false,
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
gestureEnabled: false,
});
}, [navigation]);
const isLargeScreen = useIsLargeScreen();

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import type { ParamListBase } from '@react-navigation/native';
import type { StackNavigationProp } from '@react-navigation/stack';
import type { StackScreenProps } from '@react-navigation/stack';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts';
@@ -14,14 +14,14 @@ type MaterialTopTabParams = {
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function MaterialTopTabsScreen({ navigation }: Props) {
navigation.setOptions({
cardStyle: { flex: 1 },
});
export default function MaterialTopTabsScreen({
navigation,
}: StackScreenProps<ParamListBase>) {
React.useLayoutEffect(() => {
navigation.setOptions({
cardStyle: { flex: 1 },
});
}, [navigation]);
return (
<MaterialTopTabs.Navigator>

View File

@@ -1,10 +1,11 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button } from 'react-native-paper';
import type { RouteProp, ParamListBase } from '@react-navigation/native';
import type { ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
StackNavigationOptions,
TransitionPresets,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
@@ -15,17 +16,12 @@ type ModalStackParams = {
Albums: undefined;
};
type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
}: {
navigation: ModalStackNavigation;
route: RouteProp<ModalStackParams, 'Article'>;
}) => {
}: StackScreenProps<ModalStackParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -52,7 +48,7 @@ const ArticleScreen = ({
);
};
const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
const AlbumsScreen = ({ navigation }: StackScreenProps<ModalStackParams>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -78,15 +74,16 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
const ModalPresentationStack = createStackNavigator<ModalStackParams>();
type Props = {
options?: React.ComponentProps<typeof ModalPresentationStack.Navigator>;
navigation: StackNavigationProp<ParamListBase>;
type Props = StackScreenProps<ParamListBase> & {
options?: StackNavigationOptions;
};
export default function SimpleStackScreen({ navigation, options }: Props) {
navigation.setOptions({
headerShown: false,
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return (
<ModalPresentationStack.Navigator

View File

@@ -1,13 +1,11 @@
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native-paper';
import type { StackNavigationProp } from '@react-navigation/stack';
import type { StackScreenProps } from '@react-navigation/stack';
const NotFoundScreen = ({
navigation,
}: {
navigation: StackNavigationProp<{ Home: undefined }>;
}) => {
}: StackScreenProps<{ Home: undefined }>) => {
return (
<View style={styles.container}>
<Text style={styles.title}>404 Not Found</Text>

View File

@@ -0,0 +1,170 @@
import * as React from 'react';
import {
Alert,
View,
TextInput,
ScrollView,
StyleSheet,
Platform,
} from 'react-native';
import { Button } from 'react-native-paper';
import {
useTheme,
CommonActions,
ParamListBase,
NavigationAction,
} from '@react-navigation/native';
import {
createStackNavigator,
StackScreenProps,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
type PreventRemoveParams = {
Article: { author: string };
Input: undefined;
};
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
}: StackScreenProps<PreventRemoveParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Input')}
style={styles.button}
>
Push Input
</Button>
<Button
mode="outlined"
onPress={() => navigation.popToTop()}
style={styles.button}
>
Pop to top
</Button>
</View>
<Article
author={{ name: route.params?.author ?? 'Unknown' }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
const InputScreen = ({
navigation,
}: StackScreenProps<PreventRemoveParams, 'Input'>) => {
const [text, setText] = React.useState('');
const { colors } = useTheme();
const hasUnsavedChanges = Boolean(text);
React.useEffect(
() =>
navigation.addListener('beforeRemove', (e) => {
const action: NavigationAction & { payload?: { confirmed?: boolean } } =
e.data.action;
if (!hasUnsavedChanges || action.payload?.confirmed) {
return;
}
e.preventDefault();
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure to discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
onPress: () => navigation.dispatch(action),
},
]
);
}),
[hasUnsavedChanges, navigation]
);
return (
<View style={styles.content}>
<TextInput
autoFocus
style={[
styles.input,
{ backgroundColor: colors.card, color: colors.text },
]}
value={text}
placeholder="Type something…"
onChangeText={setText}
/>
<Button
mode="outlined"
color="tomato"
onPress={() =>
navigation.dispatch({
...CommonActions.goBack(),
payload: { confirmed: true },
})
}
style={styles.button}
>
Discard and go back
</Button>
<Button
mode="outlined"
onPress={() => navigation.push('Article', { author: text })}
style={styles.button}
>
Push Article
</Button>
</View>
);
};
const SimpleStack = createStackNavigator<PreventRemoveParams>();
type Props = StackScreenProps<ParamListBase>;
export default function SimpleStackScreen({ navigation }: Props) {
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return (
<SimpleStack.Navigator>
<SimpleStack.Screen name="Input" component={InputScreen} />
<SimpleStack.Screen name="Article" component={ArticleScreen} />
</SimpleStack.Navigator>
);
}
const styles = StyleSheet.create({
content: {
flex: 1,
padding: 16,
},
input: {
margin: 8,
padding: 10,
borderRadius: 3,
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(0, 0, 0, 0.08)',
},
buttons: {
flexDirection: 'row',
padding: 8,
},
button: {
margin: 8,
},
});

View File

@@ -1,38 +1,33 @@
import * as React from 'react';
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'react-native-paper';
import type { RouteProp, ParamListBase } from '@react-navigation/native';
import type { ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
type SimpleStackParams = {
Article: { author: string };
NewsFeed: undefined;
Article: { author: string } | undefined;
NewsFeed: { date: number };
Albums: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
}: {
navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'Article'>;
}) => {
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.replace('NewsFeed')}
onPress={() => navigation.replace('NewsFeed', { date: Date.now() })}
style={styles.button}
>
Replace with feed
@@ -46,7 +41,7 @@ const ArticleScreen = ({
</Button>
</View>
<Article
author={{ name: route.params.author }}
author={{ name: route.params?.author ?? 'Unknown' }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
@@ -54,10 +49,9 @@ const ArticleScreen = ({
};
const NewsFeedScreen = ({
route,
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
}: StackScreenProps<SimpleStackParams, 'NewsFeed'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -76,16 +70,14 @@ const NewsFeedScreen = ({
Go back
</Button>
</View>
<NewsFeed scrollEnabled={scrollEnabled} />
<NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
</ScrollView>
);
};
const AlbumsScreen = ({
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
}: StackScreenProps<SimpleStackParams, 'Albums'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -111,14 +103,14 @@ const AlbumsScreen = ({
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function SimpleStackScreen({ navigation }: Props) {
navigation.setOptions({
headerShown: false,
});
export default function SimpleStackScreen({
navigation,
}: StackScreenProps<ParamListBase>) {
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return (
<SimpleStack.Navigator>
@@ -126,7 +118,7 @@ export default function SimpleStackScreen({ navigation }: Props) {
name="Article"
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params.author}`,
title: `Article by ${route.params?.author ?? 'Unknown'}`,
})}
initialParams={{ author: 'Gandalf' }}
/>

View File

@@ -9,10 +9,10 @@ import {
} from 'react-native';
import { Button, Appbar } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTheme, RouteProp, ParamListBase } from '@react-navigation/native';
import { useTheme, ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
HeaderBackground,
useHeaderHeight,
Header,
@@ -27,17 +27,12 @@ type SimpleStackParams = {
Albums: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
}: {
navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'Article'>;
}) => {
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -64,11 +59,7 @@ const ArticleScreen = ({
);
};
const AlbumsScreen = ({
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
const headerHeight = useHeaderHeight();
return (
@@ -96,9 +87,8 @@ const AlbumsScreen = ({
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
StackScreenProps<ParamListBase>;
function CustomHeader(props: StackHeaderProps) {
const { current, next } = props.scene.progress;
@@ -120,9 +110,11 @@ function CustomHeader(props: StackHeaderProps) {
}
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
const { colors, dark } = useTheme();

View File

@@ -1,10 +1,10 @@
import * as React from 'react';
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button, Paragraph } from 'react-native-paper';
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
import { ParamListBase, useTheme } from '@react-navigation/native';
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
@@ -13,17 +13,12 @@ type SimpleStackParams = {
Dialog: undefined;
};
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
}: {
navigation: SimpleStackNavigation;
route: RouteProp<SimpleStackParams, 'Article'>;
}) => {
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
@@ -50,11 +45,7 @@ const ArticleScreen = ({
);
};
const DialogScreen = ({
navigation,
}: {
navigation: SimpleStackNavigation;
}) => {
const DialogScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
const { colors } = useTheme();
return (
@@ -81,14 +72,15 @@ const DialogScreen = ({
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
navigation: StackNavigationProp<ParamListBase>;
};
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
StackScreenProps<ParamListBase>;
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
navigation.setOptions({
headerShown: false,
});
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return (
<SimpleStack.Navigator mode="modal" {...rest}>

View File

@@ -18,7 +18,9 @@ import {
} from 'react-native-paper';
import Color from 'color';
type Props = Partial<ScrollViewProps>;
type Props = Partial<ScrollViewProps> & {
date?: number;
};
const Author = () => {
return (

View File

@@ -31,11 +31,11 @@ import {
} from '@react-navigation/native';
import {
createDrawerNavigator,
DrawerNavigationProp,
DrawerScreenProps,
} from '@react-navigation/drawer';
import {
createStackNavigator,
StackNavigationProp,
StackScreenProps,
HeaderStyleInterpolators,
} from '@react-navigation/stack';
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
@@ -53,9 +53,10 @@ import MaterialTopTabsScreen from './Screens/MaterialTopTabs';
import MaterialBottomTabs from './Screens/MaterialBottomTabs';
import NotFound from './Screens/NotFound';
import DynamicTabs from './Screens/DynamicTabs';
import AuthFlow from './Screens/AuthFlow';
import CompatAPI from './Screens/CompatAPI';
import MasterDetail from './Screens/MasterDetail';
import AuthFlow from './Screens/AuthFlow';
import PreventRemove from './Screens/PreventRemove';
import CompatAPI from './Screens/CompatAPI';
import LinkComponent from './Screens/LinkComponent';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
@@ -109,6 +110,10 @@ const SCREENS = {
title: 'Auth Flow',
component: AuthFlow,
},
PreventRemove: {
title: 'Prevent removing screen',
component: PreventRemove,
},
CompatAPI: {
title: 'Compat Layer',
component: CompatAPI,
@@ -272,6 +277,10 @@ export default function App() {
},
}}
fallback={<Text>Loading</Text>}
documentTitle={{
formatter: (options, route) =>
`${options?.title ?? route?.name} - React Navigation Example`,
}}
>
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
<Drawer.Screen
@@ -283,11 +292,7 @@ export default function App() {
),
}}
>
{({
navigation,
}: {
navigation: DrawerNavigationProp<RootDrawerParamList>;
}) => (
{({ navigation }: DrawerScreenProps<RootDrawerParamList>) => (
<Stack.Navigator
screenOptions={{
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
@@ -308,11 +313,7 @@ export default function App() {
),
}}
>
{({
navigation,
}: {
navigation: StackNavigationProp<RootStackParamList>;
}) => (
{({ navigation }: StackScreenProps<RootStackParamList>) => (
<ScrollView
style={{ backgroundColor: theme.colors.background }}
>
@@ -361,7 +362,7 @@ export default function App() {
<Stack.Screen
key={name}
name={name}
component={SCREENS[name].component}
getComponent={() => SCREENS[name].component}
options={{ title: SCREENS[name].title }}
/>
)

View File

@@ -1,3 +1,21 @@
/* eslint-env jest */
/* eslint-disable import/no-extraneous-dependencies */
import 'react-native-gesture-handler/jestSetup';
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');
// The mock for `call` immediately calls the callback which is incorrect
// So we override it with a no-op
Reanimated.default.call = () => {};
return Reanimated;
});
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');
const error = console.error;
console.error = (...args) =>

View File

@@ -17,7 +17,7 @@
},
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
"scripts": {
"lint": "eslint --ext '.js,.ts,.tsx' .",
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"typescript": "tsc --noEmit --composite false",
"test": "jest",
"prerelease": "lerna run clean",
@@ -25,24 +25,17 @@
"example": "yarn --cwd example"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-optional-chaining": "^7.10.1",
"@babel/preset-env": "^7.10.2",
"@babel/preset-flow": "^7.10.1",
"@babel/preset-react": "^7.10.1",
"@babel/preset-typescript": "^7.10.1",
"@babel/runtime": "^7.10.2",
"@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^26.0.0",
"babel-jest": "^26.0.1",
"codecov": "^3.7.0",
"commitlint": "^8.3.5",
"core-js": "^3.6.5",
"eslint": "^7.2.0",
"eslint-config-satya164": "^3.1.7",
"husky": "^4.2.5",
"jest": "^26.0.1",
"lerna": "^3.22.1",
"metro-react-native-babel-preset": "^0.59.0",
"prettier": "^2.0.5",
"typescript": "^3.9.5"
},
@@ -59,9 +52,6 @@
"jest": {
"testEnvironment": "node",
"testRegex": "/__tests__/.*\\.(test|spec)\\.(js|tsx?)$",
"transform": {
"^.+\\.(js|ts|tsx)$": "babel-jest"
},
"setupFiles": [
"<rootDir>/jest/setup.js"
],

View File

@@ -3,6 +3,61 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.7.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.7.2...@react-navigation/bottom-tabs@5.7.3) (2020-07-28)
### Bug Fixes
* add accessibilityState property ([#8548](https://github.com/react-navigation/react-navigation/issues/8548)) ([ce4eb7e](https://github.com/react-navigation/react-navigation/commit/ce4eb7e9273a25e4433eb82e255a58ba3bf4d632))
* pass label position flag to label rendering in BottomTabBar ([#8557](https://github.com/react-navigation/react-navigation/issues/8557)) ([baea77e](https://github.com/react-navigation/react-navigation/commit/baea77e3325f0d7e5ce331ad61979a9362dd01fa))
## [5.7.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.7.1...@react-navigation/bottom-tabs@5.7.2) (2020-07-19)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.7.0...@react-navigation/bottom-tabs@5.7.1) (2020-07-14)
### Bug Fixes
* don't render badge on bottom tabs if not visible. closes [#8577](https://github.com/react-navigation/react-navigation/issues/8577) ([2f74541](https://github.com/react-navigation/react-navigation/commit/2f74541811bac4d36e89c159cd1f4b267063e7f9))
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.6.1...@react-navigation/bottom-tabs@5.7.0) (2020-07-10)
### Bug Fixes
* fix bottom tab bar to match iOS defaults ([849e04a](https://github.com/react-navigation/react-navigation/commit/849e04ab6a541fffb490ffdfa9819608b88494f4))
### Features
* add support for badges to bottom tab bar ([96c7b68](https://github.com/react-navigation/react-navigation/commit/96c7b688ce773b3dd1f1cf7775367cd7080c94a2))
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.6.0...@react-navigation/bottom-tabs@5.6.1) (2020-06-25)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@5.5.2...@react-navigation/bottom-tabs@5.6.0) (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines",
"version": "5.6.0",
"version": "5.7.3",
"keywords": [
"react-native-component",
"react-component",
@@ -41,7 +41,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.15.1",
"@react-navigation/native": "^5.6.0",
"@react-navigation/native": "^5.7.2",
"@types/color": "^3.0.1",
"@types/react": "^16.9.36",
"@types/react-native": "^0.62.7",
@@ -50,6 +50,7 @@
"react-native": "~0.61.5",
"react-native-safe-area-context": "^1.0.0",
"react-native-screens": "^2.7.0",
"react-native-testing-library": "^2.1.0",
"typescript": "^3.9.5"
},
"peerDependencies": {

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { render, fireEvent } from 'react-native-testing-library';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import { createBottomTabNavigator, BottomTabScreenProps } from '../index';
it('renders a bottom tab navigator with screens', async () => {
const Test = ({ route, navigation }: BottomTabScreenProps<ParamListBase>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);
const Tab = createBottomTabNavigator();
const { findByText, queryByText } = render(
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="A" component={Test} />
<Tab.Screen name="B" component={Test} />
</Tab.Navigator>
</NavigationContainer>
);
expect(queryByText('Screen A')).not.toBeNull();
expect(queryByText('Screen B')).toBeNull();
fireEvent.press(await findByText('Go to B'));
expect(queryByText('Screen B')).not.toBeNull();
});

View File

@@ -63,12 +63,16 @@ export type BottomTabNavigationOptions = {
/**
* Title string of a tab displayed in the tab bar
* or a function that given { focused: boolean, color: string } returns a React.Node to display in tab bar.
* or a function that given { focused: boolean, color: string, position: 'below-icon' | 'beside-icon' } returns a React.Node to display in tab bar.
* When undefined, scene title is used. To hide, see tabBarOptions.showLabel in the previous section.
*/
tabBarLabel?:
| string
| ((props: { focused: boolean; color: string }) => React.ReactNode);
| ((props: {
focused: boolean;
color: string;
position: LabelPosition;
}) => React.ReactNode);
/**
* A function that given { focused: boolean, color: string } returns a React.Node to display in the tab bar.
@@ -79,6 +83,11 @@ export type BottomTabNavigationOptions = {
size: number;
}) => React.ReactNode;
/**
* Text to show in a badge on the tab icon.
*/
tabBarBadge?: number | string;
/**
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
* It's recommended to set this if you don't have a label for the tab.
@@ -178,7 +187,8 @@ export type BottomTabBarOptions = {
tabStyle?: StyleProp<ViewStyle>;
/**
* Whether the label is rendered below the icon or beside the icon.
* By default, in `vertical` orientation, label is rendered below and in `horizontal` orientation, it's rendered beside.
* By default, the position is chosen automatically based on device width.
* In `below-icon` orientation (typical for iPhones), the label is rendered below and in `beside-icon` orientation, it's rendered beside (typical for iPad).
*/
labelPosition?: LabelPosition;
/**

View File

@@ -0,0 +1,108 @@
import * as React from 'react';
import { Animated, StyleSheet, StyleProp, TextStyle } from 'react-native';
import color from 'color';
import { useTheme } from '@react-navigation/native';
type Props = {
/**
* Whether the badge is visible
*/
visible: boolean;
/**
* Content of the `Badge`.
*/
children?: string | number;
/**
* Size of the `Badge`.
*/
size?: number;
/**
* Style object for the tab bar container.
*/
style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
};
export default function Badge({
visible = true,
size = 18,
children,
style,
...rest
}: Props) {
const [opacity] = React.useState(() => new Animated.Value(visible ? 1 : 0));
const [rendered, setRendered] = React.useState(visible ? true : false);
const theme = useTheme();
React.useEffect(() => {
if (!rendered) {
return;
}
Animated.timing(opacity, {
toValue: visible ? 1 : 0,
duration: 150,
useNativeDriver: true,
}).start(({ finished }) => {
if (finished && !visible) {
setRendered(false);
}
});
}, [opacity, rendered, visible]);
if (visible && !rendered) {
setRendered(true);
}
if (!visible && !rendered) {
return null;
}
// @ts-expect-error: backgroundColor definitely exists
const { backgroundColor = theme.colors.notification, ...restStyle } =
StyleSheet.flatten(style) || {};
const textColor = color(backgroundColor).isLight() ? 'black' : 'white';
const borderRadius = size / 2;
const fontSize = Math.floor((size * 3) / 4);
return (
<Animated.Text
numberOfLines={1}
style={[
{
opacity,
transform: [
{
scale: opacity.interpolate({
inputRange: [0, 1],
outputRange: [0.5, 1],
}),
},
],
backgroundColor,
color: textColor,
fontSize,
lineHeight: size - 1,
height: size,
minWidth: size,
borderRadius,
},
styles.container,
restStyle,
]}
{...rest}
>
{children}
</Animated.Text>
);
}
const styles = StyleSheet.create({
container: {
alignSelf: 'flex-end',
textAlign: 'center',
paddingHorizontal: 4,
overflow: 'hidden',
},
});

View File

@@ -25,7 +25,7 @@ type Props = BottomTabBarProps & {
inactiveTintColor?: string;
};
const DEFAULT_TABBAR_HEIGHT = 50;
const DEFAULT_TABBAR_HEIGHT = 49;
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
const useNativeDriver = Platform.OS !== 'web';
@@ -152,6 +152,8 @@ export default function BottomTabBar({
left: safeAreaInsets?.left ?? defaultInsets.left,
};
const paddingBottom = Math.max(insets.bottom - 4, 0);
return (
<Animated.View
style={[
@@ -165,7 +167,7 @@ export default function BottomTabBar({
{
translateY: visible.interpolate({
inputRange: [0, 1],
outputRange: [layout.height + insets.bottom, 0],
outputRange: [layout.height + paddingBottom, 0],
}),
},
],
@@ -174,8 +176,8 @@ export default function BottomTabBar({
position: isTabBarHidden ? 'absolute' : null,
},
{
height: DEFAULT_TABBAR_HEIGHT + insets.bottom,
paddingBottom: insets.bottom,
height: DEFAULT_TABBAR_HEIGHT + paddingBottom,
paddingBottom,
paddingHorizontal: Math.max(insets.left, insets.right),
},
style,
@@ -245,6 +247,7 @@ export default function BottomTabBar({
inactiveBackgroundColor={inactiveBackgroundColor}
button={options.tabBarButton}
icon={options.tabBarIcon}
badge={options.tabBarBadge}
label={label}
showLabel={showLabel}
labelStyle={labelStyle}

View File

@@ -14,7 +14,7 @@ import { Link, Route, useTheme } from '@react-navigation/native';
import Color from 'color';
import TabBarIcon from './TabBarIcon';
import type { BottomTabBarButtonProps } from '../types';
import type { BottomTabBarButtonProps, LabelPosition } from '../types';
type Props = {
/**
@@ -30,7 +30,11 @@ type Props = {
*/
label:
| string
| ((props: { focused: boolean; color: string }) => React.ReactNode);
| ((props: {
focused: boolean;
color: string;
position: LabelPosition;
}) => React.ReactNode);
/**
* Icon to display for the tab.
*/
@@ -39,6 +43,10 @@ type Props = {
size: number;
color: string;
}) => React.ReactNode;
/**
* Text to show in a badge on the tab icon.
*/
badge?: number | string;
/**
* URL to use for the link to the tab.
*/
@@ -113,6 +121,7 @@ export default function BottomTabBarItem({
route,
label,
icon,
badge,
to,
button = ({
children,
@@ -206,7 +215,11 @@ export default function BottomTabBarItem({
);
}
return label({ focused, color });
return label({
focused,
color,
position: horizontal ? 'beside-icon' : 'below-icon',
});
};
const renderIcon = ({ focused }: { focused: boolean }) => {
@@ -220,16 +233,14 @@ export default function BottomTabBarItem({
return (
<TabBarIcon
route={route}
size={horizontal ? 17 : 24}
horizontal={horizontal}
badge={badge}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
activeTintColor={activeTintColor}
inactiveTintColor={inactiveTintColor}
renderIcon={icon}
style={[
horizontal ? styles.iconHorizontal : styles.iconVertical,
iconStyle,
]}
style={iconStyle}
/>
);
};
@@ -247,6 +258,7 @@ export default function BottomTabBarItem({
testID,
accessibilityLabel,
accessibilityRole: 'button',
accessibilityState: { selected: focused },
accessibilityStates: focused ? ['selected'] : [],
style: [
styles.tab,
@@ -276,23 +288,17 @@ const styles = StyleSheet.create({
justifyContent: 'center',
flexDirection: 'row',
},
iconVertical: {
flex: 1,
},
iconHorizontal: {
height: '100%',
},
label: {
textAlign: 'center',
backgroundColor: 'transparent',
},
labelBeneath: {
fontSize: 11,
marginBottom: 1.5,
fontSize: 10,
},
labelBeside: {
fontSize: 12,
fontSize: 13,
marginLeft: 20,
marginTop: 3,
},
button: {
display: 'flex',

View File

@@ -1,10 +1,12 @@
import React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import type { Route } from '@react-navigation/native';
import Badge from './Badge';
type Props = {
route: Route<string>;
size: number;
horizontal: boolean;
badge?: string | number;
activeOpacity: number;
inactiveOpacity: number;
activeTintColor: string;
@@ -18,18 +20,23 @@ type Props = {
};
export default function TabBarIcon({
horizontal,
badge,
activeOpacity,
inactiveOpacity,
activeTintColor,
inactiveTintColor,
renderIcon,
size,
style,
}: Props) {
const size = 25;
// We render the icon twice at the same position on top of each other:
// active and inactive one, so we can fade between them.
return (
<View style={style}>
<View
style={[horizontal ? styles.iconHorizontal : styles.iconVertical, style]}
>
<View style={[styles.icon, { opacity: activeOpacity }]}>
{renderIcon({
focused: true,
@@ -44,6 +51,16 @@ export default function TabBarIcon({
color: inactiveTintColor,
})}
</View>
<Badge
visible={badge != null}
style={[
styles.badge,
horizontal ? styles.badgeHorizontal : styles.badgeVertical,
]}
size={(size * 3) / 4}
>
{badge}
</Badge>
</View>
);
}
@@ -62,4 +79,21 @@ const styles = StyleSheet.create({
// Workaround for react-native >= 0.54 layout bug
minWidth: 25,
},
iconVertical: {
flex: 1,
},
iconHorizontal: {
height: '100%',
marginTop: 3,
},
badge: {
position: 'absolute',
left: 3,
},
badgeVertical: {
top: 3,
},
badgeHorizontal: {
top: 7,
},
});

View File

@@ -3,6 +3,63 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.3...@react-navigation/compat@5.2.4) (2020-07-28)
**Note:** Version bump only for package @react-navigation/compat
## [5.2.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.1...@react-navigation/compat@5.2.3) (2020-07-22)
### Bug Fixes
* fix false warning due to change in Object.assign in metro preset ([5e358b3](https://github.com/react-navigation/react-navigation/commit/5e358b3aadac7bb186521872d515fff2e571a940)), closes [#8584](https://github.com/react-navigation/react-navigation/issues/8584)
## [5.2.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.1...@react-navigation/compat@5.2.2) (2020-07-22)
### Bug Fixes
* fix false warning due to change in Object.assign in metro preset ([240a706](https://github.com/react-navigation/react-navigation/commit/240a706a56220b63d603a52407a738c2872349dd)), closes [#8584](https://github.com/react-navigation/react-navigation/issues/8584)
## [5.2.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.2.0...@react-navigation/compat@5.2.1) (2020-07-19)
**Note:** Version bump only for package @react-navigation/compat
# [5.2.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.28...@react-navigation/compat@5.2.0) (2020-07-10)
### Features
* add a getComponent prop to lazily specify components ([f418029](https://github.com/react-navigation/react-navigation/commit/f4180295bf22e32c65f6a7ab7089523cb2de58fb))
## [5.1.28](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.27...@react-navigation/compat@5.1.28) (2020-06-25)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.27](https://github.com/react-navigation/react-navigation/compare/@react-navigation/compat@5.1.26...@react-navigation/compat@5.1.27) (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.1.27",
"version": "5.2.4",
"license": "MIT",
"repository": {
"type": "git",
@@ -32,7 +32,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.15.1",
"@react-navigation/native": "^5.6.0",
"@react-navigation/native": "^5.7.2",
"@types/react": "^16.9.36",
"react": "~16.9.0",
"typescript": "^3.9.5"

View File

@@ -1,25 +1,17 @@
import * as React from 'react';
import type {
NavigationProp,
ParamListBase,
RouteProp,
} from '@react-navigation/native';
import ScreenPropsContext from './ScreenPropsContext';
import useCompatNavigation from './useCompatNavigation';
type Props<ParamList extends ParamListBase> = {
navigation: NavigationProp<ParamList>;
route: RouteProp<ParamList, string>;
component: React.ComponentType<any>;
type Props = {
getComponent: () => React.ComponentType<any>;
};
function ScreenComponent<ParamList extends ParamListBase>(
props: Props<ParamList>
) {
function CompatScreen({ getComponent }: Props) {
const navigation = useCompatNavigation();
const screenProps = React.useContext(ScreenPropsContext);
const ScreenComponent = getComponent();
return <props.component navigation={navigation} screenProps={screenProps} />;
return <ScreenComponent navigation={navigation} screenProps={screenProps} />;
}
export default React.memo(ScreenComponent);
export default React.memo(CompatScreen);

View File

@@ -143,9 +143,12 @@ export default function createCompatNavigationProp<
}
},
state: {
...state,
// @ts-expect-error: these properties may actually exist
key: state.key,
// @ts-expect-error
routeName: state.name,
// @ts-expect-error
params: state.params ?? {},
get index() {
// @ts-expect-error
if (state.index !== undefined) {
@@ -154,11 +157,11 @@ export default function createCompatNavigationProp<
}
console.warn(
"Accessing child navigation state for a route is not safe and won't work correctly."
"Looks like you are using 'navigation.state.index' in your code. Accessing child navigation state for a route is not safe and won't work correctly. You should refactor it not to access the 'index' property in the child navigation state."
);
// @ts-expect-error
return state.state ? state.state.index : undefined;
return state.state?.index;
},
get routes() {
// @ts-expect-error
@@ -168,11 +171,11 @@ export default function createCompatNavigationProp<
}
console.warn(
"Accessing child navigation state for a route is not safe and won't work correctly."
"Looks like you are using 'navigation.state.routes' in your code. Accessing child navigation state for a route is not safe and won't work correctly. You should refactor it not to access the 'routes' property in the child navigation state."
);
// @ts-expect-error
return state.state ? state.state.routes : undefined;
return state.state?.routes;
},
},
getParam<T extends keyof ParamList>(

View File

@@ -142,13 +142,7 @@ export default function createCompatNavigatorFactory<
initialParams={{ ...parentRouteParams, ...initialParams }}
options={screenOptions}
>
{({ navigation, route }) => (
<CompatScreen
navigation={navigation}
route={route}
component={getScreenComponent()}
/>
)}
{() => <CompatScreen getComponent={getScreenComponent} />}
</Pair.Screen>
);
}),

View File

@@ -3,6 +3,57 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.12.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.1...@react-navigation/core@5.12.2) (2020-07-28)
**Note:** Version bump only for package @react-navigation/core
## [5.12.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.12.0...@react-navigation/core@5.12.1) (2020-07-19)
### Bug Fixes
* make sure new state events are emitted when new navigators mount ([af8b274](https://github.com/react-navigation/react-navigation/commit/af8b27414c8628570d946003f4fdff3341cb8954))
# [5.12.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.11.1...@react-navigation/core@5.12.0) (2020-07-10)
### Bug Fixes
* avoid error setting warning for devtools migration. closes [#8534](https://github.com/react-navigation/react-navigation/issues/8534) ([1801a13](https://github.com/react-navigation/react-navigation/commit/1801a13323eff149fb6bc4e3c3f12422b401f178))
* fix bubbling actions to correct target when specified ([9671c76](https://github.com/react-navigation/react-navigation/commit/9671c76c5121aaa64a956e2ca696b2f1712cd6f4))
* fix options event being emitted incorrectly ([#8559](https://github.com/react-navigation/react-navigation/issues/8559)) ([a255e35](https://github.com/react-navigation/react-navigation/commit/a255e350f9a54c6d8e410167c9c8661e70b23779))
* improve the warning message for non-serializable values ([e63580e](https://github.com/react-navigation/react-navigation/commit/e63580edbef8e77239f3dbefc919d1a41723eff1))
* mark some types as read-only ([7c3a0a0](https://github.com/react-navigation/react-navigation/commit/7c3a0a0f23629da0beb956ba5a9689ab965061ce))
### Features
* add a `beforeRemove` event ([6925e92](https://github.com/react-navigation/react-navigation/commit/6925e92dc3e9885e3f552ca5e5eb51ae1521e54e))
* add a getComponent prop to lazily specify components ([f418029](https://github.com/react-navigation/react-navigation/commit/f4180295bf22e32c65f6a7ab7089523cb2de58fb))
## [5.11.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.11.0...@react-navigation/core@5.11.1) (2020-06-25)
### Bug Fixes
* fix error with type definitions. closes [#8511](https://github.com/react-navigation/react-navigation/issues/8511) ([d1210a8](https://github.com/react-navigation/react-navigation/commit/d1210a861b37201827c333a5c012c4f0ebd9bb6a))
# [5.11.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@5.10.0...@react-navigation/core@5.11.0) (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/core",
"description": "Core utilities for building navigators",
"version": "5.11.0",
"version": "5.12.2",
"keywords": [
"react",
"react-native",
@@ -35,7 +35,7 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/routers": "^5.4.8",
"@react-navigation/routers": "^5.4.10",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.9",
"query-string": "^6.13.1",

View File

@@ -12,12 +12,12 @@ import NavigationBuilderContext from './NavigationBuilderContext';
import NavigationStateContext from './NavigationStateContext';
import UnhandledActionContext from './UnhandledActionContext';
import { ScheduleUpdateContext } from './useScheduleUpdate';
import useFocusedListeners from './useFocusedListeners';
import useStateGetters from './useStateGetters';
import useChildListeners from './useChildListeners';
import useKeyedChildListeners from './useKeyedChildListeners';
import useOptionsGetters from './useOptionsGetters';
import useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState';
import isSerializable from './isSerializable';
import checkSerializable from './checkSerializable';
import type {
NavigationContainerEventMap,
NavigationContainerRef,
@@ -29,22 +29,26 @@ type State = NavigationState | PartialState<NavigationState> | undefined;
const NOT_INITIALIZED_ERROR =
"The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.";
let hasWarnedForSerialization = false;
const serializableWarnings: string[] = [];
/**
* Migration instructions for removal of devtools from core
*/
Object.defineProperty(
global,
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED',
{
set(_) {
console.warn(
"Redux devtools extension integration can be enabled with the '@react-navigation/devtools' package. For more details, see https://reactnavigation.org/docs/devtools"
);
},
}
);
try {
/**
* Migration instructions for removal of devtools from core
*/
Object.defineProperty(
global,
'REACT_NAVIGATION_REDUX_DEVTOOLS_EXTENSION_INTEGRATION_ENABLED',
{
set(_) {
console.warn(
"Redux devtools extension integration can be enabled with the '@react-navigation/devtools' package. For more details, see https://reactnavigation.org/docs/devtools"
);
},
}
);
} catch (e) {
// Ignore
}
/**
* Remove `key` and `routeNames` from the state objects recursively to get partial state.
@@ -123,29 +127,26 @@ const BaseNavigationContainer = React.forwardRef(
navigatorKeyRef.current = key;
}, []);
const {
listeners,
addListener: addFocusedListener,
} = useFocusedListeners();
const { listeners, addListener } = useChildListeners();
const { getStateForRoute, addStateGetter } = useStateGetters();
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
const dispatch = (
action: NavigationAction | ((state: NavigationState) => NavigationAction)
) => {
if (listeners[0] == null) {
if (listeners.focus[0] == null) {
throw new Error(NOT_INITIALIZED_ERROR);
}
listeners[0]((navigation) => navigation.dispatch(action));
listeners.focus[0]((navigation) => navigation.dispatch(action));
};
const canGoBack = () => {
if (listeners[0] == null) {
if (listeners.focus[0] == null) {
return false;
}
const { result, handled } = listeners[0]((navigation) =>
const { result, handled } = listeners.focus[0]((navigation) =>
navigation.canGoBack()
);
@@ -164,8 +165,8 @@ const BaseNavigationContainer = React.forwardRef(
);
const getRootState = React.useCallback(() => {
return getStateForRoute('root');
}, [getStateForRoute]);
return keyedListeners.getState.root?.();
}, [keyedListeners.getState]);
const getCurrentRoute = React.useCallback(() => {
let state = getRootState();
@@ -213,8 +214,16 @@ const BaseNavigationContainer = React.forwardRef(
[emitter]
);
const lastEmittedOptionsRef = React.useRef<object | undefined>();
const onOptionsChange = React.useCallback(
(options) => {
if (lastEmittedOptionsRef.current === options) {
return;
}
lastEmittedOptionsRef.current = options;
emitter.emit({
type: 'options',
data: { options },
@@ -225,12 +234,12 @@ const BaseNavigationContainer = React.forwardRef(
const builderContext = React.useMemo(
() => ({
addFocusedListener,
addStateGetter,
addListener,
addKeyedListener,
onDispatchAction,
onOptionsChange,
}),
[addFocusedListener, addStateGetter, onDispatchAction, onOptionsChange]
[addListener, addKeyedListener, onDispatchAction, onOptionsChange]
);
const scheduleContext = React.useMemo(
@@ -238,6 +247,10 @@ const BaseNavigationContainer = React.forwardRef(
[scheduleUpdate, flushUpdates]
);
const isInitialRef = React.useRef(true);
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
const context = React.useMemo(
() => ({
state,
@@ -245,29 +258,78 @@ const BaseNavigationContainer = React.forwardRef(
setState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
}),
[getKey, getState, setKey, setState, state, addOptionsGetter]
[
state,
getState,
setState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
]
);
const onStateChangeRef = React.useRef(onStateChange);
React.useEffect(() => {
isInitialRef.current = false;
onStateChangeRef.current = onStateChange;
});
React.useEffect(() => {
if (process.env.NODE_ENV !== 'production') {
if (
state !== undefined &&
!isSerializable(state) &&
!hasWarnedForSerialization
) {
hasWarnedForSerialization = true;
if (state !== undefined) {
const result = checkSerializable(state);
console.warn(
"Non-serializable values were found in the navigation state, which can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details."
);
if (!result.serializable) {
const { location, reason } = result;
let path = '';
let pointer: Record<any, any> = state;
let params = false;
for (let i = 0; i < location.length; i++) {
const curr = location[i];
const prev = location[i - 1];
pointer = pointer[curr];
if (!params && curr === 'state') {
continue;
} else if (!params && curr === 'routes') {
if (path) {
path += ' > ';
}
} else if (
!params &&
typeof curr === 'number' &&
prev === 'routes'
) {
path += pointer?.name;
} else if (!params) {
path += ` > ${curr}`;
params = true;
} else {
if (typeof curr === 'number' || /^[0-9]+$/.test(curr)) {
path += `[${curr}]`;
} else if (/^[a-z$_]+$/i.test(curr)) {
path += `.${curr}`;
} else {
path += `[${JSON.stringify(curr)}]`;
}
}
}
const message = `Non-serializable values were found in the navigation state. Check:\n\n${path} (${reason})\n\nThis can break usage such as persisting and restoring state. This might happen if you passed non-serializable values such as function, class instances etc. in params. If you need to use components with callbacks in your options, you can use 'navigation.setOptions' instead. See https://reactnavigation.org/docs/troubleshooting#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state for more details.`;
if (!serializableWarnings.includes(message)) {
serializableWarnings.push(message);
console.warn(message);
}
}
}
}

View File

@@ -6,6 +6,27 @@ import type {
} from '@react-navigation/routers';
import type { NavigationHelpers } from './types';
export type ListenerMap = {
action: ChildActionListener;
focus: FocusedNavigationListener;
};
export type KeyedListenerMap = {
getState: GetStateListener;
beforeRemove: ChildBeforeRemoveListener;
};
export type AddListener = <T extends keyof ListenerMap>(
type: T,
listener: ListenerMap[T]
) => void;
export type AddKeyedListener = <T extends keyof KeyedListenerMap>(
type: T,
key: string,
listener: KeyedListenerMap[T]
) => void;
export type ChildActionListener = (
action: NavigationAction,
visitedNavigators?: Set<string>
@@ -19,7 +40,9 @@ export type FocusedNavigationListener = <T>(
callback: FocusedNavigationCallback<T>
) => { handled: boolean; result: T };
export type NavigatorStateGetter = () => NavigationState;
export type GetStateListener = () => NavigationState;
export type ChildBeforeRemoveListener = (action: NavigationAction) => boolean;
/**
* Context which holds the required helpers needed to build nested navigators.
@@ -29,11 +52,10 @@ const NavigationBuilderContext = React.createContext<{
action: NavigationAction,
visitedNavigators?: Set<string>
) => boolean;
addActionListener?: (listener: ChildActionListener) => void;
addFocusedListener?: (listener: FocusedNavigationListener) => void;
addListener?: AddListener;
addKeyedListener?: AddKeyedListener;
onRouteFocus?: (key: string) => void;
onDispatchAction: (action: NavigationAction, noop: boolean) => void;
addStateGetter?: (key: string, getter: NavigatorStateGetter) => void;
onOptionsChange: (options: object) => void;
}>({
onDispatchAction: () => undefined,

View File

@@ -13,6 +13,7 @@ export default React.createContext<{
setState: (
state: NavigationState | PartialState<NavigationState> | undefined
) => void;
getIsInitial: () => boolean;
addOptionsGetter?: (
key: string,
getter: () => object | undefined | null
@@ -32,4 +33,7 @@ export default React.createContext<{
get setState(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
get getIsInitial(): any {
throw new Error(MISSING_CONTEXT_ERROR);
},
});

View File

@@ -9,8 +9,6 @@ import NavigationStateContext from './NavigationStateContext';
import StaticContainer from './StaticContainer';
import EnsureSingleNavigator from './EnsureSingleNavigator';
import useOptionsGetters from './useOptionsGetters';
import NavigationBuilderContext from './NavigationBuilderContext';
import useFocusEffect from './useFocusEffect';
import type { NavigationProp, RouteConfig, EventMapBase } from './types';
type Props<
@@ -45,26 +43,14 @@ export default function SceneView<
options,
}: Props<State, ScreenOptions, EventMap>) {
const navigatorKeyRef = React.useRef<string | undefined>();
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
const getKey = React.useCallback(() => navigatorKeyRef.current, []);
const optionsRef = React.useRef<object | undefined>(options);
const getOptions = React.useCallback(() => optionsRef.current, []);
const { addOptionsGetter, hasAnyChildListener } = useOptionsGetters({
const { addOptionsGetter } = useOptionsGetters({
key: route.key,
getState,
getOptions,
options,
navigation,
});
const optionsChange = React.useCallback(() => {
optionsRef.current = options;
if (!hasAnyChildListener) {
onOptionsChange(options);
}
}, [onOptionsChange, options, hasAnyChildListener]);
useFocusEffect(optionsChange);
const setKey = React.useCallback((key: string) => {
navigatorKeyRef.current = key;
}, []);
@@ -90,6 +76,14 @@ export default function SceneView<
[getState, route.key, setState]
);
const isInitialRef = React.useRef(true);
React.useEffect(() => {
isInitialRef.current = false;
});
const getIsInitial = React.useCallback(() => isInitialRef.current, []);
const context = React.useMemo(
() => ({
state: route.state,
@@ -97,31 +91,36 @@ export default function SceneView<
setState: setCurrentState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
}),
[
getCurrentState,
getKey,
route.state,
getCurrentState,
setCurrentState,
getKey,
setKey,
getIsInitial,
addOptionsGetter,
]
);
const ScreenComponent = screen.getComponent
? screen.getComponent()
: screen.component;
return (
<NavigationStateContext.Provider value={context}>
<EnsureSingleNavigator>
<StaticContainer
name={screen.name}
// @ts-expect-error: these properties exist on screen, but TS is confused
render={screen.component || screen.children}
render={ScreenComponent || screen.children}
navigation={navigation}
route={route}
>
{'component' in screen && screen.component !== undefined ? (
<screen.component navigation={navigation} route={route} />
) : 'children' in screen && screen.children !== undefined ? (
{ScreenComponent !== undefined ? (
<ScreenComponent navigation={navigation} route={route} />
) : screen.children !== undefined ? (
screen.children({ navigation, route })
) : null}
</StaticContainer>

View File

@@ -1,9 +1,11 @@
import * as React from 'react';
import { act, render } from 'react-native-testing-library';
import type {
import {
DefaultRouterOptions,
NavigationState,
Router,
StackRouter,
TabRouter,
} from '@react-navigation/routers';
import BaseNavigationContainer from '../BaseNavigationContainer';
import NavigationStateContext from '../NavigationStateContext';
@@ -364,6 +366,7 @@ it('handles getRootState', () => {
type: 'test',
});
});
it('emits state events when the state changes', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -399,6 +402,7 @@ it('emits state events when the state changes', () => {
ref.current?.navigate('bar');
});
expect(listener).toBeCalledTimes(1);
expect(listener.mock.calls[0][0].data.state).toEqual({
type: 'test',
stale: false,
@@ -416,6 +420,7 @@ it('emits state events when the state changes', () => {
ref.current?.navigate('baz', { answer: 42 });
});
expect(listener).toBeCalledTimes(2);
expect(listener.mock.calls[1][0].data.state).toEqual({
type: 'test',
stale: false,
@@ -430,7 +435,9 @@ it('emits state events when the state changes', () => {
});
});
it('emits state events when options change', () => {
it('emits state events when new navigator mounts', () => {
jest.useFakeTimers();
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -443,6 +450,95 @@ it('emits state events when options change', () => {
const ref = React.createRef<NavigationContainerRef>();
const NestedNavigator = () => {
const [isRendered, setIsRendered] = React.useState(false);
React.useEffect(() => {
setTimeout(() => setIsRendered(true), 100);
}, []);
if (!isRendered) {
return null;
}
return (
<TestNavigator>
<Screen name="baz">{() => null}</Screen>
<Screen name="bax">{() => null}</Screen>
</TestNavigator>
);
};
const onStateChange = jest.fn();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar" component={NestedNavigator} />
</TestNavigator>
</BaseNavigationContainer>
);
render(element).update(element);
const listener = jest.fn();
ref.current?.addListener('state', listener);
expect(listener).not.toHaveBeenCalled();
expect(onStateChange).not.toHaveBeenCalled();
act(() => {
jest.runAllTimers();
});
const resultState = {
stale: false,
type: 'test',
index: 0,
key: '10',
routeNames: ['foo', 'bar'],
routes: [
{ key: 'foo', name: 'foo' },
{
key: 'bar',
name: 'bar',
state: {
stale: false,
type: 'test',
index: 0,
key: '11',
routeNames: ['baz', 'bax'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bax', name: 'bax' },
],
},
},
],
};
expect(listener).toBeCalledTimes(1);
expect(listener.mock.calls[0][0].data.state).toEqual(resultState);
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).lastCalledWith(resultState);
});
it('emits option events when options change with tab router', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(TabRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref}>
<TestNavigator>
@@ -455,7 +551,10 @@ it('emits state events when options change', () => {
<Screen name="baz" options={{ v: 3 }}>
{() => (
<TestNavigator>
<Screen name="foo" options={{ g: 5 }}>
<Screen name="qux" options={{ g: 5 }}>
{() => null}
</Screen>
<Screen name="quxx" options={{ h: 9 }}>
{() => null}
</Screen>
</TestNavigator>
@@ -474,19 +573,105 @@ it('emits state events when options change', () => {
ref.current?.navigate('bar');
});
expect(listener.mock.calls[0][0].data.options).toEqual({
y: 2,
});
expect(listener).toBeCalledTimes(1);
expect(listener.mock.calls[0][0].data.options).toEqual({ y: 2 });
expect(ref.current?.getCurrentOptions()).toEqual({ y: 2 });
ref.current?.removeListener('options', listener);
const listener2 = jest.fn();
ref.current?.addListener('options', listener2);
act(() => {
ref.current?.navigate('baz');
});
expect(listener2).toBeCalledTimes(1);
expect(listener2.mock.calls[0][0].data.options).toEqual({ g: 5 });
expect(ref.current?.getCurrentOptions()).toEqual({ g: 5 });
act(() => {
ref.current?.navigate('quxx');
});
expect(listener2).toBeCalledTimes(2);
expect(listener2.mock.calls[1][0].data.options).toEqual({ h: 9 });
expect(ref.current?.getCurrentOptions()).toEqual({ h: 9 });
});
it('emits option events when options change with stack router', () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref}>
<TestNavigator>
<Screen name="foo" options={{ x: 1 }}>
{() => null}
</Screen>
<Screen name="bar" options={{ y: 2 }}>
{() => null}
</Screen>
<Screen name="baz" options={{ v: 3 }}>
{() => (
<TestNavigator>
<Screen name="qux" options={{ g: 5 }}>
{() => null}
</Screen>
<Screen name="quxx" options={{ h: 9 }}>
{() => null}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
const listener = jest.fn();
render(element).update(element);
ref.current?.addListener('options', listener);
act(() => {
ref.current?.navigate('bar');
});
expect(listener).toBeCalledTimes(1);
expect(listener.mock.calls[0][0].data.options).toEqual({ y: 2 });
expect(ref.current?.getCurrentOptions()).toEqual({ y: 2 });
ref.current?.removeListener('options', listener);
const listener2 = jest.fn();
ref.current?.addListener('options', listener2);
act(() => {
ref.current?.navigate('baz');
});
expect(listener2).toBeCalledTimes(1);
expect(listener2.mock.calls[0][0].data.options).toEqual({ g: 5 });
expect(ref.current?.getCurrentOptions()).toEqual({ g: 5 });
act(() => {
ref.current?.navigate('quxx');
});
expect(listener2).toBeCalledTimes(2);
expect(listener2.mock.calls[1][0].data.options).toEqual({ h: 9 });
expect(ref.current?.getCurrentOptions()).toEqual({ h: 9 });
});
it('throws if there is no navigator rendered', () => {

View File

@@ -1,8 +1,8 @@
import isSerializable from '../isSerializable';
import checkSerializable from '../checkSerializable';
it('returns true for serializable object', () => {
expect(
isSerializable({
checkSerializable({
index: 0,
key: '7',
routeNames: ['foo', 'bar'],
@@ -22,12 +22,12 @@ it('returns true for serializable object', () => {
},
],
})
).toBe(true);
).toEqual({ serializable: true });
});
it('returns false for non-serializable object', () => {
expect(
isSerializable({
checkSerializable({
index: 0,
key: '7',
routeNames: ['foo', 'bar'],
@@ -47,7 +47,38 @@ it('returns false for non-serializable object', () => {
},
],
})
).toBe(false);
).toEqual({
serializable: false,
location: ['routes', 0, 'state', 'routes', 0, 'params'],
reason: 'Function',
});
expect(
checkSerializable({
index: 0,
key: '7',
routeNames: ['foo', 'bar'],
routes: [
{
key: 'foo',
name: 'foo',
state: {
index: 0,
key: '8',
routeNames: ['qux', 'lex'],
routes: [
{ key: 'qux', name: 'qux', params: { foo: Symbol('test') } },
{ key: 'lex', name: 'lex' },
],
},
},
],
})
).toEqual({
serializable: false,
location: ['routes', 0, 'state', 'routes', 0, 'params', 'foo'],
reason: 'Symbol(test)',
});
});
it('returns false for circular references', () => {
@@ -59,7 +90,11 @@ it('returns false for circular references', () => {
x.b.b2 = x;
x.c = x.b;
expect(isSerializable(x)).toBe(false);
expect(checkSerializable(x)).toEqual({
serializable: false,
location: ['b', 'b2'],
reason: 'Circular reference',
});
const y: any = [
{
@@ -72,7 +107,11 @@ it('returns false for circular references', () => {
y[0].children[0].parent = y[0];
y[1].extend.home = y[0].children[0];
expect(isSerializable(y)).toBe(false);
expect(checkSerializable(y)).toEqual({
serializable: false,
location: [0, 'children', 0, 'parent'],
reason: 'Circular reference',
});
const z: any = {
name: 'sun',
@@ -81,14 +120,18 @@ it('returns false for circular references', () => {
z.child[0].parent = z;
expect(isSerializable(z)).toBe(false);
expect(checkSerializable(z)).toEqual({
serializable: false,
location: ['child', 0, 'parent'],
reason: 'Circular reference',
});
});
it("doesn't fail if same object used multiple times", () => {
const o = { foo: 'bar' };
expect(
isSerializable({
checkSerializable({
baz: 'bax',
first: o,
second: o,
@@ -96,5 +139,5 @@ it("doesn't fail if same object used multiple times", () => {
b: o,
},
})
).toBe(true);
).toEqual({ serializable: true });
});

View File

@@ -1403,6 +1403,7 @@ it('throws if both children and component are passed', () => {
const element = (
<BaseNavigationContainer>
<TestNavigator>
{/* @ts-ignore */}
<Screen name="foo" component={jest.fn()}>
{jest.fn()}
</Screen>
@@ -1415,6 +1416,48 @@ it('throws if both children and component are passed', () => {
);
});
it('throws if both children and getComponent are passed', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);
return null;
};
const element = (
<BaseNavigationContainer>
<TestNavigator>
{/* @ts-ignore */}
<Screen name="foo" getComponent={jest.fn()}>
{jest.fn()}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
"Got both 'getComponent' and 'children' props for the screen 'foo'. You must pass only one of them."
);
});
it('throws if both component and getComponent are passed', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);
return null;
};
const element = (
<BaseNavigationContainer>
<TestNavigator>
{/* @ts-ignore */}
<Screen name="foo" component={jest.fn()} getComponent={jest.fn()} />
</TestNavigator>
</BaseNavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
"Got both 'component' and 'getComponent' props for the screen 'foo'. You must pass only one of them."
);
});
it('throws descriptive error for undefined screen component', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);
@@ -1430,7 +1473,7 @@ it('throws descriptive error for undefined screen component', () => {
);
expect(() => render(element).update(element)).toThrowError(
"Couldn't find a 'component' or 'children' prop for the screen 'foo'"
"Couldn't find a 'component', 'getComponent' or 'children' prop for the screen 'foo'"
);
});
@@ -1453,6 +1496,25 @@ it('throws descriptive error for invalid screen component', () => {
);
});
it('throws descriptive error for invalid getComponent prop', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);
return null;
};
const element = (
<BaseNavigationContainer>
<TestNavigator>
<Screen name="foo" getComponent={{} as any} />
</TestNavigator>
</BaseNavigationContainer>
);
expect(() => render(element).update(element)).toThrowError(
"Got an invalid value for 'getComponent' prop for the screen 'foo'. It must be a function returning a React Component."
);
});
it('throws descriptive error for invalid children', () => {
const TestNavigator = (props: any) => {
useNavigationBuilder(MockRouter, props);

View File

@@ -1,9 +1,10 @@
import * as React from 'react';
import { render } from 'react-native-testing-library';
import type {
import { act, render } from 'react-native-testing-library';
import {
Router,
DefaultRouterOptions,
NavigationState,
StackRouter,
} from '@react-navigation/routers';
import useNavigationBuilder from '../useNavigationBuilder';
import BaseNavigationContainer from '../BaseNavigationContainer';
@@ -12,8 +13,19 @@ import MockRouter, {
MockActions,
MockRouterKey,
} from './__fixtures__/MockRouter';
import type { NavigationContainerRef } from '../types';
beforeEach(() => (MockRouterKey.current = 0));
jest.mock('nanoid/non-secure', () => {
const m = { nanoid: () => String(++m.__key), __key: 0 };
return m;
});
beforeEach(() => {
MockRouterKey.current = 0;
require('nanoid/non-secure').__key = 0;
});
it("lets parent handle the action if child didn't", () => {
function CurrentRouter(options: DefaultRouterOptions) {
@@ -224,6 +236,147 @@ it("lets children handle the action if parent didn't", () => {
});
});
it('action goes to correct navigator if target is specified', () => {
function CurrentTestRouter(options: DefaultRouterOptions) {
const CurrentMockRouter = MockRouter(options);
const TestRouter: Router<
NavigationState,
MockActions | { type: 'REVERSE' }
> = {
...CurrentMockRouter,
shouldActionChangeFocus() {
return true;
},
getStateForAction(state, action, options) {
if (action.type === 'REVERSE') {
return {
...state,
routes: state.routes.slice().reverse(),
};
}
return CurrentMockRouter.getStateForAction(state, action, options);
},
};
return TestRouter;
}
const ChildNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(
CurrentTestRouter,
props
);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const ParentNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(
CurrentTestRouter,
props
);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const TestScreen = (props: any) => {
React.useEffect(() => {
props.navigation.dispatch({ type: 'REVERSE', target: '0' });
}, [props.navigation]);
return null;
};
const initialState = {
stale: false,
type: 'test',
index: 1,
key: '0',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{
key: 'baz',
name: 'baz',
state: {
stale: false,
type: 'test',
index: 0,
key: '1',
routeNames: ['qux', 'lex'],
routes: [
{ key: 'lex', name: 'lex' },
{ key: 'qux', name: 'qux' },
],
},
},
{ key: 'bar', name: 'bar' },
{ key: 'foo', name: 'foo' },
],
};
const onStateChange = jest.fn();
const element = (
<BaseNavigationContainer
initialState={initialState}
onStateChange={onStateChange}
>
<ParentNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar">{() => null}</Screen>
<Screen name="baz">
{() => (
<ChildNavigator>
<Screen name="qux">{() => null}</Screen>
<Screen name="lex" component={TestScreen} />
</ChildNavigator>
)}
</Screen>
</ParentNavigator>
</BaseNavigationContainer>
);
render(element).update(element);
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).toBeCalledWith({
stale: false,
type: 'test',
index: 1,
key: '0',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo', name: 'foo' },
{ key: 'bar', name: 'bar' },
{
key: 'baz',
name: 'baz',
state: {
stale: false,
type: 'test',
index: 0,
key: '1',
routeNames: ['qux', 'lex'],
routes: [
{ key: 'lex', name: 'lex' },
{ key: 'qux', name: 'qux' },
],
},
},
],
});
});
it("action doesn't bubble if target is specified", () => {
const CurrentParentRouter = MockRouter;
@@ -379,3 +532,649 @@ it('logs error if no navigator handled the action', () => {
spy.mockRestore();
});
it("prevents removing a screen with 'beforeRemove' event", () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const onBeforeRemove = jest.fn();
let shouldPrevent = true;
let shouldContinue = false;
const TestScreen = (props: any) => {
React.useEffect(
() =>
props.navigation.addListener('beforeRemove', (e: any) => {
onBeforeRemove();
if (shouldPrevent) {
e.preventDefault();
if (shouldContinue) {
props.navigation.dispatch(e.data.action);
}
}
}),
[props.navigation]
);
return null;
};
const onStateChange = jest.fn();
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar" component={TestScreen} />
<Screen name="baz">{() => null}</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
act(() => ref.current?.navigate('bar'));
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).toBeCalledWith({
index: 1,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
],
stale: false,
type: 'stack',
});
act(() => ref.current?.navigate('baz'));
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).toBeCalledWith({
index: 2,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{
key: 'baz-5',
name: 'baz',
},
],
stale: false,
type: 'stack',
});
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(2);
expect(onBeforeRemove).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual({
index: 2,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{ key: 'baz-5', name: 'baz' },
],
stale: false,
type: 'stack',
});
shouldPrevent = false;
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(3);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
shouldPrevent = true;
shouldContinue = true;
act(() => ref.current?.navigate('bar'));
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(5);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
});
it("prevents removing a child screen with 'beforeRemove' event", () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const onBeforeRemove = jest.fn();
let shouldPrevent = true;
let shouldContinue = false;
const TestScreen = (props: any) => {
React.useEffect(
() =>
props.navigation.addListener('beforeRemove', (e: any) => {
onBeforeRemove();
if (shouldPrevent) {
e.preventDefault();
if (shouldContinue) {
props.navigation.dispatch(e.data.action);
}
}
}),
[props.navigation]
);
return null;
};
const onStateChange = jest.fn();
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar">{() => null}</Screen>
<Screen name="baz">
{() => (
<TestNavigator>
<Screen name="qux" component={TestScreen} />
<Screen name="lex">{() => null}</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
act(() => ref.current?.navigate('bar'));
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).toBeCalledWith({
index: 1,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
],
stale: false,
type: 'stack',
});
act(() => ref.current?.navigate('baz'));
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).toBeCalledWith({
index: 2,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{
key: 'baz-5',
name: 'baz',
state: {
index: 0,
key: 'stack-7',
routeNames: ['qux', 'lex'],
routes: [{ key: 'qux-8', name: 'qux' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
});
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(2);
expect(onBeforeRemove).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual({
index: 2,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{
key: 'baz-5',
name: 'baz',
state: {
index: 0,
key: 'stack-7',
routeNames: ['qux', 'lex'],
routes: [{ key: 'qux-8', name: 'qux' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
});
shouldPrevent = false;
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(3);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
shouldPrevent = true;
shouldContinue = true;
act(() => ref.current?.navigate('bar'));
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(5);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
});
it("prevents removing a grand child screen with 'beforeRemove' event", () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const onBeforeRemove = jest.fn();
let shouldPrevent = true;
let shouldContinue = false;
const TestScreen = (props: any) => {
React.useEffect(
() =>
props.navigation.addListener('beforeRemove', (e: any) => {
onBeforeRemove();
if (shouldPrevent) {
e.preventDefault();
if (shouldContinue) {
props.navigation.dispatch(e.data.action);
}
}
}),
[props.navigation]
);
return null;
};
const onStateChange = jest.fn();
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar">{() => null}</Screen>
<Screen name="baz">
{() => (
<TestNavigator>
<Screen name="qux">
{() => (
<TestNavigator>
<Screen name="lex" component={TestScreen} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
act(() => ref.current?.navigate('bar'));
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).toBeCalledWith({
index: 1,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
],
stale: false,
type: 'stack',
});
act(() => ref.current?.navigate('baz'));
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).toBeCalledWith({
index: 2,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{
key: 'baz-5',
name: 'baz',
state: {
index: 0,
key: 'stack-7',
routeNames: ['qux'],
routes: [
{
key: 'qux-8',
name: 'qux',
state: {
index: 0,
key: 'stack-10',
routeNames: ['lex'],
routes: [{ key: 'lex-11', name: 'lex' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
});
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(2);
expect(onBeforeRemove).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual({
index: 2,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{
key: 'baz-5',
name: 'baz',
state: {
index: 0,
key: 'stack-7',
routeNames: ['qux'],
routes: [
{
key: 'qux-8',
name: 'qux',
state: {
index: 0,
key: 'stack-10',
routeNames: ['lex'],
routes: [{ key: 'lex-11', name: 'lex' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
});
shouldPrevent = false;
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(3);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
shouldPrevent = true;
shouldContinue = true;
act(() => ref.current?.navigate('bar'));
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(5);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
});
it("prevents removing by multiple screens with 'beforeRemove' event", () => {
const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(StackRouter, props);
return (
<React.Fragment>
{state.routes.map((route) => descriptors[route.key].render())}
</React.Fragment>
);
};
const onBeforeRemove = {
bar: jest.fn(),
baz: jest.fn(),
lex: jest.fn(),
};
const shouldPrevent = {
bar: true,
baz: true,
lex: true,
};
const TestScreen = (props: any) => {
React.useEffect(
() =>
props.navigation.addListener('beforeRemove', (e: any) => {
// @ts-expect-error: we should have the required mocks
onBeforeRemove[props.route.name]();
e.preventDefault();
// @ts-expect-error: we should have the required properties
if (!shouldPrevent[props.route.name]) {
props.navigation.dispatch(e.data.action);
}
}),
[props.navigation, props.route.name]
);
return null;
};
const onStateChange = jest.fn();
const ref = React.createRef<NavigationContainerRef>();
const element = (
<BaseNavigationContainer ref={ref} onStateChange={onStateChange}>
<TestNavigator>
<Screen name="foo">{() => null}</Screen>
<Screen name="bar" component={TestScreen} />
<Screen name="baz" component={TestScreen} />
<Screen name="bax">
{() => (
<TestNavigator>
<Screen name="qux">
{() => (
<TestNavigator>
<Screen name="lex" component={TestScreen} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(element);
act(() => {
ref.current?.navigate('bar');
ref.current?.navigate('baz');
ref.current?.navigate('bax');
});
const preventedState = {
index: 3,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz', 'bax'],
routes: [
{ key: 'foo-3', name: 'foo' },
{ key: 'bar-4', name: 'bar' },
{ key: 'baz-5', name: 'baz' },
{
key: 'bax-6',
name: 'bax',
state: {
index: 0,
key: 'stack-8',
routeNames: ['qux'],
routes: [
{
key: 'qux-9',
name: 'qux',
state: {
index: 0,
key: 'stack-11',
routeNames: ['lex'],
routes: [{ key: 'lex-12', name: 'lex' }],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
},
},
],
stale: false,
type: 'stack',
};
expect(onStateChange).toBeCalledTimes(1);
expect(onStateChange).toBeCalledWith(preventedState);
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(1);
expect(onBeforeRemove.lex).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual(preventedState);
shouldPrevent.lex = false;
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(1);
expect(onBeforeRemove.baz).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual(preventedState);
shouldPrevent.baz = false;
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(1);
expect(onBeforeRemove.bar).toBeCalledTimes(1);
expect(ref.current?.getRootState()).toEqual(preventedState);
shouldPrevent.bar = false;
act(() => ref.current?.navigate('foo'));
expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).toBeCalledWith({
index: 0,
key: 'stack-2',
routeNames: ['foo', 'bar', 'baz', 'bax'],
routes: [{ key: 'foo-3', name: 'foo' }],
stale: false,
type: 'stack',
});
});

View File

@@ -0,0 +1,74 @@
const checkSerializableWithoutCircularReference = (
o: { [key: string]: any },
seen: Set<any>,
location: (string | number)[]
):
| { serializable: true }
| {
serializable: false;
location: (string | number)[];
reason: string;
} => {
if (
o === undefined ||
o === null ||
typeof o === 'boolean' ||
typeof o === 'number' ||
typeof o === 'string'
) {
return { serializable: true };
}
if (
Object.prototype.toString.call(o) !== '[object Object]' &&
!Array.isArray(o)
) {
return {
serializable: false,
location,
reason: typeof o === 'function' ? 'Function' : String(o),
};
}
if (seen.has(o)) {
return {
serializable: false,
reason: 'Circular reference',
location,
};
}
seen.add(o);
if (Array.isArray(o)) {
for (let i = 0; i < o.length; i++) {
const childResult = checkSerializableWithoutCircularReference(
o[i],
new Set<any>(seen),
[...location, i]
);
if (!childResult.serializable) {
return childResult;
}
}
} else {
for (const key in o) {
const childResult = checkSerializableWithoutCircularReference(
o[key],
new Set<any>(seen),
[...location, key]
);
if (!childResult.serializable) {
return childResult;
}
}
}
return { serializable: true };
};
export default function checkSerializable(o: { [key: string]: any }) {
return checkSerializableWithoutCircularReference(o, new Set<any>(), []);
}

View File

@@ -260,6 +260,7 @@ export default function getStateFromPath(
);
if (params) {
// @ts-expect-error: params should be treated as read-only, but we're creating the state here so it doesn't matter
route.params = { ...route.params, ...params };
}

View File

@@ -1,47 +0,0 @@
const isSerializableWithoutCircularReference = (
o: { [key: string]: any },
seen: Set<any>
): boolean => {
if (
o === undefined ||
o === null ||
typeof o === 'boolean' ||
typeof o === 'number' ||
typeof o === 'string'
) {
return true;
}
if (
Object.prototype.toString.call(o) !== '[object Object]' &&
!Array.isArray(o)
) {
return false;
}
if (seen.has(o)) {
return false;
}
seen.add(o);
if (Array.isArray(o)) {
for (const it of o) {
if (!isSerializableWithoutCircularReference(it, new Set<any>(seen))) {
return false;
}
}
} else {
for (const key in o) {
if (!isSerializableWithoutCircularReference(o[key], new Set<any>(seen))) {
return false;
}
}
}
return true;
};
export default function isSerializable(o: { [key: string]: any }) {
return isSerializableWithoutCircularReference(o, new Set<any>());
}

View File

@@ -37,6 +37,7 @@ export type EventMapCore<State extends NavigationState> = {
focus: { data: undefined };
blur: { data: undefined };
state: { data: { state: State } };
beforeRemove: { data: { action: NavigationAction }; canPreventDefault: true };
};
export type EventArg<
@@ -61,7 +62,9 @@ export type EventArg<
preventDefault(): void;
}
: {}) &
(undefined extends Data ? {} : { readonly data: Data });
(undefined extends Data
? { readonly data?: Readonly<Data> }
: { readonly data: Readonly<Data> });
export type EventListenerCallback<
EventMap extends EventMapBase,
@@ -108,7 +111,7 @@ export type EventEmitter<EventMap extends EventMapBase> = {
? { canPreventDefault: true }
: {}) &
(undefined extends EventMap[EventName]['data']
? {}
? { data?: EventMap[EventName]['data'] }
: { data: EventMap[EventName]['data'] })
): EventArg<
EventName,
@@ -276,13 +279,18 @@ export type RouteProp<
RouteName extends keyof ParamList
> = Omit<Route<Extract<RouteName, string>>, 'params'> &
(undefined extends ParamList[RouteName]
? {}
: {
? Readonly<{
/**
* Params for this route
*/
params: ParamList[RouteName];
});
params?: Readonly<ParamList[RouteName]>;
}>
: Readonly<{
/**
* Params for this route
*/
params: Readonly<ParamList[RouteName]>;
}>);
export type CompositeNavigationProp<
A extends NavigationProp<ParamListBase, string, any, any>,
@@ -398,6 +406,16 @@ export type RouteConfig<
* React component to render for this screen.
*/
component: React.ComponentType<any>;
getComponent?: never;
children?: never;
}
| {
/**
* Lazily get a React component to render for this screen.
*/
getComponent: () => React.ComponentType<any>;
component?: never;
children?: never;
}
| {
/**
@@ -407,6 +425,8 @@ export type RouteConfig<
route: RouteProp<ParamList, RouteName>;
navigation: any;
}) => React.ReactNode;
component?: never;
getComponent?: never;
}
);

View File

@@ -1,27 +0,0 @@
import * as React from 'react';
import type { ChildActionListener } from './NavigationBuilderContext';
/**
* Hook which lets child navigators add action listeners.
*/
export default function useChildActionListeners() {
const { current: listeners } = React.useRef<ChildActionListener[]>([]);
const addListener = React.useCallback(
(listener: ChildActionListener) => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
},
[listeners]
);
return {
listeners,
addListener,
};
}

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import type { ListenerMap } from './NavigationBuilderContext';
/**
* Hook which lets child navigators add action listeners.
*/
export default function useChildListeners() {
const { current: listeners } = React.useRef<
{
[K in keyof ListenerMap]: ListenerMap[K][];
}
>({
action: [],
focus: [],
});
const addListener = React.useCallback(
<T extends keyof ListenerMap>(type: T, listener: ListenerMap[T]) => {
// @ts-expect-error: listener should be correct type according to `type`
listeners[type].push(listener);
return () => {
// @ts-expect-error: listener should be correct type according to `type`
const index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
};
},
[listeners]
);
return {
listeners,
addListener,
};
}

View File

@@ -7,12 +7,13 @@ import type {
} from '@react-navigation/routers';
import SceneView from './SceneView';
import NavigationBuilderContext, {
ChildActionListener,
FocusedNavigationListener,
NavigatorStateGetter,
AddListener,
AddKeyedListener,
} from './NavigationBuilderContext';
import type { NavigationEventEmitter } from './useEventEmitter';
import useNavigationCache from './useNavigationCache';
import NavigationContext from './NavigationContext';
import NavigationRouteContext from './NavigationRouteContext';
import type {
Descriptor,
NavigationHelpers,
@@ -20,8 +21,6 @@ import type {
RouteProp,
EventMapBase,
} from './types';
import NavigationContext from './NavigationContext';
import NavigationRouteContext from './NavigationRouteContext';
type Options<
State extends NavigationState,
@@ -46,9 +45,8 @@ type Options<
) => boolean;
getState: () => State;
setState: (state: State) => void;
addActionListener: (listener: ChildActionListener) => void;
addFocusedListener: (listener: FocusedNavigationListener) => void;
addStateGetter: (key: string, getter: NavigatorStateGetter) => void;
addListener: AddListener;
addKeyedListener: AddKeyedListener;
onRouteFocus: (key: string) => void;
router: Router<State, NavigationAction>;
emitter: NavigationEventEmitter<any>;
@@ -74,9 +72,8 @@ export default function useDescriptors<
onAction,
getState,
setState,
addActionListener,
addFocusedListener,
addStateGetter,
addListener,
addKeyedListener,
onRouteFocus,
router,
emitter,
@@ -90,21 +87,19 @@ export default function useDescriptors<
() => ({
navigation,
onAction,
addActionListener,
addFocusedListener,
addStateGetter,
addListener,
addKeyedListener,
onRouteFocus,
onDispatchAction,
onOptionsChange,
}),
[
addActionListener,
addFocusedListener,
addStateGetter,
navigation,
onAction,
onDispatchAction,
addListener,
addKeyedListener,
onRouteFocus,
onDispatchAction,
onOptionsChange,
]
);

View File

@@ -1,27 +0,0 @@
import * as React from 'react';
import type { FocusedNavigationListener } from './NavigationBuilderContext';
/**
* Hook which lets child navigators add listeners to be called for focused navigators.
*/
export default function useFocusedListeners() {
const { current: listeners } = React.useRef<FocusedNavigationListener[]>([]);
const addListener = React.useCallback(
(listener: FocusedNavigationListener) => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
},
[listeners]
);
return {
listeners,
addListener,
};
}

View File

@@ -18,7 +18,7 @@ export default function useFocusedListenersChildrenAdapter({
navigation,
focusedListeners,
}: Options) {
const { addFocusedListener } = React.useContext(NavigationBuilderContext);
const { addListener } = React.useContext(NavigationBuilderContext);
const listener = React.useCallback(
(callback: FocusedNavigationCallback<any>) => {
@@ -39,8 +39,8 @@ export default function useFocusedListenersChildrenAdapter({
[focusedListeners, navigation]
);
React.useEffect(() => addFocusedListener?.(listener), [
addFocusedListener,
React.useEffect(() => addListener?.('focus', listener), [
addListener,
listener,
]);
}

View File

@@ -0,0 +1,42 @@
import * as React from 'react';
import type { KeyedListenerMap } from './NavigationBuilderContext';
/**
* Hook which lets child navigators add getters to be called for obtaining rehydrated state.
*/
export default function useKeyedChildListeners() {
const { current: keyedListeners } = React.useRef<
{
[K in keyof KeyedListenerMap]: Record<
string,
KeyedListenerMap[K] | undefined
>;
}
>({
getState: {},
beforeRemove: {},
});
const addKeyedListener = React.useCallback(
<T extends keyof KeyedListenerMap>(
type: T,
key: string,
listener: KeyedListenerMap[T]
) => {
// @ts-expect-error: listener should be correct type according to `type`
keyedListeners[type][key] = listener;
return () => {
// @ts-expect-error: listener should be correct type according to `type`
keyedListeners[type][key] = undefined;
};
},
[keyedListeners]
);
return {
keyedListeners,
addKeyedListener,
};
}

View File

@@ -21,8 +21,7 @@ import useNavigationHelpers from './useNavigationHelpers';
import useOnAction from './useOnAction';
import useFocusEvents from './useFocusEvents';
import useOnRouteFocus from './useOnRouteFocus';
import useChildActionListeners from './useChildActionListeners';
import useFocusedListeners from './useFocusedListeners';
import useChildListeners from './useChildListeners';
import useFocusedListenersChildrenAdapter from './useFocusedListenersChildrenAdapter';
import {
DefaultNavigatorOptions,
@@ -31,7 +30,7 @@ import {
EventMapBase,
EventMapCore,
} from './types';
import useStateGetters from './useStateGetters';
import useKeyedChildListeners from './useKeyedChildListeners';
import useOnGetState from './useOnGetState';
import useScheduleUpdate from './useScheduleUpdate';
import useCurrentRender from './useCurrentRender';
@@ -103,7 +102,7 @@ const getRouteConfigsFromChildren = <
if (process.env.NODE_ENV !== 'production') {
configs.forEach((config) => {
const { name, children, component } = config as any;
const { name, children, component, getComponent } = config;
if (typeof name !== 'string' || !name) {
throw new Error(
@@ -113,13 +112,29 @@ const getRouteConfigsFromChildren = <
);
}
if (children != null || component !== undefined) {
if (
children != null ||
component !== undefined ||
getComponent !== undefined
) {
if (children != null && component !== undefined) {
throw new Error(
`Got both 'component' and 'children' props for the screen '${name}'. You must pass only one of them.`
);
}
if (children != null && getComponent !== undefined) {
throw new Error(
`Got both 'getComponent' and 'children' props for the screen '${name}'. You must pass only one of them.`
);
}
if (component !== undefined && getComponent !== undefined) {
throw new Error(
`Got both 'component' and 'getComponent' props for the screen '${name}'. You must pass only one of them.`
);
}
if (children != null && typeof children !== 'function') {
throw new Error(
`Got an invalid value for 'children' prop for the screen '${name}'. It must be a function returning a React Element.`
@@ -132,6 +147,12 @@ const getRouteConfigsFromChildren = <
);
}
if (getComponent !== undefined && typeof getComponent !== 'function') {
throw new Error(
`Got an invalid value for 'getComponent' prop for the screen '${name}'. It must be a function returning a React Component.`
);
}
if (typeof component === 'function' && component.name === 'component') {
// Inline anonymous functions passed in the `component` prop will have the name of the prop
// It's relatively safe to assume that it's not a component since it should also have PascalCase name
@@ -142,7 +163,7 @@ const getRouteConfigsFromChildren = <
}
} else {
throw new Error(
`Couldn't find a 'component' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.`
`Couldn't find a 'component', 'getComponent' or 'children' prop for the screen '${name}'. This can happen if you passed 'undefined'. You likely forgot to export your component from the file it's defined in, or mixed up default import and named import when importing.`
);
}
});
@@ -258,6 +279,7 @@ export default function useNavigationBuilder<
setState,
setKey,
getKey,
getIsInitial,
} = React.useContext(NavigationStateContext);
const [initializedState, isFirstStateInitialization] = React.useMemo(() => {
@@ -351,6 +373,13 @@ export default function useNavigationBuilder<
React.useEffect(() => {
setKey(navigatorKey);
if (!getIsInitial()) {
// If it's not initial render, we need to update the state
// This will make sure that our container gets notifier of state changes due to new mounts
// This is necessary for proper screen tracking, URL updates etc.
setState(nextState);
}
return () => {
// We need to clean up state for this navigator on unmount
// We do it in a timeout because we need to detect if another navigator mounted in the meantime
@@ -430,28 +459,22 @@ export default function useNavigationBuilder<
emitter.emit({ type: 'state', data: { state } });
}, [emitter, state]);
const {
listeners: actionListeners,
addListener: addActionListener,
} = useChildActionListeners();
const { listeners: childListeners, addListener } = useChildListeners();
const {
listeners: focusedListeners,
addListener: addFocusedListener,
} = useFocusedListeners();
const { getStateForRoute, addStateGetter } = useStateGetters();
const { keyedListeners, addKeyedListener } = useKeyedChildListeners();
const onAction = useOnAction({
router,
getState,
setState,
key: route?.key,
listeners: actionListeners,
actionListeners: childListeners.action,
beforeRemoveListeners: keyedListeners.beforeRemove,
routerConfigOptions: {
routeNames,
routeParamList,
},
emitter,
});
const onRouteFocus = useOnRouteFocus({
@@ -470,12 +493,12 @@ export default function useNavigationBuilder<
useFocusedListenersChildrenAdapter({
navigation,
focusedListeners,
focusedListeners: childListeners.focus,
});
useOnGetState({
getState,
getStateForRoute,
getStateListeners: keyedListeners.getState,
});
const descriptors = useDescriptors<State, ScreenOptions, EventMap>({
@@ -487,9 +510,8 @@ export default function useNavigationBuilder<
getState,
setState,
onRouteFocus,
addActionListener,
addFocusedListener,
addStateGetter,
addListener,
addKeyedListener,
router,
emitter,
});

View File

@@ -8,15 +8,21 @@ import type {
} from '@react-navigation/routers';
import NavigationBuilderContext, {
ChildActionListener,
ChildBeforeRemoveListener,
} from './NavigationBuilderContext';
import useOnPreventRemove, { shouldPreventRemove } from './useOnPreventRemove';
import type { NavigationEventEmitter } from './useEventEmitter';
import type { EventMapCore } from './types';
type Options = {
router: Router<NavigationState, NavigationAction>;
key?: string;
getState: () => NavigationState;
setState: (state: NavigationState | PartialState<NavigationState>) => void;
listeners: ChildActionListener[];
actionListeners: ChildActionListener[];
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
routerConfigOptions: RouterConfigOptions;
emitter: NavigationEventEmitter<EventMapCore<any>>;
};
/**
@@ -33,13 +39,15 @@ export default function useOnAction({
getState,
setState,
key,
listeners,
actionListeners,
beforeRemoveListeners,
routerConfigOptions,
emitter,
}: Options) {
const {
onAction: onActionParent,
onRouteFocus: onRouteFocusParent,
addActionListener: addActionListenerParent,
addListener: addListenerParent,
onDispatchAction,
} = React.useContext(NavigationBuilderContext);
@@ -66,38 +74,56 @@ export default function useOnAction({
visitedNavigators.add(state.key);
if (typeof action.target === 'string' && action.target !== state.key) {
return false;
}
if (typeof action.target !== 'string' || action.target === state.key) {
let result = router.getStateForAction(
state,
action,
routerConfigOptionsRef.current
);
let result = router.getStateForAction(
state,
action,
routerConfigOptionsRef.current
);
// If a target is specified and set to current navigator, the action shouldn't bubble
// So instead of `null`, we use the state object for such cases to signal that action was handled
result =
result === null && action.target === state.key ? state : result;
// If a target is specified and set to current navigator, the action shouldn't bubble
// So instead of `null`, we use the state object for such cases to signal that action was handled
result = result === null && action.target === state.key ? state : result;
if (result !== null) {
onDispatchAction(action, state === result);
if (result !== null) {
onDispatchAction(action, state === result);
if (state !== result) {
const nextRouteKeys = (result.routes as any[]).map(
(route: { key?: string }) => route.key
);
if (state !== result) {
setState(result);
}
const removedRoutes = state.routes.filter(
(route) => !nextRouteKeys.includes(route.key)
);
if (onRouteFocusParent !== undefined) {
// Some actions such as `NAVIGATE` also want to bring the navigated route to focus in the whole tree
// This means we need to focus all of the parent navigators of this navigator as well
const shouldFocus = router.shouldActionChangeFocus(action);
const isPrevented = shouldPreventRemove(
emitter,
beforeRemoveListeners,
removedRoutes,
action
);
if (shouldFocus && key !== undefined) {
onRouteFocusParent(key);
if (isPrevented) {
return true;
}
setState(result);
}
}
return true;
if (onRouteFocusParent !== undefined) {
// Some actions such as `NAVIGATE` also want to bring the navigated route to focus in the whole tree
// This means we need to focus all of the parent navigators of this navigator as well
const shouldFocus = router.shouldActionChangeFocus(action);
if (shouldFocus && key !== undefined) {
onRouteFocusParent(key);
}
}
return true;
}
}
if (onActionParent !== undefined) {
@@ -108,8 +134,8 @@ export default function useOnAction({
}
// If the action wasn't handled by current navigator or a parent navigator, let children handle it
for (let i = listeners.length - 1; i >= 0; i--) {
const listener = listeners[i];
for (let i = actionListeners.length - 1; i >= 0; i--) {
const listener = actionListeners[i];
if (listener(action, visitedNavigators)) {
return true;
@@ -119,19 +145,27 @@ export default function useOnAction({
return false;
},
[
actionListeners,
beforeRemoveListeners,
emitter,
getState,
router,
key,
onActionParent,
onDispatchAction,
onRouteFocusParent,
router,
setState,
key,
listeners,
]
);
React.useEffect(() => addActionListenerParent?.(onAction), [
addActionListenerParent,
useOnPreventRemove({
getState,
emitter,
beforeRemoveListeners,
});
React.useEffect(() => addListenerParent?.('action', onAction), [
addListenerParent,
onAction,
]);

View File

@@ -1,17 +1,21 @@
import * as React from 'react';
import type { NavigationState } from '@react-navigation/routers';
import NavigationBuilderContext from './NavigationBuilderContext';
import NavigationBuilderContext, {
GetStateListener,
} from './NavigationBuilderContext';
import NavigationRouteContext from './NavigationRouteContext';
import isArrayEqual from './isArrayEqual';
export default function useOnGetState({
getStateForRoute,
getState,
}: {
getStateForRoute: (routeName: string) => NavigationState | undefined;
type Options = {
getState: () => NavigationState;
}) {
const { addStateGetter } = React.useContext(NavigationBuilderContext);
getStateListeners: Record<string, GetStateListener | undefined>;
};
export default function useOnGetState({
getState,
getStateListeners,
}: Options) {
const { addKeyedListener } = React.useContext(NavigationBuilderContext);
const route = React.useContext(NavigationRouteContext);
const key = route ? route.key : 'root';
@@ -20,7 +24,7 @@ export default function useOnGetState({
// Avoid returning new route objects if we don't need to
const routes = state.routes.map((route) => {
const childState = getStateForRoute(route.key);
const childState = getStateListeners[route.key]?.();
if (route.state === childState) {
return route;
@@ -34,9 +38,9 @@ export default function useOnGetState({
}
return { ...state, routes };
}, [getState, getStateForRoute]);
}, [getState, getStateListeners]);
React.useEffect(() => {
return addStateGetter?.(key, getRehydratedState);
}, [addStateGetter, getRehydratedState, key]);
return addKeyedListener?.('getState', key, getRehydratedState);
}, [addKeyedListener, getRehydratedState, key]);
}

View File

@@ -0,0 +1,93 @@
import * as React from 'react';
import type {
NavigationState,
Route,
NavigationAction,
} from '@react-navigation/routers';
import NavigationBuilderContext, {
ChildBeforeRemoveListener,
} from './NavigationBuilderContext';
import NavigationRouteContext from './NavigationRouteContext';
import type { NavigationEventEmitter } from './useEventEmitter';
import type { EventMapCore } from './types';
type Options = {
getState: () => NavigationState;
emitter: NavigationEventEmitter<EventMapCore<any>>;
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
};
const VISITED_ROUTE_KEYS = Symbol('VISITED_ROUTE_KEYS');
export const shouldPreventRemove = (
emitter: NavigationEventEmitter<EventMapCore<any>>,
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>,
routes: Route<string>[],
action: NavigationAction
) => {
// Call these in reverse order so last screens handle the event first
const reversedRoutes = [...routes].reverse();
const visitedRouteKeys: Set<string> =
// @ts-expect-error: add this property to mark that we've already emitted this action
action[VISITED_ROUTE_KEYS] ?? new Set<string>();
const beforeRemoveAction = {
...action,
[VISITED_ROUTE_KEYS]: visitedRouteKeys,
};
for (const route of reversedRoutes) {
if (visitedRouteKeys.has(route.key)) {
// Skip if we've already emitted this action for this screen
continue;
}
// First, we need to check if any child screens want to prevent it
const isPrevented = beforeRemoveListeners[route.key]?.(beforeRemoveAction);
if (isPrevented) {
return true;
}
visitedRouteKeys.add(route.key);
const event = emitter.emit({
type: 'beforeRemove',
target: route.key,
data: { action: beforeRemoveAction },
canPreventDefault: true,
});
if (event.defaultPrevented) {
return true;
}
}
return false;
};
export default function useOnPreventRemove({
getState,
emitter,
beforeRemoveListeners,
}: Options) {
const { addKeyedListener } = React.useContext(NavigationBuilderContext);
const route = React.useContext(NavigationRouteContext);
const routeKey = route?.key;
React.useEffect(() => {
if (routeKey) {
return addKeyedListener?.('beforeRemove', routeKey, (action) => {
const state = getState();
return shouldPreventRemove(
emitter,
beforeRemoveListeners,
state.routes,
action
);
});
}
}, [addKeyedListener, beforeRemoveListeners, emitter, getState, routeKey]);
}

View File

@@ -1,56 +1,76 @@
import * as React from 'react';
import type { ParamListBase, NavigationState } from '@react-navigation/routers';
import NavigationStateContext from './NavigationStateContext';
import type { NavigationState } from '@react-navigation/routers';
import NavigationBuilderContext from './NavigationBuilderContext';
import type { NavigationProp } from './types';
type Options = {
key?: string;
navigation?: NavigationProp<ParamListBase, string, NavigationState, object>;
options?: object | undefined;
};
export default function useOptionsGetters({
key,
getOptions,
getState,
}: {
key?: string;
getOptions?: () => object | undefined;
getState?: () => NavigationState;
}) {
let [
numberOfChildrenListeners,
setNumberOfChildrenListeners,
] = React.useState(0);
const optionsGettersFromChild = React.useRef<
options,
navigation,
}: Options) {
const optionsRef = React.useRef<object | undefined>(options);
const optionsGettersFromChildRef = React.useRef<
Record<string, () => object | undefined | null>
>({});
const { onOptionsChange } = React.useContext(NavigationBuilderContext);
const { addOptionsGetter: parentAddOptionsGetter } = React.useContext(
NavigationStateContext
);
const optionsChangeListener = React.useCallback(() => {
const isFocused = navigation?.isFocused() ?? true;
const hasChildren = Object.keys(optionsGettersFromChildRef.current).length;
if (isFocused && !hasChildren) {
onOptionsChange(optionsRef.current ?? {});
}
}, [navigation, onOptionsChange]);
React.useEffect(() => {
optionsRef.current = options;
optionsChangeListener();
return navigation?.addListener('focus', optionsChangeListener);
}, [navigation, options, optionsChangeListener]);
const getOptionsFromListener = React.useCallback(() => {
for (let key in optionsGettersFromChild.current) {
if (optionsGettersFromChild.current.hasOwnProperty(key)) {
const result = optionsGettersFromChild.current[key]?.();
for (let key in optionsGettersFromChildRef.current) {
if (optionsGettersFromChildRef.current.hasOwnProperty(key)) {
const result = optionsGettersFromChildRef.current[key]?.();
// null means unfocused route
if (result !== null) {
return result;
}
}
}
return null;
}, []);
const getCurrentOptions = React.useCallback(() => {
if (getState) {
const state = getState();
if (state.routes[state.index].key !== key) {
// null means unfocused route
return null;
}
const isFocused = navigation?.isFocused() ?? true;
if (!isFocused) {
return null;
}
const optionsFromListener = getOptionsFromListener();
if (optionsFromListener !== null) {
return optionsFromListener;
}
return getOptions?.() ?? undefined;
}, [getState, getOptionsFromListener, getOptions, key]);
return optionsRef.current;
}, [navigation, getOptionsFromListener]);
React.useEffect(() => {
return parentAddOptionsGetter?.(key!, getCurrentOptions);
@@ -58,26 +78,20 @@ export default function useOptionsGetters({
const addOptionsGetter = React.useCallback(
(key: string, getter: () => object | undefined | null) => {
optionsGettersFromChild.current[key] = getter;
setNumberOfChildrenListeners((prev) => prev + 1);
optionsGettersFromChildRef.current[key] = getter;
optionsChangeListener();
return () => {
setNumberOfChildrenListeners((prev) => prev - 1);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete optionsGettersFromChild.current[key];
delete optionsGettersFromChildRef.current[key];
optionsChangeListener();
};
},
[]
);
const hasAnyChildListener = React.useMemo(
() => numberOfChildrenListeners > 0,
[numberOfChildrenListeners]
[optionsChangeListener]
);
return {
addOptionsGetter,
getCurrentOptions,
hasAnyChildListener,
};
}

View File

@@ -1,36 +0,0 @@
import * as React from 'react';
import type { NavigatorStateGetter } from './NavigationBuilderContext';
/**
* Hook which lets child navigators add getters to be called for obtaining rehydrated state.
*/
export default function useStateGetters() {
const stateGetters = React.useRef<
Record<string, NavigatorStateGetter | undefined>
>({});
const getStateForRoute = React.useCallback(
(routeKey: string) => {
const getter = stateGetters.current[routeKey];
return getter === undefined ? undefined : getter();
},
[stateGetters]
);
const addStateGetter = React.useCallback(
(key: string, getter: NavigatorStateGetter) => {
stateGetters.current[key] = getter;
return () => {
stateGetters.current[key] = undefined;
};
},
[]
);
return {
getStateForRoute,
addStateGetter,
};
}

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.3...@react-navigation/devtools@5.1.4) (2020-07-28)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.2...@react-navigation/devtools@5.1.3) (2020-07-19)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.1...@react-navigation/devtools@5.1.2) (2020-07-10)
**Note:** Version bump only for package @react-navigation/devtools
## [5.1.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@5.1.0...@react-navigation/devtools@5.1.1) (2020-06-25)
**Note:** Version bump only for package @react-navigation/devtools
# 5.1.0 (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/devtools",
"description": "Developer tools for React Navigation",
"version": "5.1.0",
"version": "5.1.4",
"keywords": [
"react",
"react-native",
@@ -36,7 +36,7 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^5.11.0",
"@react-navigation/core": "^5.12.2",
"deep-equal": "^2.0.3"
},
"devDependencies": {

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.8.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.6...@react-navigation/drawer@5.8.7) (2020-07-28)
### Bug Fixes
* add accessibilityState property ([#8548](https://github.com/react-navigation/react-navigation/issues/8548)) ([ce4eb7e](https://github.com/react-navigation/react-navigation/commit/ce4eb7e9273a25e4433eb82e255a58ba3bf4d632))
## [5.8.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.5...@react-navigation/drawer@5.8.6) (2020-07-19)
**Note:** Version bump only for package @react-navigation/drawer
## [5.8.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.4...@react-navigation/drawer@5.8.5) (2020-07-10)
**Note:** Version bump only for package @react-navigation/drawer
## [5.8.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.3...@react-navigation/drawer@5.8.4) (2020-06-25)
**Note:** Version bump only for package @react-navigation/drawer
## [5.8.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@5.8.2...@react-navigation/drawer@5.8.3) (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.8.3",
"version": "5.8.7",
"keywords": [
"react-native-component",
"react-component",
@@ -46,7 +46,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.15.1",
"@react-navigation/native": "^5.6.0",
"@react-navigation/native": "^5.7.2",
"@types/react": "^16.9.36",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.1",
@@ -56,6 +56,7 @@
"react-native-reanimated": "^1.8.0",
"react-native-safe-area-context": "^1.0.0",
"react-native-screens": "^2.7.0",
"react-native-testing-library": "^2.1.0",
"typescript": "^3.9.5"
},
"peerDependencies": {

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { render, fireEvent } from 'react-native-testing-library';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import { createDrawerNavigator, DrawerScreenProps } from '../index';
it('renders a drawer navigator with screens', async () => {
const Test = ({ route, navigation }: DrawerScreenProps<ParamListBase>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);
const Drawer = createDrawerNavigator();
const { findByText, queryByText } = render(
<NavigationContainer>
<Drawer.Navigator>
<Drawer.Screen name="A" component={Test} />
<Drawer.Screen name="B" component={Test} />
</Drawer.Navigator>
</NavigationContainer>
);
expect(queryByText('Screen A')).not.toBeNull();
expect(queryByText('Screen B')).toBeNull();
fireEvent(await findByText('Go to B'), 'press');
expect(queryByText('Screen B')).not.toBeNull();
});

View File

@@ -156,6 +156,7 @@ export default function DrawerItem(props: Props) {
accessibilityTraits={focused ? ['button', 'selected'] : 'button'}
accessibilityComponentType="button"
accessibilityRole="button"
accessibilityState={{ selected: focused }}
accessibilityStates={focused ? ['selected'] : []}
to={to}
>

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.14...@react-navigation/material-bottom-tabs@5.2.15) (2020-07-28)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.13...@react-navigation/material-bottom-tabs@5.2.14) (2020-07-19)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.12...@react-navigation/material-bottom-tabs@5.2.13) (2020-07-10)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.11...@react-navigation/material-bottom-tabs@5.2.12) (2020-06-25)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@5.2.10...@react-navigation/material-bottom-tabs@5.2.11) (2020-06-24)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper",
"version": "5.2.11",
"version": "5.2.15",
"keywords": [
"react-native-component",
"react-component",
@@ -42,7 +42,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.15.1",
"@react-navigation/native": "^5.6.0",
"@react-navigation/native": "^5.7.2",
"@types/react": "^16.9.36",
"@types/react-native": "^0.62.7",
"@types/react-native-vector-icons": "^6.4.5",
@@ -50,6 +50,7 @@
"react": "~16.9.0",
"react-native": "~0.61.5",
"react-native-paper": "^3.10.1",
"react-native-testing-library": "^2.1.0",
"react-native-vector-icons": "^6.6.0",
"typescript": "^3.9.5"
},

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { render, fireEvent } from 'react-native-testing-library';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import {
createMaterialBottomTabNavigator,
MaterialBottomTabScreenProps,
} from '../index';
it('renders a material bottom tab navigator with screens', async () => {
const Test = ({
route,
navigation,
}: MaterialBottomTabScreenProps<ParamListBase>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);
const Tab = createMaterialBottomTabNavigator();
const { findByText, queryByText } = render(
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="A" component={Test} />
<Tab.Screen name="B" component={Test} />
</Tab.Navigator>
</NavigationContainer>
);
expect(queryByText('Screen A')).not.toBeNull();
expect(queryByText('Screen B')).toBeNull();
fireEvent(await findByText('Go to B'), 'press');
expect(queryByText('Screen B')).not.toBeNull();
});

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.15](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.14...@react-navigation/material-top-tabs@5.2.15) (2020-07-28)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.14](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.13...@react-navigation/material-top-tabs@5.2.14) (2020-07-19)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.13](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.12...@react-navigation/material-top-tabs@5.2.13) (2020-07-10)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.12](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.11...@react-navigation/material-top-tabs@5.2.12) (2020-06-25)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.11](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@5.2.10...@react-navigation/material-top-tabs@5.2.11) (2020-06-24)
**Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.2.11",
"version": "5.2.15",
"keywords": [
"react-native-component",
"react-component",
@@ -45,7 +45,7 @@
},
"devDependencies": {
"@react-native-community/bob": "^0.15.1",
"@react-navigation/native": "^5.6.0",
"@react-navigation/native": "^5.7.2",
"@types/react": "^16.9.36",
"@types/react-native": "^0.62.7",
"del-cli": "^3.0.1",
@@ -54,6 +54,7 @@
"react-native-gesture-handler": "^1.6.0",
"react-native-reanimated": "^1.8.0",
"react-native-tab-view": "^2.14.4",
"react-native-testing-library": "^2.1.0",
"typescript": "^3.9.5"
},
"peerDependencies": {

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { render, fireEvent } from 'react-native-testing-library';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import {
createMaterialTopTabNavigator,
MaterialTopTabScreenProps,
} from '../index';
it('renders a material bottom tab navigator with screens', async () => {
const Test = ({
route,
navigation,
}: MaterialTopTabScreenProps<ParamListBase>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);
const Tab = createMaterialTopTabNavigator();
const { findByText, queryByText } = render(
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="A" component={Test} />
<Tab.Screen name="B" component={Test} />
</Tab.Navigator>
</NavigationContainer>
);
expect(queryByText('Screen A')).not.toBeNull();
expect(queryByText('Screen B')).toBeNull();
fireEvent(await findByText('Go to B'), 'press');
expect(queryByText('Screen B')).not.toBeNull();
});

View File

@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.7.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.1...@react-navigation/native@5.7.2) (2020-07-28)
**Note:** Version bump only for package @react-navigation/native
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.7.0...@react-navigation/native@5.7.1) (2020-07-19)
**Note:** Version bump only for package @react-navigation/native
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.6.1...@react-navigation/native@5.7.0) (2020-07-10)
### Bug Fixes
* ensure correct document title after going back on Chrome ([8f5286e](https://github.com/react-navigation/react-navigation/commit/8f5286ef501d2e88cffbe4f7d8cdeb23a4af6cf1))
* tweak border color to match iOS default ([c665c02](https://github.com/react-navigation/react-navigation/commit/c665c027a6531cf841690940a7e2cb4ea498ba03))
### Features
* add a hook to update document title ([13c9d1e](https://github.com/react-navigation/react-navigation/commit/13c9d1e281b4626199671bce11ba62d83767564f))
* add support for badges to bottom tab bar ([96c7b68](https://github.com/react-navigation/react-navigation/commit/96c7b688ce773b3dd1f1cf7775367cd7080c94a2))
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.6.0...@react-navigation/native@5.6.1) (2020-06-25)
**Note:** Version bump only for package @react-navigation/native
# [5.6.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@5.5.1...@react-navigation/native@5.6.0) (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/native",
"description": "React Native integration for React Navigation",
"version": "5.6.0",
"version": "5.7.2",
"keywords": [
"react-native",
"react-navigation",
@@ -37,7 +37,7 @@
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^5.11.0",
"@react-navigation/core": "^5.12.2",
"nanoid": "^3.1.9"
},
"devDependencies": {

View File

@@ -9,13 +9,15 @@ import DefaultTheme from './theming/DefaultTheme';
import LinkingContext from './LinkingContext';
import useThenable from './useThenable';
import useLinking from './useLinking';
import useDocumentTitle from './useDocumentTitle';
import useBackButton from './useBackButton';
import type { Theme, LinkingOptions } from './types';
import type { Theme, LinkingOptions, DocumentTitleOptions } from './types';
type Props = NavigationContainerProps & {
theme?: Theme;
linking?: LinkingOptions;
fallback?: React.ReactNode;
documentTitle?: DocumentTitleOptions;
onReady?: () => void;
};
@@ -29,11 +31,19 @@ type Props = NavigationContainerProps & {
* @param props.theme Theme object for the navigators.
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
* @param props.fallback Fallback component to render until we have finished getting initial state when linking is enabled. Defaults to `null`.
* @param props.documentTitle Options to configure the document title on Web. Updating document title is handled by default unless `documentTitle.enabled` is `false`.
* @param props.children Child elements to render the content.
* @param props.ref Ref object which refers to the navigation object containing helper methods.
*/
const NavigationContainer = React.forwardRef(function NavigationContainer(
{ theme = DefaultTheme, linking, fallback = null, onReady, ...rest }: Props,
{
theme = DefaultTheme,
linking,
fallback = null,
documentTitle,
onReady,
...rest
}: Props,
ref?: React.Ref<NavigationContainerRef | null>
) {
const isLinkingEnabled = linking ? linking.enabled !== false : false;
@@ -41,6 +51,7 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
const refContainer = React.useRef<NavigationContainerRef>(null);
useBackButton(refContainer);
useDocumentTitle(refContainer, documentTitle);
const { getInitialState } = useLinking(refContainer, {
enabled: isLinkingEnabled,

View File

@@ -62,6 +62,7 @@ const removeEventListener = (type: 'popstate', listener: () => void) => {
};
export default {
document: { title: '' },
location,
history,
addEventListener,

View File

@@ -8,6 +8,7 @@ const DarkTheme: Theme = {
card: 'rgb(18, 18, 18)',
text: 'rgb(229, 229, 231)',
border: 'rgb(39, 39, 41)',
notification: 'rgb(255, 69, 58)',
},
};

View File

@@ -7,7 +7,8 @@ const DefaultTheme: Theme = {
background: 'rgb(242, 242, 242)',
card: 'rgb(255, 255, 255)',
text: 'rgb(28, 28, 30)',
border: 'rgb(224, 224, 224)',
border: 'rgb(216, 216, 216)',
notification: 'rgb(255, 59, 48)',
},
};

View File

@@ -2,6 +2,7 @@ import type {
getStateFromPath as getStateFromPathDefault,
getPathFromState as getPathFromStateDefault,
PathConfigMap,
Route,
} from '@react-navigation/core';
export type Theme = {
@@ -12,6 +13,7 @@ export type Theme = {
card: string;
text: string;
border: string;
notification: string;
};
};
@@ -52,6 +54,14 @@ export type LinkingOptions = {
getPathFromState?: typeof getPathFromStateDefault;
};
export type DocumentTitleOptions = {
enabled?: boolean;
formatter?: (
options: Record<string, any> | undefined,
route: Route<string> | undefined
) => string;
};
export type ServerContainerRef = {
getCurrentOptions(): Record<string, any> | undefined;
};

View File

@@ -0,0 +1,3 @@
export default function useDocumentTitle() {
// Noop for React Native
}

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import type { NavigationContainerRef } from '@react-navigation/core';
import type { DocumentTitleOptions } from './types';
/**
* Set the document title for the active screen
*/
export default function useDocumentTitle(
ref: React.RefObject<NavigationContainerRef>,
{
enabled = true,
formatter = (options, route) => options?.title ?? route?.name,
}: DocumentTitleOptions = {}
) {
React.useEffect(() => {
if (!enabled) {
return;
}
const navigation = ref.current;
if (navigation) {
const title = formatter(
navigation.getCurrentOptions(),
navigation.getCurrentRoute()
);
document.title = title;
}
return navigation?.addListener('options', (e) => {
const title = formatter(e.data.options, navigation?.getCurrentRoute());
document.title = title;
});
});
}

View File

@@ -116,6 +116,19 @@ const createMemoryHistory = () => {
pending = true;
const done = () => {
// There seems to be a bug in Chrome regarding updating the title
// If we set a title just before calling `history.go`, the title gets lost
// However the value of `document.title` is still what we set it to
// It's just not displayed in the tab bar
// To update the tab bar, we need to reset the title to something else first (e.g. '')
// And set the title to what it was before so it gets applied
// It won't work without setting it to empty string coz otherwise title isn't changing
// Which means that the browser won't do anything after setting the title
const { title } = window.document;
window.document.title = '';
window.document.title = title;
pending = false;
window.removeEventListener('popstate', done);

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.10](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.9...@react-navigation/routers@5.4.10) (2020-07-28)
### Bug Fixes
* make sure history is correct after rehydration ([b70e3fe](https://github.com/react-navigation/react-navigation/commit/b70e3fe61852502322b2cb46c5934800462b0267))
* make sure index is correct when rehydrating state for tabs ([#8638](https://github.com/react-navigation/react-navigation/issues/8638)) ([1aa8219](https://github.com/react-navigation/react-navigation/commit/1aa8219021f6c231a3e150fc9bea73f12542f85c))
## [5.4.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.8...@react-navigation/routers@5.4.9) (2020-07-10)
### Bug Fixes
* mark some types as read-only ([7c3a0a0](https://github.com/react-navigation/react-navigation/commit/7c3a0a0f23629da0beb956ba5a9689ab965061ce))
* only remove non-existed routes from tab history. closes [#8567](https://github.com/react-navigation/react-navigation/issues/8567) ([374b081](https://github.com/react-navigation/react-navigation/commit/374b081b1c4b2e590259a050430eb1fcdbad3557))
## [5.4.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@5.4.7...@react-navigation/routers@5.4.8) (2020-06-24)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/routers",
"description": "Routers to help build custom navigators",
"version": "5.4.8",
"version": "5.4.10",
"keywords": [
"react",
"react-native",

View File

@@ -196,37 +196,28 @@ export default function TabRouter({
});
const index = Math.min(
Math.max(
typeof state.index === 'number'
? state.index
: routeNames.indexOf(state.routes[0].name),
0
),
Math.max(routeNames.indexOf(state.routes[state?.index ?? 0]?.name), 0),
routes.length - 1
);
let history = state.history?.filter((it) =>
routes.find((r) => r.key === it.key)
);
const history =
state.history?.filter((it) => routes.find((r) => r.key === it.key)) ??
[];
if (!history?.length) {
history = getRouteHistory(
routes,
return changeIndex(
{
stale: false,
type: 'tab',
key: `tab-${nanoid()}`,
index,
backBehavior,
initialRouteName
);
}
return {
stale: false,
type: 'tab',
key: `tab-${nanoid()}`,
routeNames,
history,
routes,
},
index,
routeNames,
history,
routes,
};
backBehavior,
initialRouteName
);
},
getStateForRouteNamesChange(state, { routeNames, routeParamList }) {
@@ -244,8 +235,9 @@ export default function TabRouter({
routeNames.indexOf(state.routes[state.index].name)
);
let history = state.history.filter((it) =>
routes.find((r) => r.key === it.key)
let history = state.history.filter(
// Type will always be 'route' for tabs, but could be different in a router extending this (e.g. drawer)
(it) => it.type !== 'route' || routes.find((r) => r.key === it.key)
);
if (!history.length) {

View File

@@ -150,7 +150,7 @@ it('gets rehydrated state from partial state', () => {
options
)
).toEqual({
index: 2,
index: 0,
key: 'drawer-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
@@ -158,7 +158,7 @@ it('gets rehydrated state from partial state', () => {
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
history: [{ type: 'route', key: 'qux-test' }],
history: [{ type: 'route', key: 'bar-test' }],
stale: false,
type: 'drawer',
});
@@ -178,7 +178,7 @@ it('gets rehydrated state from partial state', () => {
options
)
).toEqual({
index: 1,
index: 0,
key: 'drawer-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
@@ -187,8 +187,8 @@ it('gets rehydrated state from partial state', () => {
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
history: [
{ type: 'route', key: 'bar-test' },
{ type: 'route', key: 'qux-test' },
{ type: 'route', key: 'bar-test' },
{ type: 'drawer' },
],
stale: false,

View File

@@ -942,3 +942,68 @@ it('changes index on focus change', () => {
expect(router.getStateForRouteFocus(state, 'qux-0')).toEqual(state);
});
it('merges params on navigate to an existing screen', () => {
const router = StackRouter({});
const options = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 2,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
},
CommonActions.navigate('bar'),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
],
},
CommonActions.navigate('bar', { fruit: 'orange' }),
options
)
).toEqual({
stale: false,
type: 'stack',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
],
});
});

View File

@@ -139,8 +139,11 @@ it('gets rehydrated state from partial state', () => {
expect(
router.getRehydratedState(
{
index: 4,
routes: [],
index: 1,
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'qux-2', name: 'qux' },
],
},
options
)
@@ -148,12 +151,34 @@ it('gets rehydrated state from partial state', () => {
index: 2,
key: 'tab-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-0', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-2', name: 'qux', params: { name: 'Jane' } },
],
history: [{ type: 'route', key: 'qux-2' }],
stale: false,
type: 'tab',
});
expect(
router.getRehydratedState(
{
index: 4,
routes: [],
},
options
)
).toEqual({
index: 0,
key: 'tab-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz', params: { answer: 42 } },
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
history: [{ type: 'route', key: 'qux-test' }],
history: [{ type: 'route', key: 'bar-test' }],
stale: false,
type: 'tab',
});
@@ -172,7 +197,7 @@ it('gets rehydrated state from partial state', () => {
options
)
).toEqual({
index: 1,
index: 0,
key: 'tab-test',
routeNames: ['bar', 'baz', 'qux'],
routes: [
@@ -181,8 +206,8 @@ it('gets rehydrated state from partial state', () => {
{ key: 'qux-test', name: 'qux', params: { name: 'Jane' } },
],
history: [
{ type: 'route', key: 'bar-test' },
{ type: 'route', key: 'qux-test' },
{ type: 'route', key: 'bar-test' },
],
stale: false,
type: 'tab',
@@ -1076,3 +1101,159 @@ it('updates route key history on focus change', () => {
{ type: 'route', key: 'baz-0' },
]);
});
it('merges params on navigate to an existing screen', () => {
const router = TabRouter({});
const options = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
CommonActions.navigate('bar'),
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'bar' },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
CommonActions.navigate('bar', { fruit: 'orange' }),
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
{ key: 'qux', name: 'qux' },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'bar' },
],
});
});
it('merges params on jump to an existing screen', () => {
const router = TabRouter({});
const options = {
routeNames: ['baz', 'bar', 'qux'],
routeParamList: {},
};
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
TabActions.jumpTo('bar'),
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'bar' },
],
});
expect(
router.getStateForAction(
{
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42 } },
{ key: 'qux', name: 'qux' },
],
history: [{ type: 'route', key: 'baz' }],
},
TabActions.jumpTo('bar', { fruit: 'orange' }),
options
)
).toEqual({
stale: false,
type: 'tab',
key: 'root',
index: 1,
routeNames: ['baz', 'bar', 'qux'],
routes: [
{ key: 'baz', name: 'baz' },
{ key: 'bar', name: 'bar', params: { answer: 42, fruit: 'orange' } },
{ key: 'qux', name: 'qux' },
],
history: [
{ type: 'route', key: 'baz' },
{ type: 'route', key: 'bar' },
],
});
});

View File

@@ -2,7 +2,7 @@ import type * as CommonActions from './CommonActions';
export type CommonNavigationAction = CommonActions.Action;
export type NavigationState = {
export type NavigationState = Readonly<{
/**
* Unique key for the navigation state.
*/
@@ -35,26 +35,27 @@ export type NavigationState = {
* Whether the navigation state has been rehydrated.
*/
stale: false;
};
}>;
export type InitialState = Partial<
Omit<NavigationState, 'stale' | 'routes'>
> & {
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
};
export type InitialState = Readonly<
Partial<Omit<NavigationState, 'stale' | 'routes'>> & {
routes: (Omit<Route<string>, 'key'> & { state?: InitialState })[];
}
>;
export type PartialState<State extends NavigationState> = Partial<
Omit<State, 'stale' | 'type' | 'key' | 'routes' | 'routeNames'>
> & {
stale?: true;
type?: string;
routes: (Omit<Route<string>, 'key'> & {
key?: string;
state?: InitialState;
})[];
};
> &
Readonly<{
stale?: true;
type?: string;
routes: (Omit<Route<string>, 'key'> & {
key?: string;
state?: InitialState;
})[];
}>;
export type Route<RouteName extends string> = {
export type Route<RouteName extends string> = Readonly<{
/**
* Unique key for the route.
*/
@@ -67,11 +68,11 @@ export type Route<RouteName extends string> = {
* Params for the route.
*/
params?: object;
};
}>;
export type ParamListBase = Record<string, object | undefined>;
export type NavigationAction = {
export type NavigationAction = Readonly<{
/**
* Type of the action (e.g. `NAVIGATE`)
*/
@@ -88,7 +89,7 @@ export type NavigationAction = {
* Key of the navigator which should handle this action.
*/
target?: string;
};
}>;
export type ActionCreators<Action extends NavigationAction> = {
[key: string]: (...args: any) => Action;

View File

@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.8.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.7.1...@react-navigation/stack@5.8.0) (2020-07-28)
### Features
* allow style overrides for HeaderBackButton ([#8626](https://github.com/react-navigation/react-navigation/issues/8626)) ([486c3de](https://github.com/react-navigation/react-navigation/commit/486c3defd27592bf4170af4962a1c66f4710b17a))
* emit gesture navigation events from stack view ([#8524](https://github.com/react-navigation/react-navigation/issues/8524)) ([15f9b95](https://github.com/react-navigation/react-navigation/commit/15f9b9573e52666f88b0f917396496b03218f160))
## [5.7.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.7.0...@react-navigation/stack@5.7.1) (2020-07-19)
**Note:** Version bump only for package @react-navigation/stack
# [5.7.0](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.6.2...@react-navigation/stack@5.7.0) (2020-07-10)
### Features
* add a `beforeRemove` event ([6925e92](https://github.com/react-navigation/react-navigation/commit/6925e92dc3e9885e3f552ca5e5eb51ae1521e54e))
## [5.6.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.6.1...@react-navigation/stack@5.6.2) (2020-06-25)
**Note:** Version bump only for package @react-navigation/stack
## [5.6.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@5.6.0...@react-navigation/stack@5.6.1) (2020-06-25)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/stack",
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
"version": "5.6.1",
"version": "5.8.0",
"keywords": [
"react-native-component",
"react-component",
@@ -46,7 +46,7 @@
"devDependencies": {
"@react-native-community/bob": "^0.15.1",
"@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.6.0",
"@react-navigation/native": "^5.7.2",
"@types/color": "^3.0.1",
"@types/react": "^16.9.36",
"@types/react-native": "^0.62.7",
@@ -56,6 +56,7 @@
"react-native-gesture-handler": "^1.6.0",
"react-native-safe-area-context": "^1.0.0",
"react-native-screens": "^2.7.0",
"react-native-testing-library": "^2.1.0",
"typescript": "^3.9.5"
},
"peerDependencies": {

View File

@@ -0,0 +1,33 @@
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { render, fireEvent } from 'react-native-testing-library';
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
import { createStackNavigator, StackScreenProps } from '../index';
it('renders a stack navigator with screens', async () => {
const Test = ({ route, navigation }: StackScreenProps<ParamListBase>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);
const Stack = createStackNavigator();
const { findByText, queryByText } = render(
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="A" component={Test} />
<Stack.Screen name="B" component={Test} />
</Stack.Navigator>
</NavigationContainer>
);
expect(queryByText('Screen A')).not.toBeNull();
expect(queryByText('Screen B')).toBeNull();
fireEvent.press(await findByText('Go to B'));
expect(queryByText('Screen B')).not.toBeNull();
});

View File

@@ -27,6 +27,18 @@ export type StackNavigationEventMap = {
* Event which fires when a transition animation ends.
*/
transitionEnd: { data: { closing: boolean } };
/**
* Event which fires when navigation gesture starts.
*/
gestureStart: { data: undefined };
/**
* Event which fires when navigation gesture is completed.
*/
gestureEnd: { data: undefined };
/**
* Event which fires when navigation gesture is canceled.
*/
gestureCancel: { data: undefined };
};
export type StackNavigationHelpers = NavigationHelpers<
@@ -406,6 +418,10 @@ export type StackHeaderLeftButtonProps = {
* Accessibility label for the button for screen readers.
*/
accessibilityLabel?: string;
/**
* Style object for the button.
*/
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};
export type StackHeaderTitleProps = {

View File

@@ -30,6 +30,7 @@ export default function HeaderBackButton({
titleLayout,
truncatedLabel = 'Back',
accessibilityLabel = label && label !== 'Back' ? `${label}, back` : 'Go back',
style,
}: Props) {
const { dark, colors } = useTheme();
@@ -160,7 +161,7 @@ export default function HeaderBackButton({
delayPressIn={0}
onPress={disabled ? undefined : handlePress}
pressColor={pressColorAndroid}
style={[styles.container, disabled && styles.disabled]}
style={[styles.container, disabled && styles.disabled, style]}
hitSlop={Platform.select({
ios: undefined,
default: { top: 16, right: 16, bottom: 16, left: 16 },

View File

@@ -97,6 +97,7 @@ export default class Card extends React.Component<Props> {
componentDidMount() {
this.animate({ closing: this.props.closing });
this.isCurrentlyMounted = true;
}
componentDidUpdate(prevProps: Props) {
@@ -115,8 +116,11 @@ export default class Card extends React.Component<Props> {
this.inverted.setValue(getInvertedMultiplier(gestureDirection));
}
const toValue = this.getAnimateToValue(this.props);
if (
this.getAnimateToValue(this.props) !== this.getAnimateToValue(prevProps)
this.getAnimateToValue(prevProps) !== toValue ||
this.lastToValue !== toValue
) {
// We need to trigger the animation when route was closed
// Thr route might have been closed by a `POP` action or by a gesture
@@ -128,9 +132,12 @@ export default class Card extends React.Component<Props> {
}
componentWillUnmount() {
this.isCurrentlyMounted = false;
this.handleEndInteraction();
}
private isCurrentlyMounted = false;
private isClosing = new Animated.Value(FALSE);
private inverted = new Animated.Value(
@@ -148,6 +155,8 @@ export default class Card extends React.Component<Props> {
private pendingGestureCallback: number | undefined;
private lastToValue: number | undefined;
private animate = ({
closing,
velocity,
@@ -168,6 +177,8 @@ export default class Card extends React.Component<Props> {
closing,
});
this.lastToValue = toValue;
const spec = closing ? transitionSpec.close : transitionSpec.open;
const animation =
@@ -196,6 +207,11 @@ export default class Card extends React.Component<Props> {
} else {
onOpen();
}
if (this.isCurrentlyMounted) {
// Make sure to re-open screen if it wasn't removed
this.forceUpdate();
}
}
});
};
@@ -301,10 +317,13 @@ export default class Card extends React.Component<Props> {
if (closing) {
// We call onClose with a delay to make sure that the animation has already started
// This will make sure that the state update caused by this doesn't affect start of animation
this.pendingGestureCallback = (setTimeout(
onClose,
32
) as any) as number;
this.pendingGestureCallback = (setTimeout(() => {
onClose();
// Trigger an update after we dispatch the action to remove the screen
// This will make sure that we check if the screen didn't get removed so we can cancel the animation
this.forceUpdate();
}, 32) as any) as number;
}
onGestureEnd?.();

View File

@@ -46,6 +46,9 @@ type Props = TransitionPreset & {
onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void;
onPageChangeCancel?: () => void;
onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void;
gestureEnabled?: boolean;
gestureResponseDistance?: {
vertical?: number;
@@ -95,6 +98,9 @@ function CardContainer({
onPageChangeCancel,
onPageChangeConfirm,
onPageChangeStart,
onGestureCancel,
onGestureEnd,
onGestureStart,
onTransitionEnd,
onTransitionStart,
renderHeader,
@@ -120,6 +126,20 @@ function CardContainer({
onCloseRoute({ route: scene.route });
};
const handleGestureBegin = () => {
onPageChangeStart?.();
onGestureStart?.({ route: scene.route });
};
const handleGestureCanceled = () => {
onPageChangeCancel?.();
onGestureCancel?.({ route: scene.route });
};
const handleGestureEnd = () => {
onGestureEnd?.({ route: scene.route });
};
const handleTransitionStart = ({ closing }: { closing: boolean }) => {
if (active && closing) {
onPageChangeConfirm?.();
@@ -179,8 +199,9 @@ function CardContainer({
overlayEnabled={cardOverlayEnabled}
shadowEnabled={cardShadowEnabled}
onTransitionStart={handleTransitionStart}
onGestureBegin={onPageChangeStart}
onGestureCanceled={onPageChangeCancel}
onGestureBegin={handleGestureBegin}
onGestureCanceled={handleGestureCanceled}
onGestureEnd={handleGestureEnd}
gestureEnabled={gestureEnabled}
gestureResponseDistance={gestureResponseDistance}
gestureVelocityImpact={gestureVelocityImpact}

View File

@@ -60,6 +60,9 @@ type Props = {
onPageChangeStart?: () => void;
onPageChangeConfirm?: () => void;
onPageChangeCancel?: () => void;
onGestureStart?: (props: { route: Route<string> }) => void;
onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void;
};
type State = {
@@ -372,6 +375,9 @@ export default class CardStack extends React.Component<Props, State> {
onPageChangeStart,
onPageChangeConfirm,
onPageChangeCancel,
onGestureStart,
onGestureEnd,
onGestureCancel,
} = this.props;
const { scenes, layout, gestures, headerHeights } = this.state;
@@ -568,6 +574,9 @@ export default class CardStack extends React.Component<Props, State> {
onPageChangeStart={onPageChangeStart}
onPageChangeConfirm={onPageChangeConfirm}
onPageChangeCancel={onPageChangeCancel}
onGestureStart={onGestureStart}
onGestureCancel={onGestureCancel}
onGestureEnd={onGestureEnd}
gestureResponseDistance={gestureResponseDistance}
headerHeight={headerHeight}
onHeaderHeightChange={this.handleHeaderLayout}

View File

@@ -405,6 +405,27 @@ export default class StackView extends React.Component<Props, State> {
target: route.key,
});
private handleGestureStart = ({ route }: { route: Route<string> }) => {
this.props.navigation.emit({
type: 'gestureStart',
target: route.key,
});
};
private handleGestureEnd = ({ route }: { route: Route<string> }) => {
this.props.navigation.emit({
type: 'gestureEnd',
target: route.key,
});
};
private handleGestureCancel = ({ route }: { route: Route<string> }) => {
this.props.navigation.emit({
type: 'gestureCancel',
target: route.key,
});
};
render() {
const {
state,
@@ -451,6 +472,9 @@ export default class StackView extends React.Component<Props, State> {
headerMode={headerMode}
state={state}
descriptors={descriptors}
onGestureStart={this.handleGestureStart}
onGestureEnd={this.handleGestureEnd}
onGestureCancel={this.handleGestureCancel}
{...rest}
{...props}
/>

View File

@@ -422,6 +422,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz#ec5a5cf0eec925b66c60580328b122c01230a127"
integrity sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==
"@babel/helper-plugin-utils@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
"@babel/helper-regex@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96"
@@ -897,6 +902,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.1"
"@babel/plugin-syntax-jsx@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c"
integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897"
@@ -904,7 +916,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.3"
"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
"@babel/plugin-syntax-nullish-coalescing-operator@^7.0.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
@@ -939,7 +951,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
"@babel/plugin-syntax-optional-chaining@^7.0.0", "@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
@@ -1383,6 +1395,14 @@
"@babel/helper-plugin-utils" "^7.10.1"
"@babel/plugin-syntax-jsx" "^7.10.1"
"@babel/plugin-transform-react-jsx-self@^7.0.0":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369"
integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-syntax-jsx" "^7.10.4"
"@babel/plugin-transform-react-jsx-self@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz#22143e14388d72eb88649606bb9e46f421bc3821"
@@ -1825,13 +1845,6 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2":
version "7.10.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.0.0", "@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@@ -5854,7 +5867,7 @@ babel-jest@^26.0.1:
graceful-fs "^4.2.4"
slash "^3.0.0"
babel-loader@8.1.0:
babel-loader@8.1.0, babel-loader@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==
@@ -7575,7 +7588,7 @@ core-js@^2.2.2, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.6.5:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
core-js@^3.2.1, core-js@^3.6.5:
core-js@^3.2.1:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
@@ -13745,6 +13758,50 @@ metro-react-native-babel-preset@^0.56.0, metro-react-native-babel-preset@^0.56.4
"@babel/template" "^7.0.0"
react-refresh "^0.4.0"
metro-react-native-babel-preset@^0.59.0:
version "0.59.0"
resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.59.0.tgz#20e020bc6ac9849e1477de1333d303ed42aba225"
integrity sha512-BoO6ncPfceIDReIH8pQ5tQptcGo5yRWQXJGVXfANbiKLq4tfgdZB1C1e2rMUJ6iypmeJU9dzl+EhPmIFKtgREg==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.0.0"
"@babel/plugin-proposal-export-default-from" "^7.0.0"
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0"
"@babel/plugin-proposal-object-rest-spread" "^7.0.0"
"@babel/plugin-proposal-optional-catch-binding" "^7.0.0"
"@babel/plugin-proposal-optional-chaining" "^7.0.0"
"@babel/plugin-syntax-dynamic-import" "^7.0.0"
"@babel/plugin-syntax-export-default-from" "^7.0.0"
"@babel/plugin-syntax-flow" "^7.2.0"
"@babel/plugin-syntax-nullish-coalescing-operator" "^7.0.0"
"@babel/plugin-syntax-optional-chaining" "^7.0.0"
"@babel/plugin-transform-arrow-functions" "^7.0.0"
"@babel/plugin-transform-block-scoping" "^7.0.0"
"@babel/plugin-transform-classes" "^7.0.0"
"@babel/plugin-transform-computed-properties" "^7.0.0"
"@babel/plugin-transform-destructuring" "^7.0.0"
"@babel/plugin-transform-exponentiation-operator" "^7.0.0"
"@babel/plugin-transform-flow-strip-types" "^7.0.0"
"@babel/plugin-transform-for-of" "^7.0.0"
"@babel/plugin-transform-function-name" "^7.0.0"
"@babel/plugin-transform-literals" "^7.0.0"
"@babel/plugin-transform-modules-commonjs" "^7.0.0"
"@babel/plugin-transform-object-assign" "^7.0.0"
"@babel/plugin-transform-parameters" "^7.0.0"
"@babel/plugin-transform-react-display-name" "^7.0.0"
"@babel/plugin-transform-react-jsx" "^7.0.0"
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
"@babel/plugin-transform-regenerator" "^7.0.0"
"@babel/plugin-transform-runtime" "^7.0.0"
"@babel/plugin-transform-shorthand-properties" "^7.0.0"
"@babel/plugin-transform-spread" "^7.0.0"
"@babel/plugin-transform-sticky-regex" "^7.0.0"
"@babel/plugin-transform-template-literals" "^7.0.0"
"@babel/plugin-transform-typescript" "^7.5.0"
"@babel/plugin-transform-unicode-regex" "^7.0.0"
"@babel/template" "^7.0.0"
react-refresh "^0.4.0"
metro-react-native-babel-transformer@^0.56.0:
version "0.56.4"
resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.56.4.tgz#3c6e48b605c305362ee624e45ff338656e35fc1d"