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

@@ -13,6 +13,7 @@
"@react-navigation/bottom-tabs",
"@react-navigation/material-top-tabs",
"@react-navigation/material-bottom-tabs",
"@react-navigation/elements",
"@react-navigation/devtools"
]
},

View File

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

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

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

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

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

View 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/).

View 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"
}
]
]
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
export { default as PlatformPressable } from './PlatformPressable';
export { default as ResourceSavingScene } from './ResourceSavingScene';
export { default as SafeAreaProviderCompat } from './SafeAreaProviderCompat';

View File

@@ -0,0 +1,6 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"paths": {}
}
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"outDir": "./lib/typescript"
}
}

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

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