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

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

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

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

View File

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

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 = {
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>
);
}
}

View File

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

View File

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