mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: initial implementation of @react-navigation/elements
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"@react-navigation/bottom-tabs",
|
||||
"@react-navigation/material-top-tabs",
|
||||
"@react-navigation/material-bottom-tabs",
|
||||
"@react-navigation/elements",
|
||||
"@react-navigation/devtools"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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<Props, State> {
|
||||
|
||||
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<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
<ScreenFallback
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
visible={isFocused}
|
||||
enabled={detachInactiveScreens}
|
||||
>
|
||||
<SceneContent
|
||||
@@ -178,7 +176,7 @@ export default class BottomTabView extends React.Component<Props, State> {
|
||||
{descriptor.render()}
|
||||
</BottomTabBarHeightContext.Provider>
|
||||
</SceneContent>
|
||||
</ResourceSavingScene>
|
||||
</ScreenFallback>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
|
||||
@@ -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 (
|
||||
<SafeAreaInsetsContext.Consumer>
|
||||
{(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 (
|
||||
<SafeAreaProvider initialMetrics={initialMetrics}>
|
||||
{children}
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}}
|
||||
</SafeAreaInsetsContext.Consumer>
|
||||
);
|
||||
}
|
||||
41
packages/bottom-tabs/src/views/ScreenFallback.tsx
Normal file
41
packages/bottom-tabs/src/views/ScreenFallback.tsx
Normal file
@@ -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<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function ScreenFallback({ visible, children, ...rest }: Props) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
<Screen activityState={visible ? 2 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Screen active={visible ? 1 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSavingScene visible={visible} {...rest}>
|
||||
{children}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../routers" },
|
||||
{ "path": "../native" }
|
||||
{ "path": "../native" },
|
||||
{ "path": "../elements" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib/typescript"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<typeof TouchableItem> & {
|
||||
}: React.ComponentProps<typeof PlatformPressable> & {
|
||||
to?: string;
|
||||
children: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
@@ -115,14 +115,14 @@ const Touchable = ({
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableItem
|
||||
<PlatformPressable
|
||||
{...rest}
|
||||
accessibilityRole={accessibilityRole}
|
||||
delayPressIn={delayPressIn}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={style}>{children}</View>
|
||||
</TouchableItem>
|
||||
</PlatformPressable>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<ResourceSavingScene
|
||||
<ScreenFallback
|
||||
key={route.key}
|
||||
style={[StyleSheet.absoluteFill, { opacity: isFocused ? 1 : 0 }]}
|
||||
isVisible={isFocused}
|
||||
visible={isFocused}
|
||||
enabled={detachInactiveScreens}
|
||||
>
|
||||
{headerShown ? (
|
||||
@@ -192,7 +192,7 @@ function DrawerViewBase({
|
||||
</NavigationContext.Provider>
|
||||
) : null}
|
||||
{descriptor.render()}
|
||||
</ResourceSavingScene>
|
||||
</ScreenFallback>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
|
||||
@@ -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 })
|
||||
) : (
|
||||
<TouchableItem
|
||||
<PlatformPressable
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={headerLeftAccessibilityLabel}
|
||||
@@ -89,7 +89,7 @@ export default function HeaderSegment({
|
||||
source={require('./assets/toggle-drawer-icon.png')}
|
||||
fadeDuration={0}
|
||||
/>
|
||||
</TouchableItem>
|
||||
</PlatformPressable>
|
||||
);
|
||||
const rightButton = headerRight
|
||||
? headerRight({ tintColor: headerTintColor })
|
||||
|
||||
@@ -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 (
|
||||
<Screen activityState={isVisible ? 2 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Screen active={isVisible ? 1 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
return (
|
||||
<View
|
||||
// @ts-expect-error: hidden exists on web, but not in React Native
|
||||
hidden={!isVisible}
|
||||
style={[
|
||||
{ display: isVisible ? 'flex' : 'none' },
|
||||
styles.container,
|
||||
style,
|
||||
]}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, style]}
|
||||
// box-none doesn't seem to work properly on Android
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={
|
||||
// On iOS, set removeClippedSubviews to true only when not focused
|
||||
// This is an workaround for a bug where the clipped view never re-appears
|
||||
Platform.OS === 'ios' ? !isVisible : true
|
||||
}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
style={isVisible ? styles.attached : styles.detached}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
attached: {
|
||||
flex: 1,
|
||||
},
|
||||
detached: {
|
||||
flex: 1,
|
||||
top: FAR_FAR_AWAY,
|
||||
},
|
||||
});
|
||||
@@ -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 (
|
||||
<SafeAreaInsetsContext.Consumer>
|
||||
{(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 (
|
||||
<SafeAreaProvider initialMetrics={initialMetrics}>
|
||||
{children}
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}}
|
||||
</SafeAreaInsetsContext.Consumer>
|
||||
);
|
||||
}
|
||||
41
packages/drawer/src/views/ScreenFallback.tsx
Normal file
41
packages/drawer/src/views/ScreenFallback.tsx
Normal file
@@ -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<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function ScreenFallback({ visible, children, ...rest }: Props) {
|
||||
// react-native-screens is buggy on web
|
||||
if (screensEnabled?.() && Platform.OS !== 'web') {
|
||||
if (shouldUseActivityState) {
|
||||
return (
|
||||
<Screen activityState={visible ? 2 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Screen active={visible ? 1 : 0} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceSavingScene visible={visible} {...rest}>
|
||||
{children}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
}
|
||||
@@ -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<Props> {
|
||||
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
|
||||
<AnimatedBaseButton
|
||||
{...rest}
|
||||
onActiveStateChange={this.handleActiveStateChange}
|
||||
style={[style, enabled && { opacity: this.opacity }]}
|
||||
>
|
||||
{children}
|
||||
</AnimatedBaseButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../routers" },
|
||||
{ "path": "../native" }
|
||||
{ "path": "../native" },
|
||||
{ "path": "../elements" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib/typescript"
|
||||
|
||||
21
packages/elements/LICENSE
Normal file
21
packages/elements/LICENSE
Normal file
@@ -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.
|
||||
5
packages/elements/README.md
Normal file
5
packages/elements/README.md
Normal file
@@ -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/).
|
||||
69
packages/elements/package.json
Normal file
69
packages/elements/package.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<TouchableOpacity style={style} {...rest}>
|
||||
<TouchableOpacity style={style} activeOpacity={pressOpacity} {...rest}>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -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<ViewStyle>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Screen activityState={isVisible ? 2 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Screen active={isVisible ? 1 : 0} style={style} {...rest}>
|
||||
{children}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
return (
|
||||
<View
|
||||
// @ts-expect-error: hidden exists on web, but not in React Native
|
||||
hidden={!isVisible}
|
||||
hidden={!visible}
|
||||
style={[
|
||||
{ display: isVisible ? 'flex' : 'none' },
|
||||
{ display: visible ? 'flex' : 'none' },
|
||||
styles.container,
|
||||
style,
|
||||
]}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
@@ -61,17 +37,17 @@ export default function ResourceSavingScene({
|
||||
<View
|
||||
style={[styles.container, style]}
|
||||
// box-none doesn't seem to work properly on Android
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
>
|
||||
<View
|
||||
collapsable={false}
|
||||
removeClippedSubviews={
|
||||
// On iOS, set removeClippedSubviews to true only when not focused
|
||||
// This is an workaround for a bug where the clipped view never re-appears
|
||||
Platform.OS === 'ios' ? !isVisible : true
|
||||
Platform.OS === 'ios' ? !visible : true
|
||||
}
|
||||
pointerEvents={isVisible ? 'auto' : 'none'}
|
||||
style={isVisible ? styles.attached : styles.detached}
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
style={visible ? styles.attached : styles.detached}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
@@ -15,7 +15,7 @@ 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 =
|
||||
const initialMetrics =
|
||||
Platform.OS === 'web' || initialWindowMetrics == null
|
||||
? {
|
||||
frame: { x: 0, y: 0, width, height },
|
||||
@@ -43,3 +43,5 @@ export default function SafeAreaProviderCompat({ children }: Props) {
|
||||
</SafeAreaInsetsContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
SafeAreaProviderCompat.initialMetrics = initialMetrics;
|
||||
3
packages/elements/src/index.tsx
Normal file
3
packages/elements/src/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as PlatformPressable } from './PlatformPressable';
|
||||
export { default as ResourceSavingScene } from './ResourceSavingScene';
|
||||
export { default as SafeAreaProviderCompat } from './SafeAreaProviderCompat';
|
||||
6
packages/elements/tsconfig.build.json
Normal file
6
packages/elements/tsconfig.build.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {}
|
||||
}
|
||||
}
|
||||
6
packages/elements/tsconfig.json
Normal file
6
packages/elements/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib/typescript"
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
StyleSheet,
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
import { PlatformPressable } from '@react-navigation/elements';
|
||||
import { useTheme } from '@react-navigation/native';
|
||||
import MaskedView from '../MaskedView';
|
||||
import TouchableItem from '../TouchableItem';
|
||||
import type { StackHeaderLeftButtonProps } from '../../types';
|
||||
|
||||
type Props = StackHeaderLeftButtonProps;
|
||||
@@ -151,7 +151,7 @@ export default function HeaderBackButton({
|
||||
const handlePress = () => onPress && requestAnimationFrame(onPress);
|
||||
|
||||
return (
|
||||
<TouchableItem
|
||||
<PlatformPressable
|
||||
disabled={disabled}
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
@@ -171,7 +171,7 @@ export default function HeaderBackButton({
|
||||
{renderBackImage()}
|
||||
{renderLabel()}
|
||||
</React.Fragment>
|
||||
</TouchableItem>
|
||||
</PlatformPressable>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ import type {
|
||||
Route,
|
||||
StackNavigationState,
|
||||
} from '@react-navigation/native';
|
||||
import { SafeAreaProviderCompat } from '@react-navigation/elements';
|
||||
|
||||
import {
|
||||
MaybeScreenContainer,
|
||||
MaybeScreen,
|
||||
shouldUseActivityState,
|
||||
} from '../Screens';
|
||||
import { initialMetrics } from '../SafeAreaProviderCompat';
|
||||
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
|
||||
import type { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
||||
import CardContainer from './CardContainer';
|
||||
@@ -290,7 +290,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
routes: [],
|
||||
scenes: [],
|
||||
gestures: {},
|
||||
layout: initialMetrics.frame,
|
||||
layout: SafeAreaProviderCompat.initialMetrics.frame,
|
||||
descriptors: this.props.descriptors,
|
||||
// Used when card's header is null and mode is float to make transition
|
||||
// between screens with headers and those without headers smooth.
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Route,
|
||||
ParamListBase,
|
||||
} from '@react-navigation/native';
|
||||
import { SafeAreaProviderCompat } from '@react-navigation/elements';
|
||||
|
||||
import { GestureHandlerRootView } from '../GestureHandler';
|
||||
import CardStack from './CardStack';
|
||||
@@ -18,7 +19,6 @@ import KeyboardManager from '../KeyboardManager';
|
||||
import HeaderContainer, {
|
||||
Props as HeaderContainerProps,
|
||||
} from '../Header/HeaderContainer';
|
||||
import SafeAreaProviderCompat from '../SafeAreaProviderCompat';
|
||||
import HeaderShownContext from '../../utils/HeaderShownContext';
|
||||
import type {
|
||||
StackNavigationHelpers,
|
||||
|
||||
@@ -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<Props> {
|
||||
static defaultProps = {
|
||||
activeOpacity: 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
|
||||
<AnimatedBaseButton
|
||||
{...rest}
|
||||
onActiveStateChange={this.handleActiveStateChange}
|
||||
style={[style, enabled && { opacity: this.opacity }]}
|
||||
>
|
||||
{children}
|
||||
</AnimatedBaseButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
TouchableNativeFeedback,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
|
||||
export type Props = ViewProps & {
|
||||
pressColor?: string;
|
||||
disabled?: boolean;
|
||||
borderless?: boolean;
|
||||
delayPressIn?: number;
|
||||
onPress?: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const ANDROID_VERSION_LOLLIPOP = 21;
|
||||
|
||||
export default function TouchableItem({
|
||||
borderless = false,
|
||||
pressColor = 'rgba(0, 0, 0, .32)',
|
||||
style,
|
||||
children,
|
||||
...rest
|
||||
}: Props) {
|
||||
/*
|
||||
* TouchableNativeFeedback.Ripple causes a crash on old Android versions,
|
||||
* therefore only enable it on Android Lollipop and above.
|
||||
*
|
||||
* All touchables on Android should have the ripple effect according to
|
||||
* platform design guidelines.
|
||||
* We need to pass the background prop to specify a borderless ripple effect.
|
||||
*/
|
||||
if (
|
||||
Platform.OS === 'android' &&
|
||||
Platform.Version >= ANDROID_VERSION_LOLLIPOP
|
||||
) {
|
||||
return (
|
||||
<TouchableNativeFeedback
|
||||
{...rest}
|
||||
useForeground={TouchableNativeFeedback.canUseNativeForeground()}
|
||||
background={TouchableNativeFeedback.Ripple(pressColor, borderless)}
|
||||
>
|
||||
<View style={style}>{React.Children.only(children)}</View>
|
||||
</TouchableNativeFeedback>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TouchableOpacity style={style} {...rest}>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../routers" },
|
||||
{ "path": "../native" }
|
||||
{ "path": "../native" },
|
||||
{ "path": "../elements" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib/typescript"
|
||||
|
||||
Reference in New Issue
Block a user