refactor: migrate codebase to typescript

This commit is contained in:
satyajit.happy
2019-04-26 10:20:51 +02:00
parent 22bfd4f314
commit 3b907d39ee
54 changed files with 8258 additions and 1712 deletions

View File

@@ -7,7 +7,6 @@
# VSCode
.vscode/
tsconfig.json
jsconfig.json
# Xcode

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import { Asset } from 'expo';
import { FlatList, I18nManager } from 'react-native';
import {

View File

@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import { View } from 'react-native';
import { createStackNavigator } from 'react-navigation-stack';

View File

@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import {
ActivityIndicator,
Button,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import {
ActivityIndicator,
Button,

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,6 +0,0 @@
{
"extends": "../../../.eslintrc",
"env": {
"jest": true
},
}

View File

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

View File

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

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

View File

@@ -1,4 +1,5 @@
import { NativeModules } from 'react-native';
const { PlatformConstants } = NativeModules;
export const supportsImprovedSpringAnimation = () => {

View File

@@ -1,3 +0,0 @@
import React from 'react';
export default React.createContext(null);

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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