Compare commits

..

33 Commits

Author SHA1 Message Date
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
Satyajit Sahoo
923722cbb0 chore: publish
- @react-navigation/bottom-tabs@6.0.0-next.2
 - @react-navigation/drawer@6.0.0-next.2
 - @react-navigation/elements@1.0.0-next.2
 - @react-navigation/stack@6.0.0-next.3
2021-03-12 03:04:24 +01:00
Satyajit Sahoo
2c1adc9043 refactor: rename ResourceSavingScene to ResourceSavingView 2021-03-12 03:02:07 +01:00
Satyajit Sahoo
40439ccb42 fix: use theme in PlatformPressable 2021-03-12 02:54:02 +01:00
Satyajit Sahoo
24b3f739da feat: return nearest parent header height for useHeaderHeight 2021-03-12 01:59:40 +01:00
Satyajit Sahoo
2c8401d5cb feat: export drawer button 2021-03-12 01:43:57 +01:00
Satyajit Sahoo
6cc463f20d refactor: drop safeAreaInsets option
Custom insets can be provided by wrapping the navigator in a SafeAreaProvider, so we don't need an option.
2021-03-12 01:15:01 +01:00
Satyajit Sahoo
e6c6cc8331 chore: publish
- @react-navigation/stack@6.0.0-next.2
2021-03-11 13:14:07 +01:00
Adam Trzciński
8a6511c491 fix: respect headerStatusBarHeight option in Stack (#9405)
Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
2021-03-11 13:12:54 +01:00
68 changed files with 3431 additions and 3403 deletions

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,41 @@
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/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
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/bottom-tabs@6.0.0...@react-navigation/bottom-tabs@6.0.0-next.1) (2021-03-10)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines",
"version": "6.0.0-next.1",
"version": "6.0.0-next.4",
"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.1",
"@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

@@ -285,7 +285,11 @@ export default function BottomTabBar({
]}
pointerEvents={isTabBarHidden ? 'none' : 'auto'}
>
<View style={styles.content} onLayout={handleLayout}>
<View
accessibilityRole="tablist"
style={styles.content}
onLayout={handleLayout}
>
{routes.map((route, index) => {
const focused = index === state.index;
const { options } = descriptors[route.key];
@@ -322,7 +326,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

@@ -6,7 +6,7 @@ import {
// @ts-ignore
shouldUseActivityState,
} from 'react-native-screens';
import { ResourceSavingScene } from '@react-navigation/elements';
import { ResourceSavingView } from '@react-navigation/elements';
type Props = {
visible: boolean;
@@ -34,8 +34,8 @@ export default function ScreenFallback({ visible, children, ...rest }: Props) {
}
return (
<ResourceSavingScene visible={visible} {...rest}>
<ResourceSavingView visible={visible} {...rest}>
{children}
</ResourceSavingScene>
</ResourceSavingView>
);
}

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,41 @@
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)
### Features
* export drawer button ([2c8401d](https://github.com/react-navigation/react-navigation/commit/2c8401d5cb347d37c96e5b30f8ad05c17fd22ea4))
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/drawer@6.0.0...@react-navigation/drawer@6.0.0-next.1) (2021-03-10)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess",
"version": "6.0.0-next.1",
"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.1",
"@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

@@ -11,12 +11,16 @@ export { default as DrawerItem } from './views/DrawerItem';
export { default as DrawerItemList } from './views/DrawerItemList';
export { default as DrawerContent } from './views/DrawerContent';
export { default as DrawerContentScrollView } from './views/DrawerContentScrollView';
export { default as DrawerToggleButton } from './views/DrawerToggleButton';
/**
* Utilities
*/
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

@@ -6,7 +6,7 @@ import {
// @ts-ignore
shouldUseActivityState,
} from 'react-native-screens';
import { ResourceSavingScene } from '@react-navigation/elements';
import { ResourceSavingView } from '@react-navigation/elements';
type Props = {
visible: boolean;
@@ -34,8 +34,8 @@ export default function ScreenFallback({ visible, children, ...rest }: Props) {
}
return (
<ResourceSavingScene visible={visible} {...rest}>
<ResourceSavingView visible={visible} {...rest}>
{children}
</ResourceSavingScene>
</ResourceSavingView>
);
}

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,41 @@
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)
### Bug Fixes
* use theme in PlatformPressable ([40439cc](https://github.com/react-navigation/react-navigation/commit/40439ccb420825a1aa480648526a816f2422ea6e))
### Features
* return nearest parent header height for useHeaderHeight ([24b3f73](https://github.com/react-navigation/react-navigation/commit/24b3f739da4b8af8dca77d92c72cfdaa762e564a))
# [1.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/elements@1.0.0...@react-navigation/elements@1.0.0-next.1) (2021-03-10)

View File

@@ -1,7 +1,7 @@
{
"name": "@react-navigation/elements",
"description": "UI Components for React Navigation",
"version": "1.0.0-next.1",
"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

@@ -22,7 +22,8 @@ export default function HeaderBackButton({
labelVisible = Platform.OS === 'ios',
onLabelLayout,
onPress,
pressColorAndroid: customPressColorAndroid,
pressColor,
pressOpacity,
screenLayout,
tintColor: customTintColor,
titleLayout,
@@ -31,7 +32,7 @@ export default function HeaderBackButton({
testID,
style,
}: HeaderBackButtonProps) {
const { dark, colors } = useTheme();
const { colors } = useTheme();
const [initialLabelWidth, setInitialLabelWidth] = React.useState<
undefined | number
@@ -45,13 +46,6 @@ export default function HeaderBackButton({
default: colors.text,
});
const pressColorAndroid =
customPressColorAndroid !== undefined
? customPressColorAndroid
: dark
? 'rgba(255, 255, 255, .32)'
: 'rgba(0, 0, 0, .32)';
const handleLabelLayout = (e: LayoutChangeEvent) => {
onLabelLayout?.(e);
@@ -156,7 +150,8 @@ export default function HeaderBackButton({
accessibilityLabel={accessibilityLabel}
testID={testID}
onPress={disabled ? undefined : handlePress}
pressColor={pressColorAndroid}
pressColor={pressColor}
pressOpacity={pressOpacity}
android_ripple={{ borderless: true }}
style={[styles.container, disabled && styles.disabled, style]}
hitSlop={Platform.select({

View File

@@ -1,5 +1,6 @@
import * as React from 'react';
import { Platform, Pressable, PressableProps } from 'react-native';
import { useTheme } from '@react-navigation/native';
export type Props = PressableProps & {
pressColor?: string;
@@ -12,24 +13,30 @@ const ANDROID_SUPPORTS_RIPPLE =
Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_LOLLIPOP;
/**
* PlatformPressable provides an abstraction on top of TouchableNativeFeedback and
* TouchableOpacity to handle platform differences.
*
* On Android, you can pass the props of TouchableNativeFeedback.
* On other platforms, you can pass the props of TouchableOpacity.
* PlatformPressable provides an abstraction on top of Pressable to handle platform differences.
*/
export default function PlatformPressable({
android_ripple,
pressColor = 'rgba(0, 0, 0, .32)',
pressColor,
pressOpacity,
style,
...rest
}: Props) {
const { dark } = useTheme();
return (
<Pressable
android_ripple={
ANDROID_SUPPORTS_RIPPLE
? { color: pressColor, ...android_ripple }
? {
color:
pressColor !== undefined
? pressColor
: dark
? 'rgba(255, 255, 255, .32)'
: 'rgba(0, 0, 0, .32)',
...android_ripple,
}
: undefined
}
style={({ pressed }) => [

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;
};
@@ -30,14 +33,17 @@ export default function Screen(props: Props) {
const insets = useSafeAreaInsets();
const isParentHeaderShown = React.useContext(HeaderShownContext);
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(() =>
@@ -45,12 +51,18 @@ 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}
>
<HeaderHeightContext.Provider value={headerShown ? headerHeight : 0}>
<HeaderHeightContext.Provider
value={headerShown ? headerHeight : parentHeaderHeight}
>
{children}
</HeaderHeightContext.Provider>
</HeaderShownContext.Provider>
@@ -70,7 +82,7 @@ export default function Screen(props: Props) {
</NavigationRouteContext.Provider>
</NavigationContext.Provider>
) : null}
</View>
</Background>
);
}

View File

@@ -11,9 +11,10 @@ export { default as getHeaderTitle } from './Header/getHeaderTitle';
export { default as MissingIcon } from './MissingIcon';
export { default as PlatformPressable } from './PlatformPressable';
export { default as ResourceSavingScene } from './ResourceSavingScene';
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

@@ -127,13 +127,16 @@ export type HeaderBackButtonProps = {
disabled?: boolean;
/**
* Callback to call when the button is pressed.
* By default, this triggers `goBack`.
*/
onPress?: () => void;
/**
* Color for material ripple (Android >= 5.0 only).
*/
pressColorAndroid?: string;
pressColor?: string;
/**
* Opacity when the button is pressed, used when ripple is not supported.
*/
pressOpacity?: number;
/**
* Function which returns a React Element to display custom image in header's back button.
*/

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,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-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.2",
"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

@@ -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,105 @@
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)
### Features
* export drawer button ([2c8401d](https://github.com/react-navigation/react-navigation/commit/2c8401d5cb347d37c96e5b30f8ad05c17fd22ea4))
* return nearest parent header height for useHeaderHeight ([24b3f73](https://github.com/react-navigation/react-navigation/commit/24b3f739da4b8af8dca77d92c72cfdaa762e564a))
# [6.0.0-next.2](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0-next.1...@react-navigation/stack@6.0.0-next.2) (2021-03-11)
### Bug Fixes
* respect headerStatusBarHeight option in Stack ([#9405](https://github.com/react-navigation/react-navigation/issues/9405)) ([8a6511c](https://github.com/react-navigation/react-navigation/commit/8a6511c491b2affbe378d720e613a3e3041ca9c2))
# [6.0.0-next.1](https://github.com/react-navigation/react-navigation/compare/@react-navigation/stack@6.0.0...@react-navigation/stack@6.0.0-next.1) (2021-03-10)

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.1",
"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.1",
"@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

@@ -1,6 +1,5 @@
import type * as React from 'react';
import type { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import type {
NavigationProp,
ParamListBase,
@@ -149,10 +148,6 @@ export type StackHeaderProps = {
* Layout of the screen.
*/
layout: Layout;
/**
* Safe area insets to use in the header, e.g. to apply extra spacing for statusbar and notch.
*/
insets: EdgeInsets;
/**
* Options for the back button.
*/
@@ -203,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;
@@ -249,35 +249,15 @@ 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.
*/
gestureVelocityImpact?: number;
/**
* Safe area insets for the screen. This is used to avoid elements like notch and status bar.
* By default, the device's safe area insets are automatically detected. You can override the behavior with this option.
* For example, to remove the extra spacing for status bar, pass `safeAreaInsets: { top: 0 }`.
*/
safeAreaInsets?: {
top?: number;
right?: number;
bottom?: number;
left?: number;
};
/**
* Whether to detach the previous screen from the view hierarchy to save memory.
* Set it to `false` if you need the previous screen to be seen through the active screen.
@@ -289,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

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { StackActions, useNavigationState } from '@react-navigation/native';
import { getHeaderTitle, HeaderShownContext } from '@react-navigation/elements';
@@ -10,13 +11,14 @@ import type { StackHeaderProps } from '../../types';
export default React.memo(function Header({
back,
layout,
insets,
progress,
options,
route,
navigation,
styleInterpolator,
}: StackHeaderProps) {
const insets = useSafeAreaInsets();
let previousTitle;
// The label for the left back button shows the title of the previous screen
@@ -47,7 +49,11 @@ export default React.memo(function Header({
);
const statusBarHeight =
(isModal && !isFirstRouteInParent) || isParentHeaderShown ? 0 : insets.top;
options.headerStatusBarHeight !== undefined
? options.headerStatusBarHeight
: (isModal && !isFirstRouteInParent) || isParentHeaderShown
? 0
: insets.top;
return (
<HeaderSegment

View File

@@ -7,7 +7,6 @@ import {
ParamListBase,
} from '@react-navigation/native';
import { HeaderBackContext, getHeaderTitle } from '@react-navigation/elements';
import type { EdgeInsets } from 'react-native-safe-area-context';
import Header from './Header';
import {
@@ -22,13 +21,11 @@ import type {
StackHeaderStyleInterpolator,
StackNavigationProp,
StackHeaderProps,
GestureDirection,
} from '../../types';
export type Props = {
mode: 'float' | 'screen';
layout: Layout;
insets: EdgeInsets;
scenes: (Scene | undefined)[];
getPreviousScene: (props: { route: Route<string> }) => Scene | undefined;
getFocusedRoute: () => Route<string>;
@@ -37,7 +34,6 @@ export type Props = {
height: number;
}) => void;
styleInterpolator: StackHeaderStyleInterpolator;
gestureDirection: GestureDirection;
style?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
};
@@ -45,11 +41,9 @@ export default function HeaderContainer({
mode,
scenes,
layout,
insets,
getPreviousScene,
getFocusedRoute,
onContentHeightChange,
gestureDirection,
styleInterpolator,
style,
}: Props) {
@@ -63,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;
}
@@ -90,22 +84,27 @@ 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,
insets,
back: headerBack,
progress: scene.progress,
options: scene.descriptor.options,
@@ -115,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

@@ -111,6 +111,10 @@ export default function HeaderSegment(props: Props) {
headerBackTestID,
headerBackAllowFontScaling,
headerBackTitleStyle,
headerTitleContainerStyle,
headerLeftContainerStyle,
headerRightContainerStyle,
headerBackgroundContainerStyle,
headerStyle: customHeaderStyle,
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
styleInterpolator,
@@ -172,10 +176,13 @@ export default function HeaderSegment(props: Props) {
layout={layout}
headerTitle={headerTitle}
headerLeft={headerLeft}
headerTitleContainerStyle={titleStyle}
headerLeftContainerStyle={leftButtonStyle}
headerRightContainerStyle={rightButtonStyle}
headerBackgroundContainerStyle={backgroundStyle}
headerTitleContainerStyle={[titleStyle, headerTitleContainerStyle]}
headerLeftContainerStyle={[leftButtonStyle, headerLeftContainerStyle]}
headerRightContainerStyle={[rightButtonStyle, headerRightContainerStyle]}
headerBackgroundContainerStyle={[
backgroundStyle,
headerBackgroundContainerStyle,
]}
headerStyle={customHeaderStyle}
headerStatusBarHeight={headerStatusBarHeight}
{...rest}

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,
@@ -119,6 +118,8 @@ function CardContainer({
scene,
transitionSpec,
}: Props) {
const parentHeaderHeight = React.useContext(HeaderHeightContext);
const handleOpen = () => {
const { route } = scene.descriptor;
@@ -246,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={[
{
@@ -263,7 +269,9 @@ function CardContainer({
<HeaderShownContext.Provider
value={isParentHeaderShown || headerShown !== false}
>
<HeaderHeightContext.Provider value={headerHeight}>
<HeaderHeightContext.Provider
value={headerShown ? headerHeight : parentHeaderHeight}
>
{renderScene({ route: scene.descriptor.route })}
</HeaderHeightContext.Provider>
</HeaderShownContext.Provider>
@@ -274,11 +282,9 @@ function CardContainer({
{renderHeader({
mode: 'screen',
layout,
insets,
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> },
@@ -108,13 +107,8 @@ const getHeaderHeights = (
const height =
typeof style.height === 'number' ? style.height : previous[curr.key];
const safeAreaInsets = {
...insets,
...options.safeAreaInsets,
};
const {
headerStatusBarHeight = isParentHeaderShown ? 0 : safeAreaInsets.top,
headerStatusBarHeight = isParentHeaderShown ? 0 : insets.top,
} = options;
acc[curr.key] =
@@ -386,7 +380,6 @@ export default class CardStack extends React.Component<Props, State> {
getGesturesEnabled,
renderHeader,
renderScene,
headerMode,
isParentHeaderShown,
onTransitionStart,
onTransitionEnd,
@@ -413,20 +406,6 @@ export default class CardStack extends React.Component<Props, State> {
let defaultTransitionPreset =
mode === 'modal' ? ModalTransition : DefaultTransition;
if (headerMode !== 'float') {
defaultTransitionPreset = {
...defaultTransitionPreset,
headerStyleInterpolator: forNoAnimationHeader,
};
}
const {
top = insets.top,
right = insets.right,
bottom = insets.bottom,
left = insets.left,
} = focusedOptions.safeAreaInsets || {};
let activeScreensLimit = 1;
for (let i = scenes.length - 1; i >= 0; i--) {
@@ -444,54 +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,
insets: { top, right, bottom, left },
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}
@@ -540,9 +517,11 @@ export default class CardStack extends React.Component<Props, State> {
}
const {
safeAreaInsets,
headerShown = true,
headerMode = 'screen',
headerTransparent,
headerStyle,
headerTintColor,
cardShadowEnabled,
cardOverlayEnabled = Platform.OS !== 'ios' || mode === 'modal',
cardOverlay,
@@ -598,16 +577,27 @@ export default class CardStack extends React.Component<Props, State> {
}
}
const {
top: safeAreaInsetTop = insets.top,
right: safeAreaInsetRight = insets.right,
bottom: safeAreaInsetBottom = insets.bottom,
left: safeAreaInsetLeft = insets.left,
} = safeAreaInsets || {};
const safeAreaInsetTop = insets.top;
const safeAreaInsetRight = insets.right;
const safeAreaInsetBottom = insets.bottom;
const safeAreaInsetLeft = insets.left;
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}
@@ -647,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}
@@ -665,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