feat: initial implementation of @react-navigation/elements

This commit is contained in:
Satyajit Sahoo
2021-01-29 20:11:23 +01:00
parent c345ef1d0b
commit 07ba7a9687
29 changed files with 251 additions and 425 deletions

View File

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

View File

@@ -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>
);
}
};

View File

@@ -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>

View File

@@ -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 })

View File

@@ -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,
},
});

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}

View File

@@ -3,7 +3,8 @@
"references": [
{ "path": "../core" },
{ "path": "../routers" },
{ "path": "../native" }
{ "path": "../native" },
{ "path": "../elements" }
],
"compilerOptions": {
"outDir": "./lib/typescript"