mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-26 09:14:22 +08:00
refactor: migrate codebase to typescript
This commit is contained in:
1
packages/stack/.gitignore
vendored
1
packages/stack/.gitignore
vendored
@@ -7,7 +7,6 @@
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
tsconfig.json
|
||||
jsconfig.json
|
||||
|
||||
# Xcode
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"increment": "conventional:angular",
|
||||
"scripts": {
|
||||
"changelog": "conventional-changelog -p angular | tail -n +3"
|
||||
},
|
||||
"git": {
|
||||
"commitMessage": "chore: release %s",
|
||||
"tagName": "v%s"
|
||||
@@ -12,5 +8,10 @@
|
||||
},
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/conventional-changelog": {
|
||||
"preset": "angular"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Asset } from 'expo';
|
||||
import { FlatList, I18nManager } from 'react-native';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Dimensions, Button, Image, View } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
import { FlatList, BorderlessButton } from 'react-native-gesture-handler';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
import { createAppContainer } from '@react-navigation/native';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Button, Text, View, StyleSheet } from 'react-native';
|
||||
import { BarCodeScanner } from 'expo';
|
||||
import { withNavigationFocus } from '@react-navigation/core';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Button, View, Text } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Dimensions, Button, View, Text } from 'react-native';
|
||||
import { withNavigation } from '@react-navigation/core';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Button, Text, View } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
import { createDrawerNavigator } from 'react-navigation-drawer';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Animated, Button, Easing, View, Text } from 'react-native';
|
||||
import { createStackNavigator } from 'react-navigation-stack';
|
||||
|
||||
|
||||
6210
packages/stack/example/yarn.lock
Normal file
6210
packages/stack/example/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
||||
"description": "Stack navigator component for React Navigation",
|
||||
"main": "lib/commonjs/index.js",
|
||||
"module": "lib/module/index.js",
|
||||
"react-native": "src/index.js",
|
||||
"react-native": "lib/module/index.js",
|
||||
"typescript": "lib/typescript/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"src",
|
||||
@@ -12,8 +13,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"format": "eslint . --fix",
|
||||
"lint": "eslint --ext .js,.ts,.tsx .",
|
||||
"typescript": "tsc --noEmit",
|
||||
"prepare": "bob build",
|
||||
"release": "release-it",
|
||||
"example": "yarn --cwd example",
|
||||
@@ -41,18 +42,21 @@
|
||||
},
|
||||
"homepage": "https://github.com/react-navigation/react-navigation-stack#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@commitlint/config-conventional": "^7.5.0",
|
||||
"@expo/vector-icons": "^10.0.1",
|
||||
"@react-native-community/bob": "^0.3.4",
|
||||
"@react-navigation/core": "^3.3.1",
|
||||
"@react-native-community/bob": "^0.4.1",
|
||||
"@react-navigation/core": "^3.4.1",
|
||||
"@react-navigation/native": "^3.4.1",
|
||||
"@release-it/conventional-changelog": "^1.0.0",
|
||||
"@types/jest": "^24.0.12",
|
||||
"@types/react": "^16.8.14",
|
||||
"@types/react-native": "^0.57.51",
|
||||
"commitlint": "^7.5.2",
|
||||
"conventional-changelog-cli": "^2.0.12",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-satya164": "^2.4.1",
|
||||
"eslint-plugin-react-native-globals": "^0.1.0",
|
||||
"husky": "^1.3.1",
|
||||
"husky": "^2.2.0",
|
||||
"jest": "^24.7.1",
|
||||
"prettier": "^1.17.0",
|
||||
"react": "16.5.0",
|
||||
@@ -61,8 +65,9 @@
|
||||
"react-native-gesture-handler": "^1.1.0",
|
||||
"react-native-screens": "^1.0.0-alpha.22",
|
||||
"react-test-renderer": "16.5.0",
|
||||
"release-it": "^10.4.2",
|
||||
"scheduler": "^0.14.0"
|
||||
"release-it": "^11.0.0",
|
||||
"scheduler": "^0.14.0",
|
||||
"typescript": "^3.4.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/core": "^3.0.0",
|
||||
@@ -74,7 +79,7 @@
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native",
|
||||
"testRegex": "/__tests__/[^/]+-test\\.js$",
|
||||
"testEnvironment": "node",
|
||||
"setupFiles": [
|
||||
"<rootDir>/jest-setup.js"
|
||||
],
|
||||
@@ -87,7 +92,11 @@
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation-deprecated-tab-navigator|@react-navigation/core|@react-navigation/native)"
|
||||
]
|
||||
],
|
||||
"testRegex": "/__tests__/.*\\.(test|spec)\\.(js|tsx?)$",
|
||||
"transform": {
|
||||
"^.+\\.(js|ts|tsx)$": "babel-jest"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@@ -104,7 +113,8 @@
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module"
|
||||
"module",
|
||||
"typescript"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "../../../.eslintrc",
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { createAppContainer } from '@react-navigation/native';
|
||||
import StackNavigator from '../createStackNavigator';
|
||||
|
||||
@@ -2,8 +2,23 @@ import { StackRouter, createNavigator } from '@react-navigation/core';
|
||||
import { createKeyboardAwareNavigator } from '@react-navigation/native';
|
||||
import { Platform } from 'react-native';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
import { NavigationStackOptions, NavigationProp, Screen } from '../types';
|
||||
|
||||
function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
function createStackNavigator(
|
||||
routeConfigMap: {
|
||||
[key: string]:
|
||||
| Screen
|
||||
| ({ screen: Screen } | { getScreen(): Screen }) & {
|
||||
path?: string;
|
||||
navigationOptions?:
|
||||
| NavigationStackOptions
|
||||
| ((options: {
|
||||
navigation: NavigationProp;
|
||||
}) => NavigationStackOptions);
|
||||
};
|
||||
},
|
||||
stackConfig: NavigationStackOptions = {}
|
||||
) {
|
||||
const router = StackRouter(routeConfigMap, stackConfig);
|
||||
|
||||
// Create a navigator with StackView as the view
|
||||
200
packages/stack/src/types.tsx
Normal file
200
packages/stack/src/types.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';
|
||||
import { SafeAreaView } from '@react-navigation/native';
|
||||
|
||||
export type Route = {
|
||||
key: string;
|
||||
routeName: string;
|
||||
};
|
||||
|
||||
export type Scene = {
|
||||
key: string;
|
||||
index: number;
|
||||
isStale: boolean;
|
||||
isActive: boolean;
|
||||
route: Route;
|
||||
descriptor: SceneDescriptor;
|
||||
};
|
||||
|
||||
export type NavigationEventName =
|
||||
| 'willFocus'
|
||||
| 'didFocus'
|
||||
| 'willBlur'
|
||||
| 'didBlur';
|
||||
|
||||
export type NavigationState = {
|
||||
key: string;
|
||||
index: number;
|
||||
routes: Route[];
|
||||
isTransitioning?: boolean;
|
||||
};
|
||||
|
||||
export type NavigationProp<RouteName = string, Params = object> = {
|
||||
navigate(routeName: RouteName): void;
|
||||
goBack(): void;
|
||||
goBack(key: string | null): void;
|
||||
addListener: (
|
||||
event: NavigationEventName,
|
||||
callback: () => void
|
||||
) => { remove: () => void };
|
||||
isFocused(): boolean;
|
||||
state: NavigationState;
|
||||
setParams(params: Params): void;
|
||||
getParam(): Params;
|
||||
dispatch(action: { type: string }): void;
|
||||
dangerouslyGetParent(): NavigationProp | undefined;
|
||||
};
|
||||
|
||||
export type HeaderMode = 'float' | 'screen';
|
||||
|
||||
export type HeaderLayoutPreset = 'left' | 'center';
|
||||
|
||||
export type HeaderTransitionPreset = 'fade-in-place' | 'uikit';
|
||||
|
||||
export type HeaderBackgroundTransitionPreset = 'translate' | 'fade';
|
||||
|
||||
export type HeaderProps = {
|
||||
mode: HeaderMode;
|
||||
position: Animated.Value;
|
||||
navigation: NavigationProp;
|
||||
layout: TransitionerLayout;
|
||||
scene: Scene;
|
||||
scenes: Scene[];
|
||||
layoutPreset: HeaderLayoutPreset;
|
||||
transitionPreset?: HeaderTransitionPreset;
|
||||
backTitleVisible?: boolean;
|
||||
leftInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
titleInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
rightInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
backgroundInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
isLandscape: boolean;
|
||||
};
|
||||
|
||||
export type HeaderTransitionConfig = {
|
||||
headerLeftInterpolator: SceneInterpolator;
|
||||
headerLeftLabelInterpolator: SceneInterpolator;
|
||||
headerLeftButtonInterpolator: SceneInterpolator;
|
||||
headerTitleFromLeftInterpolator: SceneInterpolator;
|
||||
headerTitleInterpolator: SceneInterpolator;
|
||||
headerRightInterpolator: SceneInterpolator;
|
||||
headerBackgroundInterpolator: SceneInterpolator;
|
||||
headerLayoutInterpolator: SceneInterpolator;
|
||||
};
|
||||
|
||||
export type NavigationStackOptions = {
|
||||
title?: string;
|
||||
header?: (props: HeaderProps) => React.ReactNode | null;
|
||||
headerTitle?: string;
|
||||
headerTitleStyle?: StyleProp<TextStyle>;
|
||||
headerTitleContainerStyle?: StyleProp<ViewStyle>;
|
||||
headerTintColor?: string;
|
||||
headerTitleAllowFontScaling?: boolean;
|
||||
headerBackAllowFontScaling?: boolean;
|
||||
headerBackTitle?: string;
|
||||
headerBackTitleStyle?: StyleProp<TextStyle>;
|
||||
headerTruncatedBackTitle?: string;
|
||||
headerLeft?: React.ComponentType<HeaderBackbuttonProps>;
|
||||
headerLeftContainerStyle?: StyleProp<ViewStyle>;
|
||||
headerRight?: React.ComponentType<{}>;
|
||||
headerRightContainerStyle?: StyleProp<ViewStyle>;
|
||||
headerBackImage?: React.ComponentType<{
|
||||
tintColor: string;
|
||||
title?: string | null;
|
||||
}>;
|
||||
headerPressColorAndroid?: string;
|
||||
headerBackground?: string;
|
||||
headerTransparent?: boolean;
|
||||
headerStyle?: StyleProp<ViewStyle>;
|
||||
headerForceInset?: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
gesturesEnabled?: boolean;
|
||||
gestureDirection?: 'inverted' | 'normal';
|
||||
gestureResponseDistance?: {
|
||||
vertical: number;
|
||||
horizontal: number;
|
||||
};
|
||||
disableKeyboardHandling?: boolean;
|
||||
};
|
||||
|
||||
export type NavigationConfig = {
|
||||
mode: 'card' | 'modal';
|
||||
headerMode: HeaderMode;
|
||||
headerLayoutPreset: HeaderLayoutPreset;
|
||||
headerTransitionPreset: HeaderTransitionPreset;
|
||||
headerBackgroundTransitionPreset: HeaderBackgroundTransitionPreset;
|
||||
headerBackTitleVisible?: boolean;
|
||||
cardShadowEnabled?: boolean;
|
||||
cardOverlayEnabled?: boolean;
|
||||
onTransitionStart?: () => void;
|
||||
onTransitionEnd?: () => void;
|
||||
transitionConfig: (
|
||||
transitionProps: TransitionProps,
|
||||
prevTransitionProps?: TransitionProps,
|
||||
isModal?: boolean
|
||||
) => HeaderTransitionConfig;
|
||||
};
|
||||
|
||||
export type SceneDescriptor = {
|
||||
key: string;
|
||||
options: NavigationStackOptions;
|
||||
navigation: NavigationProp;
|
||||
getComponent(): React.ComponentType;
|
||||
};
|
||||
|
||||
export type HeaderBackbuttonProps = {
|
||||
disabled?: boolean;
|
||||
onPress: () => void;
|
||||
pressColorAndroid?: string;
|
||||
tintColor: string;
|
||||
backImage?: NavigationStackOptions['headerBackImage'];
|
||||
title?: string | null;
|
||||
truncatedTitle?: string | null;
|
||||
backTitleVisible?: boolean;
|
||||
allowFontScaling?: boolean;
|
||||
titleStyle?: StyleProp<TextStyle>;
|
||||
layoutPreset: HeaderLayoutPreset;
|
||||
width?: number;
|
||||
scene: Scene;
|
||||
};
|
||||
|
||||
export type SceneInterpolatorProps = {
|
||||
mode?: HeaderMode;
|
||||
layout: TransitionerLayout;
|
||||
scene: Scene;
|
||||
scenes: Scene[];
|
||||
position: Animated.AnimatedInterpolation;
|
||||
navigation: NavigationProp;
|
||||
shadowEnabled?: boolean;
|
||||
cardOverlayEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type SceneInterpolator = (props: SceneInterpolatorProps) => any;
|
||||
|
||||
export type TransitionerLayout = {
|
||||
height: Animated.Value;
|
||||
width: Animated.Value;
|
||||
initHeight: number;
|
||||
initWidth: number;
|
||||
isMeasured: boolean;
|
||||
};
|
||||
|
||||
export type TransitionProps = {
|
||||
layout: TransitionerLayout;
|
||||
navigation: NavigationProp;
|
||||
position: Animated.Value;
|
||||
scenes: Scene[];
|
||||
scene: Scene;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type TransitionConfig = {
|
||||
transitionSpec: {
|
||||
timing: Function;
|
||||
};
|
||||
screenInterpolator: SceneInterpolator;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type Screen = React.ComponentType<any> & {
|
||||
navigationOptions?: NavigationStackOptions & {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
const { PlatformConstants } = NativeModules;
|
||||
|
||||
export const supportsImprovedSpringAnimation = () => {
|
||||
@@ -1,3 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default React.createContext(null);
|
||||
4
packages/stack/src/utils/StackGestureContext.tsx
Normal file
4
packages/stack/src/utils/StackGestureContext.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { PanGestureHandler } from 'react-native-gesture-handler';
|
||||
|
||||
export default React.createContext<React.Ref<PanGestureHandler> | null>(null);
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function clamp(min, value, max) {
|
||||
export default function clamp(min: number, value: number, max: number) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
function getSceneIndicesForInterpolationInputRange(props) {
|
||||
import { Scene } from '../types';
|
||||
|
||||
type Props = {
|
||||
scene: Scene;
|
||||
scenes: Scene[];
|
||||
};
|
||||
|
||||
function getSceneIndicesForInterpolationInputRange(props: Props) {
|
||||
const { scene, scenes } = props;
|
||||
const index = scene.index;
|
||||
const lastSceneIndexInScenes = scenes.length - 1;
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Use invariant() to assert state which your program assumes to be true.
|
||||
*
|
||||
* Provide sprintf-style format (only %s is supported) and arguments
|
||||
* to provide information about what broke and what you were
|
||||
* expecting.
|
||||
*
|
||||
* The invariant message will be stripped in production, but the invariant
|
||||
* will remain to ensure logic does not differ in production.
|
||||
*/
|
||||
|
||||
export default function invariant(condition, format, a, b, c, d, e, f) {
|
||||
if (format === undefined) {
|
||||
throw new Error('invariant requires an error message argument');
|
||||
}
|
||||
|
||||
if (!condition) {
|
||||
var error;
|
||||
if (format === undefined) {
|
||||
error = new Error(
|
||||
'Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.'
|
||||
);
|
||||
} else {
|
||||
var args = [a, b, c, d, e, f];
|
||||
var argIndex = 0;
|
||||
error = new Error(format.replace(/%s/g, () => args[argIndex++]));
|
||||
error.name = 'Invariant Violation';
|
||||
}
|
||||
|
||||
error.framesToPop = 1; // we don't care about invariant's own frame
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
* inlined Object.is polyfill to avoid requiring consumers ship their own
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
|
||||
*/
|
||||
function is(x, y) {
|
||||
function is(x: any, y: any) {
|
||||
// SameValue algorithm
|
||||
if (x === y) {
|
||||
// Steps 1-5, 7-10
|
||||
@@ -22,7 +22,7 @@ function is(x, y) {
|
||||
* when any key has values which are not strictly equal between the arguments.
|
||||
* Returns true when the values of all keys are strictly equal.
|
||||
*/
|
||||
function shallowEqual(objA, objB) {
|
||||
function shallowEqual(objA: any, objB: any) {
|
||||
if (is(objA, objB)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Animated, Platform } from 'react-native';
|
||||
import { BaseButton } from 'react-native-gesture-handler';
|
||||
|
||||
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
|
||||
|
||||
export default class BorderlessButton extends React.Component {
|
||||
type Props = React.ComponentProps<typeof BaseButton> & {
|
||||
activeOpacity: number;
|
||||
};
|
||||
|
||||
export default class BorderlessButton extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
activeOpacity: 0.3,
|
||||
borderless: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._opacity = new Animated.Value(1);
|
||||
}
|
||||
private opacity = new Animated.Value(1);
|
||||
|
||||
_onActiveStateChange = active => {
|
||||
private handleActiveStateChange = (active: boolean) => {
|
||||
if (Platform.OS !== 'android') {
|
||||
Animated.spring(this._opacity, {
|
||||
Animated.spring(this.opacity, {
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
@@ -38,10 +39,10 @@ export default class BorderlessButton extends React.Component {
|
||||
return (
|
||||
<AnimatedBaseButton
|
||||
{...rest}
|
||||
onActiveStateChange={this._onActiveStateChange}
|
||||
onActiveStateChange={this.handleActiveStateChange}
|
||||
style={[
|
||||
style,
|
||||
Platform.OS === 'ios' && enabled && { opacity: this._opacity },
|
||||
Platform.OS === 'ios' && enabled && { opacity: this.opacity },
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default ({ tintColor }) => (
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill={tintColor}
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
16
packages/stack/src/views/Header/BackButtonWeb.tsx
Normal file
16
packages/stack/src/views/Header/BackButtonWeb.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as React from 'react';
|
||||
|
||||
type Props = {
|
||||
tintColor: string;
|
||||
};
|
||||
|
||||
export default function BackButton({ tintColor }: Props) {
|
||||
return (
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill={tintColor}
|
||||
d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Animated,
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
View,
|
||||
I18nManager,
|
||||
MaskedViewIOS,
|
||||
ViewStyle,
|
||||
LayoutChangeEvent,
|
||||
StyleProp,
|
||||
} from 'react-native';
|
||||
|
||||
import { withOrientation, SafeAreaView } from '@react-navigation/native';
|
||||
@@ -16,6 +19,31 @@ import HeaderTitle from './HeaderTitle';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import ModularHeaderBackButton from './ModularHeaderBackButton';
|
||||
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
|
||||
import {
|
||||
Scene,
|
||||
HeaderLayoutPreset,
|
||||
SceneInterpolatorProps,
|
||||
HeaderProps,
|
||||
} from '../../types';
|
||||
|
||||
type Props = HeaderProps & {
|
||||
leftLabelInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
leftButtonInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
titleFromLeftInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
layoutInterpolator: (props: SceneInterpolatorProps) => any;
|
||||
};
|
||||
|
||||
type SubviewProps = {
|
||||
position: Animated.AnimatedInterpolation;
|
||||
scene: Scene;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
type SubviewName = 'left' | 'right' | 'title' | 'background';
|
||||
|
||||
type State = {
|
||||
widths: { [key: string]: number };
|
||||
};
|
||||
|
||||
const APPBAR_HEIGHT = Platform.select({
|
||||
ios: 44,
|
||||
@@ -40,11 +68,10 @@ const TITLE_OFFSET_LEFT_ALIGN = Platform.select({
|
||||
});
|
||||
|
||||
const getTitleOffsets = (
|
||||
layoutPreset,
|
||||
forceBackTitle,
|
||||
hasLeftComponent,
|
||||
hasRightComponent
|
||||
) => {
|
||||
layoutPreset: HeaderLayoutPreset,
|
||||
hasLeftComponent: boolean,
|
||||
hasRightComponent: boolean
|
||||
): ViewStyle | undefined => {
|
||||
if (layoutPreset === 'left') {
|
||||
// Maybe at some point we should do something different if the back title is
|
||||
// explicitly enabled, for now people can control it manually
|
||||
@@ -74,10 +101,13 @@ const getTitleOffsets = (
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
const getAppBarHeight = (isLandscape: boolean) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
// @ts-ignore
|
||||
if (isLandscape && !Platform.isPad) {
|
||||
return 32;
|
||||
} else {
|
||||
@@ -90,7 +120,7 @@ const getAppBarHeight = isLandscape => {
|
||||
}
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
class Header extends React.PureComponent<Props, State> {
|
||||
static get HEIGHT() {
|
||||
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
|
||||
}
|
||||
@@ -106,11 +136,11 @@ class Header extends React.PureComponent {
|
||||
backgroundInterpolator: HeaderStyleInterpolator.forBackground,
|
||||
};
|
||||
|
||||
state = {
|
||||
state: State = {
|
||||
widths: {},
|
||||
};
|
||||
|
||||
_getHeaderTitleString(scene) {
|
||||
private getHeaderTitleString(scene: Scene) {
|
||||
const options = scene.descriptor.options;
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
@@ -127,12 +157,12 @@ class Header extends React.PureComponent {
|
||||
return options.title;
|
||||
}
|
||||
|
||||
_getLastScene(scene) {
|
||||
private getLastScene(scene: Scene) {
|
||||
return this.props.scenes.find(s => s.index === scene.index - 1);
|
||||
}
|
||||
|
||||
_getBackButtonTitleString(scene) {
|
||||
const lastScene = this._getLastScene(scene);
|
||||
private getBackButtonTitleString(scene: Scene) {
|
||||
const lastScene = this.getLastScene(scene);
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
@@ -140,25 +170,25 @@ class Header extends React.PureComponent {
|
||||
if (headerBackTitle || headerBackTitle === null) {
|
||||
return headerBackTitle;
|
||||
}
|
||||
return this._getHeaderTitleString(lastScene);
|
||||
return this.getHeaderTitleString(lastScene);
|
||||
}
|
||||
|
||||
_getTruncatedBackButtonTitle(scene) {
|
||||
const lastScene = this._getLastScene(scene);
|
||||
private getTruncatedBackButtonTitle(scene: Scene) {
|
||||
const lastScene = this.getLastScene(scene);
|
||||
if (!lastScene) {
|
||||
return null;
|
||||
}
|
||||
return lastScene.descriptor.options.headerTruncatedBackTitle;
|
||||
}
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
private renderTitleComponent = (props: SubviewProps) => {
|
||||
const { layoutPreset } = this.props;
|
||||
const { options } = props.scene.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
return headerTitle;
|
||||
}
|
||||
const titleString = this._getHeaderTitleString(props.scene);
|
||||
const titleString = this.getHeaderTitleString(props.scene);
|
||||
|
||||
const titleStyle = options.headerTitleStyle;
|
||||
const color = options.headerTintColor;
|
||||
@@ -168,7 +198,7 @@ class Header extends React.PureComponent {
|
||||
// calculated size of the title.
|
||||
const onLayout =
|
||||
layoutPreset === 'center'
|
||||
? e => {
|
||||
? (e: LayoutChangeEvent) => {
|
||||
const { width } = e.nativeEvent.layout;
|
||||
|
||||
this.setState(state => ({
|
||||
@@ -203,7 +233,7 @@ class Header extends React.PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
_renderLeftComponent = props => {
|
||||
private renderLeftComponent = (props: SubviewProps) => {
|
||||
const { options } = props.scene.descriptor;
|
||||
if (
|
||||
React.isValidElement(options.headerLeft) ||
|
||||
@@ -216,8 +246,8 @@ class Header extends React.PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
const backButtonTitle = this.getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this.getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
);
|
||||
const width = this.state.widths[props.scene.key]
|
||||
@@ -248,14 +278,18 @@ class Header extends React.PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
_renderModularLeftComponent = (
|
||||
props,
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
private renderModularLeftComponent = (
|
||||
props: SubviewProps,
|
||||
ButtonContainerComponent: React.ComponentProps<
|
||||
typeof ModularHeaderBackButton
|
||||
>['ButtonContainerComponent'],
|
||||
LabelContainerComponent: React.ComponentProps<
|
||||
typeof ModularHeaderBackButton
|
||||
>['LabelContainerComponent']
|
||||
) => {
|
||||
const { options, navigation } = props.scene.descriptor;
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
const backButtonTitle = this.getBackButtonTitleString(props.scene);
|
||||
const truncatedBackButtonTitle = this.getTruncatedBackButtonTitle(
|
||||
props.scene
|
||||
);
|
||||
const width = this.state.widths[props.scene.key]
|
||||
@@ -281,17 +315,19 @@ class Header extends React.PureComponent {
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
layoutPreset={this.props.layoutPreset}
|
||||
width={width}
|
||||
scene={props.scene}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderRightComponent = props => {
|
||||
private renderRightComponent = (props: SubviewProps) => {
|
||||
const { headerRight } = props.scene.descriptor.options;
|
||||
return headerRight || null;
|
||||
};
|
||||
|
||||
_renderLeft(props) {
|
||||
private renderLeft = (props: SubviewProps) => {
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
@@ -309,47 +345,53 @@ class Header extends React.PureComponent {
|
||||
options.headerLeft ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return this._renderSubView(
|
||||
return this.renderSubView(
|
||||
{ ...props, style },
|
||||
'left',
|
||||
this._renderLeftComponent,
|
||||
this.renderLeftComponent,
|
||||
this.props.leftInterpolator
|
||||
);
|
||||
} else {
|
||||
return this._renderModularSubView(
|
||||
return this.renderModularSubView(
|
||||
{ ...props, style },
|
||||
'left',
|
||||
this._renderModularLeftComponent,
|
||||
this.renderModularLeftComponent,
|
||||
this.props.leftLabelInterpolator,
|
||||
this.props.leftButtonInterpolator
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_renderTitle(props, options) {
|
||||
private renderTitle = (
|
||||
props: SubviewProps,
|
||||
options: {
|
||||
hasLeftComponent: boolean;
|
||||
hasRightComponent: boolean;
|
||||
headerTitleContainerStyle: StyleProp<ViewStyle>;
|
||||
}
|
||||
) => {
|
||||
const { layoutPreset, transitionPreset } = this.props;
|
||||
let style = [
|
||||
let style: StyleProp<ViewStyle> = [
|
||||
{ justifyContent: layoutPreset === 'center' ? 'center' : 'flex-start' },
|
||||
getTitleOffsets(
|
||||
layoutPreset,
|
||||
false,
|
||||
options.hasLeftComponent,
|
||||
options.hasRightComponent
|
||||
),
|
||||
options.headerTitleContainerStyle,
|
||||
];
|
||||
|
||||
return this._renderSubView(
|
||||
return this.renderSubView(
|
||||
{ ...props, style },
|
||||
'title',
|
||||
this._renderTitleComponent,
|
||||
this.renderTitleComponent,
|
||||
transitionPreset === 'uikit'
|
||||
? this.props.titleFromLeftInterpolator
|
||||
: this.props.titleInterpolator
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_renderRight(props) {
|
||||
private renderRight = (props: SubviewProps) => {
|
||||
const { options } = props.scene.descriptor;
|
||||
|
||||
let { style } = props;
|
||||
@@ -357,15 +399,15 @@ class Header extends React.PureComponent {
|
||||
style = [style, options.headerRightContainerStyle];
|
||||
}
|
||||
|
||||
return this._renderSubView(
|
||||
return this.renderSubView(
|
||||
{ ...props, style },
|
||||
'right',
|
||||
this._renderRightComponent,
|
||||
this.renderRightComponent,
|
||||
this.props.rightInterpolator
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_renderBackground(props) {
|
||||
private renderBackground = (props: SubviewProps) => {
|
||||
const {
|
||||
index,
|
||||
descriptor: { options },
|
||||
@@ -379,21 +421,29 @@ class Header extends React.PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._renderSubView(
|
||||
return this.renderSubView(
|
||||
{ ...props, style: StyleSheet.absoluteFill },
|
||||
'background',
|
||||
() => options.headerBackground,
|
||||
this.props.backgroundInterpolator
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_renderModularSubView(
|
||||
props,
|
||||
name,
|
||||
renderer,
|
||||
labelStyleInterpolator,
|
||||
buttonStyleInterpolator
|
||||
) {
|
||||
private renderModularSubView = (
|
||||
props: SubviewProps,
|
||||
name: SubviewName,
|
||||
renderer: (
|
||||
props: SubviewProps,
|
||||
ButtonContainerComponent: React.ComponentProps<
|
||||
typeof ModularHeaderBackButton
|
||||
>['ButtonContainerComponent'],
|
||||
LabelContainerComponent: React.ComponentProps<
|
||||
typeof ModularHeaderBackButton
|
||||
>['LabelContainerComponent']
|
||||
) => React.ReactNode,
|
||||
labelStyleInterpolator: (props: SceneInterpolatorProps) => any,
|
||||
buttonStyleInterpolator: (props: SceneInterpolatorProps) => any
|
||||
) => {
|
||||
const { scene } = props;
|
||||
const { index, isStale, key } = scene;
|
||||
|
||||
@@ -410,7 +460,7 @@ class Header extends React.PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ButtonContainer = ({ children }) => (
|
||||
const ButtonContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
<Animated.View
|
||||
style={[buttonStyleInterpolator({ ...this.props, ...props })]}
|
||||
>
|
||||
@@ -418,7 +468,7 @@ class Header extends React.PureComponent {
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const LabelContainer = ({ children }) => (
|
||||
const LabelContainer = ({ children }: { children: React.ReactNode }) => (
|
||||
<Animated.View
|
||||
style={[labelStyleInterpolator({ ...this.props, ...props })]}
|
||||
>
|
||||
@@ -426,7 +476,11 @@ class Header extends React.PureComponent {
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const subView = renderer(props, ButtonContainer, LabelContainer);
|
||||
const subView = renderer(
|
||||
props,
|
||||
ButtonContainer as any,
|
||||
LabelContainer as any
|
||||
);
|
||||
|
||||
if (subView === null) {
|
||||
return subView;
|
||||
@@ -443,9 +497,14 @@ class Header extends React.PureComponent {
|
||||
{subView}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_renderSubView(props, name, renderer, styleInterpolator) {
|
||||
private renderSubView = (
|
||||
props: SubviewProps,
|
||||
name: SubviewName,
|
||||
renderer: (props: SubviewProps) => React.ReactNode,
|
||||
styleInterpolator: (props: SceneInterpolatorProps) => any
|
||||
) => {
|
||||
const { scene } = props;
|
||||
const { index, isStale, key } = scene;
|
||||
|
||||
@@ -482,16 +541,16 @@ class Header extends React.PureComponent {
|
||||
{subView}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_renderHeader(props) {
|
||||
private renderHeader = (props: SubviewProps) => {
|
||||
const { options } = props.scene.descriptor;
|
||||
if (options.header === null) {
|
||||
return null;
|
||||
}
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props, {
|
||||
const left = this.renderLeft(props);
|
||||
const right = this.renderRight(props);
|
||||
const title = this.renderTitle(props, {
|
||||
hasLeftComponent: !!left,
|
||||
hasRightComponent: !!right,
|
||||
headerTitleContainerStyle: options.headerTitleContainerStyle,
|
||||
@@ -537,7 +596,7 @@ class Header extends React.PureComponent {
|
||||
</MaskedViewIOS>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let appBar;
|
||||
@@ -545,7 +604,7 @@ class Header extends React.PureComponent {
|
||||
const { mode, scene, isLandscape } = this.props;
|
||||
|
||||
if (mode === 'float') {
|
||||
const scenesByIndex = {};
|
||||
const scenesByIndex: { [key: string]: Scene } = {};
|
||||
this.props.scenes.forEach(scene => {
|
||||
scenesByIndex[scene.index] = scene;
|
||||
});
|
||||
@@ -553,21 +612,21 @@ class Header extends React.PureComponent {
|
||||
position: this.props.position,
|
||||
scene,
|
||||
}));
|
||||
appBar = scenesProps.map(this._renderHeader, this);
|
||||
background = scenesProps.map(this._renderBackground, this);
|
||||
appBar = scenesProps.map(props => this.renderHeader(props));
|
||||
background = scenesProps.map(props => this.renderBackground(props));
|
||||
} else {
|
||||
const headerProps = {
|
||||
position: new Animated.Value(this.props.scene.index),
|
||||
scene: this.props.scene,
|
||||
};
|
||||
|
||||
appBar = this._renderHeader(headerProps);
|
||||
background = this._renderBackground(headerProps);
|
||||
appBar = this.renderHeader(headerProps);
|
||||
background = this.renderBackground(headerProps);
|
||||
}
|
||||
|
||||
const { options } = scene.descriptor;
|
||||
const { headerStyle = {} } = options;
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle);
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle) as ViewStyle;
|
||||
const appBarHeight = getAppBarHeight(isLandscape);
|
||||
|
||||
const {
|
||||
@@ -654,7 +713,7 @@ class Header extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function warnIfHeaderStyleDefined(value, styleProp) {
|
||||
function warnIfHeaderStyleDefined(value: any, styleProp: string) {
|
||||
if (styleProp === 'position' && value === 'absolute') {
|
||||
console.warn(
|
||||
"position: 'absolute' is not supported on headerStyle. If you would like to render content under the header, use the headerTransparent navigationOption."
|
||||
@@ -726,6 +785,8 @@ const styles = StyleSheet.create({
|
||||
transform: [{ scaleX: I18nManager.isRTL ? -1 : 1 }],
|
||||
},
|
||||
// eslint-disable-next-line react-native/no-unused-styles
|
||||
background: {},
|
||||
// eslint-disable-next-line react-native/no-unused-styles
|
||||
title: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
I18nManager,
|
||||
Image,
|
||||
@@ -6,14 +6,23 @@ import {
|
||||
View,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
import defaultBackImage from '../assets/back-icon.png';
|
||||
import BackButtonWeb from './BackButton.web';
|
||||
import BackButtonWeb from './BackButtonWeb';
|
||||
import { HeaderBackbuttonProps } from '../../types';
|
||||
|
||||
class HeaderBackButton extends React.PureComponent {
|
||||
type State = {
|
||||
initialTextWidth?: number;
|
||||
};
|
||||
|
||||
class HeaderBackButton extends React.PureComponent<
|
||||
HeaderBackbuttonProps,
|
||||
State
|
||||
> {
|
||||
static defaultProps = {
|
||||
pressColorAndroid: 'rgba(0, 0, 0, .32)',
|
||||
tintColor: Platform.select({
|
||||
@@ -26,9 +35,9 @@ class HeaderBackButton extends React.PureComponent {
|
||||
}),
|
||||
};
|
||||
|
||||
state = {};
|
||||
state: State = {};
|
||||
|
||||
_onTextLayout = e => {
|
||||
private handleTextLayout = (e: LayoutChangeEvent) => {
|
||||
if (this.state.initialTextWidth) {
|
||||
return;
|
||||
}
|
||||
@@ -37,37 +46,33 @@ class HeaderBackButton extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
private renderBackImage() {
|
||||
const { backImage, backTitleVisible, tintColor } = this.props;
|
||||
let title = this._getTitleText();
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
let title = this.getTitleText();
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
title,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!backTitleVisible && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
const BackImage = backImage;
|
||||
|
||||
return <BackImage {...props} fadeDuration={0} />;
|
||||
return <BackImage tintColor={tintColor} title={title} />;
|
||||
} else {
|
||||
return (
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
!!backTitleVisible && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
]}
|
||||
source={defaultBackImage}
|
||||
fadeDuration={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_getTitleText = () => {
|
||||
private getTitleText = () => {
|
||||
const { width, title, truncatedTitle } = this.props;
|
||||
|
||||
let { initialTextWidth } = this.state;
|
||||
@@ -83,14 +88,14 @@ class HeaderBackButton extends React.PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_maybeRenderTitle() {
|
||||
private maybeRenderTitle() {
|
||||
const {
|
||||
allowFontScaling,
|
||||
backTitleVisible,
|
||||
titleStyle,
|
||||
tintColor,
|
||||
} = this.props;
|
||||
let backTitleText = this._getTitleText();
|
||||
let backTitleText = this.getTitleText();
|
||||
|
||||
if (!backTitleVisible || backTitleText === null) {
|
||||
return null;
|
||||
@@ -99,12 +104,12 @@ class HeaderBackButton extends React.PureComponent {
|
||||
return (
|
||||
<Text
|
||||
accessible={false}
|
||||
onLayout={this._onTextLayout}
|
||||
onLayout={this.handleTextLayout}
|
||||
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
|
||||
numberOfLines={1}
|
||||
allowFontScaling={!!allowFontScaling}
|
||||
>
|
||||
{this._getTitleText()}
|
||||
{this.getTitleText()}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -128,8 +133,8 @@ class HeaderBackButton extends React.PureComponent {
|
||||
borderless
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{this._renderBackImage()}
|
||||
{this._maybeRenderTitle()}
|
||||
{this.renderBackImage()}
|
||||
{this.maybeRenderTitle()}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Dimensions, I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
import { Scene, SceneInterpolatorProps } from '../../types';
|
||||
|
||||
function hasHeader(scene) {
|
||||
function hasHeader(scene: Scene) {
|
||||
if (!scene) {
|
||||
return true;
|
||||
}
|
||||
@@ -9,7 +10,12 @@ function hasHeader(scene) {
|
||||
return descriptor.options.header !== null;
|
||||
}
|
||||
|
||||
const crossFadeInterpolation = (scenes, first, index, last) => ({
|
||||
const crossFadeInterpolation = (
|
||||
scenes: Scene[],
|
||||
first: number,
|
||||
index: number,
|
||||
last: number
|
||||
): { inputRange: number[]; outputRange: number[]; extrapolate: 'clamp' } => ({
|
||||
inputRange: [
|
||||
first,
|
||||
first + 0.001,
|
||||
@@ -42,12 +48,12 @@ const crossFadeInterpolation = (scenes, first, index, last) => ({
|
||||
* +-------------+-------------+-------------+
|
||||
*/
|
||||
|
||||
function isGoingBack(scenes) {
|
||||
function isGoingBack(scenes: Scene[]) {
|
||||
const lastSceneIndexInScenes = scenes.length - 1;
|
||||
return !scenes[lastSceneIndexInScenes].isActive;
|
||||
}
|
||||
|
||||
function forLayout(props) {
|
||||
function forLayout(props: SceneInterpolatorProps) {
|
||||
const { layout, position, scene, scenes, mode } = props;
|
||||
if (mode !== 'float') {
|
||||
return {};
|
||||
@@ -96,7 +102,7 @@ function forLayout(props) {
|
||||
};
|
||||
}
|
||||
|
||||
function forLeft(props) {
|
||||
function forLeft(props: SceneInterpolatorProps) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
@@ -112,7 +118,7 @@ function forLeft(props) {
|
||||
};
|
||||
}
|
||||
|
||||
function forCenter(props) {
|
||||
function forCenter(props: SceneInterpolatorProps) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
@@ -128,7 +134,7 @@ function forCenter(props) {
|
||||
};
|
||||
}
|
||||
|
||||
function forRight(props) {
|
||||
function forRight(props: SceneInterpolatorProps) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
@@ -147,7 +153,7 @@ function forRight(props) {
|
||||
* iOS UINavigationController style interpolators
|
||||
*/
|
||||
|
||||
function forLeftButton(props) {
|
||||
function forLeftButton(props: SceneInterpolatorProps) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
@@ -197,7 +203,8 @@ function forLeftButton(props) {
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
|
||||
function forLeftLabel(props) {
|
||||
|
||||
function forLeftLabel(props: SceneInterpolatorProps) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
@@ -273,7 +280,8 @@ function forLeftLabel(props) {
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
|
||||
function forCenterFromLeft(props) {
|
||||
|
||||
function forCenterFromLeft(props: SceneInterpolatorProps) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
@@ -332,7 +340,7 @@ function forCenterFromLeft(props) {
|
||||
}
|
||||
|
||||
// Fade in background of header while transitioning
|
||||
function forBackgroundWithFade(props) {
|
||||
function forBackgroundWithFade(props: SceneInterpolatorProps) {
|
||||
const { position, scene } = props;
|
||||
const sceneRange = getSceneIndicesForInterpolationInputRange(props);
|
||||
if (!sceneRange) return { opacity: 0 };
|
||||
@@ -349,13 +357,17 @@ const VISIBLE = { opacity: 1 };
|
||||
const HIDDEN = { opacity: 0 };
|
||||
|
||||
// Toggle visibility of header without fading
|
||||
function forBackgroundWithInactiveHidden({ navigation, scene }) {
|
||||
function forBackgroundWithInactiveHidden({
|
||||
navigation,
|
||||
scene,
|
||||
}: SceneInterpolatorProps) {
|
||||
return navigation.state.index === scene.index ? VISIBLE : HIDDEN;
|
||||
}
|
||||
|
||||
// Translate the background with the card
|
||||
const BACKGROUND_OFFSET = Dimensions.get('window').width;
|
||||
function forBackgroundWithTranslation(props) {
|
||||
|
||||
function forBackgroundWithTranslation(props: SceneInterpolatorProps) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet, Animated } from 'react-native';
|
||||
|
||||
const AnimatedText = Animated.Text;
|
||||
|
||||
const HeaderTitle = ({ style, ...rest }) => (
|
||||
<AnimatedText
|
||||
const HeaderTitle = ({
|
||||
style,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof Animated.Text>) => (
|
||||
<Animated.Text
|
||||
numberOfLines={1}
|
||||
{...rest}
|
||||
style={[styles.title, style]}
|
||||
@@ -1,19 +1,36 @@
|
||||
import React from 'react';
|
||||
import { I18nManager, Image, Text, View, StyleSheet } from 'react-native';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
I18nManager,
|
||||
Image,
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
|
||||
import TouchableItem from '../TouchableItem';
|
||||
|
||||
import defaultBackImage from '../assets/back-icon.png';
|
||||
import { HeaderBackbuttonProps } from '../../types';
|
||||
|
||||
class ModularHeaderBackButton extends React.PureComponent {
|
||||
type Props = HeaderBackbuttonProps & {
|
||||
LabelContainerComponent: React.ComponentType;
|
||||
ButtonContainerComponent: React.ComponentType;
|
||||
};
|
||||
|
||||
type State = {
|
||||
initialTextWidth?: number;
|
||||
};
|
||||
|
||||
class ModularHeaderBackButton extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
tintColor: '#037aff',
|
||||
truncatedTitle: 'Back',
|
||||
};
|
||||
|
||||
state = {};
|
||||
state: State = {};
|
||||
|
||||
_onTextLayout = e => {
|
||||
private onTextLayout = (e: LayoutChangeEvent) => {
|
||||
if (this.state.initialTextWidth) {
|
||||
return;
|
||||
}
|
||||
@@ -22,35 +39,30 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
_renderBackImage() {
|
||||
private renderBackImage() {
|
||||
const { backImage, backTitleVisible, tintColor } = this.props;
|
||||
|
||||
let BackImage;
|
||||
let props;
|
||||
|
||||
if (React.isValidElement(backImage)) {
|
||||
return backImage;
|
||||
} else if (backImage) {
|
||||
BackImage = backImage;
|
||||
props = {
|
||||
tintColor,
|
||||
};
|
||||
} else {
|
||||
BackImage = Image;
|
||||
props = {
|
||||
style: [
|
||||
styles.icon,
|
||||
!!backTitleVisible && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
],
|
||||
source: defaultBackImage,
|
||||
};
|
||||
}
|
||||
const BackImage = backImage;
|
||||
|
||||
return <BackImage {...props} />;
|
||||
return <BackImage tintColor={tintColor} />;
|
||||
} else {
|
||||
return (
|
||||
<Image
|
||||
style={[
|
||||
styles.icon,
|
||||
!!backTitleVisible && styles.iconWithTitle,
|
||||
!!tintColor && { tintColor },
|
||||
]}
|
||||
source={defaultBackImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_getTitleText = () => {
|
||||
private getTitleText = () => {
|
||||
const { width, title, truncatedTitle } = this.props;
|
||||
|
||||
let { initialTextWidth } = this.state;
|
||||
@@ -66,9 +78,9 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
_maybeRenderTitle() {
|
||||
private maybeRenderTitle() {
|
||||
const { backTitleVisible, titleStyle, tintColor } = this.props;
|
||||
let backTitleText = this._getTitleText();
|
||||
let backTitleText = this.getTitleText();
|
||||
|
||||
if (!backTitleVisible || backTitleText === null) {
|
||||
return null;
|
||||
@@ -80,7 +92,7 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
<LabelContainerComponent>
|
||||
<Text
|
||||
accessible={false}
|
||||
onLayout={this._onTextLayout}
|
||||
onLayout={this.onTextLayout}
|
||||
style={[
|
||||
styles.title,
|
||||
!!tintColor && { color: tintColor },
|
||||
@@ -88,7 +100,7 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this._getTitleText()}
|
||||
{this.getTitleText()}
|
||||
</Text>
|
||||
</LabelContainerComponent>
|
||||
);
|
||||
@@ -111,9 +123,9 @@ class ModularHeaderBackButton extends React.PureComponent {
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<ButtonContainerComponent>
|
||||
{this._renderBackImage()}
|
||||
{this.renderBackImage()}
|
||||
</ButtonContainerComponent>
|
||||
{this._maybeRenderTitle()}
|
||||
{this.maybeRenderTitle()}
|
||||
</View>
|
||||
</TouchableItem>
|
||||
);
|
||||
@@ -1,12 +1,12 @@
|
||||
import invariant from '../utils/invariant';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
import { Scene, Route, NavigationState, SceneDescriptor } from '../types';
|
||||
|
||||
const SCENE_KEY_PREFIX = 'scene_';
|
||||
|
||||
/**
|
||||
* Helper function to compare route keys (e.g. "9", "11").
|
||||
*/
|
||||
function compareKey(one, two) {
|
||||
function compareKey(one: string, two: string) {
|
||||
const delta = one.length - two.length;
|
||||
if (delta > 0) {
|
||||
return 1;
|
||||
@@ -20,7 +20,7 @@ function compareKey(one, two) {
|
||||
/**
|
||||
* Helper function to sort scenes based on their index and view key.
|
||||
*/
|
||||
function compareScenes(one, two) {
|
||||
function compareScenes(one: Scene, two: Scene) {
|
||||
if (one.index > two.index) {
|
||||
return 1;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ function compareScenes(one, two) {
|
||||
/**
|
||||
* Whether two routes are the same.
|
||||
*/
|
||||
function areScenesShallowEqual(one, two) {
|
||||
function areScenesShallowEqual(one: Scene, two: Scene) {
|
||||
return (
|
||||
one.key === two.key &&
|
||||
one.index === two.index &&
|
||||
@@ -47,7 +47,7 @@ function areScenesShallowEqual(one, two) {
|
||||
/**
|
||||
* Whether two routes are the same.
|
||||
*/
|
||||
function areRoutesShallowEqual(one, two) {
|
||||
function areRoutesShallowEqual(one: Route, two: Route) {
|
||||
if (!one || !two) {
|
||||
return one === two;
|
||||
}
|
||||
@@ -60,10 +60,10 @@ function areRoutesShallowEqual(one, two) {
|
||||
}
|
||||
|
||||
export default function ScenesReducer(
|
||||
scenes,
|
||||
nextState,
|
||||
prevState,
|
||||
descriptors
|
||||
scenes: Scene[],
|
||||
nextState: NavigationState,
|
||||
prevState: NavigationState | null,
|
||||
descriptors: { [key: string]: SceneDescriptor }
|
||||
) {
|
||||
// Always update the descriptors
|
||||
// This is a workaround for https://github.com/react-navigation/react-navigation/issues/4271
|
||||
@@ -107,7 +107,7 @@ export default function ScenesReducer(
|
||||
|
||||
let descriptor = descriptors && descriptors[route.key];
|
||||
|
||||
const scene = {
|
||||
const scene: Scene = {
|
||||
index,
|
||||
isActive: false,
|
||||
isStale: false,
|
||||
@@ -115,11 +115,14 @@ export default function ScenesReducer(
|
||||
route,
|
||||
descriptor,
|
||||
};
|
||||
invariant(
|
||||
!nextKeys.has(key),
|
||||
`navigation.state.routes[${index}].key "${key}" conflicts with ` +
|
||||
'another route!'
|
||||
);
|
||||
|
||||
if (nextKeys.has(key)) {
|
||||
throw new Error(
|
||||
`navigation.state.routes[${index}].key "${key}" conflicts with ` +
|
||||
'another route!'
|
||||
);
|
||||
}
|
||||
|
||||
nextKeys.add(key);
|
||||
|
||||
if (staleScenes.has(key)) {
|
||||
@@ -168,9 +171,9 @@ export default function ScenesReducer(
|
||||
});
|
||||
}
|
||||
|
||||
const nextScenes = [];
|
||||
const nextScenes: Scene[] = [];
|
||||
|
||||
const mergeScene = nextScene => {
|
||||
const mergeScene = (nextScene: Scene) => {
|
||||
const { key } = nextScene;
|
||||
const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null;
|
||||
if (prevScene && areScenesShallowEqual(prevScene, nextScene)) {
|
||||
@@ -201,11 +204,11 @@ export default function ScenesReducer(
|
||||
}
|
||||
});
|
||||
|
||||
invariant(
|
||||
activeScenesCount === 1,
|
||||
'there should always be only one scene active, not %s.',
|
||||
activeScenesCount
|
||||
);
|
||||
if (activeScenesCount !== 1) {
|
||||
throw new Error(
|
||||
`There should always be only one scene active, not ${activeScenesCount}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (nextScenes.length !== scenes.length) {
|
||||
return nextScenes;
|
||||
@@ -1,9 +1,27 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { StackActions } from '@react-navigation/core';
|
||||
import StackViewLayout from './StackViewLayout';
|
||||
import Transitioner from '../Transitioner';
|
||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||
import {
|
||||
NavigationProp,
|
||||
SceneDescriptor,
|
||||
NavigationConfig,
|
||||
TransitionProps,
|
||||
Scene,
|
||||
} from '../../types';
|
||||
|
||||
type Props = {
|
||||
screenProps: unknown;
|
||||
navigation: NavigationProp;
|
||||
descriptors: { [key: string]: SceneDescriptor };
|
||||
navigationConfig: NavigationConfig;
|
||||
onTransitionStart?: () => void;
|
||||
onGestureBegin?: () => void;
|
||||
onGestureCanceled?: () => void;
|
||||
onGestureEnd?: () => void;
|
||||
};
|
||||
|
||||
const USE_NATIVE_DRIVER = true;
|
||||
|
||||
@@ -16,12 +34,12 @@ const DefaultNavigationConfig = {
|
||||
cardOverlayEnabled: false,
|
||||
};
|
||||
|
||||
class StackView extends React.Component {
|
||||
class StackView extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<Transitioner
|
||||
render={this._render}
|
||||
configureTransition={this._configureTransition}
|
||||
render={this.renderStackviewLayout}
|
||||
configureTransition={this.configureTransition}
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
@@ -29,7 +47,7 @@ class StackView extends React.Component {
|
||||
this.props.onTransitionStart ||
|
||||
this.props.navigationConfig.onTransitionStart
|
||||
}
|
||||
onTransitionEnd={this._onTransitionEnd}
|
||||
onTransitionEnd={this.handleTransitionEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +63,10 @@ class StackView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_configureTransition = (transitionProps, prevTransitionProps) => {
|
||||
private configureTransition = (
|
||||
transitionProps: TransitionProps,
|
||||
prevTransitionProps?: TransitionProps
|
||||
) => {
|
||||
return {
|
||||
useNativeDriver: USE_NATIVE_DRIVER,
|
||||
...TransitionConfigs.getTransitionConfig(
|
||||
@@ -57,7 +78,7 @@ class StackView extends React.Component {
|
||||
};
|
||||
};
|
||||
|
||||
_getShadowEnabled = () => {
|
||||
private getShadowEnabled = () => {
|
||||
const { navigationConfig } = this.props;
|
||||
return navigationConfig &&
|
||||
navigationConfig.hasOwnProperty('cardShadowEnabled')
|
||||
@@ -65,7 +86,7 @@ class StackView extends React.Component {
|
||||
: DefaultNavigationConfig.cardShadowEnabled;
|
||||
};
|
||||
|
||||
_getCardOverlayEnabled = () => {
|
||||
private getCardOverlayEnabled = () => {
|
||||
const { navigationConfig } = this.props;
|
||||
return navigationConfig &&
|
||||
navigationConfig.hasOwnProperty('cardOverlayEnabled')
|
||||
@@ -73,28 +94,34 @@ class StackView extends React.Component {
|
||||
: DefaultNavigationConfig.cardOverlayEnabled;
|
||||
};
|
||||
|
||||
_render = (transitionProps, lastTransitionProps) => {
|
||||
private renderStackviewLayout = (
|
||||
transitionProps: TransitionProps,
|
||||
lastTransitionProps?: TransitionProps
|
||||
) => {
|
||||
const { screenProps, navigationConfig } = this.props;
|
||||
return (
|
||||
<StackViewLayout
|
||||
{...navigationConfig}
|
||||
shadowEnabled={this._getShadowEnabled()}
|
||||
cardOverlayEnabled={this._getCardOverlayEnabled()}
|
||||
shadowEnabled={this.getShadowEnabled()}
|
||||
cardOverlayEnabled={this.getCardOverlayEnabled()}
|
||||
onGestureBegin={this.props.onGestureBegin}
|
||||
onGestureCanceled={this.props.onGestureCanceled}
|
||||
onGestureEnd={this.props.onGestureEnd}
|
||||
screenProps={screenProps}
|
||||
descriptors={this.props.descriptors}
|
||||
transitionProps={transitionProps}
|
||||
lastTransitionProps={lastTransitionProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_onTransitionEnd = (transition, lastTransition) => {
|
||||
private handleTransitionEnd = (
|
||||
transition: { scene: Scene; navigation: NavigationProp },
|
||||
lastTransition?: { scene: Scene; navigation: NavigationProp }
|
||||
) => {
|
||||
const {
|
||||
navigationConfig,
|
||||
navigation,
|
||||
// @ts-ignore
|
||||
onTransitionEnd = navigationConfig.onTransitionEnd,
|
||||
} = this.props;
|
||||
const transitionDestKey = transition.scene.route.key;
|
||||
@@ -1,10 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Animated, StyleSheet, Platform } from 'react-native';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Screen } from 'react-native-screens';
|
||||
import createPointerEventsContainer from './createPointerEventsContainer';
|
||||
import createPointerEventsContainer, {
|
||||
InputProps,
|
||||
InjectedProps,
|
||||
} from './createPointerEventsContainer';
|
||||
|
||||
type Props = InputProps &
|
||||
InjectedProps & {
|
||||
style: StyleProp<ViewStyle>;
|
||||
animatedStyle: any;
|
||||
position: Animated.AnimatedInterpolation;
|
||||
transparent?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const EPS = 1e-5;
|
||||
function getAccessibilityProps(isActive) {
|
||||
|
||||
function getAccessibilityProps(isActive: boolean) {
|
||||
if (Platform.OS === 'ios') {
|
||||
return {
|
||||
accessibilityElementsHidden: !isActive,
|
||||
@@ -21,7 +40,7 @@ function getAccessibilityProps(isActive) {
|
||||
/**
|
||||
* Component that renders the scene as card for the <StackView />.
|
||||
*/
|
||||
class Card extends React.Component {
|
||||
class Card extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
@@ -32,8 +51,9 @@ class Card extends React.Component {
|
||||
scene: { index, isActive },
|
||||
} = this.props;
|
||||
|
||||
const active = Platform.select({
|
||||
const active: Animated.Value | number | boolean = Platform.select({
|
||||
web: isActive,
|
||||
// @ts-ignore
|
||||
default:
|
||||
transparent || isActive
|
||||
? 1
|
||||
@@ -61,6 +81,7 @@ class Card extends React.Component {
|
||||
pointerEvents={pointerEvents}
|
||||
onComponentRef={this.props.onComponentRef}
|
||||
style={[containerAnimatedStyle, screenStyle]}
|
||||
// @ts-ignore
|
||||
active={active}
|
||||
>
|
||||
{!transparent && shadowOpacity ? (
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
I18nManager,
|
||||
Easing,
|
||||
Dimensions,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
import {
|
||||
SceneView,
|
||||
@@ -16,7 +19,13 @@ import {
|
||||
} from '@react-navigation/core';
|
||||
import { withOrientation } from '@react-navigation/native';
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
State as GestureState,
|
||||
PanGestureHandlerGestureEvent,
|
||||
GestureHandlerGestureEventNativeEvent,
|
||||
PanGestureHandlerEventExtra,
|
||||
} from 'react-native-gesture-handler';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
import Header from '../Header/Header';
|
||||
@@ -25,13 +34,52 @@ import HeaderStyleInterpolator from '../Header/HeaderStyleInterpolator';
|
||||
import StackGestureContext from '../../utils/StackGestureContext';
|
||||
import clamp from '../../utils/clamp';
|
||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||
import {
|
||||
Scene,
|
||||
HeaderMode,
|
||||
TransitionProps,
|
||||
TransitionConfig,
|
||||
HeaderTransitionConfig,
|
||||
HeaderProps,
|
||||
} from '../../types';
|
||||
|
||||
type Props = {
|
||||
mode: 'modal' | 'card';
|
||||
headerMode: 'screen' | 'float';
|
||||
headerLayoutPreset: 'left' | 'center';
|
||||
headerTransitionPreset: 'fade-in-place' | 'uikit';
|
||||
headerBackgroundTransitionPreset: 'fade' | 'translate' | 'toggle';
|
||||
headerBackTitleVisible?: boolean;
|
||||
isLandscape: boolean;
|
||||
shadowEnabled?: boolean;
|
||||
cardOverlayEnabled?: boolean;
|
||||
transparentCard?: boolean;
|
||||
cardStyle?: StyleProp<ViewStyle>;
|
||||
transitionProps: TransitionProps;
|
||||
lastTransitionProps?: TransitionProps;
|
||||
transitionConfig: (
|
||||
transitionProps: TransitionProps,
|
||||
prevTransitionProps?: TransitionProps,
|
||||
isModal?: boolean
|
||||
) => HeaderTransitionConfig;
|
||||
onGestureBegin?: () => void;
|
||||
onGestureEnd?: () => void;
|
||||
onGestureCanceled?: () => void;
|
||||
screenProps: unknown;
|
||||
};
|
||||
|
||||
type State = {
|
||||
floatingHeaderHeight: number;
|
||||
};
|
||||
|
||||
const IPHONE_XS_HEIGHT = 812; // iPhone X and XS
|
||||
const IPHONE_XR_HEIGHT = 896; // iPhone XR and XS Max
|
||||
const { width: WINDOW_WIDTH, height: WINDOW_HEIGHT } = Dimensions.get('window');
|
||||
const IS_IPHONE_X =
|
||||
Platform.OS === 'ios' &&
|
||||
// @ts-ignore
|
||||
!Platform.isPad &&
|
||||
// @ts-ignore
|
||||
!Platform.isTVOS &&
|
||||
(WINDOW_HEIGHT === IPHONE_XS_HEIGHT ||
|
||||
WINDOW_WIDTH === IPHONE_XS_HEIGHT ||
|
||||
@@ -69,8 +117,9 @@ const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
||||
|
||||
const USE_NATIVE_DRIVER = true;
|
||||
|
||||
const getDefaultHeaderHeight = isLandscape => {
|
||||
const getDefaultHeaderHeight = (isLandscape: boolean) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
// @ts-ignore
|
||||
if (isLandscape && !Platform.isPad) {
|
||||
return 32;
|
||||
} else if (IS_IPHONE_X) {
|
||||
@@ -85,7 +134,18 @@ const getDefaultHeaderHeight = isLandscape => {
|
||||
}
|
||||
};
|
||||
|
||||
class StackViewLayout extends React.Component {
|
||||
class StackViewLayout extends React.Component<Props, State> {
|
||||
private panGestureRef: React.RefObject<PanGestureHandler>;
|
||||
private gestureX: Animated.Value;
|
||||
private gestureY: Animated.Value;
|
||||
private positionSwitch: Animated.Value;
|
||||
private gestureSwitch: Animated.AnimatedInterpolation;
|
||||
private gestureEvent: (...args: any[]) => void;
|
||||
private gesturePosition: Animated.AnimatedInterpolation | undefined;
|
||||
|
||||
// @ts-ignore
|
||||
private position: Animated.Value;
|
||||
|
||||
/**
|
||||
* immediateIndex is used to represent the expected index that we will be on after a
|
||||
* transition. To achieve a smooth animation when swiping back, the action to go back
|
||||
@@ -93,9 +153,13 @@ class StackViewLayout extends React.Component {
|
||||
* the transition so that gestures can be handled correctly. This is a work-around for
|
||||
* cases when the user quickly swipes back several times.
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
private immediateIndex: number | null = null;
|
||||
private transitionConfig:
|
||||
| HeaderTransitionConfig & TransitionConfig
|
||||
| undefined;
|
||||
private prevProps: Props | undefined;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.panGestureRef = React.createRef();
|
||||
this.gestureX = new Animated.Value(0);
|
||||
@@ -133,7 +197,7 @@ class StackViewLayout extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
_renderHeader(scene, headerMode) {
|
||||
private renderHeader(scene: Scene, headerMode: HeaderMode) {
|
||||
const { options } = scene.descriptor;
|
||||
const { header } = options;
|
||||
|
||||
@@ -153,16 +217,18 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
|
||||
// Handle the case where the header option is a function, and provide the default
|
||||
const renderHeader = header || (props => <Header {...props} />);
|
||||
const renderHeader =
|
||||
// @ts-ignore TS warns about missing props, but they are in default props
|
||||
header || ((props: HeaderProps) => <Header {...props} />);
|
||||
|
||||
let {
|
||||
headerLeftInterpolator,
|
||||
headerTitleInterpolator,
|
||||
headerRightInterpolator,
|
||||
headerBackgroundInterpolator,
|
||||
} = this._transitionConfig;
|
||||
} = this.transitionConfig as HeaderTransitionConfig;
|
||||
|
||||
const backgroundTransitionPresetInterpolator = this._getHeaderBackgroundTransitionPreset();
|
||||
const backgroundTransitionPresetInterpolator = this.getHeaderBackgroundTransitionPreset();
|
||||
if (backgroundTransitionPresetInterpolator) {
|
||||
headerBackgroundInterpolator = backgroundTransitionPresetInterpolator;
|
||||
}
|
||||
@@ -177,9 +243,9 @@ class StackViewLayout extends React.Component {
|
||||
position: this.position,
|
||||
scene,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
layoutPreset: this._getHeaderLayoutPreset(),
|
||||
backTitleVisible: this._getHeaderBackTitleVisible(),
|
||||
transitionPreset: this.getHeaderTransitionPreset(),
|
||||
layoutPreset: this.getHeaderLayoutPreset(),
|
||||
backTitleVisible: this.getHeaderBackTitleVisible(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
@@ -189,8 +255,9 @@ class StackViewLayout extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_reset(resetToIndex, duration) {
|
||||
private reset(resetToIndex: number, duration: number) {
|
||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||
// @ts-ignore
|
||||
Animated.spring(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
stiffness: 6000,
|
||||
@@ -202,6 +269,7 @@ class StackViewLayout extends React.Component {
|
||||
useNativeDriver: USE_NATIVE_DRIVER,
|
||||
}).start();
|
||||
} else {
|
||||
// @ts-ignore
|
||||
Animated.timing(this.props.transitionProps.position, {
|
||||
toValue: resetToIndex,
|
||||
duration,
|
||||
@@ -211,16 +279,16 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_goBack(backFromIndex, duration) {
|
||||
private goBack(backFromIndex: number, duration: number) {
|
||||
const { navigation, position, scenes } = this.props.transitionProps;
|
||||
const toValue = Math.max(backFromIndex - 1, 0);
|
||||
|
||||
// set temporary index for gesture handler to respect until the action is
|
||||
// dispatched at the end of the transition.
|
||||
this._immediateIndex = toValue;
|
||||
this.immediateIndex = toValue;
|
||||
|
||||
const onCompleteAnimation = () => {
|
||||
this._immediateIndex = null;
|
||||
this.immediateIndex = null;
|
||||
const backFromScene = scenes.find(s => s.index === toValue + 1);
|
||||
if (backFromScene) {
|
||||
navigation.dispatch(
|
||||
@@ -234,6 +302,7 @@ class StackViewLayout extends React.Component {
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||
// @ts-ignore
|
||||
Animated.spring(position, {
|
||||
toValue,
|
||||
stiffness: 7000,
|
||||
@@ -245,6 +314,7 @@ class StackViewLayout extends React.Component {
|
||||
useNativeDriver: USE_NATIVE_DRIVER,
|
||||
}).start(onCompleteAnimation);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
Animated.timing(position, {
|
||||
toValue,
|
||||
duration,
|
||||
@@ -254,26 +324,26 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_onFloatingHeaderLayout = e => {
|
||||
private handleFloatingHeaderLayout = (e: LayoutChangeEvent) => {
|
||||
const { height } = e.nativeEvent.layout;
|
||||
if (height !== this.state.floatingHeaderHeight) {
|
||||
this.setState({ floatingHeaderHeight: height });
|
||||
}
|
||||
};
|
||||
|
||||
_prepareAnimated() {
|
||||
if (this.props === this._prevProps) {
|
||||
private prepareAnimated() {
|
||||
if (this.props === this.prevProps) {
|
||||
return;
|
||||
}
|
||||
this._prevProps = this.props;
|
||||
this.prevProps = this.props;
|
||||
|
||||
this._prepareGesture();
|
||||
this._preparePosition();
|
||||
this._prepareTransitionConfig();
|
||||
this.prepareGesture();
|
||||
this.preparePosition();
|
||||
this.prepareTransitionConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
this._prepareAnimated();
|
||||
this.prepareAnimated();
|
||||
|
||||
const { transitionProps } = this.props;
|
||||
const {
|
||||
@@ -283,7 +353,7 @@ class StackViewLayout extends React.Component {
|
||||
scenes,
|
||||
} = transitionProps;
|
||||
|
||||
const headerMode = this._getHeaderMode();
|
||||
const headerMode = this.getHeaderMode();
|
||||
let floatingHeader = null;
|
||||
if (headerMode === 'float') {
|
||||
const { scene } = transitionProps;
|
||||
@@ -291,27 +361,27 @@ class StackViewLayout extends React.Component {
|
||||
<View
|
||||
style={styles.floatingHeader}
|
||||
pointerEvents="box-none"
|
||||
onLayout={this._onFloatingHeaderLayout}
|
||||
onLayout={this.handleFloatingHeaderLayout}
|
||||
>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
{this.renderHeader(scene, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanGestureHandler
|
||||
{...this._gestureActivationCriteria()}
|
||||
{...this.gestureActivationCriteria()}
|
||||
ref={this.panGestureRef}
|
||||
onGestureEvent={this.gestureEvent}
|
||||
onHandlerStateChange={this._handlePanGestureStateChange}
|
||||
enabled={index > 0 && this._isGestureEnabled()}
|
||||
onHandlerStateChange={this.handlePanGestureStateChange}
|
||||
enabled={index > 0 && this.isGestureEnabled()}
|
||||
>
|
||||
<Animated.View
|
||||
style={[styles.container, this._transitionConfig.containerStyle]}
|
||||
style={[styles.container, this.transitionConfig!.containerStyle]}
|
||||
>
|
||||
<StackGestureContext.Provider value={this.panGestureRef}>
|
||||
<ScreenContainer style={styles.scenes}>
|
||||
{scenes.map(this._renderCard)}
|
||||
{scenes.map(this.renderCard)}
|
||||
</ScreenContainer>
|
||||
{floatingHeader}
|
||||
</StackGestureContext.Provider>
|
||||
@@ -320,36 +390,40 @@ class StackViewLayout extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { state: prevState } = prevProps.transitionProps.navigation;
|
||||
const { state } = this.props.transitionProps.navigation;
|
||||
if (prevState.index !== state.index) {
|
||||
this._maybeCancelGesture();
|
||||
this.maybeCancelGesture();
|
||||
}
|
||||
}
|
||||
|
||||
_getGestureResponseDistance() {
|
||||
private getGestureResponseDistance() {
|
||||
const { scene } = this.props.transitionProps;
|
||||
const { options } = scene.descriptor;
|
||||
const {
|
||||
gestureResponseDistance: userGestureResponseDistance = {},
|
||||
gestureResponseDistance: userGestureResponseDistance = {} as {
|
||||
vertical?: number;
|
||||
horizontal?: number;
|
||||
},
|
||||
} = options;
|
||||
|
||||
// Doesn't make sense for a response distance of 0, so this works fine
|
||||
return this._isModal()
|
||||
return this.isModal()
|
||||
? userGestureResponseDistance.vertical ||
|
||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
||||
: userGestureResponseDistance.horizontal ||
|
||||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
||||
}
|
||||
|
||||
_gestureActivationCriteria() {
|
||||
private gestureActivationCriteria() {
|
||||
const { layout } = this.props.transitionProps;
|
||||
const gestureResponseDistance = this._getGestureResponseDistance();
|
||||
const isMotionInverted = this._isMotionInverted();
|
||||
const gestureResponseDistance = this.getGestureResponseDistance();
|
||||
const isMotionInverted = this.isMotionInverted();
|
||||
|
||||
if (this._isMotionVertical()) {
|
||||
const height = layout.height.__getValue();
|
||||
if (this.isMotionVertical()) {
|
||||
// @ts-ignore
|
||||
const height: number = layout.height.__getValue();
|
||||
|
||||
return {
|
||||
maxDeltaX: 15,
|
||||
@@ -359,7 +433,8 @@ class StackViewLayout extends React.Component {
|
||||
: { bottom: -height + gestureResponseDistance },
|
||||
};
|
||||
} else {
|
||||
const width = layout.width.__getValue();
|
||||
// @ts-ignore
|
||||
const width: number = layout.width.__getValue();
|
||||
const hitSlop = -width + gestureResponseDistance;
|
||||
|
||||
return {
|
||||
@@ -370,7 +445,7 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_isGestureEnabled() {
|
||||
private isGestureEnabled() {
|
||||
const gesturesEnabled = this.props.transitionProps.scene.descriptor.options
|
||||
.gesturesEnabled;
|
||||
return typeof gesturesEnabled === 'boolean'
|
||||
@@ -378,23 +453,23 @@ class StackViewLayout extends React.Component {
|
||||
: Platform.OS === 'ios';
|
||||
}
|
||||
|
||||
_isMotionVertical() {
|
||||
return this._isModal();
|
||||
private isMotionVertical() {
|
||||
return this.isModal();
|
||||
}
|
||||
|
||||
_isModal() {
|
||||
private isModal() {
|
||||
return this.props.mode === 'modal';
|
||||
}
|
||||
|
||||
// This only currently applies to the horizontal gesture!
|
||||
_isMotionInverted() {
|
||||
private isMotionInverted() {
|
||||
const {
|
||||
transitionProps: { scene },
|
||||
} = this.props;
|
||||
const { options } = scene.descriptor;
|
||||
const { gestureDirection } = options;
|
||||
|
||||
if (this._isModal()) {
|
||||
if (this.isModal()) {
|
||||
return gestureDirection === 'inverted';
|
||||
} else {
|
||||
return typeof gestureDirection === 'string'
|
||||
@@ -403,7 +478,11 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_computeHorizontalGestureValue({ translationX }) {
|
||||
private computeHorizontalGestureValue({
|
||||
translationX,
|
||||
}: {
|
||||
translationX: number;
|
||||
}) {
|
||||
const {
|
||||
transitionProps: { navigation, layout },
|
||||
} = this.props;
|
||||
@@ -411,15 +490,20 @@ class StackViewLayout extends React.Component {
|
||||
const { index } = navigation.state;
|
||||
|
||||
// TODO: remove this __getValue!
|
||||
const distance = layout.width.__getValue();
|
||||
// @ts-ignore
|
||||
const distance: number = layout.width.__getValue();
|
||||
|
||||
const x = this._isMotionInverted() ? -1 * translationX : translationX;
|
||||
const x = this.isMotionInverted() ? -1 * translationX : translationX;
|
||||
|
||||
const value = index - x / distance;
|
||||
return clamp(index - 1, value, index);
|
||||
}
|
||||
|
||||
_computeVerticalGestureValue({ translationY }) {
|
||||
private computeVerticalGestureValue({
|
||||
translationY,
|
||||
}: {
|
||||
translationY: number;
|
||||
}) {
|
||||
const {
|
||||
transitionProps: { navigation, layout },
|
||||
} = this.props;
|
||||
@@ -427,27 +511,32 @@ class StackViewLayout extends React.Component {
|
||||
const { index } = navigation.state;
|
||||
|
||||
// TODO: remove this __getValue!
|
||||
const distance = layout.height.__getValue();
|
||||
// @ts-ignore
|
||||
const distance: number = layout.height.__getValue();
|
||||
|
||||
const y = this._isMotionInverted() ? -1 * translationY : translationY;
|
||||
const y = this.isMotionInverted() ? -1 * translationY : translationY;
|
||||
const value = index - y / distance;
|
||||
return clamp(index - 1, value, index);
|
||||
}
|
||||
|
||||
_handlePanGestureStateChange = ({ nativeEvent }) => {
|
||||
if (nativeEvent.oldState === State.ACTIVE) {
|
||||
private handlePanGestureStateChange = ({
|
||||
nativeEvent,
|
||||
}: PanGestureHandlerGestureEvent) => {
|
||||
// @ts-ignore
|
||||
if (nativeEvent.oldState === GestureState.ACTIVE) {
|
||||
// Gesture was cancelled! For example, some navigation state update
|
||||
// arrived while the gesture was active that cancelled it out
|
||||
// @ts-ignore
|
||||
if (this.positionSwitch.__getValue() === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._isMotionVertical()) {
|
||||
this._handleReleaseVertical(nativeEvent);
|
||||
if (this.isMotionVertical()) {
|
||||
this.handleReleaseVertical(nativeEvent);
|
||||
} else {
|
||||
this._handleReleaseHorizontal(nativeEvent);
|
||||
this.handleReleaseHorizontal(nativeEvent);
|
||||
}
|
||||
} else if (nativeEvent.state === State.ACTIVE) {
|
||||
} else if (nativeEvent.state === GestureState.ACTIVE) {
|
||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
||||
|
||||
// Switch to using gesture position
|
||||
@@ -471,12 +560,13 @@ class StackViewLayout extends React.Component {
|
||||
// of the gesturePosition, so if we are in the middle of swiping the screen away
|
||||
// and back is programatically fired then we will reset to the initial position
|
||||
// and animate from there
|
||||
_maybeCancelGesture() {
|
||||
private maybeCancelGesture() {
|
||||
this.positionSwitch.setValue(1);
|
||||
}
|
||||
|
||||
_prepareGesture() {
|
||||
if (!this._isGestureEnabled()) {
|
||||
private prepareGesture() {
|
||||
if (!this.isGestureEnabled()) {
|
||||
// @ts-ignore
|
||||
if (this.positionSwitch.__getValue() !== 1) {
|
||||
this.positionSwitch.setValue(1);
|
||||
}
|
||||
@@ -486,23 +576,25 @@ class StackViewLayout extends React.Component {
|
||||
|
||||
// We can't run the gesture if width or height layout is unavailable
|
||||
if (
|
||||
// @ts-ignore
|
||||
this.props.transitionProps.layout.width.__getValue() === 0 ||
|
||||
// @ts-ignore
|
||||
this.props.transitionProps.layout.height.__getValue() === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._isMotionVertical()) {
|
||||
this._prepareGestureVertical();
|
||||
if (this.isMotionVertical()) {
|
||||
this.prepareGestureVertical();
|
||||
} else {
|
||||
this._prepareGestureHorizontal();
|
||||
this.prepareGestureHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
_prepareGestureHorizontal() {
|
||||
private prepareGestureHorizontal() {
|
||||
const { index } = this.props.transitionProps.navigation.state;
|
||||
|
||||
if (this._isMotionInverted()) {
|
||||
if (this.isMotionInverted()) {
|
||||
this.gesturePosition = Animated.add(
|
||||
index,
|
||||
Animated.divide(this.gestureX, this.props.transitionProps.layout.width)
|
||||
@@ -529,10 +621,10 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_prepareGestureVertical() {
|
||||
private prepareGestureVertical() {
|
||||
const { index } = this.props.transitionProps.navigation.state;
|
||||
|
||||
if (this._isMotionInverted()) {
|
||||
if (this.isMotionInverted()) {
|
||||
this.gesturePosition = Animated.add(
|
||||
index,
|
||||
Animated.divide(this.gestureY, this.props.transitionProps.layout.height)
|
||||
@@ -559,31 +651,35 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_handleReleaseHorizontal(nativeEvent) {
|
||||
private handleReleaseHorizontal(
|
||||
nativeEvent: GestureHandlerGestureEventNativeEvent &
|
||||
PanGestureHandlerEventExtra
|
||||
) {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout },
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
this.immediateIndex == null ? index : this.immediateIndex;
|
||||
|
||||
// Calculate animate duration according to gesture speed and moved distance
|
||||
// @ts-ignore
|
||||
const distance = layout.width.__getValue();
|
||||
const movementDirection = this._isMotionInverted() ? -1 : 1;
|
||||
const movementDirection = this.isMotionInverted() ? -1 : 1;
|
||||
const movedDistance = movementDirection * nativeEvent.translationX;
|
||||
const gestureVelocity = movementDirection * nativeEvent.velocityX;
|
||||
const defaultVelocity = distance / ANIMATION_DURATION;
|
||||
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
|
||||
const resetDuration = this._isMotionInverted()
|
||||
const resetDuration = this.isMotionInverted()
|
||||
? (distance - movedDistance) / velocity
|
||||
: movedDistance / velocity;
|
||||
const goBackDuration = this._isMotionInverted()
|
||||
const goBackDuration = this.isMotionInverted()
|
||||
? movedDistance / velocity
|
||||
: (distance - movedDistance) / velocity;
|
||||
|
||||
// Get the current position value and reset to using the statically driven
|
||||
// (rather than gesture driven) position.
|
||||
const value = this._computeHorizontalGestureValue(nativeEvent);
|
||||
const value = this.computeHorizontalGestureValue(nativeEvent);
|
||||
position.setValue(value);
|
||||
this.positionSwitch.setValue(1);
|
||||
|
||||
@@ -591,12 +687,12 @@ class StackViewLayout extends React.Component {
|
||||
// of intent
|
||||
if (gestureVelocity < -50) {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
this.reset(immediateIndex, resetDuration);
|
||||
return;
|
||||
}
|
||||
if (gestureVelocity > 50) {
|
||||
this.props.onGestureEnd && this.props.onGestureEnd();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
this.goBack(immediateIndex, goBackDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -604,24 +700,28 @@ class StackViewLayout extends React.Component {
|
||||
// and the back will happen.
|
||||
if (value <= index - POSITION_THRESHOLD) {
|
||||
this.props.onGestureEnd && this.props.onGestureEnd();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
this.goBack(immediateIndex, goBackDuration);
|
||||
} else {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
this.reset(immediateIndex, resetDuration);
|
||||
}
|
||||
}
|
||||
|
||||
_handleReleaseVertical(nativeEvent) {
|
||||
private handleReleaseVertical(
|
||||
nativeEvent: GestureHandlerGestureEventNativeEvent &
|
||||
PanGestureHandlerEventExtra
|
||||
) {
|
||||
const {
|
||||
transitionProps: { navigation, position, layout },
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
const immediateIndex =
|
||||
this._immediateIndex == null ? index : this._immediateIndex;
|
||||
this.immediateIndex == null ? index : this.immediateIndex;
|
||||
|
||||
// Calculate animate duration according to gesture speed and moved distance
|
||||
// @ts-ignore
|
||||
const distance = layout.height.__getValue();
|
||||
const isMotionInverted = this._isMotionInverted();
|
||||
const isMotionInverted = this.isMotionInverted();
|
||||
const movementDirection = isMotionInverted ? -1 : 1;
|
||||
const movedDistance = movementDirection * nativeEvent.translationY;
|
||||
const gestureVelocity = movementDirection * nativeEvent.velocityY;
|
||||
@@ -634,7 +734,7 @@ class StackViewLayout extends React.Component {
|
||||
? movedDistance / velocity
|
||||
: (distance - movedDistance) / velocity;
|
||||
|
||||
const value = this._computeVerticalGestureValue(nativeEvent);
|
||||
const value = this.computeVerticalGestureValue(nativeEvent);
|
||||
position.setValue(value);
|
||||
this.positionSwitch.setValue(1);
|
||||
|
||||
@@ -642,12 +742,12 @@ class StackViewLayout extends React.Component {
|
||||
// of intent
|
||||
if (gestureVelocity < -50) {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
this.reset(immediateIndex, resetDuration);
|
||||
return;
|
||||
}
|
||||
if (gestureVelocity > 50) {
|
||||
this.props.onGestureEnd && this.props.onGestureEnd();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
this.goBack(immediateIndex, goBackDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -655,14 +755,14 @@ class StackViewLayout extends React.Component {
|
||||
// and the back will happen.
|
||||
if (value <= index - POSITION_THRESHOLD) {
|
||||
this.props.onGestureEnd && this.props.onGestureEnd();
|
||||
this._goBack(immediateIndex, goBackDuration);
|
||||
this.goBack(immediateIndex, goBackDuration);
|
||||
} else {
|
||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||
this._reset(immediateIndex, resetDuration);
|
||||
this.reset(immediateIndex, resetDuration);
|
||||
}
|
||||
}
|
||||
|
||||
_getHeaderMode() {
|
||||
private getHeaderMode() {
|
||||
if (this.props.headerMode) {
|
||||
return this.props.headerMode;
|
||||
}
|
||||
@@ -674,7 +774,7 @@ class StackViewLayout extends React.Component {
|
||||
return 'float';
|
||||
}
|
||||
|
||||
_getHeaderBackgroundTransitionPreset() {
|
||||
private getHeaderBackgroundTransitionPreset() {
|
||||
const { headerBackgroundTransitionPreset } = this.props;
|
||||
if (headerBackgroundTransitionPreset) {
|
||||
if (
|
||||
@@ -701,12 +801,12 @@ class StackViewLayout extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
_getHeaderLayoutPreset() {
|
||||
private getHeaderLayoutPreset() {
|
||||
const { headerLayoutPreset } = this.props;
|
||||
if (headerLayoutPreset) {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
this._getHeaderTransitionPreset() === 'uikit' &&
|
||||
this.getHeaderTransitionPreset() === 'uikit' &&
|
||||
headerLayoutPreset === 'left' &&
|
||||
Platform.OS === 'ios'
|
||||
) {
|
||||
@@ -735,10 +835,10 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_getHeaderTransitionPreset() {
|
||||
private getHeaderTransitionPreset() {
|
||||
// On Android or with header mode screen, we always just use in-place,
|
||||
// we ignore the option entirely (at least until we have other presets)
|
||||
if (Platform.OS !== 'ios' || this._getHeaderMode() === 'screen') {
|
||||
if (Platform.OS !== 'ios' || this.getHeaderMode() === 'screen') {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
@@ -760,9 +860,9 @@ class StackViewLayout extends React.Component {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
_getHeaderBackTitleVisible() {
|
||||
private getHeaderBackTitleVisible() {
|
||||
const { headerBackTitleVisible } = this.props;
|
||||
const layoutPreset = this._getHeaderLayoutPreset();
|
||||
const layoutPreset = this.getHeaderLayoutPreset();
|
||||
|
||||
// Even when we align to center on Android, people should need to opt-in to
|
||||
// showing the back title
|
||||
@@ -775,12 +875,12 @@ class StackViewLayout extends React.Component {
|
||||
: enabledByDefault;
|
||||
}
|
||||
|
||||
_renderInnerScene(scene) {
|
||||
private renderInnerScene(scene: Scene) {
|
||||
const { navigation, getComponent } = scene.descriptor;
|
||||
const SceneComponent = getComponent();
|
||||
|
||||
const { screenProps } = this.props;
|
||||
const headerMode = this._getHeaderMode();
|
||||
const headerMode = this.getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -791,7 +891,7 @@ class StackViewLayout extends React.Component {
|
||||
component={SceneComponent}
|
||||
/>
|
||||
</View>
|
||||
{this._renderHeader(scene, headerMode)}
|
||||
{this.renderHeader(scene, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -804,20 +904,22 @@ class StackViewLayout extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_prepareTransitionConfig() {
|
||||
this._transitionConfig = TransitionConfigs.getTransitionConfig(
|
||||
private prepareTransitionConfig() {
|
||||
this.transitionConfig = TransitionConfigs.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
{
|
||||
...this.props.transitionProps,
|
||||
position: this.position,
|
||||
},
|
||||
this.props.lastTransitionProps,
|
||||
this._isModal()
|
||||
this.isModal()
|
||||
);
|
||||
}
|
||||
|
||||
_preparePosition() {
|
||||
private preparePosition() {
|
||||
if (this.gesturePosition) {
|
||||
// FIXME: this doesn't seem right, there is setValue called in some places
|
||||
// @ts-ignore
|
||||
this.position = Animated.add(
|
||||
Animated.multiply(
|
||||
this.props.transitionProps.position,
|
||||
@@ -830,7 +932,7 @@ class StackViewLayout extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_renderCard = scene => {
|
||||
private renderCard = (scene: Scene) => {
|
||||
const {
|
||||
transitionProps,
|
||||
shadowEnabled,
|
||||
@@ -839,7 +941,7 @@ class StackViewLayout extends React.Component {
|
||||
cardStyle,
|
||||
} = this.props;
|
||||
|
||||
const { screenInterpolator } = this._transitionConfig;
|
||||
const { screenInterpolator } = this.transitionConfig as TransitionConfig;
|
||||
const style =
|
||||
screenInterpolator &&
|
||||
screenInterpolator({
|
||||
@@ -854,8 +956,10 @@ class StackViewLayout extends React.Component {
|
||||
// padding on the scene.
|
||||
const { options } = scene.descriptor;
|
||||
const hasHeader = options.header !== null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
let floatingContainerStyle = StyleSheet.absoluteFill;
|
||||
const headerMode = this.getHeaderMode();
|
||||
|
||||
let floatingContainerStyle: ViewStyle = StyleSheet.absoluteFill as ViewStyle;
|
||||
|
||||
if (hasHeader && headerMode === 'float' && !options.headerTransparent) {
|
||||
floatingContainerStyle = {
|
||||
...Platform.select({ web: {}, default: StyleSheet.absoluteFillObject }),
|
||||
@@ -874,7 +978,7 @@ class StackViewLayout extends React.Component {
|
||||
style={[floatingContainerStyle, cardStyle]}
|
||||
scene={scene}
|
||||
>
|
||||
{this._renderInnerScene(scene)}
|
||||
{this.renderInnerScene(scene)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -894,6 +998,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
floatingHeader: {
|
||||
// @ts-ignore
|
||||
position: Platform.select({ default: 'absolute', web: 'fixed' }),
|
||||
left: 0,
|
||||
top: 0,
|
||||
@@ -1,5 +1,6 @@
|
||||
import { I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
import { SceneInterpolatorProps } from '../../types';
|
||||
|
||||
const EPS = 1e-5;
|
||||
|
||||
@@ -21,7 +22,7 @@ const EPS = 1e-5;
|
||||
/**
|
||||
* Render the initial style when the initial layout isn't measured yet.
|
||||
*/
|
||||
function forInitial(props) {
|
||||
function forInitial(props: SceneInterpolatorProps) {
|
||||
const { navigation, scene } = props;
|
||||
|
||||
const focused = navigation.state.index === scene.index;
|
||||
@@ -37,7 +38,7 @@ function forInitial(props) {
|
||||
/**
|
||||
* Standard iOS-style slide in from the right.
|
||||
*/
|
||||
function forHorizontal(props) {
|
||||
function forHorizontal(props: SceneInterpolatorProps) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
@@ -85,7 +86,7 @@ function forHorizontal(props) {
|
||||
/**
|
||||
* Standard iOS-style slide in from the bottom (used for modals).
|
||||
*/
|
||||
function forVertical(props) {
|
||||
function forVertical(props: SceneInterpolatorProps) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
@@ -112,7 +113,7 @@ function forVertical(props) {
|
||||
/**
|
||||
* Standard Android-style fade in from the bottom.
|
||||
*/
|
||||
function forFadeFromBottomAndroid(props) {
|
||||
function forFadeFromBottomAndroid(props: SceneInterpolatorProps) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
@@ -144,7 +145,7 @@ function forFadeFromBottomAndroid(props) {
|
||||
};
|
||||
}
|
||||
|
||||
function forFadeToBottomAndroid(props) {
|
||||
function forFadeToBottomAndroid(props: SceneInterpolatorProps) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
@@ -182,7 +183,7 @@ function forFadeToBottomAndroid(props) {
|
||||
/**
|
||||
* fadeIn and fadeOut
|
||||
*/
|
||||
function forFade(props) {
|
||||
function forFade(props: SceneInterpolatorProps) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Animated, Easing, Platform } from 'react-native';
|
||||
import StyleInterpolator from './StackViewStyleInterpolator';
|
||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||
import { TransitionProps, TransitionConfig } from '../../types';
|
||||
|
||||
let IOSTransitionSpec;
|
||||
if (supportsImprovedSpringAnimation()) {
|
||||
@@ -75,10 +76,10 @@ const NoAnimation = {
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
transitionProps: TransitionProps,
|
||||
prevTransitionProps?: TransitionProps,
|
||||
isModal?: boolean
|
||||
): TransitionConfig {
|
||||
if (Platform.OS !== 'ios') {
|
||||
// Use the default Android animation no matter if the screen is a modal.
|
||||
// Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
@@ -98,12 +99,18 @@ function defaultTransitionConfig(
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(
|
||||
transitionConfigurer,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
function getTransitionConfig<T = {}>(
|
||||
transitionConfigurer:
|
||||
| undefined
|
||||
| ((
|
||||
transitionProps: TransitionProps,
|
||||
prevTransitionProps?: TransitionProps,
|
||||
isModal?: boolean
|
||||
) => T),
|
||||
transitionProps: TransitionProps,
|
||||
prevTransitionProps?: TransitionProps,
|
||||
isModal?: boolean
|
||||
): TransitionConfig & T {
|
||||
const defaultConfig = defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
@@ -115,7 +122,8 @@ function getTransitionConfig(
|
||||
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
|
||||
return defaultConfig as any;
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -1,99 +0,0 @@
|
||||
import React from 'react';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
const MIN_POSITION_OFFSET = 0.01;
|
||||
|
||||
/**
|
||||
* Create a higher-order component that automatically computes the
|
||||
* `pointerEvents` property for a component whenever navigation position
|
||||
* changes.
|
||||
*/
|
||||
export default function createPointerEventsContainer(Component) {
|
||||
class Container extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._pointerEvents = this._computePointerEvents();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._positionListener && this._positionListener.remove();
|
||||
}
|
||||
|
||||
render() {
|
||||
this._bindPosition();
|
||||
this._pointerEvents = this._computePointerEvents();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...this.props}
|
||||
pointerEvents={this._pointerEvents}
|
||||
onComponentRef={this._onComponentRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onComponentRef = component => {
|
||||
this._component = component;
|
||||
if (component) {
|
||||
invariant(
|
||||
typeof component.setNativeProps === 'function',
|
||||
'component must implement method `setNativeProps`'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_bindPosition() {
|
||||
this._positionListener && this._positionListener.remove();
|
||||
this._positionListener = new AnimatedValueSubscription(
|
||||
this.props.realPosition,
|
||||
this._onPositionChange
|
||||
);
|
||||
}
|
||||
|
||||
_onPositionChange = (/* { value } */) => {
|
||||
// This should log each frame when releasing the gesture or when pressing
|
||||
// the back button! If not, something has gone wrong with the animated
|
||||
// value subscription
|
||||
// console.log(value);
|
||||
|
||||
if (this._component) {
|
||||
const pointerEvents = this._computePointerEvents();
|
||||
if (this._pointerEvents !== pointerEvents) {
|
||||
this._pointerEvents = pointerEvents;
|
||||
this._component.setNativeProps({ pointerEvents });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_computePointerEvents() {
|
||||
const { navigation, realPosition, scene } = this.props;
|
||||
|
||||
if (scene.isStale || navigation.state.index !== scene.index) {
|
||||
// The scene isn't focused.
|
||||
return scene.index > navigation.state.index ? 'box-only' : 'none';
|
||||
}
|
||||
|
||||
const offset = realPosition.__getAnimatedValue() - navigation.state.index;
|
||||
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
|
||||
// The positon is still away from scene's index.
|
||||
// Scene's children should not receive touches until the position
|
||||
// is close enough to scene's index.
|
||||
return 'box-only';
|
||||
}
|
||||
|
||||
return 'auto';
|
||||
}
|
||||
}
|
||||
return Container;
|
||||
}
|
||||
|
||||
class AnimatedValueSubscription {
|
||||
constructor(value, callback) {
|
||||
this._value = value;
|
||||
this._token = value.addListener(callback);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this._value.removeListener(this._token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import * as React from 'react';
|
||||
import { Animated, View } from 'react-native';
|
||||
import { NavigationProp, Scene } from '../../types';
|
||||
|
||||
const MIN_POSITION_OFFSET = 0.01;
|
||||
|
||||
export type PointerEvents = 'box-only' | 'none' | 'auto';
|
||||
|
||||
export type InputProps = {
|
||||
scene: Scene;
|
||||
navigation: NavigationProp;
|
||||
realPosition: Animated.Value;
|
||||
};
|
||||
|
||||
export type InjectedProps = {
|
||||
pointerEvents: PointerEvents;
|
||||
onComponentRef: (ref: View | null) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a higher-order component that automatically computes the
|
||||
* `pointerEvents` property for a component whenever navigation position
|
||||
* changes.
|
||||
*/
|
||||
export default function createPointerEventsContainer<
|
||||
Props extends InjectedProps & InputProps
|
||||
>(
|
||||
Component: React.ComponentType<Props>
|
||||
): React.ComponentType<Pick<Props, Exclude<keyof Props, keyof InjectedProps>>> {
|
||||
class Container extends React.Component<Props> {
|
||||
private pointerEvents = this.computePointerEvents();
|
||||
private component: View | null = null;
|
||||
private positionListener: AnimatedValueSubscription | undefined;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.positionListener && this.positionListener.remove();
|
||||
}
|
||||
|
||||
private handleComponentRef = (component: View | null) => {
|
||||
this.component = component;
|
||||
|
||||
if (component && typeof component.setNativeProps !== 'function') {
|
||||
throw new Error('Component must implement method `setNativeProps`');
|
||||
}
|
||||
};
|
||||
|
||||
private bindPosition() {
|
||||
this.positionListener && this.positionListener.remove();
|
||||
this.positionListener = new AnimatedValueSubscription(
|
||||
this.props.realPosition,
|
||||
this.handlePositionChange
|
||||
);
|
||||
}
|
||||
|
||||
private handlePositionChange = (/* { value } */) => {
|
||||
// This should log each frame when releasing the gesture or when pressing
|
||||
// the back button! If not, something has gone wrong with the animated
|
||||
// value subscription
|
||||
// console.log(value);
|
||||
|
||||
if (this.component) {
|
||||
const pointerEvents = this.computePointerEvents();
|
||||
if (this.pointerEvents !== pointerEvents) {
|
||||
this.pointerEvents = pointerEvents;
|
||||
this.component.setNativeProps({ pointerEvents });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private computePointerEvents() {
|
||||
const { navigation, realPosition, scene } = this.props;
|
||||
|
||||
if (scene.isStale || navigation.state.index !== scene.index) {
|
||||
// The scene isn't focused.
|
||||
return scene.index > navigation.state.index ? 'box-only' : 'none';
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const offset = realPosition.__getAnimatedValue() - navigation.state.index;
|
||||
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
|
||||
// The positon is still away from scene's index.
|
||||
// Scene's children should not receive touches until the position
|
||||
// is close enough to scene's index.
|
||||
return 'box-only';
|
||||
}
|
||||
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
render() {
|
||||
this.bindPosition();
|
||||
this.pointerEvents = this.computePointerEvents();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...this.props}
|
||||
pointerEvents={this.pointerEvents}
|
||||
onComponentRef={this.handleComponentRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Container as any;
|
||||
}
|
||||
|
||||
class AnimatedValueSubscription {
|
||||
private value: Animated.Value;
|
||||
private token: string;
|
||||
|
||||
constructor(value: Animated.Value, callback: Animated.ValueListenerCallback) {
|
||||
this.value = value;
|
||||
this.token = value.addListener(callback);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.value.removeListener(this.token);
|
||||
}
|
||||
}
|
||||
@@ -7,19 +7,28 @@
|
||||
* On iOS you can pass the props of TouchableOpacity, on Android pass the props
|
||||
* of TouchableNativeFeedback.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Platform,
|
||||
TouchableNativeFeedback,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
|
||||
import BorderlessButton from './BorderlessButton';
|
||||
|
||||
type Props = ViewProps & {
|
||||
pressColor: string;
|
||||
disabled?: boolean;
|
||||
borderless?: boolean;
|
||||
delayPressIn?: number;
|
||||
onPress?: () => void;
|
||||
};
|
||||
|
||||
const ANDROID_VERSION_LOLLIPOP = 21;
|
||||
|
||||
export default class TouchableItem extends React.Component {
|
||||
export default class TouchableItem extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
borderless: false,
|
||||
pressColor: 'rgba(0, 0, 0, .32)',
|
||||
@@ -1,8 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Animated, Easing, StyleSheet, View } from 'react-native';
|
||||
import invariant from '../utils/invariant';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Easing,
|
||||
StyleSheet,
|
||||
View,
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
|
||||
import NavigationScenesReducer from './ScenesReducer';
|
||||
import {
|
||||
NavigationProp,
|
||||
Scene,
|
||||
SceneDescriptor,
|
||||
TransitionerLayout,
|
||||
TransitionProps,
|
||||
} from '../types';
|
||||
|
||||
type TransitionSpec = {};
|
||||
|
||||
type Props = {
|
||||
render: (
|
||||
current: TransitionProps,
|
||||
previous?: TransitionProps
|
||||
) => React.ReactNode;
|
||||
configureTransition?: (
|
||||
current: TransitionProps,
|
||||
previous?: TransitionProps
|
||||
) => TransitionSpec;
|
||||
onTransitionStart?: (
|
||||
current: TransitionProps,
|
||||
previous?: TransitionProps
|
||||
) => void | Promise<any>;
|
||||
onTransitionEnd?: (
|
||||
current: TransitionProps,
|
||||
previous?: TransitionProps
|
||||
) => void | Promise<any>;
|
||||
navigation: NavigationProp;
|
||||
descriptors: { [key: string]: SceneDescriptor };
|
||||
screenProps: unknown;
|
||||
};
|
||||
|
||||
type State = {
|
||||
layout: TransitionerLayout;
|
||||
position: Animated.Value;
|
||||
scenes: Scene[];
|
||||
nextScenes?: Scene[];
|
||||
};
|
||||
|
||||
// Used for all animations unless overriden
|
||||
const DefaultTransitionSpec = {
|
||||
@@ -11,13 +54,22 @@ const DefaultTransitionSpec = {
|
||||
timing: Animated.timing,
|
||||
};
|
||||
|
||||
class Transitioner extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
class Transitioner extends React.Component<Props, State> {
|
||||
private positionListener: string;
|
||||
|
||||
private prevTransitionProps: TransitionProps | undefined;
|
||||
private transitionProps: TransitionProps;
|
||||
|
||||
private isComponentMounted: boolean;
|
||||
private isTransitionRunning: boolean;
|
||||
private queuedTransition: { prevProps: Props } | null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// The initial layout isn't measured. Measured layout will be only available
|
||||
// when the component is mounted.
|
||||
const layout = {
|
||||
const layout: TransitionerLayout = {
|
||||
height: new Animated.Value(0),
|
||||
initHeight: 0,
|
||||
initWidth: 0,
|
||||
@@ -26,7 +78,7 @@ class Transitioner extends React.Component {
|
||||
};
|
||||
|
||||
const position = new Animated.Value(this.props.navigation.state.index);
|
||||
this._positionListener = position.addListener((/* { value } */) => {
|
||||
this.positionListener = position.addListener((/* { value } */) => {
|
||||
// This should work until we detach position from a view! so we have to be
|
||||
// careful to not ever detach it, thus the gymnastics in _getPosition in
|
||||
// StackViewLayout
|
||||
@@ -47,36 +99,36 @@ class Transitioner extends React.Component {
|
||||
),
|
||||
};
|
||||
|
||||
this._prevTransitionProps = null;
|
||||
this._transitionProps = buildTransitionProps(props, this.state);
|
||||
this.prevTransitionProps = undefined;
|
||||
this.transitionProps = buildTransitionProps(props, this.state);
|
||||
|
||||
this._isMounted = false;
|
||||
this._isTransitionRunning = false;
|
||||
this._queuedTransition = null;
|
||||
this.isComponentMounted = false;
|
||||
this.isTransitionRunning = false;
|
||||
this.queuedTransition = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.isComponentMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this._positionListener &&
|
||||
this.state.position.removeListener(this._positionListener);
|
||||
this.isComponentMounted = false;
|
||||
this.positionListener &&
|
||||
this.state.position.removeListener(this.positionListener);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this._isTransitionRunning) {
|
||||
if (!this._queuedTransition) {
|
||||
this._queuedTransition = { prevProps: this.props };
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.isTransitionRunning) {
|
||||
if (!this.queuedTransition) {
|
||||
this.queuedTransition = { prevProps: this.props };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._startTransition(this.props, nextProps);
|
||||
this.startTransition(this.props, nextProps);
|
||||
}
|
||||
|
||||
_computeScenes = (props, nextProps) => {
|
||||
private computeScenes = (props: Props, nextProps: Props) => {
|
||||
let nextScenes = NavigationScenesReducer(
|
||||
this.state.scenes,
|
||||
nextProps.navigation.state,
|
||||
@@ -101,15 +153,15 @@ class Transitioner extends React.Component {
|
||||
return nextScenes;
|
||||
};
|
||||
|
||||
_startTransition(props, nextProps) {
|
||||
private startTransition(props: Props, nextProps: Props) {
|
||||
const indexHasChanged =
|
||||
props.navigation.state.index !== nextProps.navigation.state.index;
|
||||
let nextScenes = this._computeScenes(props, nextProps);
|
||||
let nextScenes = this.computeScenes(props, nextProps);
|
||||
|
||||
if (!nextScenes) {
|
||||
// prevTransitionProps are the same as transitionProps in this case
|
||||
// because nothing changed
|
||||
this._prevTransitionProps = this._transitionProps;
|
||||
this.prevTransitionProps = this.transitionProps;
|
||||
|
||||
// Unsure if this is actually a good idea... Also related to
|
||||
// https://github.com/react-navigation/react-navigation/issues/5247
|
||||
@@ -118,7 +170,7 @@ class Transitioner extends React.Component {
|
||||
// onTransitionEnd
|
||||
this.state.position.setValue(props.navigation.state.index);
|
||||
|
||||
this._onTransitionEnd();
|
||||
this.handleTransitionEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -134,9 +186,9 @@ class Transitioner extends React.Component {
|
||||
const toValue = nextProps.navigation.state.index;
|
||||
|
||||
// compute transitionProps
|
||||
this._prevTransitionProps = this._transitionProps;
|
||||
this._transitionProps = buildTransitionProps(nextProps, nextState);
|
||||
let { isTransitioning } = this._transitionProps.navigation.state;
|
||||
this.prevTransitionProps = this.transitionProps;
|
||||
this.transitionProps = buildTransitionProps(nextProps, nextState);
|
||||
let { isTransitioning } = this.transitionProps.navigation.state;
|
||||
|
||||
// if the state isn't transitioning that is meant to signal that we should
|
||||
// transition immediately to the new index. if the index hasn't changed, do
|
||||
@@ -146,8 +198,8 @@ class Transitioner extends React.Component {
|
||||
this.setState(nextState, async () => {
|
||||
if (nextProps.onTransitionStart) {
|
||||
const result = nextProps.onTransitionStart(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
this.transitionProps,
|
||||
this.prevTransitionProps
|
||||
);
|
||||
if (result instanceof Promise) {
|
||||
// why do we bother awaiting the result here?
|
||||
@@ -157,15 +209,15 @@ class Transitioner extends React.Component {
|
||||
// jump immediately to the new value
|
||||
indexHasChanged && position.setValue(toValue);
|
||||
// end the transition
|
||||
this._onTransitionEnd();
|
||||
this.handleTransitionEnd();
|
||||
});
|
||||
} else if (isTransitioning) {
|
||||
this._isTransitionRunning = true;
|
||||
this.isTransitionRunning = true;
|
||||
this.setState(nextState, async () => {
|
||||
if (nextProps.onTransitionStart) {
|
||||
const result = nextProps.onTransitionStart(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
this.transitionProps,
|
||||
this.prevTransitionProps
|
||||
);
|
||||
|
||||
// Wait for the onTransitionStart to resolve if needed.
|
||||
@@ -177,8 +229,8 @@ class Transitioner extends React.Component {
|
||||
// get the transition spec.
|
||||
const transitionUserSpec = nextProps.configureTransition
|
||||
? nextProps.configureTransition(
|
||||
this._transitionProps,
|
||||
this._prevTransitionProps
|
||||
this.transitionProps,
|
||||
this.prevTransitionProps
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -191,6 +243,7 @@ class Transitioner extends React.Component {
|
||||
delete transitionSpec.timing;
|
||||
|
||||
// if swiped back, indexHasChanged == true && positionHasChanged == false
|
||||
// @ts-ignore
|
||||
const positionHasChanged = position.__getValue() !== toValue;
|
||||
if (indexHasChanged && positionHasChanged) {
|
||||
timing(position, {
|
||||
@@ -200,10 +253,10 @@ class Transitioner extends React.Component {
|
||||
// In case the animation is immediately interrupted for some reason,
|
||||
// we move this to the next frame so that onTransitionStart can fire
|
||||
// first (https://github.com/react-navigation/react-navigation/issues/5247)
|
||||
requestAnimationFrame(this._onTransitionEnd);
|
||||
requestAnimationFrame(this.handleTransitionEnd);
|
||||
});
|
||||
} else {
|
||||
this._onTransitionEnd();
|
||||
this.handleTransitionEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,13 +264,13 @@ class Transitioner extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View onLayout={this._onLayout} style={styles.main}>
|
||||
{this.props.render(this._transitionProps, this._prevTransitionProps)}
|
||||
<View onLayout={this.handleLayout} style={styles.main}>
|
||||
{this.props.render(this.transitionProps, this.prevTransitionProps)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_onLayout = event => {
|
||||
private handleLayout = (event: LayoutChangeEvent) => {
|
||||
const { height, width } = event.nativeEvent.layout;
|
||||
if (
|
||||
this.state.layout.initWidth === width &&
|
||||
@@ -225,7 +278,7 @@ class Transitioner extends React.Component {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const layout = {
|
||||
const layout: TransitionerLayout = {
|
||||
...this.state.layout,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
@@ -240,16 +293,16 @@ class Transitioner extends React.Component {
|
||||
layout,
|
||||
};
|
||||
|
||||
this._transitionProps = buildTransitionProps(this.props, nextState);
|
||||
this.transitionProps = buildTransitionProps(this.props, nextState);
|
||||
this.setState(nextState);
|
||||
};
|
||||
|
||||
_onTransitionEnd = () => {
|
||||
if (!this._isMounted) {
|
||||
private handleTransitionEnd = () => {
|
||||
if (!this.isComponentMounted) {
|
||||
return;
|
||||
}
|
||||
const prevTransitionProps = this._prevTransitionProps;
|
||||
this._prevTransitionProps = null;
|
||||
const prevTransitionProps = this.prevTransitionProps;
|
||||
this.prevTransitionProps = undefined;
|
||||
|
||||
const scenes = filterStale(this.state.scenes);
|
||||
|
||||
@@ -258,12 +311,12 @@ class Transitioner extends React.Component {
|
||||
scenes,
|
||||
};
|
||||
|
||||
this._transitionProps = buildTransitionProps(this.props, nextState);
|
||||
this.transitionProps = buildTransitionProps(this.props, nextState);
|
||||
|
||||
this.setState(nextState, async () => {
|
||||
if (this.props.onTransitionEnd) {
|
||||
const result = this.props.onTransitionEnd(
|
||||
this._transitionProps,
|
||||
this.transitionProps,
|
||||
prevTransitionProps
|
||||
);
|
||||
|
||||
@@ -272,25 +325,27 @@ class Transitioner extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._queuedTransition) {
|
||||
let { prevProps } = this._queuedTransition;
|
||||
this._queuedTransition = null;
|
||||
this._startTransition(prevProps, this.props);
|
||||
if (this.queuedTransition) {
|
||||
let { prevProps } = this.queuedTransition;
|
||||
this.queuedTransition = null;
|
||||
this.startTransition(prevProps, this.props);
|
||||
} else {
|
||||
this._isTransitionRunning = false;
|
||||
this.isTransitionRunning = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function buildTransitionProps(props, state) {
|
||||
const { navigation, options } = props;
|
||||
function buildTransitionProps(props: Props, state: State): TransitionProps {
|
||||
const { navigation } = props;
|
||||
|
||||
const { layout, position, scenes } = state;
|
||||
|
||||
const scene = scenes.find(isSceneActive);
|
||||
|
||||
invariant(scene, 'Could not find active scene');
|
||||
if (!scene) {
|
||||
throw new Error('Could not find active scene');
|
||||
}
|
||||
|
||||
return {
|
||||
layout,
|
||||
@@ -298,16 +353,15 @@ function buildTransitionProps(props, state) {
|
||||
position,
|
||||
scenes,
|
||||
scene,
|
||||
options,
|
||||
index: scene.index,
|
||||
};
|
||||
}
|
||||
|
||||
function isSceneNotStale(scene) {
|
||||
function isSceneNotStale(scene: Scene) {
|
||||
return !scene.isStale;
|
||||
}
|
||||
|
||||
function filterStale(scenes) {
|
||||
function filterStale(scenes: Scene[]) {
|
||||
const filtered = scenes.filter(isSceneNotStale);
|
||||
if (filtered.length === scenes.length) {
|
||||
return scenes;
|
||||
@@ -315,7 +369,7 @@ function filterStale(scenes) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function isSceneActive(scene) {
|
||||
function isSceneActive(scene: Scene) {
|
||||
return scene.isActive;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint react/display-name:0 */
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Transitioner from '../Transitioner';
|
||||
|
||||
22
packages/stack/tsconfig.json
Normal file
22
packages/stack/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "react",
|
||||
"lib": ["esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitUseStrict": false,
|
||||
"noStrictGenericChecks": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "esnext"
|
||||
}
|
||||
}
|
||||
31
packages/stack/types/@react-navigation/core.d.ts
vendored
Normal file
31
packages/stack/types/@react-navigation/core.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
declare module '@react-navigation/core' {
|
||||
import * as React from 'react';
|
||||
|
||||
export const StackActions: {
|
||||
completeTransition<T extends { key?: string } | undefined>(
|
||||
options?: T
|
||||
): { type: string } & T;
|
||||
};
|
||||
|
||||
export const NavigationActions: {
|
||||
back(action: { key: string; immediate?: boolean });
|
||||
};
|
||||
|
||||
export const NavigationProvider: React.ComponentType<{
|
||||
value: object;
|
||||
}>;
|
||||
|
||||
export const SceneView: React.ComponentType<{
|
||||
screenProps: unknown;
|
||||
navigation: object;
|
||||
component: React.ComponentType<any>;
|
||||
}>;
|
||||
|
||||
export function createNavigator(
|
||||
StackView: React.ComponentType<any>,
|
||||
router: any,
|
||||
stackConfig: object
|
||||
): React.ComponentType;
|
||||
|
||||
export function StackRouter(routeConfigMap: object, stackConfig: object);
|
||||
}
|
||||
28
packages/stack/types/@react-navigation/native.d.ts
vendored
Normal file
28
packages/stack/types/@react-navigation/native.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
declare module '@react-navigation/native' {
|
||||
import { ComponentType } from 'react';
|
||||
import { StyleProp, ViewStyle, ViewProps } from 'react-native';
|
||||
|
||||
export function withOrientation<Props extends { isLandscape: boolean }>(
|
||||
Comp: React.ComponentType<Props>
|
||||
): React.ComponentType<Pick<Props, Exclude<keyof Props, 'isLandscape'>>>;
|
||||
|
||||
export function createKeyboardAwareNavigator<Props>(
|
||||
Comp: React.ComponentType<Props>,
|
||||
stackConfig: object
|
||||
): React.ComponentType<Props>;
|
||||
|
||||
export type SafeAreaViewForceInsetValue = 'always' | 'never';
|
||||
|
||||
export const SafeAreaView: ComponentType<
|
||||
ViewProps & {
|
||||
forceInset?: {
|
||||
top?: SafeAreaViewForceInsetValue;
|
||||
bottom?: SafeAreaViewForceInsetValue;
|
||||
left?: SafeAreaViewForceInsetValue;
|
||||
right?: SafeAreaViewForceInsetValue;
|
||||
horizontal?: SafeAreaViewForceInsetValue;
|
||||
vertical?: SafeAreaViewForceInsetValue;
|
||||
};
|
||||
}
|
||||
>;
|
||||
}
|
||||
7
packages/stack/types/index.d.ts
vendored
Normal file
7
packages/stack/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module '*.png' {
|
||||
import { ImageSourcePropType } from 'react-native';
|
||||
|
||||
declare const value: ImageSourcePropType;
|
||||
|
||||
export default value;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user