Compare commits

..

31 Commits

Author SHA1 Message Date
Satyajit Sahoo
aef35c5046 chore: publish
- @react-navigation/bottom-tabs@6.0.0-next.5
2021-04-16 12:40:53 +02:00
Satyajit Sahoo
338ed6ff07 fix: update tab bar height correctly. fixes #9296 2021-04-16 12:39:17 +02:00
Satyajit Sahoo
47e371609d chore: use locally installed expo-cli 2021-04-14 22:34:20 +02:00
Satyajit Sahoo
ef42fa2d36 chore: add 'needs repro' label automatically 2021-04-14 22:05:43 +02:00
Satyajit Sahoo
cc5d195f9a chore: publish
- @react-navigation/material-top-tabs@6.0.0-next.3
2021-04-08 07:41:46 +02:00
Satyajit Sahoo
7e10bcd089 refactor: move lazyPlaceholder to options 2021-04-08 07:39:36 +02:00
Satyajit Sahoo
c8dd70a033 chore: publish
- @react-navigation/bottom-tabs@6.0.0-next.4
 - @react-navigation/core@6.0.0-next.2
 - @react-navigation/devtools@6.0.0-next.2
 - @react-navigation/drawer@6.0.0-next.4
 - @react-navigation/elements@1.0.0-next.4
 - @react-navigation/material-bottom-tabs@6.0.0-next.2
 - @react-navigation/material-top-tabs@6.0.0-next.2
 - @react-navigation/native@6.0.0-next.2
 - @react-navigation/routers@6.0.0-next.2
 - @react-navigation/stack@6.0.0-next.9
2021-04-08 06:19:12 +02:00
Satyajit Sahoo
277fec481b chore: fix dist-tag for publishing 2021-04-08 06:14:41 +02:00
Satyajit Sahoo
3241190b19 fix: fix drawer overlay on web 2021-04-08 05:46:18 +02:00
Satyajit Sahoo
ef7370b215 chore: configure reanimated 2 2021-04-04 06:55:37 +02:00
Satyajit Sahoo
1149e718c1 refactor: add reanimated 2 implementation for drawer 2021-04-04 06:43:12 +02:00
Satyajit Sahoo
d85a4fd8ed chore: upgrade expo to SDK 41 2021-04-04 06:41:26 +02:00
Satyajit Sahoo
b89396888f fix: don't handle back button with permanent drawer 2021-04-04 01:09:05 +02:00
WoLewicki
c38906a7a0 fix: properly resolve initialRouteNames 2021-04-04 01:08:57 +02:00
Janic Duplessis
d87857e5d9 fix: remove calls to removed Keyboard.removeListener in useIsKeyboardShown (#9457) 2021-04-04 01:06:55 +02:00
Satyajit Sahoo
5ae0badc44 fix: only handle back button in drawer when focused 2021-04-02 05:21:32 +02:00
Jemmy Phan
84020a0b27 feat: improve useNavigationState typing (#9464) 2021-03-29 05:42:19 +02:00
Satyajit Sahoo
5473982859 chore: publish
- @react-navigation/bottom-tabs@6.0.0-next.3
 - @react-navigation/drawer@6.0.0-next.3
 - @react-navigation/elements@1.0.0-next.3
 - @react-navigation/stack@6.0.0-next.8
2021-03-22 19:29:22 +01:00
Satyajit Sahoo
de805a3ebf fix: use tab role on Android for accessibility 2021-03-22 19:21:47 +01:00
Satyajit Sahoo
cbaabc1288 feat: add a Background component 2021-03-22 19:03:09 +01:00
Satyajit Sahoo
dd48fe9b15 chore: publish
- @react-navigation/stack@6.0.0-next.7
2021-03-22 02:16:16 +01:00
Satyajit Sahoo
48851c9ebd refactor: make gestureResponseDistance a number
BREAKING CHANGE: now we need to pass a number instead of an object
2021-03-22 02:15:28 +01:00
Satyajit Sahoo
197c916a23 chore: publish
- @react-navigation/stack@6.0.0-next.6
2021-03-14 16:44:53 +01:00
Satyajit Sahoo
31caaf3071 refactor: set headerMode to screen by default when custom header is specified 2021-03-14 16:43:51 +01:00
Satyajit Sahoo
5bcce9926a chore: publish
- @react-navigation/stack@6.0.0-next.5
2021-03-14 16:28:11 +01:00
Satyajit Sahoo
aacc1b525d refactor: move headerMode to options
BREAKING CHANGE: headerMode is now moved to options instead of props
2021-03-14 16:27:53 +01:00
Satyajit Sahoo
3ad2bcbaf8 fix: consider header colors when managing statusbar 2021-03-13 17:38:46 +01:00
Satyajit Sahoo
faee245d2e fix: consider header colors when managing statusbar 2021-03-13 17:37:43 +01:00
Satyajit Sahoo
e1ab06d3d7 chore: publish
- @react-navigation/stack@6.0.0-next.4
2021-03-12 23:09:39 +01:00
Satyajit Sahoo
a204edd012 fix: add special statusbar handling to modal presentation 2021-03-12 23:08:13 +01:00
Satyajit Sahoo
78b00bd814 chore: update expo-cli 2021-03-12 03:30:37 +01:00
66 changed files with 3398 additions and 3383 deletions

View File

@@ -23,26 +23,54 @@ jobs:
'gm'
);
if (!regex.test(body)) {
return;
}
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['repro provided'],
});
try {
await github.issues.removeLabel({
if (regex.test(body)) {
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'needs repro',
labels: ['repro provided'],
});
} catch (error) {
if (!/Label does not exist/.test(error.message)) {
throw error;
try {
await github.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: 'needs repro',
});
} catch (error) {
if (!/Label does not exist/.test(error.message)) {
throw error;
}
}
} else {
if (context.eventName !== 'issues') {
return;
}
const body = "Hey! Thanks for opening the issue. The issue doesn't seem to contain a link to a repro (a [snack.expo.io](https://snack.expo.io) link or link to a GitHub repo under your username).\n\nCan you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? A repro will help us debug the issue faster. Please try to keep the repro as small as possible and make sure that we can run it without additional setup.";
const comments = await github.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
if (comments.data.some(comment => comment.body === body)) {
return;
}
await github.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
await github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['needs repro'],
});
}

View File

@@ -18,9 +18,7 @@ jobs:
- name: Setup Expo
uses: expo/expo-github-action@v5
with:
expo-version: 3.x
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-token: ${{ secrets.EXPO_TOKEN }}
expo-cache: true
- name: Restore yarn cache
@@ -36,7 +34,7 @@ jobs:
- name: Publish Expo app
working-directory: ./example
run: expo publish --release-channel=pr-${{ github.event.number }}
run: yarn expo publish --release-channel=pr-${{ github.event.number }}
env:
EXPO_USE_DEV_SERVER: true

View File

@@ -21,9 +21,7 @@ jobs:
- name: Setup Expo
uses: expo/expo-github-action@v5
with:
expo-version: 3.x
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
expo-token: ${{ secrets.EXPO_TOKEN }}
expo-cache: true
- name: Restore yarn cache
@@ -39,4 +37,4 @@ jobs:
- name: Publish Expo app
working-directory: ./example
run: expo publish
run: yarn expo publish

View File

@@ -31,7 +31,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible.\n\nThe easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then please provide the repro in a GitHub repository."
body: "Hey! Thanks for opening the issue. Can you provide a [minimal repro](https://stackoverflow.com/help/minimal-reproducible-example) which demonstrates the issue? Posting a snippet of your code in the issue is useful, but it's not usually straightforward to run. A repro will help us debug the issue faster. Please try to keep the repro as small as possible and make sure that we can run it without additional setup.\n\nThe easiest way to provide a repro is on [snack.expo.io](https://snack.expo.io). If it's not possible to repro it on [snack.expo.io](https://snack.expo.io), then please provide the repro in a GitHub repository."
})
question:

View File

@@ -2,5 +2,6 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -13,50 +13,51 @@
"test": "jest"
},
"dependencies": {
"@expo/vector-icons": "^12.0.3",
"@react-native-masked-view/masked-view": "0.2.0",
"@expo/vector-icons": "^12.0.0",
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-masked-view/masked-view": "0.2.3",
"color": "^3.1.3",
"expo": "^40.0.1",
"expo-asset": "~8.2.2",
"expo-blur": "~9.0.0",
"expo-linking": "~2.1.1",
"expo-updates": "~0.4.2",
"expo": "^41.0.0-beta.2",
"expo-asset": "~8.3.1",
"expo-blur": "~9.0.3",
"expo-linking": "~2.2.1",
"expo-updates": "~0.5.3",
"koa": "^2.13.0",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-40.0.0.tar.gz",
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"react-native-appearance": "~0.3.3",
"react-native-gesture-handler": "~1.8.0",
"react-native-pager-view": "^4.2.4",
"react-native-gesture-handler": "~1.10.2",
"react-native-pager-view": "~5.0.12",
"react-native-paper": "^4.7.2",
"react-native-reanimated": "~1.13.0",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-tab-view": "^3.0.0",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "~3.2.0",
"react-native-screens": "~3.0.0",
"react-native-tab-view": "^3.0.1",
"react-native-vector-icons": "^8.1.0",
"react-native-web": "~0.15.0"
},
"devDependencies": {
"@babel/node": "^7.13.0",
"@expo/webpack-config": "~0.12.60",
"@types/cheerio": "^0.22.24",
"@babel/node": "^7.13.13",
"@expo/webpack-config": "~0.12.63",
"@types/cheerio": "^0.22.28",
"@types/jest-dev-server": "^4.2.0",
"@types/koa": "^2.13.1",
"@types/node-fetch": "^2.5.8",
"@types/node-fetch": "^2.5.9",
"@types/react": "~16.9.35",
"@types/react-dom": "~16.9.8",
"@types/react-native": "~0.63.51",
"@types/react-native": "~0.63.2",
"babel-loader": "^8.2.2",
"babel-plugin-module-resolver": "^4.0.0",
"babel-preset-expo": "8.3.0",
"cheerio": "^1.0.0-rc.3",
"expo-cli": "^4.2.1",
"expo-cli": "^4.3.4",
"jest": "^26.6.3",
"jest-dev-server": "^4.4.0",
"mock-require-assets": "^0.0.1",
"node-fetch": "^2.6.1",
"nodemon": "^2.0.6",
"playwright": "^1.9.1",
"playwright": "^1.10.0",
"serve": "^11.3.0",
"typescript": "~4.2.3"
}

View File

@@ -1,2 +0,0 @@
import * as Linking from 'expo-linking';
export default [Linking.makeUrl('/')];

View File

@@ -1 +0,0 @@
export default ['rne://127.0.0.1:19000/--/'];

View File

@@ -0,0 +1,159 @@
import * as React from 'react';
import { View, Platform, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'react-native-paper';
import type { ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
StackScreenProps,
TransitionPresets,
HeaderStyleInterpolators,
} from '@react-navigation/stack';
import Article from '../Shared/Article';
import Albums from '../Shared/Albums';
import NewsFeed from '../Shared/NewsFeed';
export type SimpleStackParams = {
Article: { author: string } | undefined;
NewsFeed: { date: number };
Albums: undefined;
};
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({
navigation,
route,
}: StackScreenProps<SimpleStackParams, 'Article'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('NewsFeed', { date: Date.now() })}
style={styles.button}
>
Push feed
</Button>
<Button
mode="outlined"
onPress={() => navigation.pop()}
style={styles.button}
>
Pop screen
</Button>
</View>
<Article
author={{ name: route.params?.author ?? 'Unknown' }}
scrollEnabled={scrollEnabled}
/>
</ScrollView>
);
};
const NewsFeedScreen = ({
route,
navigation,
}: StackScreenProps<SimpleStackParams, 'NewsFeed'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Albums')}
style={styles.button}
>
Navigate to album
</Button>
<Button
mode="outlined"
onPress={() => navigation.pop()}
style={styles.button}
>
Pop screen
</Button>
</View>
<NewsFeed scrollEnabled={scrollEnabled} date={route.params.date} />
</ScrollView>
);
};
const AlbumsScreen = ({
navigation,
}: StackScreenProps<SimpleStackParams, 'Albums'>) => {
return (
<ScrollView>
<View style={styles.buttons}>
<Button
mode="contained"
onPress={() => navigation.push('Article', { author: 'Babel fish' })}
style={styles.button}
>
Push article
</Button>
<Button
mode="outlined"
onPress={() => navigation.pop()}
style={styles.button}
>
Pop screen
</Button>
</View>
<Albums scrollEnabled={scrollEnabled} />
</ScrollView>
);
};
const SimpleStack = createStackNavigator<SimpleStackParams>();
export default function SimpleStackScreen({
navigation,
}: StackScreenProps<ParamListBase>) {
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
});
}, [navigation]);
return (
<SimpleStack.Navigator
screenOptions={{
...TransitionPresets.SlideFromRightIOS,
headerMode: 'float',
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
}}
>
<SimpleStack.Screen
name="Article"
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params?.author ?? 'Unknown'}`,
})}
initialParams={{ author: 'Gandalf' }}
/>
<SimpleStack.Screen
name="NewsFeed"
component={NewsFeedScreen}
options={{ title: 'Feed' }}
/>
<SimpleStack.Screen
name="Albums"
component={AlbumsScreen}
options={{
...TransitionPresets.ModalSlideFromBottomIOS,
headerMode: 'screen',
title: 'Albums',
}}
/>
</SimpleStack.Navigator>
);
}
const styles = StyleSheet.create({
buttons: {
flexDirection: 'row',
padding: 8,
},
button: {
margin: 8,
},
});

View File

@@ -86,8 +86,7 @@ const AlbumsScreen = ({ navigation }: StackScreenProps<SimpleStackParams>) => {
const SimpleStack = createStackNavigator<SimpleStackParams>();
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> &
StackScreenProps<ParamListBase>;
type Props = StackScreenProps<ParamListBase>;
function CustomHeader(props: StackHeaderProps) {
const { current, next } = props.progress;
@@ -108,7 +107,7 @@ function CustomHeader(props: StackHeaderProps) {
);
}
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
export default function HeaderCustomizationScreen({ navigation }: Props) {
React.useLayoutEffect(() => {
navigation.setOptions({
headerShown: false,
@@ -119,13 +118,13 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
const [headerTitleCentered, setHeaderTitleCentered] = React.useState(true);
return (
<SimpleStack.Navigator {...rest}>
<SimpleStack.Navigator screenOptions={{ headerMode: 'float' }}>
<SimpleStack.Screen
name="Article"
component={ArticleScreen}
options={({ route }) => ({
title: `Article by ${route.params?.author}`,
header: CustomHeader,
header: (props) => <CustomHeader {...props} />,
headerTintColor: '#fff',
headerStyle: { backgroundColor: '#ff005d' },
headerBackTitleVisible: false,

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import {
AsyncStorage,
ScrollView,
Platform,
StatusBar,
@@ -21,6 +20,8 @@ import {
Divider,
Text,
} from 'react-native-paper';
import { createURL } from 'expo-linking';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
InitialState,
NavigationContainer,
@@ -38,10 +39,10 @@ import {
import { useReduxDevToolsExtension } from '@react-navigation/devtools';
import { restartApp } from './Restart';
import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem';
import SimpleStack from './Screens/SimpleStack';
import ModalStack from './Screens/ModalStack';
import MixedHeaderMode from './Screens/MixedHeaderMode';
import StackTransparent from './Screens/StackTransparent';
import StackHeaderCustomization from './Screens/StackHeaderCustomization';
import BottomTabs from './Screens/BottomTabs';
@@ -70,6 +71,10 @@ const SCREENS = {
title: 'Modal Stack',
component: ModalStack,
},
MixedHeaderMode: {
title: 'Float + Screen Header Stack',
component: MixedHeaderMode,
},
StackTransparent: {
title: 'Transparent Stack',
component: StackTransparent,
@@ -220,7 +225,7 @@ export default function App() {
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
prefixes: LinkingPrefixes,
prefixes: [createURL('/')],
config: {
initialRouteName: 'Home',
screens: Object.keys(SCREENS).reduce<PathConfigMap>(

View File

@@ -11,6 +11,7 @@
"allowBranch": "main",
"conventionalCommits": true,
"createRelease": "github",
"distTag": "next",
"message": "chore: publish",
"ignoreChanges": [
"**/__fixtures__/**",

View File

@@ -8,8 +8,7 @@
]
},
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"license": "MIT",
"repository": {
@@ -26,13 +25,13 @@
"example": "yarn --cwd example"
},
"devDependencies": {
"@commitlint/config-conventional": "^12.0.1",
"@types/jest": "^26.0.19",
"@commitlint/config-conventional": "^12.1.1",
"@types/jest": "^26.0.22",
"babel-jest": "^26.6.3",
"codecov": "^3.8.1",
"commitlint": "^12.0.1",
"eslint": "^7.21.0",
"eslint-config-satya164": "^3.1.9",
"commitlint": "^12.1.1",
"eslint": "^7.23.0",
"eslint-config-satya164": "^3.1.10",
"husky": "^4.3.6",
"jest": "^26.6.3",
"lerna": "^4.0.0",

View File

@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.4...@react-navigation/bottom-tabs@6.0.0-next.5) (2021-04-16)
### Bug Fixes
* update tab bar height correctly. fixes [#9296](https://github.com/react-navigation/react-navigation/issues/9296) ([338ed6f](https://github.com/react-navigation/react-navigation/commit/338ed6ff07bd2d6efa1abdb369612ea72f540502))
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.3...@react-navigation/bottom-tabs@6.0.0-next.4) (2021-04-08)
### Bug Fixes
* remove calls to removed Keyboard.removeListener in useIsKeyboardShown ([#9457](https://github.com/react-navigation/react-navigation/issues/9457)) ([d87857e](https://github.com/react-navigation/react-navigation/commit/d87857e5d93c19ebee2fd84eb4910e36001ec2a3))
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.2...@react-navigation/bottom-tabs@6.0.0-next.3) (2021-03-22)
### Bug Fixes
* use tab role on Android for accessibility ([de805a3](https://github.com/react-navigation/react-navigation/commit/de805a3ebf35db81cb7b7bcbf5cfd4a03e69c567))
### Features
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0-next.1...@react-navigation/bottom-tabs@6.0.0-next.2) (2021-03-12)
**Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines",
"version": "6.0.0-next.2",
"version": "6.0.0-next.5",
"keywords": [
"react-native-component",
"react-component",
@@ -29,30 +29,29 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/elements": "^1.0.0-next.2",
"@react-navigation/elements": "^1.0.0-next.4",
"color": "^3.1.3",
"warn-once": "^0.0.1"
},
"devDependencies": {
"@react-navigation/native": "^6.0.0-next.1",
"@react-navigation/native": "^6.0.0-next.2",
"@testing-library/react-native": "^7.2.0",
"@types/color": "^3.0.1",
"@types/react": "^16.9.53",
"@types/react-native": "~0.63.51",
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-safe-area-context": "~3.2.0",
"react-native-screens": "~3.0.0",
"typescript": "^4.2.3"
},
"peerDependencies": {
@@ -60,7 +59,7 @@
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 2.15.0"
"react-native-screens": ">= 3.0.0"
},
"react-native-builder-bob": {
"source": "src",

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Keyboard, Platform } from 'react-native';
import { Keyboard, Platform, EmitterSubscription } from 'react-native';
export default function useIsKeyboardShown() {
const [isKeyboardShown, setIsKeyboardShown] = React.useState(false);
@@ -8,22 +8,22 @@ export default function useIsKeyboardShown() {
const handleKeyboardShow = () => setIsKeyboardShown(true);
const handleKeyboardHide = () => setIsKeyboardShown(false);
let subscriptions: EmitterSubscription[];
if (Platform.OS === 'ios') {
Keyboard.addListener('keyboardWillShow', handleKeyboardShow);
Keyboard.addListener('keyboardWillHide', handleKeyboardHide);
subscriptions = [
Keyboard.addListener('keyboardWillShow', handleKeyboardShow),
Keyboard.addListener('keyboardWillHide', handleKeyboardHide),
];
} else {
Keyboard.addListener('keyboardDidShow', handleKeyboardShow);
Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
subscriptions = [
Keyboard.addListener('keyboardDidShow', handleKeyboardShow),
Keyboard.addListener('keyboardDidHide', handleKeyboardHide),
];
}
return () => {
if (Platform.OS === 'ios') {
Keyboard.removeListener('keyboardWillShow', handleKeyboardShow);
Keyboard.removeListener('keyboardWillHide', handleKeyboardHide);
} else {
Keyboard.removeListener('keyboardDidShow', handleKeyboardShow);
Keyboard.removeListener('keyboardDidHide', handleKeyboardHide);
}
subscriptions.forEach((s) => s.remove());
};
}, []);

View File

@@ -284,8 +284,9 @@ export default function BottomTabBar({
tabBarStyle,
]}
pointerEvents={isTabBarHidden ? 'none' : 'auto'}
onLayout={handleLayout}
>
<View style={styles.content} onLayout={handleLayout}>
<View accessibilityRole="tablist" style={styles.content}>
{routes.map((route, index) => {
const focused = index === state.index;
const { options } = descriptors[route.key];
@@ -322,7 +323,7 @@ export default function BottomTabBar({
const accessibilityLabel =
options.tabBarAccessibilityLabel !== undefined
? options.tabBarAccessibilityLabel
: typeof label === 'string'
: typeof label === 'string' && Platform.OS === 'ios'
? `${label}, tab, ${index + 1} of ${routes.length}`
: undefined;

View File

@@ -263,7 +263,8 @@ export default function BottomTabBarItem({
onLongPress,
testID,
accessibilityLabel,
accessibilityRole: 'button',
// FIXME: accessibilityRole: 'tab' doesn't seem to work as expected on iOS
accessibilityRole: Platform.select({ ios: 'button', default: 'tab' }),
accessibilityState: { selected: focused },
// @ts-expect-error: keep for compatibility with older React Native versions
accessibilityStates: focused ? ['selected'] : [],

View File

@@ -1,12 +1,11 @@
import * as React from 'react';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import { StyleSheet } from 'react-native';
import { ScreenContainer } from 'react-native-screens';
import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
import {
NavigationHelpersContext,
ParamListBase,
TabNavigationState,
useTheme,
} from '@react-navigation/native';
import {
Header,
@@ -34,28 +33,6 @@ type Props = BottomTabNavigationConfig & {
descriptors: BottomTabDescriptorMap;
};
function SceneContent({
isFocused,
children,
style,
}: {
isFocused: boolean;
children: React.ReactNode;
style?: StyleProp<ViewStyle>;
}) {
const { colors } = useTheme();
return (
<View
accessibilityElementsHidden={!isFocused}
importantForAccessibility={isFocused ? 'auto' : 'no-hide-descendants'}
style={[styles.content, { backgroundColor: colors.background }, style]}
>
{children}
</View>
);
}
export default function BottomTabView(props: Props) {
const {
tabBar = (props: BottomTabBarProps) => <BottomTabBar {...props} />,
@@ -150,26 +127,26 @@ export default function BottomTabView(props: Props) {
visible={isFocused}
enabled={detachInactiveScreens}
>
<SceneContent isFocused={isFocused} style={sceneContainerStyle}>
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
<Screen
route={descriptor.route}
navigation={descriptor.navigation}
headerShown={descriptor.options.headerShown}
headerStatusBarHeight={
descriptor.options.headerStatusBarHeight
}
header={header({
layout: dimensions,
route: descriptor.route,
navigation: descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
options: descriptor.options,
})}
>
{descriptor.render()}
</Screen>
</BottomTabBarHeightContext.Provider>
</SceneContent>
<BottomTabBarHeightContext.Provider value={tabBarHeight}>
<Screen
focused={isFocused}
route={descriptor.route}
navigation={descriptor.navigation}
headerShown={descriptor.options.headerShown}
headerStatusBarHeight={
descriptor.options.headerStatusBarHeight
}
header={header({
layout: dimensions,
route: descriptor.route,
navigation: descriptor.navigation as BottomTabNavigationProp<ParamListBase>,
options: descriptor.options,
})}
style={sceneContainerStyle}
>
{descriptor.render()}
</Screen>
</BottomTabBarHeightContext.Provider>
</ScreenFallback>
);
})}
@@ -187,7 +164,4 @@ const styles = StyleSheet.create({
flex: 1,
overflow: 'hidden',
},
content: {
flex: 1,
},
});

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0-next.1...@react-navigation/core@6.0.0-next.2) (2021-04-08)
### Bug Fixes
* properly resolve initialRouteNames ([c38906a](https://github.com/react-navigation/react-navigation/commit/c38906a7a09b997f37ce56734ea823c75ea744db))
### Features
* improve useNavigationState typing ([#9464](https://github.com/react-navigation/react-navigation/issues/9464)) ([84020a0](https://github.com/react-navigation/react-navigation/commit/84020a0b27ebae50d3037438a51d95eb31b02424))
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/core@6.0.0...@react-navigation/core@6.0.0-next.1) (2021-03-10)
**Note:** Version bump only for package @react-navigation/core

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/core",
"description": "Core utilities for building navigators",
"version": "6.0.0-next.1",
"version": "6.0.0-next.2",
"keywords": [
"react",
"react-native",
@@ -28,18 +28,17 @@
"!**/__tests__"
],
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/routers": "^6.0.0-next.1",
"@react-navigation/routers": "^6.0.0-next.2",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.20",
"query-string": "^6.14.1",
"nanoid": "^3.1.22",
"query-string": "^7.0.0",
"react-is": "^16.13.0"
},
"devDependencies": {
@@ -47,7 +46,7 @@
"@types/react": "^16.9.53",
"@types/react-is": "^16.7.1",
"del-cli": "^3.0.1",
"immer": "^8.0.1",
"immer": "^9.0.1",
"react": "~16.13.1",
"react-native-builder-bob": "^0.18.1",
"react-test-renderer": "~16.13.1",

View File

@@ -2303,3 +2303,119 @@ it('throws if two screens map to the same pattern', () => {
})
).not.toThrow();
});
it('correctly applies initialRouteName for config with similar route names', () => {
const path = '/weekly-earnings';
const config = {
screens: {
RootTabs: {
screens: {
HomeTab: {
screens: {
Home: '',
WeeklyEarnings: 'weekly-earnings',
EventDetails: 'event-details/:eventId',
},
},
EarningsTab: {
initialRouteName: 'Earnings',
path: 'earnings',
screens: {
Earnings: '',
WeeklyEarnings: 'weekly-earnings',
},
},
},
},
},
};
const state = {
routes: [
{
name: 'RootTabs',
state: {
routes: [
{
name: 'HomeTab',
state: {
routes: [
{
name: 'WeeklyEarnings',
path,
},
],
},
},
],
},
},
],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});
it('correctly applies initialRouteName for config with similar route names v2', () => {
const path = '/earnings/weekly-earnings';
const config = {
screens: {
RootTabs: {
screens: {
HomeTab: {
initialRouteName: 'Home',
screens: {
Home: '',
WeeklyEarnings: 'weekly-earnings',
},
},
EarningsTab: {
initialRouteName: 'Earnings',
path: 'earnings',
screens: {
Earnings: '',
WeeklyEarnings: 'weekly-earnings',
},
},
},
},
},
};
const state = {
routes: [
{
name: 'RootTabs',
state: {
routes: [
{
name: 'EarningsTab',
state: {
index: 1,
routes: [
{
name: 'Earnings',
},
{
name: 'WeeklyEarnings',
path,
},
],
},
},
],
},
},
],
};
expect(getStateFromPath(path, config)).toEqual(state);
expect(getStateFromPath(getPathFromState(state, config), config)).toEqual(
state
);
});

View File

@@ -26,7 +26,7 @@ type RouteConfig = {
type InitialRouteConfig = {
initialRouteName: string;
connectedRoutes: string[];
parentScreens: string[];
};
type ResultState = PartialState<NavigationState> & {
@@ -69,7 +69,7 @@ export default function getStateFromPath(
if (options?.initialRouteName) {
initialRoutes.push({
initialRouteName: options.initialRouteName,
connectedRoutes: Object.keys(options.screens),
parentScreens: [],
});
}
@@ -108,7 +108,8 @@ export default function getStateFromPath(
key,
screens as PathConfigMap,
[],
initialRoutes
initialRoutes,
[]
)
)
)
@@ -309,12 +310,15 @@ const createNormalizedConfigs = (
routeConfig: PathConfigMap,
routeNames: string[] = [],
initials: InitialRouteConfig[],
parentScreens: string[],
parentPattern?: string
): RouteConfig[] => {
const configs: RouteConfig[] = [];
routeNames.push(screen);
parentScreens.push(screen);
const config = routeConfig[screen];
if (typeof config === 'string') {
@@ -350,7 +354,7 @@ const createNormalizedConfigs = (
if (config.initialRouteName) {
initials.push({
initialRouteName: config.initialRouteName,
connectedRoutes: Object.keys(config.screens),
parentScreens,
});
}
@@ -360,6 +364,7 @@ const createNormalizedConfigs = (
config.screens as PathConfigMap,
routeNames,
initials,
[...parentScreens],
pattern ?? parentPattern
);
@@ -425,13 +430,23 @@ const findParseConfigForRoute = (
// Try to find an initial route connected with the one passed
const findInitialRoute = (
routeName: string,
parentScreens: string[],
initialRoutes: InitialRouteConfig[]
): string | undefined => {
for (const config of initialRoutes) {
if (config.connectedRoutes.includes(routeName)) {
return config.initialRouteName === routeName
? undefined
: config.initialRouteName;
if (parentScreens.length === config.parentScreens.length) {
let sameParents = true;
for (let i = 0; i < parentScreens.length; i++) {
if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
sameParents = false;
break;
}
}
if (sameParents) {
return routeName !== config.initialRouteName
? config.initialRouteName
: undefined;
}
}
}
return undefined;
@@ -477,7 +492,11 @@ const createNestedStateObject = (
) => {
let state: InitialState;
let route = routes.shift() as ParsedRoute;
let initialRoute = findInitialRoute(route.name, initialRoutes);
const parentScreens: string[] = [];
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
parentScreens.push(route.name);
state = createStateObject(initialRoute, route, routes.length === 0);
@@ -485,7 +504,7 @@ const createNestedStateObject = (
let nestedState = state;
while ((route = routes.shift() as ParsedRoute)) {
initialRoute = findInitialRoute(route.name, initialRoutes);
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
const nestedStateIndex =
nestedState.index || nestedState.routes.length - 1;
@@ -500,6 +519,8 @@ const createNestedStateObject = (
nestedState = nestedState.routes[nestedStateIndex]
.state as InitialState;
}
parentScreens.push(route.name);
}
}

View File

@@ -1,16 +1,21 @@
import * as React from 'react';
import type { NavigationState } from '@react-navigation/routers';
import type { NavigationState, ParamListBase } from '@react-navigation/routers';
import useNavigation from './useNavigation';
import type { NavigationProp } from './types';
type Selector<T> = (state: NavigationState) => T;
type Selector<ParamList extends ParamListBase, T> = (
state: NavigationState<ParamList>
) => T;
/**
* Hook to get a value from the current navigation state using a selector.
*
* @param selector Selector function to get a value from the state.
*/
export default function useNavigationState<T>(selector: Selector<T>): T {
const navigation = useNavigation();
export default function useNavigationState<ParamList extends ParamListBase, T>(
selector: Selector<ParamList, T>
): T {
const navigation = useNavigation<NavigationProp<ParamList>>();
// We don't care about the state value, we run the selector again at the end
// The state is only to make sure that there's a re-render when we have a new value

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0-next.1...@react-navigation/devtools@6.0.0-next.2) (2021-04-08)
**Note:** Version bump only for package @react-navigation/devtools
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/devtools@6.0.0...@react-navigation/devtools@6.0.0-next.1) (2021-03-10)
**Note:** Version bump only for package @react-navigation/devtools

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/devtools",
"description": "Developer tools for React Navigation",
"version": "6.0.0-next.1",
"version": "6.0.0-next.2",
"keywords": [
"react",
"react-native",
@@ -29,15 +29,14 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^6.0.0-next.1",
"@react-navigation/core": "^6.0.0-next.2",
"deep-equal": "^2.0.5"
},
"devDependencies": {

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.
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.3...@react-navigation/drawer@6.0.0-next.4) (2021-04-08)
### Bug Fixes
* don't handle back button with permanent drawer ([b893968](https://github.com/react-navigation/react-navigation/commit/b89396888f46ba79af3cfd84be55fba79d8387d2))
* fix drawer overlay on web ([3241190](https://github.com/react-navigation/react-navigation/commit/3241190b19946c1cd0a744fb09a19d79ba683d74))
* only handle back button in drawer when focused ([5ae0bad](https://github.com/react-navigation/react-navigation/commit/5ae0badc44b576d464f8841822a911b18a698403))
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.2...@react-navigation/drawer@6.0.0-next.3) (2021-03-22)
### Features
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0-next.1...@react-navigation/drawer@6.0.0-next.2) (2021-03-12)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess",
"version": "6.0.0-next.2",
"version": "6.0.0-next.4",
"keywords": [
"react-native-component",
"react-component",
@@ -34,31 +34,30 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/elements": "^1.0.0-next.2",
"@react-navigation/elements": "^1.0.0-next.4",
"color": "^3.1.3",
"warn-once": "^0.0.1"
},
"devDependencies": {
"@react-navigation/native": "^6.0.0-next.1",
"@react-navigation/native": "^6.0.0-next.2",
"@testing-library/react-native": "^7.2.0",
"@types/react": "^16.9.53",
"@types/react-native": "~0.63.51",
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"react-native-gesture-handler": "~1.8.0",
"react-native-reanimated": "~1.13.0",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-gesture-handler": "~1.10.2",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "~3.2.0",
"react-native-screens": "~3.0.0",
"typescript": "^4.2.3"
},
"peerDependencies": {
@@ -68,7 +67,7 @@
"react-native-gesture-handler": ">= 1.0.0",
"react-native-reanimated": ">= 1.0.0",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 2.15.0"
"react-native-screens": ">= 3.0.0"
},
"react-native-builder-bob": {
"source": "src",

View File

@@ -18,6 +18,9 @@ export { default as DrawerToggleButton } from './views/DrawerToggleButton';
*/
export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
export { default as DrawerProgressContext } from './utils/DrawerProgressContext';
export { default as useDrawerProgress } from './utils/useDrawerProgress';
export { default as getDrawerStatusFromState } from './utils/getDrawerStatusFromState';
export { default as useDrawerStatus } from './utils/useDrawerStatus';

View File

@@ -1,6 +1,8 @@
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
import type Animated from 'react-native-reanimated';
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
import type {
PanGestureHandler,
PanGestureHandlerProperties,
} from 'react-native-gesture-handler';
import type {
Route,
ParamListBase,
@@ -33,6 +35,18 @@ export type DrawerNavigationConfig = {
* Defaults to `true`.
*/
detachInactiveScreens?: boolean;
/**
* Whether to use the legacy implementation based on Reanimated 1.
* The new implementation based on Reanimated 2 will perform better,
* but you need additional configuration and need to use Hermes with Flipper to debug.
*
* This defaults to `true` in following cases:
* - Reanimated 2 is not configured
* - App is connected to Chrome debugger (Reanimated 2 cannot be used with Chrome debugger)
*
* Otherwise, it defaults to `false`
*/
useLegacyImplementation?: boolean;
};
export type DrawerNavigationOptions = HeaderOptions & {
@@ -207,11 +221,6 @@ export type DrawerContentComponentProps = {
state: DrawerNavigationState<ParamListBase>;
navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap;
/**
* Animated node which represents the current progress of the drawer's open state.
* `0` is closed, `1` is open.
*/
progress: Animated.Node<number>;
};
export type DrawerHeaderProps = {
@@ -268,3 +277,24 @@ export type DrawerDescriptor = Descriptor<
>;
export type DrawerDescriptorMap = Record<string, DrawerDescriptor>;
export type DrawerProps = {
dimensions: { width: number; height: number };
drawerPosition: 'left' | 'right';
drawerStyle?: StyleProp<ViewStyle>;
drawerType: 'front' | 'back' | 'slide' | 'permanent';
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
hideStatusBarOnOpen: boolean;
keyboardDismissMode: 'none' | 'on-drag';
onClose: () => void;
onOpen: () => void;
open: boolean;
overlayStyle?: StyleProp<ViewStyle>;
renderDrawerContent: () => React.ReactNode;
renderSceneContent: () => React.ReactNode;
statusBarAnimation: 'slide' | 'none' | 'fade';
swipeDistanceThreshold: number;
swipeEdgeWidth: number;
swipeEnabled: boolean;
swipeVelocityThreshold: number;
};

View File

@@ -0,0 +1,6 @@
import * as React from 'react';
import type Animated from 'react-native-reanimated';
export default React.createContext<
Readonly<Animated.SharedValue<number>> | Animated.Node<number> | undefined
>(undefined);

View File

@@ -0,0 +1,17 @@
import * as React from 'react';
import type Animated from 'react-native-reanimated';
import DrawerProgressContext from './DrawerProgressContext';
export default function useDrawerProgress():
| Readonly<Animated.SharedValue<number>>
| Animated.Node<number> {
const progress = React.useContext(DrawerProgressContext);
if (progress === undefined) {
throw new Error(
"Couldn't find a drawer. Is your component inside a drawer navigator?"
);
}
return progress;
}

View File

@@ -5,10 +5,10 @@ import {
I18nManager,
Platform,
BackHandler,
NativeEventSubscription,
} from 'react-native';
import { ScreenContainer } from 'react-native-screens';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import Animated from 'react-native-reanimated';
import {
NavigationHelpersContext,
DrawerNavigationState,
@@ -27,7 +27,6 @@ import { GestureHandlerRootView } from './GestureHandler';
import ScreenFallback from './ScreenFallback';
import DrawerToggleButton from './DrawerToggleButton';
import DrawerContent from './DrawerContent';
import Drawer from './Drawer';
import DrawerStatusContext from '../utils/DrawerStatusContext';
import DrawerPositionContext from '../utils/DrawerPositionContext';
import getDrawerStatusFromState from '../utils/getDrawerStatusFromState';
@@ -38,6 +37,7 @@ import type {
DrawerContentComponentProps,
DrawerHeaderProps,
DrawerNavigationProp,
DrawerProps,
} from '../types';
type Props = DrawerNavigationConfig & {
@@ -77,7 +77,17 @@ function DrawerViewBase({
<DrawerContent {...props} />
),
detachInactiveScreens = true,
// Running in chrome debugger
// @ts-expect-error
useLegacyImplementation = !global.nativeCallSyncHook ||
// Reanimated 2 is not configured
// @ts-expect-error: the type definitions are incomplete
!Animated.isConfigured?.(),
}: Props) {
const Drawer: React.ComponentType<DrawerProps> = useLegacyImplementation
? require('./legacy/Drawer').default
: require('./modern/Drawer').default;
const focusedRouteKey = state.routes[state.index].key;
const {
drawerHideStatusBarOnOpen = false,
@@ -85,14 +95,14 @@ function DrawerViewBase({
drawerStatusBarAnimation = 'slide',
drawerStyle,
drawerType = Platform.select({ ios: 'slide', default: 'front' }),
gestureEnabled,
gestureHandlerProps,
keyboardDismissMode = 'on-drag',
overlayColor = 'rgba(0, 0, 0, 0.5)',
sceneContainerStyle,
swipeEdgeWidth,
swipeEnabled,
swipeMinDistance,
swipeEdgeWidth = 32,
swipeEnabled = Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
swipeMinDistance = 60,
} = descriptors[focusedRouteKey].options;
const [loaded, setLoaded] = React.useState([focusedRouteKey]);
@@ -122,27 +132,53 @@ function DrawerViewBase({
}, [navigation, state.key]);
React.useEffect(() => {
let subscription: NativeEventSubscription | undefined;
if (drawerStatus === 'open') {
// We only add the subscription when drawer opens
// This way we can make sure that the subscription is added as late as possible
// This will make sure that our handler will run first when back button is pressed
subscription = BackHandler.addEventListener('hardwareBackPress', () => {
handleDrawerClose();
return true;
});
if (drawerStatus !== 'open' || drawerType === 'permanent') {
return;
}
return () => subscription?.remove();
}, [handleDrawerClose, drawerStatus, navigation, state.key]);
const handleClose = () => {
// We shouldn't handle the back button if the parent screen isn't focused
// This will avoid the drawer overriding event listeners from a focused screen
if (!navigation.isFocused()) {
return false;
}
const renderDrawerContent = ({ progress }: any) => {
handleDrawerClose();
return true;
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose();
}
};
// We only add the listeners when drawer opens
// This way we can make sure that the listener is added as late as possible
// This will make sure that our handler will run first when back button is pressed
const subscription = BackHandler.addEventListener(
'hardwareBackPress',
handleClose
);
if (Platform.OS === 'web') {
document?.body?.addEventListener?.('keyup', handleEscape);
}
return () => {
subscription.remove();
if (Platform.OS === 'web') {
document?.body?.removeEventListener?.('keyup', handleEscape);
}
};
}, [drawerStatus, drawerType, handleDrawerClose, navigation]);
const renderDrawerContent = () => {
return (
<DrawerPositionContext.Provider value={drawerPosition}>
{drawerContent({
progress: progress,
state: state,
navigation: navigation,
descriptors: descriptors,
@@ -181,6 +217,7 @@ function DrawerViewBase({
}
/>
),
sceneContainerStyle,
} = descriptor.options;
return (
@@ -191,6 +228,7 @@ function DrawerViewBase({
enabled={detachInactiveScreens}
>
<Screen
focused={isFocused}
route={descriptor.route}
navigation={descriptor.navigation}
headerShown={descriptor.options.headerShown}
@@ -201,6 +239,7 @@ function DrawerViewBase({
navigation: descriptor.navigation as DrawerNavigationProp<ParamListBase>,
options: descriptor.options,
})}
style={sceneContainerStyle}
>
{descriptor.render()}
</Screen>
@@ -215,17 +254,18 @@ function DrawerViewBase({
<DrawerStatusContext.Provider value={drawerStatus}>
<Drawer
open={drawerStatus !== 'closed'}
gestureEnabled={gestureEnabled}
swipeEnabled={swipeEnabled}
onOpen={handleDrawerOpen}
onClose={handleDrawerClose}
gestureHandlerProps={gestureHandlerProps}
swipeEnabled={swipeEnabled}
swipeEdgeWidth={swipeEdgeWidth}
swipeVelocityThreshold={500}
swipeDistanceThreshold={swipeMinDistance}
hideStatusBarOnOpen={drawerHideStatusBarOnOpen}
statusBarAnimation={drawerStatusBarAnimation}
keyboardDismissMode={keyboardDismissMode}
drawerType={drawerType}
drawerPosition={drawerPosition}
sceneContainerStyle={[
{ backgroundColor: colors.background },
sceneContainerStyle,
]}
drawerStyle={[
{
width: getDefaultDrawerWidth(dimensions),
@@ -244,13 +284,8 @@ function DrawerViewBase({
drawerStyle,
]}
overlayStyle={{ backgroundColor: overlayColor }}
swipeEdgeWidth={swipeEdgeWidth}
swipeDistanceThreshold={swipeMinDistance}
hideStatusBarOnOpen={drawerHideStatusBarOnOpen}
statusBarAnimation={drawerStatusBarAnimation}
renderDrawerContent={renderDrawerContent}
renderSceneContent={renderSceneContent}
keyboardDismissMode={keyboardDismissMode}
dimensions={dimensions}
/>
</DrawerStatusContext.Provider>

View File

@@ -1,24 +1,19 @@
import * as React from 'react';
import {
StyleSheet,
ViewStyle,
LayoutChangeEvent,
I18nManager,
Platform,
Keyboard,
StatusBar,
StyleProp,
View,
InteractionManager,
Pressable,
} from 'react-native';
import Animated from 'react-native-reanimated';
import {
PanGestureHandler,
TapGestureHandler,
GestureState,
} from './GestureHandler';
import { PanGestureHandler, GestureState } from '../GestureHandler';
import Overlay from './Overlay';
import DrawerProgressContext from '../../utils/DrawerProgressContext';
import type { DrawerProps } from '../../types';
const {
Clock,
@@ -56,7 +51,6 @@ const UNSET = -1;
const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;
const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60;
const SWIPE_DISTANCE_MINIMUM = 5;
const DEFAULT_DRAWER_WIDTH = '80%';
@@ -75,54 +69,8 @@ const ANIMATED_ONE = new Animated.Value(1);
type Binary = 0 | 1;
type Renderer = (props: { progress: Animated.Node<number> }) => React.ReactNode;
type Props = {
open: boolean;
onOpen: () => void;
onClose: () => void;
gestureEnabled: boolean;
swipeEnabled: boolean;
drawerPosition: 'left' | 'right';
drawerType: 'front' | 'back' | 'slide' | 'permanent';
keyboardDismissMode: 'none' | 'on-drag';
swipeEdgeWidth: number;
swipeDistanceThreshold?: number;
swipeVelocityThreshold: number;
hideStatusBarOnOpen: boolean;
statusBarAnimation: 'slide' | 'none' | 'fade';
overlayStyle?: StyleProp<ViewStyle>;
drawerStyle?: StyleProp<ViewStyle>;
sceneContainerStyle?: StyleProp<ViewStyle>;
renderDrawerContent: Renderer;
renderSceneContent: Renderer;
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
dimensions: { width: number; height: number };
};
export default class DrawerView extends React.Component<Props> {
static defaultProps = {
drawerPosition: I18nManager.isRTL ? 'left' : 'right',
drawerType: 'front',
gestureEnabled: true,
swipeEnabled:
Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
swipeEdgeWidth: 32,
swipeVelocityThreshold: 500,
keyboardDismissMode: 'on-drag',
hideStatusBarOnOpen: false,
statusBarAnimation: 'slide',
};
componentDidMount() {
if (Platform.OS === 'web') {
document?.body?.addEventListener?.('keyup', this.handleEscape);
}
}
componentDidUpdate(prevProps: Props) {
export default class DrawerView extends React.Component<DrawerProps> {
componentDidUpdate(prevProps: DrawerProps) {
const {
open,
drawerPosition,
@@ -157,11 +105,7 @@ export default class DrawerView extends React.Component<Props> {
}
if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) {
this.swipeDistanceThreshold.setValue(
swipeDistanceThreshold !== undefined
? swipeDistanceThreshold
: SWIPE_DISTANCE_THRESHOLD_DEFAULT
);
this.swipeDistanceThreshold.setValue(swipeDistanceThreshold);
}
if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) {
@@ -172,22 +116,8 @@ export default class DrawerView extends React.Component<Props> {
componentWillUnmount() {
this.toggleStatusBar(false);
this.handleEndInteraction();
if (Platform.OS === 'web') {
document?.body?.removeEventListener?.('keyup', this.handleEscape);
}
}
private handleEscape = (e: KeyboardEvent) => {
const { open, onClose } = this.props;
if (e.key === 'Escape') {
if (open) {
onClose();
}
}
};
private handleEndInteraction = () => {
if (this.interactionHandle !== undefined) {
InteractionManager.clearInteractionHandle(this.interactionHandle);
@@ -304,9 +234,7 @@ export default class DrawerView extends React.Component<Props> {
);
private swipeDistanceThreshold = new Value<number>(
this.props.swipeDistanceThreshold !== undefined
? this.props.swipeDistanceThreshold
: SWIPE_DISTANCE_THRESHOLD_DEFAULT
this.props.swipeDistanceThreshold
);
private swipeVelocityThreshold = new Value<number>(
this.props.swipeVelocityThreshold
@@ -516,18 +444,6 @@ export default class DrawerView extends React.Component<Props> {
},
]);
private handleTapStateChange = event([
{
nativeEvent: {
oldState: (s: Animated.Value<number>) =>
cond(
eq(s, GestureState.ACTIVE),
set(this.manuallyTriggerSpring, TRUE)
),
},
},
]);
private handleContainerLayout = (e: LayoutChangeEvent) =>
this.containerWidth.setValue(e.nativeEvent.layout.width);
@@ -568,12 +484,10 @@ export default class DrawerView extends React.Component<Props> {
render() {
const {
open,
gestureEnabled,
swipeEnabled,
drawerPosition,
drawerType,
swipeEdgeWidth,
sceneContainerStyle,
drawerStyle,
overlayStyle,
renderDrawerContent,
@@ -619,109 +533,103 @@ export default class DrawerView extends React.Component<Props> {
const progress = drawerType === 'permanent' ? ANIMATED_ONE : this.progress;
return (
<PanGestureHandler
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
onGestureEvent={this.handleGestureEvent}
onHandlerStateChange={this.handleGestureStateChange}
hitSlop={hitSlop}
enabled={drawerType !== 'permanent' && gestureEnabled && swipeEnabled}
{...gestureHandlerProps}
>
<Animated.View
onLayout={this.handleContainerLayout}
style={[
styles.main,
{
flexDirection:
drawerType === 'permanent' && !isRight ? 'row-reverse' : 'row',
},
]}
<DrawerProgressContext.Provider value={progress}>
<PanGestureHandler
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
onGestureEvent={this.handleGestureEvent}
onHandlerStateChange={this.handleGestureStateChange}
hitSlop={hitSlop}
enabled={drawerType !== 'permanent' && swipeEnabled}
{...gestureHandlerProps}
>
<Animated.View
onLayout={this.handleContainerLayout}
style={[
styles.content,
{ transform: [{ translateX: contentTranslateX }] },
sceneContainerStyle as any,
]}
>
<View
accessibilityElementsHidden={isOpen && drawerType !== 'permanent'}
importantForAccessibility={
isOpen && drawerType !== 'permanent'
? 'no-hide-descendants'
: 'auto'
}
style={styles.content}
>
{renderSceneContent({ progress })}
</View>
{
// Disable overlay if sidebar is permanent
drawerType === 'permanent' ? null : Platform.OS === 'web' ||
Platform.OS === 'windows' ||
Platform.OS === 'macos' ? (
<Pressable
onPress={
gestureEnabled ? () => this.toggleDrawer(false) : undefined
}
>
<Overlay progress={progress} style={overlayStyle as any} />
</Pressable>
) : (
<TapGestureHandler
enabled={gestureEnabled}
onHandlerStateChange={this.handleTapStateChange}
>
<Overlay progress={progress} style={overlayStyle as any} />
</TapGestureHandler>
)
}
</Animated.View>
<Animated.Code
// This is needed to make sure that container width updates with `setValue`
// Without this, it won't update when not used in styles
exec={this.containerWidth}
/>
{drawerType === 'permanent' ? null : (
<Animated.Code
exec={block([
onChange(this.manuallyTriggerSpring, [
cond(eq(this.manuallyTriggerSpring, TRUE), [
set(this.nextIsOpen, FALSE),
call([], () => (this.currentOpenValue = false)),
]),
]),
])}
/>
)}
<Animated.View
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
removeClippedSubviews={Platform.OS !== 'ios'}
onLayout={this.handleDrawerLayout}
style={[
styles.container,
styles.main,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
flexDirection:
drawerType === 'permanent' && !isRight
? 'row-reverse'
: 'row',
},
drawerType === 'permanent'
? // Without this, the `left`/`right` values don't get reset
isRight
? { right: 0 }
: { left: 0 }
: [
styles.nonPermanent,
isRight ? { right: offset } : { left: offset },
{ zIndex: drawerType === 'back' ? -1 : 0 },
],
drawerStyle as any,
]}
>
{renderDrawerContent({ progress })}
<Animated.View
style={[
styles.content,
{ transform: [{ translateX: contentTranslateX }] },
]}
>
<View
accessibilityElementsHidden={
isOpen && drawerType !== 'permanent'
}
importantForAccessibility={
isOpen && drawerType !== 'permanent'
? 'no-hide-descendants'
: 'auto'
}
style={styles.content}
>
{renderSceneContent()}
</View>
{
// Disable overlay if sidebar is permanent
drawerType === 'permanent' ? null : (
<Overlay
progress={progress}
onPress={() => this.toggleDrawer(false)}
style={overlayStyle as any}
/>
)
}
</Animated.View>
<Animated.Code
// This is needed to make sure that container width updates with `setValue`
// Without this, it won't update when not used in styles
exec={this.containerWidth}
/>
{drawerType === 'permanent' ? null : (
<Animated.Code
exec={block([
onChange(this.manuallyTriggerSpring, [
cond(eq(this.manuallyTriggerSpring, TRUE), [
set(this.nextIsOpen, FALSE),
call([], () => (this.currentOpenValue = false)),
]),
]),
])}
/>
)}
<Animated.View
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
removeClippedSubviews={Platform.OS !== 'ios'}
onLayout={this.handleDrawerLayout}
style={[
styles.container,
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
},
drawerType === 'permanent'
? // Without this, the `left`/`right` values don't get reset
isRight
? { right: 0 }
: { left: 0 }
: [
styles.nonPermanent,
isRight ? { right: offset } : { left: offset },
{ zIndex: drawerType === 'back' ? -1 : 0 },
],
drawerStyle as any,
]}
>
{renderDrawerContent()}
</Animated.View>
</Animated.View>
</Animated.View>
</PanGestureHandler>
</PanGestureHandler>
</DrawerProgressContext.Provider>
);
}
}

View File

@@ -1,26 +1,26 @@
import * as React from 'react';
import { Platform, StyleSheet } from 'react-native';
import { Pressable, Platform, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';
const {
interpolate: interpolateDeprecated,
// @ts-expect-error: this property is only present in Reanimated 2
interpolateNode,
cond,
greaterThan,
} = Animated;
const interpolate: typeof interpolateDeprecated =
const interpolate: typeof interpolateNode =
interpolateNode ?? interpolateDeprecated;
const PROGRESS_EPSILON = 0.05;
type Props = React.ComponentProps<typeof Animated.View> & {
progress: Animated.Node<number>;
onPress: () => void;
};
const Overlay = React.forwardRef(function Overlay(
{ progress, style, ...props }: Props,
{ progress, onPress, style, ...props }: Props,
ref: React.Ref<Animated.View>
) {
const animatedStyle = {
@@ -46,7 +46,9 @@ const Overlay = React.forwardRef(function Overlay(
{...props}
ref={ref}
style={[styles.overlay, overlayStyle, animatedStyle, style]}
/>
>
<Pressable onPress={onPress} style={styles.pressable} />
</Animated.View>
);
});
@@ -64,6 +66,9 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
pressable: {
flex: 1,
},
});
export default Overlay;

View File

@@ -0,0 +1,379 @@
import * as React from 'react';
import {
InteractionManager,
Keyboard,
Platform,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
State as GestureState,
} from 'react-native-gesture-handler';
import Animated, {
interpolate,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import type { DrawerProps } from '../../types';
import DrawerProgressContext from '../../utils/DrawerProgressContext';
import Overlay from './Overlay';
const SWIPE_DISTANCE_MINIMUM = 5;
const DEFAULT_DRAWER_WIDTH = '80%';
const minmax = (value: number, start: number, end: number) => {
'worklet';
return Math.min(Math.max(value, start), end);
};
export default function Drawer({
dimensions,
drawerPosition,
drawerStyle,
drawerType,
gestureHandlerProps,
hideStatusBarOnOpen,
keyboardDismissMode,
onClose,
onOpen,
open,
overlayStyle,
renderDrawerContent,
renderSceneContent,
statusBarAnimation,
swipeDistanceThreshold,
swipeEdgeWidth,
swipeEnabled,
swipeVelocityThreshold,
}: DrawerProps) {
const getDrawerWidth = (): number => {
const { width = DEFAULT_DRAWER_WIDTH } =
StyleSheet.flatten(drawerStyle) || {};
if (typeof width === 'string' && width.endsWith('%')) {
// Try to calculate width if a percentage is given
const percentage = Number(width.replace(/%$/, ''));
if (Number.isFinite(percentage)) {
return dimensions.width * (percentage / 100);
}
}
return typeof width === 'number' ? width : 0;
};
const drawerWidth = getDrawerWidth();
const isOpen = drawerType === 'permanent' ? true : open;
const isRight = drawerPosition === 'right';
const getDrawerTranslationX = React.useCallback(
(open: boolean) => {
'worklet';
if (drawerPosition === 'left') {
return open ? 0 : -drawerWidth;
}
return open ? 0 : drawerWidth;
},
[drawerPosition, drawerWidth]
);
const hideStatusBar = React.useCallback(
(hide: boolean) => {
if (hideStatusBarOnOpen) {
StatusBar.setHidden(hide, statusBarAnimation);
}
},
[hideStatusBarOnOpen, statusBarAnimation]
);
React.useEffect(() => {
hideStatusBar(isOpen);
return () => hideStatusBar(false);
}, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
const interactionHandleRef = React.useRef<number | null>(null);
const startInteraction = () => {
interactionHandleRef.current = InteractionManager.createInteractionHandle();
};
const endInteraction = () => {
if (interactionHandleRef.current != null) {
InteractionManager.clearInteractionHandle(interactionHandleRef.current);
interactionHandleRef.current = null;
}
};
const hideKeyboard = () => {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
};
const onGestureStart = () => {
startInteraction();
hideKeyboard();
hideStatusBar(true);
};
const onGestureEnd = () => {
endInteraction();
};
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/kmagiera/react-native-gesture-handler/issues/569
const hitSlop = isRight
? // Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{ right: 0, width: isOpen ? undefined : swipeEdgeWidth }
: { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
const touchStartX = useSharedValue(0);
const touchX = useSharedValue(0);
const translationX = useSharedValue(getDrawerTranslationX(open));
const gestureState = useSharedValue<GestureState>(GestureState.UNDETERMINED);
const toggleDrawer = React.useCallback(
(open: boolean, velocity?: number) => {
'worklet';
const translateX = getDrawerTranslationX(open);
touchStartX.value = 0;
touchX.value = 0;
translationX.value = withSpring(
translateX,
{
velocity,
stiffness: 1000,
damping: 500,
mass: 3,
overshootClamping: true,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01,
},
() => {
if (translationX.value === getDrawerTranslationX(true)) {
runOnJS(onOpen)();
} else if (translationX.value === getDrawerTranslationX(false)) {
runOnJS(onClose)();
}
}
);
},
[getDrawerTranslationX, onClose, onOpen, touchStartX, touchX, translationX]
);
React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
const onGestureEvent = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{ startX: number }
>({
onStart: (event, ctx) => {
ctx.startX = translationX.value;
gestureState.value = event.state;
touchStartX.value = event.x;
runOnJS(onGestureStart)();
},
onActive: (event, ctx) => {
touchX.value = event.x;
translationX.value = ctx.startX + event.translationX;
gestureState.value = event.state;
},
onEnd: (event) => {
gestureState.value = event.state;
const nextOpen =
(Math.abs(event.translationX) > SWIPE_DISTANCE_MINIMUM &&
Math.abs(event.translationX) > swipeVelocityThreshold) ||
Math.abs(event.translationX) > swipeDistanceThreshold
? drawerPosition === 'left'
? // If swiped to right, open the drawer, otherwise close it
(event.velocityX === 0 ? event.translationX : event.velocityX) > 0
: // If swiped to left, open the drawer, otherwise close it
(event.velocityX === 0 ? event.translationX : event.velocityX) < 0
: open;
toggleDrawer(nextOpen, event.velocityX);
runOnJS(onGestureEnd)();
},
});
const translateX = useDerivedValue(() => {
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
// While closing the drawer when user starts gesture outside of its area (in greyed
// out part of the window), we want the drawer to follow only once finger reaches the
// edge of the drawer.
// E.g. on the diagram below drawer is illustrate by X signs and the greyed out area by
// dots. The touch gesture starts at '*' and moves left, touch path is indicated by
// an arrow pointing left
// 1) +---------------+ 2) +---------------+ 3) +---------------+ 4) +---------------+
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|.<-*..| |XXXXXXXX|<--*..| |XXXXX|<-----*..|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// +---------------+ +---------------+ +---------------+ +---------------+
//
// For the above to work properly we define animated value that will keep start position
// of the gesture. Then we use that value to calculate how much we need to subtract from
// the translationX. If the gesture started on the greyed out area we take the distance from the
// edge of the drawer to the start position. Otherwise we don't subtract at all and the
// drawer be pulled back as soon as you start the pan.
//
// This is used only when drawerType is "front"
const touchDistance =
drawerType === 'front' && gestureState.value === GestureState.ACTIVE
? minmax(
drawerPosition === 'left'
? touchStartX.value - drawerWidth
: dimensions.width - drawerWidth - touchStartX.value,
0,
dimensions.width
)
: 0;
const translateX =
drawerPosition === 'left'
? minmax(translationX.value + touchDistance, -drawerWidth, 0)
: minmax(translationX.value - touchDistance, 0, drawerWidth);
return translateX;
});
const drawerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX:
drawerType === 'permanent' || drawerType === 'back'
? 0
: translateX.value,
},
],
};
});
const contentAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX:
drawerType === 'permanent' || drawerType === 'front'
? 0
: drawerPosition === 'left'
? drawerWidth + translateX.value
: translateX.value - drawerWidth,
},
],
};
});
const progress = useDerivedValue(() => {
return drawerType === 'permanent'
? 1
: interpolate(
translateX.value,
[getDrawerTranslationX(false), getDrawerTranslationX(true)],
[0, 1]
);
});
return (
<DrawerProgressContext.Provider value={progress}>
<PanGestureHandler
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
hitSlop={hitSlop}
enabled={drawerType !== 'permanent' && swipeEnabled}
onGestureEvent={onGestureEvent}
{...gestureHandlerProps}
>
{/* Immediate child of gesture handler needs to be an Animated.View */}
<Animated.View
style={[
styles.main,
{
flexDirection:
drawerType === 'permanent' && !isRight ? 'row-reverse' : 'row',
},
]}
>
<Animated.View style={[styles.content, contentAnimatedStyle]}>
<View
accessibilityElementsHidden={isOpen && drawerType !== 'permanent'}
importantForAccessibility={
isOpen && drawerType !== 'permanent'
? 'no-hide-descendants'
: 'auto'
}
style={styles.content}
>
{renderSceneContent()}
</View>
{drawerType !== 'permanent' ? (
<Overlay
progress={progress}
onPress={() => toggleDrawer(false)}
style={overlayStyle}
/>
) : null}
</Animated.View>
<Animated.View
accessibilityViewIsModal={isOpen && drawerType !== 'permanent'}
removeClippedSubviews={Platform.OS !== 'ios'}
style={[
styles.container,
{
position: drawerType === 'permanent' ? 'relative' : 'absolute',
zIndex: drawerType === 'back' ? -1 : 0,
},
drawerAnimatedStyle,
drawerStyle as any,
]}
>
{renderDrawerContent()}
</Animated.View>
</Animated.View>
</PanGestureHandler>
</DrawerProgressContext.Provider>
);
}
const styles = StyleSheet.create({
container: {
top: 0,
bottom: 0,
maxWidth: '100%',
width: DEFAULT_DRAWER_WIDTH,
},
content: {
flex: 1,
},
main: {
flex: 1,
...Platform.select({
// FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
// But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
web: null,
default: { overflow: 'hidden' },
}),
},
});

View File

@@ -0,0 +1,56 @@
import * as React from 'react';
import { Pressable, Platform, StyleSheet } from 'react-native';
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
const PROGRESS_EPSILON = 0.05;
type Props = React.ComponentProps<typeof Animated.View> & {
progress: Animated.SharedValue<number>;
onPress: () => void;
};
const Overlay = React.forwardRef(function Overlay(
{ progress, onPress, style, ...props }: Props,
ref: React.Ref<Animated.View>
) {
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: progress.value,
// We don't want the user to be able to press through the overlay when drawer is open
// One approach is to adjust the pointerEvents based on the progress
// But we can also send the overlay behind the screen
zIndex: progress.value > PROGRESS_EPSILON ? 0 : -1,
};
});
return (
<Animated.View
{...props}
ref={ref}
style={[styles.overlay, overlayStyle, animatedStyle, style]}
>
<Pressable onPress={onPress} style={styles.pressable} />
</Animated.View>
);
});
const overlayStyle = Platform.select<Record<string, string>>({
web: {
// Disable touch highlight on mobile Safari.
// WebkitTapHighlightColor must be used outside of StyleSheet.create because react-native-web will omit the property.
WebkitTapHighlightColor: 'transparent',
},
default: {},
});
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
pressable: {
flex: 1,
},
});
export default Overlay;

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.3...@react-navigation/elements@1.0.0-next.4) (2021-04-08)
**Note:** Version bump only for package @react-navigation/elements
# [1.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.2...@react-navigation/elements@1.0.0-next.3) (2021-03-22)
### Features
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
# [1.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0-next.1...@react-navigation/elements@1.0.0-next.2) (2021-03-12)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/elements",
"description": "UI Components for React Navigation",
"version": "1.0.0-next.2",
"version": "1.0.0-next.4",
"keywords": [
"react-native",
"react-navigation",
@@ -30,22 +30,21 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"devDependencies": {
"@react-native-masked-view/masked-view": "^0.2.2",
"@react-navigation/native": "^6.0.0-next.1",
"@react-native-masked-view/masked-view": "^0.2.3",
"@react-navigation/native": "^6.0.0-next.2",
"@testing-library/react-native": "^7.2.0",
"@types/react": "^16.9.53",
"@types/react-native": "~0.63.51",
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"typescript": "^4.2.3"
},

View File

@@ -0,0 +1,18 @@
import * as React from 'react';
import { View, ViewProps } from 'react-native';
import { useTheme } from '@react-navigation/native';
type Props = ViewProps & {
children: React.ReactNode;
};
export default function Background({ style, ...rest }: Props) {
const { colors } = useTheme();
return (
<View
{...rest}
style={[{ flex: 1, backgroundColor: colors.background }, style]}
/>
);
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, StyleSheet } from 'react-native';
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import {
useSafeAreaFrame,
useSafeAreaInsets,
@@ -12,16 +12,19 @@ import {
ParamListBase,
} from '@react-navigation/native';
import Background from './Background';
import HeaderShownContext from './Header/HeaderShownContext';
import HeaderHeightContext from './Header/HeaderHeightContext';
import getDefaultHeaderHeight from './Header/getDefaultHeaderHeight';
type Props = {
focused: boolean;
navigation: NavigationProp<ParamListBase>;
route: RouteProp<ParamListBase, string>;
header: React.ReactNode;
headerShown?: boolean;
headerStatusBarHeight?: number;
style?: StyleProp<ViewStyle>;
children: React.ReactNode;
};
@@ -33,12 +36,14 @@ export default function Screen(props: Props) {
const parentHeaderHeight = React.useContext(HeaderHeightContext);
const {
focused,
header,
headerShown = true,
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
children,
navigation,
route,
children,
style,
} = props;
const [headerHeight, setHeaderHeight] = React.useState(() =>
@@ -46,7 +51,11 @@ export default function Screen(props: Props) {
);
return (
<View style={styles.container}>
<Background
accessibilityElementsHidden={!focused}
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
style={[styles.container, style]}
>
<View style={styles.content}>
<HeaderShownContext.Provider
value={isParentHeaderShown || headerShown !== false}
@@ -73,7 +82,7 @@ export default function Screen(props: Props) {
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
) : null}
</View>
</Background>
);
}

View File

@@ -14,6 +14,7 @@ export { default as PlatformPressable } from './PlatformPressable';
export { default as ResourceSavingView } from './ResourceSavingView';
export { default as SafeAreaProviderCompat } from './SafeAreaProviderCompat';
export { default as Screen } from './Screen';
export { default as Background } from './Background';
export const Assets = [
// eslint-disable-next-line import/no-commonjs

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0-next.1...@react-navigation/material-bottom-tabs@6.0.0-next.2) (2021-04-08)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-bottom-tabs@6.0.0...@react-navigation/material-bottom-tabs@6.0.0-next.1) (2021-03-10)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper",
"version": "6.0.0-next.1",
"version": "6.0.0-next.2",
"keywords": [
"react-native-component",
"react-component",
@@ -34,22 +34,21 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"devDependencies": {
"@react-navigation/native": "^6.0.0-next.1",
"@react-navigation/native": "^6.0.0-next.2",
"@testing-library/react-native": "^7.2.0",
"@types/react": "^16.9.53",
"@types/react-native": "~0.63.51",
"@types/react-native-vector-icons": "^6.4.6",
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"react-native-paper": "^4.7.2",
"react-native-vector-icons": "^8.1.0",

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.2...@react-navigation/material-top-tabs@6.0.0-next.3) (2021-04-08)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0-next.1...@react-navigation/material-top-tabs@6.0.0-next.2) (2021-04-08)
**Note:** Version bump only for package @react-navigation/material-top-tabs
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/material-top-tabs@6.0.0...@react-navigation/material-top-tabs@6.0.0-next.1) (2021-03-10)

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": "6.0.0-next.1",
"version": "6.0.0-next.3",
"keywords": [
"react-native-component",
"react-component",
@@ -34,8 +34,7 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
@@ -46,16 +45,16 @@
"warn-once": "^0.0.1"
},
"devDependencies": {
"@react-navigation/native": "^6.0.0-next.1",
"@react-navigation/native": "^6.0.0-next.2",
"@testing-library/react-native": "^7.2.0",
"@types/react": "^16.9.53",
"@types/react-native": "~0.63.51",
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"react-native-pager-view": "^4.2.4",
"react-native-tab-view": "^3.0.0",
"react-native-pager-view": "^5.0.12",
"react-native-tab-view": "^3.0.1",
"typescript": "^4.2.3"
},
"peerDependencies": {

View File

@@ -26,9 +26,7 @@ function MaterialTopTabNavigator({
backBehavior,
children,
screenOptions,
// @ts-expect-error: lazy is deprecated
lazy,
// @ts-expect-error: tabBarOptions is deprecated
tabBarOptions,
...rest
}: Props) {

View File

@@ -1,5 +1,5 @@
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
import type { SceneRendererProps, TabView } from 'react-native-tab-view';
import type { SceneRendererProps, TabViewProps } from 'react-native-tab-view';
import type {
ParamListBase,
Descriptor,
@@ -67,6 +67,16 @@ export type MaterialTopTabNavigationOptions = {
*/
lazy?: boolean;
/**
* Function that returns a React element to render if this screen hasn't been rendered yet.
* The `lazy` option also needs to be enabled for this to work.
*
* This view is usually only shown for a split second. Keep it lightweight.
*
* By default, this renders null.
*/
lazyPlaceholder?: () => React.ReactNode;
/**
* 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.
@@ -187,37 +197,21 @@ export type MaterialTopTabDescriptorMap = Record<
MaterialTopTabDescriptor
>;
export type MaterialTopTabNavigationConfig = Partial<
Omit<
React.ComponentProps<typeof TabView>,
| 'navigationState'
| 'onIndexChange'
| 'onSwipeStart'
| 'onSwipeEnd'
| 'renderScene'
| 'renderTabBar'
| 'renderLazyPlaceholder'
| 'lazy'
>
export type MaterialTopTabNavigationConfig = Omit<
TabViewProps<Route<string>>,
| 'navigationState'
| 'onIndexChange'
| 'onSwipeStart'
| 'onSwipeEnd'
| 'renderScene'
| 'renderTabBar'
| 'renderLazyPlaceholder'
| 'lazy'
> & {
/**
* Function that returns a React element to render for routes that haven't been rendered yet.
* Receives an object containing the route as the prop.
* The lazy prop also needs to be enabled.
*
* This view is usually only shown for a split second. Keep it lightweight.
*
* By default, this renders null.
*/
lazyPlaceholder?: (props: { route: Route<string> }) => React.ReactNode;
/**
* Function that returns a React element to display as the tab bar.
*/
tabBar?: (props: MaterialTopTabBarProps) => React.ReactNode;
/**
* Position of the tab bar. Defaults to `top`.
*/
tabBarPosition?: 'top' | 'bottom';
};
export type MaterialTopTabBarProps = SceneRendererProps & {

View File

@@ -21,11 +21,9 @@ type Props = MaterialTopTabNavigationConfig & {
state: TabNavigationState<ParamListBase>;
navigation: MaterialTopTabNavigationHelpers;
descriptors: MaterialTopTabDescriptorMap;
tabBarPosition?: 'top' | 'bottom';
};
export default function MaterialTopTabView({
lazyPlaceholder,
tabBar = (props: MaterialTopTabBarProps) => <MaterialTopTabBar {...props} />,
state,
navigation,
@@ -57,7 +55,9 @@ export default function MaterialTopTabView({
renderScene={({ route }) => descriptors[route.key].render()}
navigationState={state}
renderTabBar={renderTabBar}
renderLazyPlaceholder={lazyPlaceholder}
renderLazyPlaceholder={({ route }) =>
descriptors[route.key].options.lazyPlaceholder?.() ?? null
}
lazy={({ route }) => descriptors[route.key].options.lazy === true}
onSwipeStart={() => navigation.emit({ type: 'swipeStart' })}
onSwipeEnd={() => navigation.emit({ type: 'swipeEnd' })}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@6.0.0-next.1...@react-navigation/native@6.0.0-next.2) (2021-04-08)
**Note:** Version bump only for package @react-navigation/native
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/native@6.0.0...@react-navigation/native@6.0.0-next.1) (2021-03-10)
**Note:** Version bump only for package @react-navigation/native

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/native",
"description": "React Native integration for React Navigation",
"version": "6.0.0-next.1",
"version": "6.0.0-next.2",
"keywords": [
"react-native",
"react-navigation",
@@ -30,17 +30,16 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/core": "^6.0.0-next.1",
"@react-navigation/core": "^6.0.0-next.2",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.20"
"nanoid": "^3.1.22"
},
"devDependencies": {
"@testing-library/react-native": "^7.2.0",
@@ -50,7 +49,7 @@
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-dom": "^16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"typescript": "^4.2.3"
},

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@6.0.0-next.1...@react-navigation/routers@6.0.0-next.2) (2021-04-08)
**Note:** Version bump only for package @react-navigation/routers
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/routers@6.0.0...@react-navigation/routers@6.0.0-next.1) (2021-03-10)
**Note:** Version bump only for package @react-navigation/routers

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/routers",
"description": "Routers to help build custom navigators",
"version": "6.0.0-next.1",
"version": "6.0.0-next.2",
"keywords": [
"react",
"react-native",
@@ -29,15 +29,14 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"nanoid": "^3.1.20"
"nanoid": "^3.1.22"
},
"devDependencies": {
"del-cli": "^3.0.1",

View File

@@ -3,6 +3,82 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.0.0-next.9](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.8...@react-navigation/stack@6.0.0-next.9) (2021-04-08)
**Note:** Version bump only for package @react-navigation/stack
# [6.0.0-next.8](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.7...@react-navigation/stack@6.0.0-next.8) (2021-03-22)
### Features
* add a Background component ([cbaabc1](https://github.com/react-navigation/react-navigation/commit/cbaabc1288e780698e499a00b9ca06ab9746a0da))
# [6.0.0-next.7](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.6...@react-navigation/stack@6.0.0-next.7) (2021-03-22)
### Code Refactoring
* make gestureResponseDistance a number ([48851c9](https://github.com/react-navigation/react-navigation/commit/48851c9ebdcf1b835bbcb673adeb88e56b989443))
### BREAKING CHANGES
* now we need to pass a number instead of an object
# [6.0.0-next.6](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.5...@react-navigation/stack@6.0.0-next.6) (2021-03-14)
**Note:** Version bump only for package @react-navigation/stack
# [6.0.0-next.5](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.4...@react-navigation/stack@6.0.0-next.5) (2021-03-14)
### Bug Fixes
* consider header colors when managing statusbar ([3ad2bcb](https://github.com/react-navigation/react-navigation/commit/3ad2bcbaf85996ce0d5e1e961081978a32448899))
* consider header colors when managing statusbar ([faee245](https://github.com/react-navigation/react-navigation/commit/faee245d2ec8c59f9e9033d96ae21c5e60d95ba6))
### Code Refactoring
* move headerMode to options ([aacc1b5](https://github.com/react-navigation/react-navigation/commit/aacc1b525d86f0e0b1bad8016fd85e82024f16e9))
### BREAKING CHANGES
* headerMode is now moved to options instead of props
# [6.0.0-next.4](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.3...@react-navigation/stack@6.0.0-next.4) (2021-03-12)
### Bug Fixes
* add special statusbar handling to modal presentation ([a204edd](https://github.com/react-navigation/react-navigation/commit/a204edd012060f0816eddee7a093183aa379d049))
# [6.0.0-next.3](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.2...@react-navigation/stack@6.0.0-next.3) (2021-03-12)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/stack",
"description": "Stack navigator component for iOS and Android with animated transitions and gestures",
"version": "6.0.0-next.3",
"version": "6.0.0-next.9",
"keywords": [
"react-native-component",
"react-component",
@@ -33,32 +33,31 @@
],
"sideEffects": false,
"publishConfig": {
"access": "public",
"tag": "alpha"
"access": "public"
},
"scripts": {
"prepare": "bob build",
"clean": "del lib"
},
"dependencies": {
"@react-navigation/elements": "^1.0.0-next.2",
"@react-navigation/elements": "^1.0.0-next.4",
"color": "^3.1.3",
"react-native-iphone-x-helper": "^1.3.0",
"warn-once": "^0.0.1"
},
"devDependencies": {
"@react-navigation/native": "^6.0.0-next.1",
"@react-navigation/native": "^6.0.0-next.2",
"@testing-library/react-native": "^7.2.0",
"@types/color": "^3.0.1",
"@types/react": "^16.9.53",
"@types/react-native": "~0.63.51",
"del-cli": "^3.0.1",
"react": "~16.13.1",
"react-native": "~0.63.2",
"react-native": "~0.63.4",
"react-native-builder-bob": "^0.18.1",
"react-native-gesture-handler": "~1.8.0",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-gesture-handler": "~1.10.2",
"react-native-safe-area-context": "~3.2.0",
"react-native-screens": "~3.0.0",
"typescript": "^4.2.3"
},
"peerDependencies": {
@@ -67,7 +66,7 @@
"react-native": "*",
"react-native-gesture-handler": ">= 1.0.0",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 2.15.0"
"react-native-screens": ">= 3.0.0"
},
"react-native-builder-bob": {
"source": "src",

View File

@@ -160,6 +160,10 @@ export function forModalPresentationIOS({
overflow: 'hidden',
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
// We don't need these for the animation
// But different border radius for corners improves animation perf
borderBottomLeftRadius: isIphoneX() ? borderRadius : 0,
borderBottomRightRadius: isIphoneX() ? borderRadius : 0,
marginTop: index === 0 ? 0 : statusBarHeight,
marginBottom: index === 0 ? 0 : topOffset,
transform: [{ translateY }, { scale }],

View File

@@ -18,6 +18,7 @@ import type {
StackNavigationConfig,
StackNavigationOptions,
StackNavigationEventMap,
StackHeaderMode,
} from '../types';
type Props = DefaultNavigatorOptions<StackNavigationOptions> &
@@ -31,13 +32,18 @@ function StackNavigator({
...rest
}: Props) {
// @ts-expect-error: headerMode='none' is deprecated
const isHeaderModeNone = rest.headerMode === 'none';
const headerMode = rest.headerMode as StackHeaderMode | 'none' | undefined;
warnOnce(
isHeaderModeNone,
headerMode === 'none',
`Stack Navigator: 'headerMode="none"' is deprecated. Use 'headerShown: false' in 'screenOptions' instead.`
);
warnOnce(
headerMode && headerMode !== 'none',
`Stack Navigator: 'headerMode' is moved to 'options'. Moved it to 'screenOptions' to keep current behavior.`
);
const { state, descriptors, navigation } = useNavigationBuilder<
StackNavigationState<ParamListBase>,
StackRouterOptions,
@@ -48,14 +54,22 @@ function StackNavigator({
initialRouteName,
children,
screenOptions,
defaultScreenOptions: {
headerShown: !isHeaderModeNone,
defaultScreenOptions: ({ options }) => ({
headerShown: headerMode ? headerMode !== 'none' : true,
headerMode:
headerMode && headerMode !== 'none'
? headerMode
: rest.mode !== 'modal' &&
Platform.OS === 'ios' &&
options.header === undefined
? 'float'
: 'screen',
gestureEnabled: Platform.OS === 'ios',
animationEnabled:
Platform.OS !== 'web' &&
Platform.OS !== 'windows' &&
Platform.OS !== 'macos',
},
}),
});
React.useEffect(

View File

@@ -198,7 +198,12 @@ export type StackNavigationOptions = StackHeaderOptions &
*/
header?: (props: StackHeaderProps) => React.ReactNode;
/**
* Whether to show the header. The header is shown by default unless `headerMode` was set to `none`.
* Whether the header floats above the screen or part of the screen.
* Defaults to `float` on iOS for non-modals, and `screen` for the rest.
*/
headerMode?: StackHeaderMode;
/**
* Whether to show the header. The header is shown by default.
* Setting this to `false` hides the header.
*/
headerShown?: boolean;
@@ -244,19 +249,10 @@ export type StackNavigationOptions = StackHeaderOptions &
*/
gestureEnabled?: boolean;
/**
* Object to override the distance of touch start from the edge of the screen to recognize gestures.
* Distance of touch start from the edge of the screen to recognize gestures.
* Not supported on Web.
*/
gestureResponseDistance?: {
/**
* Distance for vertical direction. Defaults to 135.
*/
vertical?: number;
/**
* Distance for horizontal direction. Defaults to 25.
*/
horizontal?: number;
};
gestureResponseDistance?: number;
/**
* Number which determines the relevance of velocity for the gesture. Defaults to 0.3.
* Not supported on Web.
@@ -273,7 +269,6 @@ export type StackNavigationOptions = StackHeaderOptions &
export type StackNavigationConfig = {
mode?: StackCardMode;
headerMode?: StackHeaderMode;
/**
* If `false`, the keyboard will NOT automatically dismiss when navigating to a new screen.
* Defaults to `true`.

View File

@@ -21,7 +21,6 @@ import type {
StackHeaderStyleInterpolator,
StackNavigationProp,
StackHeaderProps,
GestureDirection,
} from '../../types';
export type Props = {
@@ -35,7 +34,6 @@ export type Props = {
height: number;
}) => void;
styleInterpolator: StackHeaderStyleInterpolator;
gestureDirection: GestureDirection;
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};
@@ -46,7 +44,6 @@ export default function HeaderContainer({
getPreviousScene,
getFocusedRoute,
onContentHeightChange,
gestureDirection,
styleInterpolator,
style,
}: Props) {
@@ -60,10 +57,10 @@ export default function HeaderContainer({
return null;
}
const { header, headerShown = true, headerTransparent } =
const { header, headerMode, headerShown = true, headerTransparent } =
scene.descriptor.options || {};
if (!headerShown) {
if (headerMode !== mode || !headerShown) {
return null;
}
@@ -87,18 +84,24 @@ export default function HeaderContainer({
const previousDescriptor = self[i - 1]?.descriptor;
const nextDescriptor = self[i + 1]?.descriptor;
const { headerShown: previousHeaderShown = true } =
previousDescriptor?.options || {};
const {
headerShown: previousHeaderShown = true,
headerMode: previousHeaderMode,
} = previousDescriptor?.options || {};
const { headerShown: nextHeaderShown = true } =
nextDescriptor?.options || {};
const {
headerShown: nextHeaderShown = true,
headerMode: nextHeaderMode,
gestureDirection: nextGestureDirection,
} = nextDescriptor?.options || {};
const isHeaderStatic =
(previousHeaderShown === false &&
((previousHeaderShown === false || previousHeaderMode === 'screen') &&
// We still need to animate when coming back from next scene
// A hacky way to check this is if the next scene exists
!nextDescriptor) ||
nextHeaderShown === false;
nextHeaderShown === false ||
nextHeaderMode === 'screen';
const props: StackHeaderProps = {
layout,
@@ -111,10 +114,10 @@ export default function HeaderContainer({
styleInterpolator:
mode === 'float'
? isHeaderStatic
? gestureDirection === 'vertical' ||
gestureDirection === 'vertical-inverted'
? nextGestureDirection === 'vertical' ||
nextGestureDirection === 'vertical-inverted'
? forSlideUp
: gestureDirection === 'horizontal-inverted'
: nextGestureDirection === 'horizontal-inverted'
? forSlideRight
: forSlideLeft
: styleInterpolator

View File

@@ -0,0 +1,58 @@
import * as React from 'react';
import { StatusBar, StyleSheet } from 'react-native';
import { useTheme } from '@react-navigation/native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import type { Layout } from '../types';
type Props = {
dark: boolean | undefined;
layout: Layout;
insets: EdgeInsets;
style: any;
};
export default function ModalStatusBarManager({
dark,
layout,
insets,
style,
}: Props) {
const { dark: darkTheme } = useTheme();
const [overlapping, setOverlapping] = React.useState(true);
const enabled = layout.width && layout.height > layout.width;
const scale = 1 - 20 / layout.width;
const offset = (insets.top - 34) * scale;
const flattenedStyle = StyleSheet.flatten(style);
const translateY = flattenedStyle?.transform?.find(
(s: any) => s.translateY !== undefined
)?.translateY;
React.useEffect(() => {
if (!enabled) {
return;
}
const listener = ({ value }: { value: number }) => {
setOverlapping(value < offset);
};
const sub = translateY?.addListener(listener);
return () => translateY?.removeListener(sub);
}, [enabled, offset, translateY]);
if (!enabled) {
return null;
}
const darkContent = dark ?? !darkTheme;
return (
<StatusBar
animated
barStyle={overlapping && darkContent ? 'dark-content' : 'light-content'}
/>
);
}

View File

@@ -18,6 +18,8 @@ import {
GestureState,
PanGestureHandlerGestureEvent,
} from '../GestureHandler';
import ModalStatusBarManager from '../ModalStatusBarManager';
import { forModalPresentationIOS } from '../../TransitionConfigs/CardStyleInterpolators';
import CardAnimationContext from '../../utils/CardAnimationContext';
import getDistanceForDirection from '../../utils/getDistanceForDirection';
import getInvertedMultiplier from '../../utils/getInvertedMultiplier';
@@ -37,6 +39,7 @@ type Props = ViewProps & {
gesture: Animated.Value;
layout: Layout;
insets: EdgeInsets;
headerDarkContent: boolean | undefined;
pageOverflowEnabled: boolean;
gestureDirection: GestureDirection;
onOpen: () => void;
@@ -52,10 +55,7 @@ type Props = ViewProps & {
overlayEnabled: boolean;
shadowEnabled: boolean;
gestureEnabled: boolean;
gestureResponseDistance?: {
vertical?: number;
horizontal?: number;
};
gestureResponseDistance?: number;
gestureVelocityImpact: number;
transitionSpec: {
open: TransitionSpec;
@@ -406,13 +406,11 @@ export default class Card extends React.Component<Props> {
const { layout, gestureDirection, gestureResponseDistance } = this.props;
const distance =
gestureDirection === 'vertical' ||
gestureDirection === 'vertical-inverted'
? gestureResponseDistance?.vertical !== undefined
? gestureResponseDistance.vertical
: GESTURE_RESPONSE_DISTANCE_VERTICAL
: gestureResponseDistance?.horizontal !== undefined
? gestureResponseDistance.horizontal
gestureResponseDistance !== undefined
? gestureResponseDistance
: gestureDirection === 'vertical' ||
gestureDirection === 'vertical-inverted'
? GESTURE_RESPONSE_DISTANCE_VERTICAL
: GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
if (gestureDirection === 'vertical') {
@@ -464,6 +462,7 @@ export default class Card extends React.Component<Props> {
gestureEnabled,
gestureDirection,
pageOverflowEnabled,
headerDarkContent,
children,
containerStyle: customContainerStyle,
contentStyle,
@@ -523,6 +522,22 @@ export default class Card extends React.Component<Props> {
return (
<CardAnimationContext.Provider value={animationContext}>
{
// StatusBar messes with translucent status bar on Android
// So we should only enable it on iOS
Platform.OS === 'ios' &&
overlayEnabled &&
index === 0 &&
next &&
styleInterpolator === forModalPresentationIOS ? (
<ModalStatusBarManager
dark={headerDarkContent}
layout={layout}
insets={insets}
style={cardStyle}
/>
) : null
}
<Animated.View
style={{
// This is a dummy style that doesn't actually change anything visually.

View File

@@ -27,6 +27,7 @@ type Props = TransitionPreset & {
layout: Layout;
gesture: Animated.Value;
scene: Scene;
headerDarkContent: boolean | undefined;
safeAreaInsetTop: number;
safeAreaInsetRight: number;
safeAreaInsetBottom: number;
@@ -55,15 +56,12 @@ type Props = TransitionPreset & {
onGestureEnd?: (props: { route: Route<string> }) => void;
onGestureCancel?: (props: { route: Route<string> }) => void;
gestureEnabled?: boolean;
gestureResponseDistance?: {
vertical?: number;
horizontal?: number;
};
gestureResponseDistance?: number;
gestureVelocityImpact?: number;
mode: StackCardMode;
headerMode: StackHeaderMode;
headerShown: boolean;
hasAbsoluteHeader: boolean;
hasAbsoluteFloatHeader: boolean;
headerHeight: number;
onHeaderHeightChange: (props: {
route: Route<string>;
@@ -91,10 +89,11 @@ function CardContainer({
getPreviousScene,
getFocusedRoute,
mode,
headerDarkContent,
headerMode,
headerShown,
headerStyleInterpolator,
hasAbsoluteHeader,
hasAbsoluteFloatHeader,
headerHeight,
onHeaderHeightChange,
isParentHeaderShown,
@@ -248,7 +247,12 @@ function CardContainer({
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
pointerEvents={active ? 'box-none' : pointerEvents}
pageOverflowEnabled={headerMode !== 'float' && mode === 'card'}
containerStyle={hasAbsoluteHeader ? { marginTop: headerHeight } : null}
headerDarkContent={headerDarkContent}
containerStyle={
hasAbsoluteFloatHeader && headerMode !== 'screen'
? { marginTop: headerHeight }
: null
}
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
style={[
{
@@ -281,7 +285,6 @@ function CardContainer({
scenes: [previousScene, scene],
getPreviousScene,
getFocusedRoute,
gestureDirection,
styleInterpolator: headerStyleInterpolator,
onContentHeightChange: onHeaderHeightChange,
})}

View File

@@ -6,6 +6,7 @@ import {
Platform,
} from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import Color from 'color';
import type {
ParamListBase,
Route,
@@ -14,6 +15,7 @@ import type {
import {
getDefaultHeaderHeight,
SafeAreaProviderCompat,
Background,
} from '@react-navigation/elements';
import {
@@ -27,12 +29,10 @@ import {
DefaultTransition,
ModalTransition,
} from '../../TransitionConfigs/TransitionPresets';
import { forNoAnimation as forNoAnimationHeader } from '../../TransitionConfigs/HeaderStyleInterpolators';
import { forNoAnimation as forNoAnimationCard } from '../../TransitionConfigs/CardStyleInterpolators';
import getDistanceForDirection from '../../utils/getDistanceForDirection';
import type {
Layout,
StackHeaderMode,
StackCardMode,
StackDescriptorMap,
StackNavigationOptions,
@@ -60,7 +60,6 @@ type Props = {
getGesturesEnabled: (props: { route: Route<string> }) => boolean;
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
renderScene: (props: { route: Route<string> }) => React.ReactNode;
headerMode: StackHeaderMode;
isParentHeaderShown: boolean;
onTransitionStart: (
props: { route: Route<string> },
@@ -381,7 +380,6 @@ export default class CardStack extends React.Component<Props, State> {
getGesturesEnabled,
renderHeader,
renderScene,
headerMode,
isParentHeaderShown,
onTransitionStart,
onTransitionEnd,
@@ -408,13 +406,6 @@ export default class CardStack extends React.Component<Props, State> {
let defaultTransitionPreset =
mode === 'modal' ? ModalTransition : DefaultTransition;
if (headerMode !== 'float') {
defaultTransitionPreset = {
...defaultTransitionPreset,
headerStyleInterpolator: forNoAnimationHeader,
};
}
let activeScreensLimit = 1;
for (let i = scenes.length - 1; i >= 0; i--) {
@@ -432,53 +423,52 @@ export default class CardStack extends React.Component<Props, State> {
}
}
const isFloatHeaderAbsolute =
headerMode === 'float'
? this.state.scenes.slice(-2).some((scene) => {
const { descriptor } = scene;
const options = descriptor ? descriptor.options : {};
const { headerTransparent, headerShown = true } = options;
const isFloatHeaderAbsolute = this.state.scenes.slice(-2).some((scene) => {
const options = scene.descriptor.options ?? {};
const {
headerMode = 'screen',
headerTransparent,
headerShown = true,
} = options;
if (headerTransparent || headerShown === false) {
return true;
}
if (
headerTransparent ||
headerShown === false ||
headerMode === 'screen'
) {
return true;
}
return false;
})
: false;
return false;
});
const floatingHeader =
headerMode === 'float' ? (
<React.Fragment key="header">
{renderHeader({
mode: 'float',
layout,
scenes,
getPreviousScene: this.getPreviousScene,
getFocusedRoute: this.getFocusedRoute,
onContentHeightChange: this.handleHeaderLayout,
gestureDirection:
focusedOptions.gestureDirection !== undefined
? focusedOptions.gestureDirection
: defaultTransitionPreset.gestureDirection,
styleInterpolator:
focusedOptions.headerStyleInterpolator !== undefined
? focusedOptions.headerStyleInterpolator
: defaultTransitionPreset.headerStyleInterpolator,
style: [
styles.floating,
isFloatHeaderAbsolute && [
// Without this, the header buttons won't be touchable on Android when headerTransparent: true
{ height: focusedHeaderHeight },
styles.absolute,
],
const floatingHeader = (
<React.Fragment key="header">
{renderHeader({
mode: 'float',
layout,
scenes,
getPreviousScene: this.getPreviousScene,
getFocusedRoute: this.getFocusedRoute,
onContentHeightChange: this.handleHeaderLayout,
styleInterpolator:
focusedOptions.headerStyleInterpolator !== undefined
? focusedOptions.headerStyleInterpolator
: defaultTransitionPreset.headerStyleInterpolator,
style: [
styles.floating,
isFloatHeaderAbsolute && [
// Without this, the header buttons won't be touchable on Android when headerTransparent: true
{ height: focusedHeaderHeight },
styles.absolute,
],
})}
</React.Fragment>
) : null;
],
})}
</React.Fragment>
);
return (
<React.Fragment>
<Background>
{isFloatHeaderAbsolute ? null : floatingHeader}
<MaybeScreenContainer
enabled={detachInactiveScreens}
@@ -528,7 +518,10 @@ export default class CardStack extends React.Component<Props, State> {
const {
headerShown = true,
headerMode = 'screen',
headerTransparent,
headerStyle,
headerTintColor,
cardShadowEnabled,
cardOverlayEnabled = Platform.OS !== 'ios' || mode === 'modal',
cardOverlay,
@@ -592,6 +585,19 @@ export default class CardStack extends React.Component<Props, State> {
const headerHeight =
headerShown !== false ? headerHeights[route.key] : 0;
const { backgroundColor: headerBackgroundColor } =
StyleSheet.flatten(headerStyle) || {};
let headerDarkContent: boolean | undefined;
if (headerShown) {
if (headerTintColor) {
headerDarkContent = Color(headerTintColor).isDark();
} else if (typeof headerBackgroundColor === 'string') {
headerDarkContent = !Color(headerBackgroundColor).isDark();
}
}
return (
<MaybeScreen
key={route.key}
@@ -631,7 +637,8 @@ export default class CardStack extends React.Component<Props, State> {
mode={mode}
headerMode={headerMode}
headerShown={headerShown}
hasAbsoluteHeader={
headerDarkContent={headerDarkContent}
hasAbsoluteFloatHeader={
isFloatHeaderAbsolute && !headerTransparent
}
renderHeader={renderHeader}
@@ -649,7 +656,7 @@ export default class CardStack extends React.Component<Props, State> {
})}
</MaybeScreenContainer>
{isFloatHeaderAbsolute ? floatingHeader : null}
</React.Fragment>
</Background>
);
}
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, Platform, StyleSheet } from 'react-native';
import { View, StyleSheet } from 'react-native';
import {
SafeAreaInsetsContext,
EdgeInsets,
@@ -439,9 +439,6 @@ export default class StackView extends React.Component<Props, State> {
navigation,
keyboardHandlingEnabled,
mode = 'card',
headerMode = mode === 'card' && Platform.OS === 'ios'
? 'float'
: 'screen',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
descriptors: _,
...rest
@@ -479,7 +476,6 @@ export default class StackView extends React.Component<Props, State> {
onTransitionEnd={this.handleTransitionEnd}
renderHeader={this.renderHeader}
renderScene={this.renderScene}
headerMode={headerMode}
state={state}
descriptors={descriptors}
onGestureStart={this.handleGestureStart}

4526
yarn.lock

File diff suppressed because it is too large Load Diff