mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 20:35:19 +08:00
feat: initial implementation of @react-navigation/elements
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +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,
|
||||
TouchableWithoutFeedbackProps,
|
||||
} from 'react-native';
|
||||
|
||||
export type Props = TouchableWithoutFeedbackProps & {
|
||||
pressColor?: string;
|
||||
pressOpacity?: string;
|
||||
disabled?: boolean | null;
|
||||
borderless?: boolean;
|
||||
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