diff --git a/.eslintrc.json b/.eslintrc.json index ced08f7c83..8abc1f839b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,6 +13,7 @@ "@react-navigation/bottom-tabs", "@react-navigation/material-top-tabs", "@react-navigation/material-bottom-tabs", + "@react-navigation/elements", "@react-navigation/devtools" ] }, diff --git a/packages/bottom-tabs/src/views/BottomTabView.tsx b/packages/bottom-tabs/src/views/BottomTabView.tsx index 1c43028a46..175e0d6438 100644 --- a/packages/bottom-tabs/src/views/BottomTabView.tsx +++ b/packages/bottom-tabs/src/views/BottomTabView.tsx @@ -8,11 +8,9 @@ import { TabNavigationState, useTheme, } from '@react-navigation/native'; +import { SafeAreaProviderCompat } from '@react-navigation/elements'; -import SafeAreaProviderCompat, { - initialMetrics, -} from './SafeAreaProviderCompat'; -import ResourceSavingScene from './ResourceSavingScene'; +import ScreenFallback from './ScreenFallback'; import BottomTabBar, { getTabBarHeight } from './BottomTabBar'; import BottomTabBarHeightCallbackContext from '../utils/BottomTabBarHeightCallbackContext'; import BottomTabBarHeightContext from '../utils/BottomTabBarHeightContext'; @@ -73,14 +71,14 @@ export default class BottomTabView extends React.Component { const { state, descriptors } = this.props; - const dimensions = initialMetrics.frame; + const dimensions = SafeAreaProviderCompat.initialMetrics.frame; const tabBarHeight = getTabBarHeight({ state, descriptors, dimensions, layout: { width: dimensions.width, height: 0 }, insets: { - ...initialMetrics.insets, + ...SafeAreaProviderCompat.initialMetrics.insets, ...props.safeAreaInsets, }, style: descriptors[state.routes[state.index].key].options.tabBarStyle, @@ -164,10 +162,10 @@ export default class BottomTabView extends React.Component { } return ( - { {descriptor.render()} - + ); })} diff --git a/packages/bottom-tabs/src/views/SafeAreaProviderCompat.tsx b/packages/bottom-tabs/src/views/SafeAreaProviderCompat.tsx deleted file mode 100644 index dd986cc777..0000000000 --- a/packages/bottom-tabs/src/views/SafeAreaProviderCompat.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { Dimensions, Platform } from 'react-native'; -import { - SafeAreaProvider, - SafeAreaInsetsContext, - initialWindowMetrics, -} from 'react-native-safe-area-context'; - -type Props = { - children: React.ReactNode; -}; - -const { width = 0, height = 0 } = Dimensions.get('window'); - -// To support SSR on web, we need to have empty insets for initial values -// Otherwise there can be mismatch between SSR and client output -// We also need to specify empty values to support tests environments -export const initialMetrics = - Platform.OS === 'web' || initialWindowMetrics == null - ? { - frame: { x: 0, y: 0, width, height }, - insets: { top: 0, left: 0, right: 0, bottom: 0 }, - } - : initialWindowMetrics; - -export default function SafeAreaProviderCompat({ children }: Props) { - return ( - - {(insets) => { - if (insets) { - // If we already have insets, don't wrap the stack in another safe area provider - // This avoids an issue with updates at the cost of potentially incorrect values - // https://github.com/react-navigation/react-navigation/issues/174 - return children; - } - - return ( - - {children} - - ); - }} - - ); -} diff --git a/packages/bottom-tabs/src/views/ScreenFallback.tsx b/packages/bottom-tabs/src/views/ScreenFallback.tsx new file mode 100644 index 0000000000..80ce440725 --- /dev/null +++ b/packages/bottom-tabs/src/views/ScreenFallback.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Platform, StyleProp, ViewStyle } from 'react-native'; +import { + Screen, + screensEnabled, + // @ts-ignore + shouldUseActivityState, +} from 'react-native-screens'; +import { ResourceSavingScene } from '@react-navigation/elements'; + +type Props = { + visible: boolean; + children: React.ReactNode; + enabled: boolean; + style?: StyleProp; +}; + +export default function ScreenFallback({ visible, children, ...rest }: Props) { + // react-native-screens is buggy on web + if (screensEnabled?.() && Platform.OS !== 'web') { + if (shouldUseActivityState) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } + } + + return ( + + {children} + + ); +} diff --git a/packages/bottom-tabs/tsconfig.json b/packages/bottom-tabs/tsconfig.json index b74421ba81..d0f53214c7 100644 --- a/packages/bottom-tabs/tsconfig.json +++ b/packages/bottom-tabs/tsconfig.json @@ -3,7 +3,8 @@ "references": [ { "path": "../core" }, { "path": "../routers" }, - { "path": "../native" } + { "path": "../native" }, + { "path": "../elements" } ], "compilerOptions": { "outDir": "./lib/typescript" diff --git a/packages/drawer/package.json b/packages/drawer/package.json index a1ff2c3d47..b5cc25ec33 100644 --- a/packages/drawer/package.json +++ b/packages/drawer/package.json @@ -45,6 +45,7 @@ "color": "^3.1.3" }, "devDependencies": { + "@react-navigation/elements": "^1.0.0", "@react-navigation/native": "^5.8.9", "@testing-library/react-native": "^7.1.0", "@types/react": "^16.9.53", diff --git a/packages/drawer/src/views/DrawerItem.tsx b/packages/drawer/src/views/DrawerItem.tsx index 08d247ec60..d1bc294e34 100644 --- a/packages/drawer/src/views/DrawerItem.tsx +++ b/packages/drawer/src/views/DrawerItem.tsx @@ -8,9 +8,9 @@ import { TextStyle, Platform, } from 'react-native'; +import { PlatformPressable } from '@react-navigation/elements'; import { Link, useTheme } from '@react-navigation/native'; import Color from 'color'; -import TouchableItem from './TouchableItem'; type Props = { /** @@ -68,7 +68,7 @@ type Props = { * * @platform ios */ - pressOpacity?: string; + pressOpacity?: number; /** * Style object for the label element. */ @@ -87,7 +87,7 @@ const Touchable = ({ accessibilityRole, delayPressIn, ...rest -}: React.ComponentProps & { +}: React.ComponentProps & { to?: string; children: React.ReactNode; onPress?: () => void; @@ -115,14 +115,14 @@ const Touchable = ({ ); } else { return ( - {children} - + ); } }; diff --git a/packages/drawer/src/views/DrawerView.tsx b/packages/drawer/src/views/DrawerView.tsx index e459e8b5d6..0f29bee597 100644 --- a/packages/drawer/src/views/DrawerView.tsx +++ b/packages/drawer/src/views/DrawerView.tsx @@ -18,10 +18,10 @@ import { useTheme, ParamListBase, } from '@react-navigation/native'; +import { SafeAreaProviderCompat } from '@react-navigation/elements'; import { GestureHandlerRootView } from './GestureHandler'; -import SafeAreaProviderCompat from './SafeAreaProviderCompat'; -import ResourceSavingScene from './ResourceSavingScene'; +import ScreenFallback from './ScreenFallback'; import Header from './Header'; import DrawerContent from './DrawerContent'; import Drawer from './Drawer'; @@ -173,10 +173,10 @@ function DrawerViewBase({ } = descriptor.options; return ( - {headerShown ? ( @@ -192,7 +192,7 @@ function DrawerViewBase({ ) : null} {descriptor.render()} - + ); })} diff --git a/packages/drawer/src/views/Header.tsx b/packages/drawer/src/views/Header.tsx index 08f7c8f7b7..4d5d2890e7 100644 --- a/packages/drawer/src/views/Header.tsx +++ b/packages/drawer/src/views/Header.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { Text, View, Image, StyleSheet, Platform } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { PlatformPressable } from '@react-navigation/elements'; import { DrawerActions, useTheme } from '@react-navigation/native'; -import TouchableItem from './TouchableItem'; import type { Layout, DrawerHeaderProps } from '../types'; export const getDefaultHeaderHeight = ( @@ -67,7 +67,7 @@ export default function HeaderSegment({ const leftButton = headerLeft ? ( headerLeft({ tintColor: headerTintColor }) ) : ( - - + ); const rightButton = headerRight ? headerRight({ tintColor: headerTintColor }) diff --git a/packages/drawer/src/views/ResourceSavingScene.tsx b/packages/drawer/src/views/ResourceSavingScene.tsx deleted file mode 100644 index 2bb8249ad9..0000000000 --- a/packages/drawer/src/views/ResourceSavingScene.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; -import { - Screen, - screensEnabled, - // @ts-ignore - shouldUseActivityState, -} from 'react-native-screens'; - -type Props = { - isVisible: boolean; - children: React.ReactNode; - enabled: boolean; - style?: any; -}; - -const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container - -export default function ResourceSavingScene({ - isVisible, - children, - style, - ...rest -}: Props) { - // react-native-screens is buggy on web - if (screensEnabled?.() && Platform.OS !== 'web') { - if (shouldUseActivityState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } - } - - if (Platform.OS === 'web') { - return ( - - ); - } - - return ( - - - {children} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - attached: { - flex: 1, - }, - detached: { - flex: 1, - top: FAR_FAR_AWAY, - }, -}); diff --git a/packages/drawer/src/views/SafeAreaProviderCompat.tsx b/packages/drawer/src/views/SafeAreaProviderCompat.tsx deleted file mode 100644 index dd986cc777..0000000000 --- a/packages/drawer/src/views/SafeAreaProviderCompat.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { Dimensions, Platform } from 'react-native'; -import { - SafeAreaProvider, - SafeAreaInsetsContext, - initialWindowMetrics, -} from 'react-native-safe-area-context'; - -type Props = { - children: React.ReactNode; -}; - -const { width = 0, height = 0 } = Dimensions.get('window'); - -// To support SSR on web, we need to have empty insets for initial values -// Otherwise there can be mismatch between SSR and client output -// We also need to specify empty values to support tests environments -export const initialMetrics = - Platform.OS === 'web' || initialWindowMetrics == null - ? { - frame: { x: 0, y: 0, width, height }, - insets: { top: 0, left: 0, right: 0, bottom: 0 }, - } - : initialWindowMetrics; - -export default function SafeAreaProviderCompat({ children }: Props) { - return ( - - {(insets) => { - if (insets) { - // If we already have insets, don't wrap the stack in another safe area provider - // This avoids an issue with updates at the cost of potentially incorrect values - // https://github.com/react-navigation/react-navigation/issues/174 - return children; - } - - return ( - - {children} - - ); - }} - - ); -} diff --git a/packages/drawer/src/views/ScreenFallback.tsx b/packages/drawer/src/views/ScreenFallback.tsx new file mode 100644 index 0000000000..80ce440725 --- /dev/null +++ b/packages/drawer/src/views/ScreenFallback.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Platform, StyleProp, ViewStyle } from 'react-native'; +import { + Screen, + screensEnabled, + // @ts-ignore + shouldUseActivityState, +} from 'react-native-screens'; +import { ResourceSavingScene } from '@react-navigation/elements'; + +type Props = { + visible: boolean; + children: React.ReactNode; + enabled: boolean; + style?: StyleProp; +}; + +export default function ScreenFallback({ visible, children, ...rest }: Props) { + // react-native-screens is buggy on web + if (screensEnabled?.() && Platform.OS !== 'web') { + if (shouldUseActivityState) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } + } + + return ( + + {children} + + ); +} diff --git a/packages/drawer/src/views/TouchableItem.ios.tsx b/packages/drawer/src/views/TouchableItem.ios.tsx deleted file mode 100644 index b8f4e804a7..0000000000 --- a/packages/drawer/src/views/TouchableItem.ios.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react'; -import { Animated, Platform } from 'react-native'; -import { BaseButton, BaseButtonProperties } from 'react-native-gesture-handler'; - -const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton); - -type Props = BaseButtonProperties & { - pressOpacity: number; -}; - -const useNativeDriver = Platform.OS !== 'web'; - -export default class TouchableItem extends React.Component { - static defaultProps = { - pressOpacity: 0.3, - borderless: true, - enabled: true, - }; - - private opacity = new Animated.Value(1); - - private handleActiveStateChange = (active: boolean) => { - Animated.spring(this.opacity, { - stiffness: 1000, - damping: 500, - mass: 3, - overshootClamping: true, - restDisplacementThreshold: 0.01, - restSpeedThreshold: 0.01, - toValue: active ? this.props.pressOpacity : 1, - useNativeDriver, - }).start(); - - this.props.onActiveStateChange?.(active); - }; - - render() { - const { children, style, enabled, ...rest } = this.props; - - return ( - // @ts-expect-error: error seems like false positive - - {children} - - ); - } -} diff --git a/packages/drawer/tsconfig.json b/packages/drawer/tsconfig.json index b74421ba81..d0f53214c7 100644 --- a/packages/drawer/tsconfig.json +++ b/packages/drawer/tsconfig.json @@ -3,7 +3,8 @@ "references": [ { "path": "../core" }, { "path": "../routers" }, - { "path": "../native" } + { "path": "../native" }, + { "path": "../elements" } ], "compilerOptions": { "outDir": "./lib/typescript" diff --git a/packages/elements/LICENSE b/packages/elements/LICENSE new file mode 100644 index 0000000000..9d268cb082 --- /dev/null +++ b/packages/elements/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 React Navigation Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/elements/README.md b/packages/elements/README.md new file mode 100644 index 0000000000..0a3ceadfd6 --- /dev/null +++ b/packages/elements/README.md @@ -0,0 +1,5 @@ +# `@react-navigation/elements` + +UI Components for React Navigation. + +Installation instructions and documentation can be found on the [React Navigation website](https://reactnavigation.org/docs/elements/). diff --git a/packages/elements/package.json b/packages/elements/package.json new file mode 100644 index 0000000000..3f1081371c --- /dev/null +++ b/packages/elements/package.json @@ -0,0 +1,69 @@ +{ + "name": "@react-navigation/elements", + "description": "UI Components for React Navigation", + "version": "1.0.0", + "keywords": [ + "react-native", + "react-navigation", + "ios", + "android" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/react-navigation/react-navigation.git", + "directory": "packages/elements" + }, + "bugs": { + "url": "https://github.com/react-navigation/react-navigation/issues" + }, + "homepage": "https://reactnavigation.org", + "main": "lib/commonjs/index.js", + "react-native": "src/index.tsx", + "source": "src/index.tsx", + "module": "lib/module/index.js", + "types": "lib/typescript/src/index.d.ts", + "files": [ + "src", + "lib", + "!**/__tests__" + ], + "sideEffects": false, + "publishConfig": { + "access": "public", + "tag": "alpha" + }, + "scripts": { + "prepare": "bob build", + "clean": "del lib" + }, + "devDependencies": { + "@testing-library/react-native": "^7.1.0", + "@types/react": "^16.9.53", + "@types/react-native": "~0.62.0", + "del-cli": "^3.0.1", + "react": "~16.13.1", + "react-native": "~0.63.2", + "react-native-builder-bob": "^0.17.1", + "typescript": "^4.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + } +} diff --git a/packages/drawer/src/views/TouchableItem.tsx b/packages/elements/src/PlatformPressable.tsx similarity index 85% rename from packages/drawer/src/views/TouchableItem.tsx rename to packages/elements/src/PlatformPressable.tsx index 6d613d27ff..827cffc182 100644 --- a/packages/drawer/src/views/TouchableItem.tsx +++ b/packages/elements/src/PlatformPressable.tsx @@ -1,10 +1,3 @@ -/** - * TouchableItem 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. - */ import * as React from 'react'; import { Platform, @@ -16,7 +9,7 @@ import { export type Props = TouchableWithoutFeedbackProps & { pressColor?: string; - pressOpacity?: string; + pressOpacity?: number; disabled?: boolean | null; borderless?: boolean; children: React.ReactNode; @@ -24,9 +17,17 @@ export type Props = TouchableWithoutFeedbackProps & { const ANDROID_VERSION_LOLLIPOP = 21; -export default function TouchableItem({ +/** + * 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. + */ +export default function PlatformPressable({ borderless = false, pressColor = 'rgba(0, 0, 0, .32)', + pressOpacity, style, children, ...rest @@ -54,7 +55,7 @@ export default function TouchableItem({ ); } else { return ( - + {children} ); diff --git a/packages/bottom-tabs/src/views/ResourceSavingScene.tsx b/packages/elements/src/ResourceSavingScene.tsx similarity index 53% rename from packages/bottom-tabs/src/views/ResourceSavingScene.tsx rename to packages/elements/src/ResourceSavingScene.tsx index e71931eab1..a9be7a7249 100644 --- a/packages/bottom-tabs/src/views/ResourceSavingScene.tsx +++ b/packages/elements/src/ResourceSavingScene.tsx @@ -1,55 +1,31 @@ import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; -import { - Screen, - screensEnabled, - // @ts-ignore - shouldUseActivityState, -} from 'react-native-screens'; +import { View, Platform, StyleSheet, StyleProp, ViewStyle } from 'react-native'; type Props = { - isVisible: boolean; + visible: boolean; children: React.ReactNode; - enabled: boolean; - style?: any; + style?: StyleProp; }; const FAR_FAR_AWAY = 30000; // this should be big enough to move the whole view out of its container export default function ResourceSavingScene({ - isVisible, + visible, children, style, ...rest }: Props) { - // react-native-screens is buggy on web - if (screensEnabled?.() && Platform.OS !== 'web') { - if (shouldUseActivityState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } - } - if (Platform.OS === 'web') { return (