mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: implement various navigators
This commit is contained in:
@@ -37,7 +37,6 @@
|
||||
"eslint-config-satya164": "^2.4.1",
|
||||
"husky": "^2.4.0",
|
||||
"jest": "^24.8.0",
|
||||
"lerna": "^3.16.4",
|
||||
"prettier": "^1.18.2",
|
||||
"typescript": "^3.5.1"
|
||||
},
|
||||
|
||||
21
packages/bottom-tabs/package.json
Normal file
21
packages/bottom-tabs/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@navigation-ex/bottom-tabs",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@navigation-ex/core": "^0.0.1",
|
||||
"@navigation-ex/routers": "^0.0.1",
|
||||
"react-native-safe-area-view": "^0.14.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.8.24",
|
||||
"@types/react-native": "^0.60.2",
|
||||
"react": "16.8.3",
|
||||
"react-native": "^0.59.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,13 @@
|
||||
export {
|
||||
default as createBottomTabNavigator,
|
||||
} from './navigators/createBottomTabNavigator';
|
||||
export {
|
||||
default as createMaterialTopTabNavigator,
|
||||
} from './navigators/createMaterialTopTabNavigator';
|
||||
|
||||
/**
|
||||
* Views
|
||||
*/
|
||||
export { default as BottomTabBar } from './views/BottomTabBar';
|
||||
export { default as MaterialTopTabBar } from './views/MaterialTopTabBar';
|
||||
|
||||
/**
|
||||
* Utils
|
||||
* Types
|
||||
*/
|
||||
export { default as createTabNavigator } from './utils/createTabNavigator';
|
||||
export { BottomTabNavigationOptions, BottomTabNavigationProp } from './types';
|
||||
|
||||
@@ -1,181 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
AccessibilityRole,
|
||||
AccessibilityState,
|
||||
} from 'react-native';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
|
||||
import createTabNavigator, {
|
||||
NavigationViewProps,
|
||||
} from '../utils/createTabNavigator';
|
||||
import BottomTabBar from '../views/BottomTabBar';
|
||||
import ResourceSavingScene from '../views/ResourceSavingScene';
|
||||
useNavigationBuilder,
|
||||
createNavigator,
|
||||
DefaultNavigatorOptions,
|
||||
} from '@navigation-ex/core';
|
||||
import {
|
||||
NavigationProp,
|
||||
Route,
|
||||
SceneDescriptor,
|
||||
NavigationBottomTabOptions,
|
||||
BottomTabBarOptions,
|
||||
TabRouter,
|
||||
TabRouterOptions,
|
||||
TabNavigationState,
|
||||
} from '@navigation-ex/routers';
|
||||
import BottomTabView from '../views/BottomTabView';
|
||||
import {
|
||||
BottomTabNavigationConfig,
|
||||
BottomTabNavigationOptions,
|
||||
} from '../types';
|
||||
|
||||
type Props = NavigationViewProps & {
|
||||
getAccessibilityRole: (props: {
|
||||
route: Route;
|
||||
}) => AccessibilityRole | undefined;
|
||||
getAccessibilityStates: (props: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
}) => AccessibilityState[];
|
||||
lazy?: boolean;
|
||||
tabBarComponent?: React.ComponentType<any>;
|
||||
tabBarOptions?: BottomTabBarOptions;
|
||||
navigation: NavigationProp;
|
||||
descriptors: { [key: string]: SceneDescriptor<NavigationBottomTabOptions> };
|
||||
screenProps?: unknown;
|
||||
};
|
||||
type Props = DefaultNavigatorOptions<BottomTabNavigationOptions> &
|
||||
TabRouterOptions &
|
||||
BottomTabNavigationConfig;
|
||||
|
||||
type State = {
|
||||
loaded: number[];
|
||||
};
|
||||
function BottomTabNavigator({
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions,
|
||||
TabRouterOptions
|
||||
>(TabRouter, {
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
class TabNavigationView extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
lazy: true,
|
||||
getAccessibilityRole: (): AccessibilityRole => 'button',
|
||||
getAccessibilityStates: ({
|
||||
focused,
|
||||
}: {
|
||||
focused: boolean;
|
||||
}): AccessibilityState[] => (focused ? ['selected'] : []),
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const { index } = nextProps.navigation.state;
|
||||
|
||||
return {
|
||||
// Set the current tab to be loaded if it was not loaded before
|
||||
loaded: prevState.loaded.includes(index)
|
||||
? prevState.loaded
|
||||
: [...prevState.loaded, index],
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
loaded: [this.props.navigation.state.index],
|
||||
};
|
||||
|
||||
_getButtonComponent = ({ route }: { route: Route }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarButtonComponent) {
|
||||
return options.tabBarButtonComponent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
_renderTabBar = () => {
|
||||
const {
|
||||
tabBarComponent: TabBarComponent = BottomTabBar,
|
||||
tabBarOptions,
|
||||
navigation,
|
||||
screenProps,
|
||||
getLabelText,
|
||||
getAccessibilityLabel,
|
||||
getAccessibilityRole,
|
||||
getAccessibilityStates,
|
||||
getTestID,
|
||||
renderIcon,
|
||||
onTabPress,
|
||||
onTabLongPress,
|
||||
} = this.props;
|
||||
|
||||
const { descriptors } = this.props;
|
||||
const { state } = this.props.navigation;
|
||||
const route = state.routes[state.index];
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarVisible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabBarComponent
|
||||
{...tabBarOptions}
|
||||
jumpTo={this._jumpTo}
|
||||
navigation={navigation}
|
||||
screenProps={screenProps}
|
||||
onTabPress={onTabPress}
|
||||
onTabLongPress={onTabLongPress}
|
||||
getLabelText={getLabelText}
|
||||
getButtonComponent={this._getButtonComponent}
|
||||
getAccessibilityLabel={getAccessibilityLabel}
|
||||
getAccessibilityRole={getAccessibilityRole}
|
||||
getAccessibilityStates={getAccessibilityStates}
|
||||
getTestID={getTestID}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_jumpTo = (key: string) => {
|
||||
const { navigation, onIndexChange } = this.props;
|
||||
|
||||
const index = navigation.state.routes.findIndex(route => route.key === key);
|
||||
|
||||
onIndexChange(index);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, renderScene, lazy } = this.props;
|
||||
const { routes } = navigation.state;
|
||||
const { loaded } = this.state;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
if (lazy && !loaded.includes(index)) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFocused = navigation.state.index === index;
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
{renderScene({ route })}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
{this._renderTabBar()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pages: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default createTabNavigator<NavigationBottomTabOptions, Props>(
|
||||
TabNavigationView
|
||||
);
|
||||
export default createNavigator<
|
||||
BottomTabNavigationOptions,
|
||||
typeof BottomTabNavigator
|
||||
>(BottomTabNavigator);
|
||||
|
||||
@@ -1,201 +1,174 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AccessibilityRole,
|
||||
AccessibilityState,
|
||||
AccessibilityStates,
|
||||
StyleProp,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
Descriptor,
|
||||
Route,
|
||||
} from '@navigation-ex/core';
|
||||
import { TabNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
export type Route = {
|
||||
key: string;
|
||||
routeName: string;
|
||||
} & (NavigationState | undefined);
|
||||
|
||||
export type NavigationEventName =
|
||||
| 'willFocus'
|
||||
| 'didFocus'
|
||||
| 'willBlur'
|
||||
| 'didBlur';
|
||||
|
||||
export type NavigationState = {
|
||||
key: string;
|
||||
index: number;
|
||||
routes: Route[];
|
||||
isTransitioning?: boolean;
|
||||
params?: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
export type NavigationProp<RouteName = string, Params = object> = {
|
||||
emit(eventName: string): void;
|
||||
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 BottomTabNavigationEventMap = {
|
||||
refocus: undefined;
|
||||
tabPress: undefined;
|
||||
tabLongPress: undefined;
|
||||
};
|
||||
|
||||
export type Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
export type LabelPosition = 'beside-icon' | 'below-icon';
|
||||
|
||||
export type BottomTabBarOptions = {
|
||||
keyboardHidesTabBar: boolean;
|
||||
activeTintColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
activeBackgroundColor?: string;
|
||||
inactiveBackgroundColor?: string;
|
||||
allowFontScaling: boolean;
|
||||
showLabel: boolean;
|
||||
showIcon: boolean;
|
||||
labelStyle: StyleProp<TextStyle>;
|
||||
tabStyle: StyleProp<ViewStyle>;
|
||||
labelPosition?:
|
||||
| LabelPosition
|
||||
| ((options: { deviceOrientation: Orientation }) => LabelPosition);
|
||||
adaptive?: boolean;
|
||||
style: StyleProp<ViewStyle>;
|
||||
export type BottomTabNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions,
|
||||
BottomTabNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Jump to an existing tab.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
};
|
||||
|
||||
export type BottomTabBarProps = BottomTabBarOptions & {
|
||||
navigation: NavigationProp;
|
||||
onTabPress: (props: { route: Route }) => void;
|
||||
onTabLongPress: (props: { route: Route }) => void;
|
||||
getAccessibilityLabel: (props: { route: Route }) => string | undefined;
|
||||
getAccessibilityRole: (props: {
|
||||
route: Route;
|
||||
}) => AccessibilityRole | undefined;
|
||||
getAccessibilityStates: (props: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
}) => AccessibilityState[];
|
||||
getButtonComponent: (props: {
|
||||
route: Route;
|
||||
}) => React.ComponentType<any> | undefined;
|
||||
getLabelText: (props: {
|
||||
route: Route;
|
||||
}) =>
|
||||
| ((scene: {
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
}) => string | undefined)
|
||||
| string
|
||||
| undefined;
|
||||
getTestID: (props: { route: Route }) => string | undefined;
|
||||
renderIcon: (props: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
horizontal?: boolean;
|
||||
}) => React.ReactNode;
|
||||
dimensions: { width: number; height: number };
|
||||
isLandscape: boolean;
|
||||
safeAreaInset: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
};
|
||||
|
||||
export type MaterialTabBarOptions = {
|
||||
activeTintColor?: string;
|
||||
allowFontScaling?: boolean;
|
||||
bounces?: boolean;
|
||||
inactiveTintColor?: string;
|
||||
pressColor?: string;
|
||||
pressOpacity?: number;
|
||||
scrollEnabled?: boolean;
|
||||
showIcon?: boolean;
|
||||
showLabel?: boolean;
|
||||
upperCaseLabel?: boolean;
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
indicatorStyle?: StyleProp<ViewStyle>;
|
||||
iconStyle?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type MaterialTabBarProps = MaterialTabBarOptions & {
|
||||
layout: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
position: Animated.Node<number>;
|
||||
jumpTo: (key: string) => void;
|
||||
getLabelText: (scene: {
|
||||
route: Route;
|
||||
}) =>
|
||||
| ((scene: { focused: boolean; tintColor: string }) => string | undefined)
|
||||
| string
|
||||
| undefined;
|
||||
getAccessible?: (scene: { route: Route }) => boolean | undefined;
|
||||
getAccessibilityLabel: (scene: { route: Route }) => string | undefined;
|
||||
getTestID: (scene: { route: Route }) => string | undefined;
|
||||
renderIcon: (scene: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal?: boolean;
|
||||
}) => React.ReactNode;
|
||||
renderBadge?: (scene: { route: Route }) => React.ReactNode;
|
||||
onTabPress?: (scene: { route: Route }) => void;
|
||||
onTabLongPress?: (scene: { route: Route }) => void;
|
||||
tabBarPosition?: 'top' | 'bottom';
|
||||
screenProps: unknown;
|
||||
navigation: NavigationProp;
|
||||
};
|
||||
|
||||
export type NavigationCommonTabOptions = {
|
||||
export type BottomTabNavigationOptions = {
|
||||
/**
|
||||
* Title text for the screen.
|
||||
*/
|
||||
title?: string;
|
||||
tabBarLabel?: React.ReactNode;
|
||||
tabBarVisible?: boolean;
|
||||
tabBarAccessibilityLabel?: string;
|
||||
tabBarTestID?: string;
|
||||
|
||||
/**
|
||||
* Title string of a tab displayed in the tab bar or React Element
|
||||
* or a function that given { focused: boolean, tintColor: string } returns a React.Node, to display in tab bar.
|
||||
* When undefined, scene title is used. To hide, see tabBarOptions.showLabel in the previous section.
|
||||
*/
|
||||
tabBarLabel?:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode);
|
||||
|
||||
/**
|
||||
* React Element or a function that given { focused: boolean, tintColor: string } returns a React.Node, to display in the tab bar.
|
||||
*/
|
||||
tabBarIcon?:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
horizontal?: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode);
|
||||
tabBarOnPress?: (props: {
|
||||
navigation: NavigationProp;
|
||||
defaultHandler: () => void;
|
||||
}) => void;
|
||||
tabBarOnLongPress?: (props: {
|
||||
navigation: NavigationProp;
|
||||
defaultHandler: () => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type NavigationBottomTabOptions = NavigationCommonTabOptions & {
|
||||
tabBarButtonComponent?: React.ComponentType<BottomTabBarProps>;
|
||||
};
|
||||
/**
|
||||
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
||||
* It's recommended to set this if you don't have a label for the tab.
|
||||
*/
|
||||
tabBarAccessibilityLabel?: string;
|
||||
|
||||
export type NavigationMaterialTabOptions = NavigationCommonTabOptions & {
|
||||
/**
|
||||
* ID to locate this tab button in tests.
|
||||
*/
|
||||
tabBarTestID?: string;
|
||||
|
||||
/**
|
||||
* Boolean indicating whether the tab bar is visible when this screen is active.
|
||||
*/
|
||||
tabBarVisible?: boolean;
|
||||
|
||||
/**
|
||||
* Buttton component to render for the tab items instead of the default `TouchableWithoutFeedback`
|
||||
*/
|
||||
tabBarButtonComponent?: React.ComponentType<any>;
|
||||
swipeEnabled?: boolean | ((state: NavigationState) => boolean);
|
||||
};
|
||||
|
||||
export type SceneDescriptor<Options extends NavigationCommonTabOptions> = {
|
||||
key: string;
|
||||
options: Options;
|
||||
navigation: NavigationProp;
|
||||
getComponent(): React.ComponentType;
|
||||
export type BottomTabDescriptor = Descriptor<
|
||||
ParamListBase,
|
||||
string,
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions
|
||||
>;
|
||||
|
||||
export type BottomTabDescriptorMap = {
|
||||
[key: string]: BottomTabDescriptor;
|
||||
};
|
||||
|
||||
export type Screen<
|
||||
Options extends NavigationCommonTabOptions
|
||||
> = React.ComponentType<any> & {
|
||||
navigationOptions?: Options & {
|
||||
[key: string]: any;
|
||||
};
|
||||
export type BottomTabNavigationConfig = {
|
||||
lazy?: boolean;
|
||||
tabBarComponent?: React.ComponentType<BottomTabBarProps>;
|
||||
tabBarOptions?: BottomTabBarOptions;
|
||||
};
|
||||
|
||||
export type BottomTabBarOptions = {
|
||||
keyboardHidesTabBar?: boolean;
|
||||
activeTintColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
activeBackgroundColor?: string;
|
||||
inactiveBackgroundColor?: string;
|
||||
allowFontScaling?: boolean;
|
||||
showLabel?: boolean;
|
||||
showIcon?: boolean;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
labelPosition?:
|
||||
| LabelPosition
|
||||
| ((options: { deviceOrientation: Orientation }) => LabelPosition);
|
||||
adaptive?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type BottomTabBarProps = BottomTabBarOptions & {
|
||||
state: TabNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
onTabPress: (props: { route: Route<string> }) => void;
|
||||
onTabLongPress: (props: { route: Route<string> }) => void;
|
||||
getAccessibilityLabel: (props: {
|
||||
route: Route<string>;
|
||||
}) => string | undefined;
|
||||
getAccessibilityRole: (props: {
|
||||
route: Route<string>;
|
||||
}) => AccessibilityRole | undefined;
|
||||
getAccessibilityStates: (props: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => AccessibilityStates[];
|
||||
getButtonComponent: (props: {
|
||||
route: Route<string>;
|
||||
}) => React.ComponentType<any> | undefined;
|
||||
getLabelText: (props: {
|
||||
route: Route<string>;
|
||||
}) =>
|
||||
| ((scene: {
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
}) => React.ReactNode | undefined)
|
||||
| React.ReactNode;
|
||||
getTestID: (props: { route: Route<string> }) => string | undefined;
|
||||
renderIcon: (props: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode;
|
||||
safeAreaInset?: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
};
|
||||
|
||||
@@ -1,64 +1,40 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
TouchableWithoutFeedback,
|
||||
StyleSheet,
|
||||
View,
|
||||
Keyboard,
|
||||
Platform,
|
||||
LayoutChangeEvent,
|
||||
ScaledSize,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
import { Route } from '@navigation-ex/core';
|
||||
|
||||
import CrossFadeIcon from './CrossFadeIcon';
|
||||
import withDimensions from '../utils/withDimensions';
|
||||
import { Route, BottomTabBarProps } from '../types';
|
||||
import TabBarIcon from './TabBarIcon';
|
||||
import TouchableWithoutFeedbackWrapper from './TouchableWithoutFeedbackWrapper';
|
||||
import { BottomTabBarProps } from '../types';
|
||||
|
||||
type State = {
|
||||
dimensions: { height: number; width: number };
|
||||
layout: { height: number; width: number };
|
||||
keyboard: boolean;
|
||||
visible: Animated.Value;
|
||||
};
|
||||
|
||||
type Props = BottomTabBarProps & {
|
||||
activeTintColor: string;
|
||||
inactiveTintColor: string;
|
||||
safeAreaInset: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
};
|
||||
|
||||
const majorVersion = parseInt(Platform.Version as string, 10);
|
||||
const isIos = Platform.OS === 'ios';
|
||||
const isIOS11 = majorVersion >= 11 && isIos;
|
||||
|
||||
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||
|
||||
class TouchableWithoutFeedbackWrapper extends React.Component<
|
||||
React.ComponentProps<typeof TouchableWithoutFeedback> & {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
> {
|
||||
render() {
|
||||
const {
|
||||
onPress,
|
||||
onLongPress,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityRole,
|
||||
accessibilityStates,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
testID={testID}
|
||||
hitSlop={{ left: 15, right: 15, top: 0, bottom: 5 }}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityStates={accessibilityStates}
|
||||
>
|
||||
<View {...props} />
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
export default class TabBarBottom extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
keyboardHidesTabBar: true,
|
||||
activeTintColor: '#007AFF',
|
||||
@@ -75,32 +51,41 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
};
|
||||
|
||||
state = {
|
||||
dimensions: Dimensions.get('window'),
|
||||
layout: { height: 0, width: 0 },
|
||||
keyboard: false,
|
||||
visible: new Animated.Value(1),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this.handleOrientationChange);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
Keyboard.addListener('keyboardWillShow', this._handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardWillHide', this._handleKeyboardHide);
|
||||
Keyboard.addListener('keyboardWillShow', this.handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardWillHide', this.handleKeyboardHide);
|
||||
} else {
|
||||
Keyboard.addListener('keyboardDidShow', this._handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardDidHide', this._handleKeyboardHide);
|
||||
Keyboard.addListener('keyboardDidShow', this.handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardDidHide', this.handleKeyboardHide);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this.handleOrientationChange);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
Keyboard.removeListener('keyboardWillShow', this._handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardWillHide', this._handleKeyboardHide);
|
||||
Keyboard.removeListener('keyboardWillShow', this.handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardWillHide', this.handleKeyboardHide);
|
||||
} else {
|
||||
Keyboard.removeListener('keyboardDidShow', this._handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardDidHide', this._handleKeyboardHide);
|
||||
Keyboard.removeListener('keyboardDidShow', this.handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardDidHide', this.handleKeyboardHide);
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyboardShow = () =>
|
||||
private handleOrientationChange = ({ window }: { window: ScaledSize }) => {
|
||||
this.setState({ dimensions: window });
|
||||
};
|
||||
|
||||
private handleKeyboardShow = () =>
|
||||
this.setState({ keyboard: true }, () =>
|
||||
Animated.timing(this.state.visible, {
|
||||
toValue: 0,
|
||||
@@ -109,7 +94,7 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
}).start()
|
||||
);
|
||||
|
||||
_handleKeyboardHide = () =>
|
||||
private handleKeyboardHide = () =>
|
||||
Animated.timing(this.state.visible, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
@@ -118,7 +103,7 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
this.setState({ keyboard: false });
|
||||
});
|
||||
|
||||
_handleLayout = (e: LayoutChangeEvent) => {
|
||||
private handleLayout = (e: LayoutChangeEvent) => {
|
||||
const { layout } = this.state;
|
||||
const { height, width } = e.nativeEvent.layout;
|
||||
|
||||
@@ -134,7 +119,13 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
});
|
||||
};
|
||||
|
||||
_renderLabel = ({ route, focused }: { route: Route; focused: boolean }) => {
|
||||
private renderLabel = ({
|
||||
route,
|
||||
focused,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => {
|
||||
const {
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
@@ -149,7 +140,7 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
}
|
||||
|
||||
const label = this.props.getLabelText({ route });
|
||||
const horizontal = this._shouldUseHorizontalLabels();
|
||||
const horizontal = this.shouldUseHorizontalLabels();
|
||||
const tintColor = focused ? activeTintColor : inactiveTintColor;
|
||||
|
||||
if (typeof label === 'string') {
|
||||
@@ -180,7 +171,13 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
return label;
|
||||
};
|
||||
|
||||
_renderIcon = ({ route, focused }: { route: Route; focused: boolean }) => {
|
||||
private renderIcon = ({
|
||||
route,
|
||||
focused,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => {
|
||||
const {
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
@@ -188,17 +185,18 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
showIcon,
|
||||
showLabel,
|
||||
} = this.props;
|
||||
|
||||
if (showIcon === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const horizontal = this._shouldUseHorizontalLabels();
|
||||
const horizontal = this.shouldUseHorizontalLabels();
|
||||
|
||||
const activeOpacity = focused ? 1 : 0;
|
||||
const inactiveOpacity = focused ? 0 : 1;
|
||||
|
||||
return (
|
||||
<CrossFadeIcon
|
||||
<TabBarIcon
|
||||
route={route}
|
||||
horizontal={horizontal}
|
||||
activeOpacity={activeOpacity}
|
||||
@@ -215,15 +213,11 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
);
|
||||
};
|
||||
|
||||
_shouldUseHorizontalLabels = () => {
|
||||
const { routes } = this.props.navigation.state;
|
||||
const {
|
||||
isLandscape,
|
||||
dimensions,
|
||||
adaptive,
|
||||
tabStyle,
|
||||
labelPosition,
|
||||
} = this.props;
|
||||
private shouldUseHorizontalLabels = () => {
|
||||
const { state, adaptive, tabStyle, labelPosition } = this.props;
|
||||
const { dimensions } = this.state;
|
||||
const { routes } = state;
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
if (labelPosition) {
|
||||
let position;
|
||||
@@ -266,23 +260,27 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
navigation,
|
||||
state,
|
||||
keyboardHidesTabBar,
|
||||
activeBackgroundColor,
|
||||
inactiveBackgroundColor,
|
||||
onTabPress,
|
||||
onTabLongPress,
|
||||
getAccessibilityLabel,
|
||||
getAccessibilityRole,
|
||||
getAccessibilityStates,
|
||||
getButtonComponent,
|
||||
getTestID,
|
||||
safeAreaInset,
|
||||
style,
|
||||
tabStyle,
|
||||
} = this.props;
|
||||
|
||||
const { routes } = navigation.state;
|
||||
const { routes } = state;
|
||||
|
||||
const tabBarStyle = [
|
||||
styles.tabBar,
|
||||
// @ts-ignore
|
||||
this._shouldUseHorizontalLabels() && !Platform.isPad
|
||||
this.shouldUseHorizontalLabels() && !Platform.isPad
|
||||
? styles.tabBarCompact
|
||||
: styles.tabBarRegular,
|
||||
style,
|
||||
@@ -313,33 +311,30 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
pointerEvents={
|
||||
keyboardHidesTabBar && this.state.keyboard ? 'none' : 'auto'
|
||||
}
|
||||
onLayout={this._handleLayout}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
<SafeAreaView style={tabBarStyle} forceInset={safeAreaInset}>
|
||||
{routes.map((route, index) => {
|
||||
const focused = index === navigation.state.index;
|
||||
const focused = index === state.index;
|
||||
const scene = { route, focused };
|
||||
const accessibilityLabel = this.props.getAccessibilityLabel({
|
||||
const accessibilityLabel = getAccessibilityLabel({
|
||||
route,
|
||||
});
|
||||
|
||||
const accessibilityRole = this.props.getAccessibilityRole({
|
||||
const accessibilityRole = getAccessibilityRole({
|
||||
route,
|
||||
});
|
||||
|
||||
const accessibilityStates = this.props.getAccessibilityStates(
|
||||
scene
|
||||
);
|
||||
const accessibilityStates = getAccessibilityStates(scene);
|
||||
|
||||
const testID = this.props.getTestID({ route });
|
||||
const testID = getTestID({ route });
|
||||
|
||||
const backgroundColor = focused
|
||||
? activeBackgroundColor
|
||||
: inactiveBackgroundColor;
|
||||
|
||||
const ButtonComponent =
|
||||
this.props.getButtonComponent({ route }) ||
|
||||
TouchableWithoutFeedbackWrapper;
|
||||
getButtonComponent({ route }) || TouchableWithoutFeedbackWrapper;
|
||||
|
||||
return (
|
||||
<ButtonComponent
|
||||
@@ -353,14 +348,14 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
style={[
|
||||
styles.tab,
|
||||
{ backgroundColor },
|
||||
this._shouldUseHorizontalLabels()
|
||||
this.shouldUseHorizontalLabels()
|
||||
? styles.tabLandscape
|
||||
: styles.tabPortrait,
|
||||
tabStyle,
|
||||
]}
|
||||
>
|
||||
{this._renderIcon(scene)}
|
||||
{this._renderLabel(scene)}
|
||||
{this.renderIcon(scene)}
|
||||
{this.renderLabel(scene)}
|
||||
</ButtonComponent>
|
||||
);
|
||||
})}
|
||||
@@ -427,5 +422,3 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default withDimensions(TabBarBottom);
|
||||
|
||||
238
packages/bottom-tabs/src/views/BottomTabView.tsx
Normal file
238
packages/bottom-tabs/src/views/BottomTabView.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
AccessibilityRole,
|
||||
AccessibilityStates,
|
||||
} from 'react-native';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
ParamListBase,
|
||||
Route,
|
||||
BaseActions,
|
||||
} from '@navigation-ex/core';
|
||||
import { TabNavigationState } from '@navigation-ex/routers';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
|
||||
import BottomTabBar from './BottomTabBar';
|
||||
import { BottomTabNavigationConfig, BottomTabDescriptorMap } from '../types';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
|
||||
type Props = BottomTabNavigationConfig & {
|
||||
state: TabNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: BottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
type State = {
|
||||
loaded: number[];
|
||||
};
|
||||
|
||||
export default class BottomTabView extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
lazy: true,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const { index } = nextProps.state;
|
||||
|
||||
return {
|
||||
// Set the current tab to be loaded if it was not loaded before
|
||||
loaded: prevState.loaded.includes(index)
|
||||
? prevState.loaded
|
||||
: [...prevState.loaded, index],
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
loaded: [this.props.state.index],
|
||||
};
|
||||
|
||||
private getButtonComponent = ({ route }: { route: Route<string> }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarButtonComponent) {
|
||||
return options.tabBarButtonComponent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private renderIcon = ({
|
||||
route,
|
||||
focused,
|
||||
tintColor,
|
||||
horizontal,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarIcon) {
|
||||
return typeof options.tabBarIcon === 'function'
|
||||
? options.tabBarIcon({ focused, tintColor, horizontal })
|
||||
: options.tabBarIcon;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private getLabelText = ({ route }: { route: Route<string> }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarLabel !== undefined) {
|
||||
return options.tabBarLabel;
|
||||
}
|
||||
|
||||
if (typeof options.title === 'string') {
|
||||
return options.title;
|
||||
}
|
||||
|
||||
return route.name;
|
||||
};
|
||||
|
||||
private getAccessibilityLabel = ({ route }: { route: Route<string> }) => {
|
||||
const { state, descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (typeof options.tabBarAccessibilityLabel !== 'undefined') {
|
||||
return options.tabBarAccessibilityLabel;
|
||||
}
|
||||
|
||||
const label = this.getLabelText({ route });
|
||||
|
||||
if (typeof label === 'string') {
|
||||
return `${label}, tab, ${state.routes.indexOf(route) + 1} of ${
|
||||
state.routes.length
|
||||
}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private getAccessibilityRole = (): AccessibilityRole => 'button';
|
||||
|
||||
private getAccessibilityStates = ({
|
||||
focused,
|
||||
}: {
|
||||
focused: boolean;
|
||||
}): AccessibilityStates[] => (focused ? ['selected'] : []);
|
||||
|
||||
private getTestID = ({ route }: { route: Route<string> }) =>
|
||||
this.props.descriptors[route.key].options.tabBarTestID;
|
||||
|
||||
private handleTabPress = ({ route }: { route: Route<string> }) => {
|
||||
const { state, navigation } = this.props;
|
||||
const event = this.props.navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
if (state.routes[state.index].key === route.key) {
|
||||
navigation.emit({
|
||||
type: 'refocus',
|
||||
target: route.key,
|
||||
});
|
||||
} else if (!event.defaultPrevented) {
|
||||
navigation.dispatch({
|
||||
...BaseActions.navigate(route.name),
|
||||
target: state.key,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleTabLongPress = ({ route }: { route: Route<string> }) => {
|
||||
this.props.navigation.emit({
|
||||
type: 'tabLongPress',
|
||||
target: route.key,
|
||||
});
|
||||
};
|
||||
|
||||
private renderTabBar = () => {
|
||||
const {
|
||||
tabBarComponent: TabBarComponent = BottomTabBar,
|
||||
tabBarOptions,
|
||||
state,
|
||||
navigation,
|
||||
} = this.props;
|
||||
|
||||
const { descriptors } = this.props;
|
||||
const route = state.routes[state.index];
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarVisible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabBarComponent
|
||||
{...tabBarOptions}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
onTabPress={this.handleTabPress}
|
||||
onTabLongPress={this.handleTabLongPress}
|
||||
getLabelText={this.getLabelText}
|
||||
getButtonComponent={this.getButtonComponent}
|
||||
getAccessibilityLabel={this.getAccessibilityLabel}
|
||||
getAccessibilityRole={this.getAccessibilityRole}
|
||||
getAccessibilityStates={this.getAccessibilityStates}
|
||||
getTestID={this.getTestID}
|
||||
renderIcon={this.renderIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, descriptors, lazy } = this.props;
|
||||
const { routes } = state;
|
||||
const { loaded } = this.state;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
if (lazy && !loaded.includes(index)) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFocused = state.index === index;
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
{descriptors[route.key].render()}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
{this.renderTabBar()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pages: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
69
packages/bottom-tabs/src/views/TabBarIcon.tsx
Normal file
69
packages/bottom-tabs/src/views/TabBarIcon.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import { Route } from '@navigation-ex/core';
|
||||
|
||||
type Props = {
|
||||
route: Route<string>;
|
||||
horizontal: boolean;
|
||||
activeOpacity: number;
|
||||
inactiveOpacity: number;
|
||||
activeTintColor: string;
|
||||
inactiveTintColor: string;
|
||||
renderIcon: (props: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode;
|
||||
style: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function TabBarIcon({
|
||||
route,
|
||||
activeOpacity,
|
||||
inactiveOpacity,
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
renderIcon,
|
||||
horizontal,
|
||||
style,
|
||||
}: Props) {
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them.
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={[styles.icon, { opacity: activeOpacity }]}>
|
||||
{renderIcon({
|
||||
route,
|
||||
focused: true,
|
||||
horizontal,
|
||||
tintColor: activeTintColor,
|
||||
})}
|
||||
</View>
|
||||
<View style={[styles.icon, { opacity: inactiveOpacity }]}>
|
||||
{renderIcon({
|
||||
route,
|
||||
focused: false,
|
||||
horizontal,
|
||||
tintColor: inactiveTintColor,
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them:
|
||||
// Cover the whole iconContainer:
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
// Workaround for react-native >= 0.54 layout bug
|
||||
minWidth: 25,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { TouchableWithoutFeedback, View } from 'react-native';
|
||||
|
||||
export default function TouchableWithoutFeedbackWrapper({
|
||||
onPress,
|
||||
onLongPress,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityRole,
|
||||
accessibilityStates,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof TouchableWithoutFeedback> & {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
testID={testID}
|
||||
hitSlop={{ left: 15, right: 15, top: 0, bottom: 5 }}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityStates={accessibilityStates}
|
||||
>
|
||||
<View {...rest} />
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
24
packages/drawer/package.json
Normal file
24
packages/drawer/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@navigation-ex/drawer",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@navigation-ex/core": "^0.0.1",
|
||||
"@navigation-ex/routers": "^0.0.1",
|
||||
"react-native-safe-area-view": "^0.14.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.8.24",
|
||||
"@types/react-native": "^0.60.2",
|
||||
"react": "16.8.3",
|
||||
"react-native": "^0.59.8",
|
||||
"react-native-gesture-handler": "^1.3.0",
|
||||
"react-native-reanimated": "^1.1.0",
|
||||
"react-native-screens": "^1.0.0-alpha.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as DrawerActions from './routers/DrawerActions';
|
||||
|
||||
/**
|
||||
* Navigators
|
||||
*/
|
||||
@@ -7,12 +5,6 @@ export {
|
||||
default as createDrawerNavigator,
|
||||
} from './navigators/createDrawerNavigator';
|
||||
|
||||
/**
|
||||
* Router
|
||||
*/
|
||||
export { DrawerActions };
|
||||
export { default as DrawerRouter } from './routers/DrawerRouter';
|
||||
|
||||
/**
|
||||
* Views
|
||||
*/
|
||||
@@ -20,4 +12,12 @@ export { default as DrawerNavigatorItems } from './views/DrawerNavigatorItems';
|
||||
export { default as DrawerSidebar } from './views/DrawerSidebar';
|
||||
export { default as DrawerView } from './views/DrawerView';
|
||||
|
||||
/**
|
||||
* Utilities
|
||||
*/
|
||||
export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export { DrawerNavigationOptions, DrawerNavigationProp } from './types';
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { Dimensions, Platform, ScrollView, I18nManager } from 'react-native';
|
||||
import { createNavigator } from '@react-navigation/core';
|
||||
import { SafeAreaView } from '@react-navigation/native';
|
||||
import DrawerRouter from '../routers/DrawerRouter';
|
||||
import {
|
||||
createNavigator,
|
||||
useNavigationBuilder,
|
||||
DefaultNavigatorOptions,
|
||||
} from '@navigation-ex/core';
|
||||
import {
|
||||
DrawerNavigationState,
|
||||
DrawerRouterOptions,
|
||||
DrawerRouter,
|
||||
} from '@navigation-ex/routers';
|
||||
|
||||
import DrawerView from '../views/DrawerView';
|
||||
import DrawerItems, { Props } from '../views/DrawerNavigatorItems';
|
||||
import { DrawerNavigationOptions, DrawerNavigationConfig } from '../types';
|
||||
|
||||
// A stack navigators props are the intersection between
|
||||
// the base navigator props (navgiation, screenProps, etc)
|
||||
// and the view's props
|
||||
type Props = DefaultNavigatorOptions<DrawerNavigationOptions> &
|
||||
DrawerRouterOptions &
|
||||
Partial<DrawerNavigationConfig>;
|
||||
|
||||
const defaultContentComponent = (props: Props) => (
|
||||
<ScrollView alwaysBounceVertical={false}>
|
||||
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
|
||||
<DrawerItems {...props} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
function DrawerNavigator({
|
||||
initialRouteName,
|
||||
children,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
DrawerNavigationState,
|
||||
DrawerNavigationOptions,
|
||||
DrawerRouterOptions
|
||||
>(DrawerRouter, {
|
||||
initialRouteName,
|
||||
children,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<DrawerView
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default createNavigator<DrawerNavigationOptions, typeof DrawerNavigator>(
|
||||
DrawerNavigator
|
||||
);
|
||||
|
||||
const DefaultDrawerConfig = {
|
||||
drawerWidth: () => {
|
||||
/*
|
||||
* Default drawer width is screen width - header height
|
||||
* with a max width of 280 on mobile and 320 on tablet
|
||||
* https://material.io/guidelines/patterns/navigation-drawer.html
|
||||
*/
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const smallerAxisSize = Math.min(height, width);
|
||||
const isLandscape = width > height;
|
||||
const isTablet = smallerAxisSize >= 600;
|
||||
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
|
||||
const maxWidth = isTablet ? 320 : 280;
|
||||
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
|
||||
},
|
||||
contentComponent: defaultContentComponent,
|
||||
drawerPosition: I18nManager.isRTL ? 'right' : 'left',
|
||||
keyboardDismissMode: 'on-drag',
|
||||
drawerBackgroundColor: 'white',
|
||||
drawerType: 'front',
|
||||
hideStatusBar: false,
|
||||
statusBarAnimation: 'slide',
|
||||
};
|
||||
|
||||
const DrawerNavigator = (routeConfigs: object, config: any = {}) => {
|
||||
const mergedConfig = { ...DefaultDrawerConfig, ...config };
|
||||
const drawerRouter = DrawerRouter(routeConfigs, mergedConfig);
|
||||
const navigator = createNavigator(DrawerView, drawerRouter, mergedConfig);
|
||||
return navigator;
|
||||
};
|
||||
|
||||
export default DrawerNavigator;
|
||||
|
||||
@@ -1,29 +1,117 @@
|
||||
import { DrawerActionType } from './routers/DrawerActions';
|
||||
|
||||
export type Route = {
|
||||
key: string;
|
||||
routeName: string;
|
||||
};
|
||||
import { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
Route,
|
||||
ParamListBase,
|
||||
NavigationProp,
|
||||
Descriptor,
|
||||
NavigationHelpers,
|
||||
} from '@navigation-ex/core';
|
||||
import { DrawerNavigationState } from '@navigation-ex/routers';
|
||||
import { PanGestureHandler } from 'react-native-gesture-handler';
|
||||
|
||||
export type Scene = {
|
||||
route: Route;
|
||||
route: Route<string>;
|
||||
index: number;
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
};
|
||||
|
||||
export type Navigation = {
|
||||
state: {
|
||||
key: string;
|
||||
index: number;
|
||||
routes: Route[];
|
||||
isDrawerOpen: boolean;
|
||||
};
|
||||
openDrawer: () => void;
|
||||
closeDrawer: () => void;
|
||||
dispatch: (action: {
|
||||
type: DrawerActionType;
|
||||
key: string;
|
||||
willShow?: boolean;
|
||||
}) => void;
|
||||
export type DrawerNavigationConfig = {
|
||||
drawerBackgroundColor: string;
|
||||
drawerPosition: 'left' | 'right';
|
||||
drawerType: 'front' | 'back' | 'slide';
|
||||
drawerWidth: number | (() => number);
|
||||
edgeWidth?: number;
|
||||
hideStatusBar: boolean;
|
||||
keyboardDismissMode: 'on-drag' | 'none';
|
||||
minSwipeDistance?: number;
|
||||
overlayColor?: string;
|
||||
statusBarAnimation: 'slide' | 'none' | 'fade';
|
||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
||||
lazy: boolean;
|
||||
unmountInactiveRoutes?: boolean;
|
||||
contentComponent: React.ComponentType<ContentComponentProps>;
|
||||
contentOptions?: object;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type DrawerNavigationOptions = {
|
||||
title?: string;
|
||||
drawerLabel?:
|
||||
| string
|
||||
| ((props: { tintColor?: string; focused: boolean }) => React.ReactElement);
|
||||
drawerIcon?: (props: {
|
||||
tintColor?: string;
|
||||
focused: boolean;
|
||||
}) => React.ReactElement;
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open';
|
||||
};
|
||||
|
||||
export type ContentComponentProps = DrawerNavigationItemsProps & {
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: { [key: string]: any };
|
||||
drawerOpenProgress: Animated.Node<number>;
|
||||
};
|
||||
|
||||
export type DrawerNavigationItemsProps = {
|
||||
items: Route<string>[];
|
||||
activeItemKey?: string | null;
|
||||
activeTintColor?: string;
|
||||
activeBackgroundColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
inactiveBackgroundColor?: string;
|
||||
getLabel: (scene: Scene) => React.ReactNode;
|
||||
renderIcon: (scene: Scene) => React.ReactNode;
|
||||
onItemPress: (scene: { route: Route<string>; focused: boolean }) => void;
|
||||
itemsContainerStyle?: ViewStyle;
|
||||
itemStyle?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
activeLabelStyle?: StyleProp<TextStyle>;
|
||||
inactiveLabelStyle?: StyleProp<TextStyle>;
|
||||
iconContainerStyle?: StyleProp<ViewStyle>;
|
||||
drawerPosition: 'left' | 'right';
|
||||
};
|
||||
|
||||
export type DrawerNavigationEventMap = {
|
||||
drawerOpen: undefined;
|
||||
drawerClose: undefined;
|
||||
};
|
||||
|
||||
export type DrawerNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
DrawerNavigationState,
|
||||
DrawerNavigationOptions,
|
||||
DrawerNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Open the drawer sidebar.
|
||||
*/
|
||||
openDrawer(): void;
|
||||
|
||||
/**
|
||||
* Close the drawer sidebar.
|
||||
*/
|
||||
closeDrawer(): void;
|
||||
|
||||
/**
|
||||
* Open the drawer sidebar if closed, or close if opened.
|
||||
*/
|
||||
toggleDrawer(): void;
|
||||
};
|
||||
|
||||
export type DrawerDescriptor = Descriptor<
|
||||
ParamListBase,
|
||||
string,
|
||||
DrawerNavigationState,
|
||||
DrawerNavigationOptions
|
||||
>;
|
||||
|
||||
export type DrawerDescriptorMap = {
|
||||
[key: string]: DrawerDescriptor;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Platform,
|
||||
Keyboard,
|
||||
StatusBar,
|
||||
StyleProp,
|
||||
} from 'react-native';
|
||||
import {
|
||||
PanGestureHandler,
|
||||
@@ -85,9 +86,9 @@ type Props = {
|
||||
swipeVelocityThreshold: number;
|
||||
hideStatusBar: boolean;
|
||||
statusBarAnimation: 'slide' | 'none' | 'fade';
|
||||
overlayStyle?: ViewStyle;
|
||||
drawerStyle?: ViewStyle;
|
||||
contentContainerStyle?: ViewStyle;
|
||||
overlayStyle?: StyleProp<ViewStyle>;
|
||||
drawerStyle?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
renderDrawerContent: Renderer;
|
||||
renderSceneContent: Renderer;
|
||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
||||
@@ -577,6 +578,7 @@ export default class DrawerView extends React.PureComponent<Props> {
|
||||
style={[
|
||||
styles.container,
|
||||
right ? { right: offset } : { left: offset },
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
{
|
||||
transform: [{ translateX: drawerTranslateX }],
|
||||
opacity: this.drawerOpacity,
|
||||
|
||||
@@ -1,27 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { View, Text, StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
||||
import { SafeAreaView } from '@react-navigation/native';
|
||||
import TouchableItem from './TouchableItem';
|
||||
import { Scene, Route } from '../types';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
export type Props = {
|
||||
items: Route[];
|
||||
activeItemKey?: string | null;
|
||||
activeTintColor?: string;
|
||||
activeBackgroundColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
inactiveBackgroundColor?: string;
|
||||
getLabel: (scene: Scene) => React.ReactNode;
|
||||
renderIcon: (scene: Scene) => React.ReactNode;
|
||||
onItemPress: (scene: { route: Route; focused: boolean }) => void;
|
||||
itemsContainerStyle?: ViewStyle;
|
||||
itemStyle?: ViewStyle;
|
||||
labelStyle?: TextStyle;
|
||||
activeLabelStyle?: TextStyle;
|
||||
inactiveLabelStyle?: TextStyle;
|
||||
iconContainerStyle?: ViewStyle;
|
||||
drawerPosition: 'left' | 'right';
|
||||
};
|
||||
import TouchableItem from './TouchableItem';
|
||||
import { DrawerNavigationItemsProps } from '../types';
|
||||
|
||||
/**
|
||||
* Component that renders the navigation list in the drawer.
|
||||
@@ -43,7 +25,7 @@ const DrawerNavigatorItems = ({
|
||||
inactiveLabelStyle,
|
||||
iconContainerStyle,
|
||||
drawerPosition,
|
||||
}: Props) => (
|
||||
}: DrawerNavigationItemsProps) => (
|
||||
<View style={[styles.container, itemsContainerStyle]}>
|
||||
{items.map((route, index: number) => {
|
||||
const focused = activeItemKey === route.key;
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, View, Animated, ViewStyle } from 'react-native';
|
||||
import { NavigationActions } from '@react-navigation/core';
|
||||
import { StyleSheet, View, ViewStyle, StyleProp } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
ParamListBase,
|
||||
Route,
|
||||
BaseActions,
|
||||
} from '@navigation-ex/core';
|
||||
import { DrawerActions, DrawerNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
import { Props as DrawerNavigatorItemsProps } from './DrawerNavigatorItems';
|
||||
import { Navigation, Scene, Route } from '../types';
|
||||
|
||||
export type ContentComponentProps = DrawerNavigatorItemsProps & {
|
||||
navigation: Navigation;
|
||||
descriptors: { [key: string]: any };
|
||||
drawerOpenProgress: Animated.AnimatedInterpolation;
|
||||
screenProps: unknown;
|
||||
};
|
||||
import { Scene, ContentComponentProps, DrawerDescriptorMap } from '../types';
|
||||
|
||||
type Props = {
|
||||
contentComponent?: React.ComponentType<ContentComponentProps>;
|
||||
contentOptions?: object;
|
||||
screenProps?: unknown;
|
||||
navigation: Navigation;
|
||||
descriptors: { [key: string]: any };
|
||||
drawerOpenProgress: Animated.AnimatedInterpolation;
|
||||
state: DrawerNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: DrawerDescriptorMap;
|
||||
drawerOpenProgress: Animated.Node<number>;
|
||||
drawerPosition: 'left' | 'right';
|
||||
style?: ViewStyle;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,7 +50,7 @@ class DrawerSidebar extends React.PureComponent<Props> {
|
||||
return title;
|
||||
}
|
||||
|
||||
return route.routeName;
|
||||
return route.name;
|
||||
};
|
||||
|
||||
private renderIcon = ({ focused, tintColor, route }: Scene) => {
|
||||
@@ -68,16 +67,17 @@ class DrawerSidebar extends React.PureComponent<Props> {
|
||||
route,
|
||||
focused,
|
||||
}: {
|
||||
route: Route;
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => {
|
||||
if (focused) {
|
||||
this.props.navigation.closeDrawer();
|
||||
} else {
|
||||
this.props.navigation.dispatch(
|
||||
NavigationActions.navigate({ routeName: route.routeName })
|
||||
);
|
||||
}
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
navigation.dispatch({
|
||||
...(focused
|
||||
? DrawerActions.closeDrawer()
|
||||
: BaseActions.navigate(route.name)),
|
||||
target: state.key,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -87,7 +87,7 @@ class DrawerSidebar extends React.PureComponent<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { state } = this.props.navigation;
|
||||
const { state } = this.props;
|
||||
|
||||
if (typeof state.index !== 'number') {
|
||||
throw new Error(
|
||||
@@ -106,7 +106,6 @@ class DrawerSidebar extends React.PureComponent<Props> {
|
||||
activeItemKey={
|
||||
state.routes[state.index] ? state.routes[state.index].key : null
|
||||
}
|
||||
screenProps={this.props.screenProps}
|
||||
getLabel={this.getLabel}
|
||||
renderIcon={this.renderIcon}
|
||||
onItemPress={this.handleItemPress}
|
||||
|
||||
@@ -1,51 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import { Dimensions, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { SceneView } from '@react-navigation/core';
|
||||
import { Dimensions, StyleSheet, I18nManager, Platform } from 'react-native';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
import { PanGestureHandler, ScrollView } from 'react-native-gesture-handler';
|
||||
import { ParamListBase, NavigationHelpers } from '@navigation-ex/core';
|
||||
import { DrawerNavigationState, DrawerActions } from '@navigation-ex/routers';
|
||||
|
||||
import * as DrawerActions from '../routers/DrawerActions';
|
||||
import DrawerSidebar, { ContentComponentProps } from './DrawerSidebar';
|
||||
import DrawerSidebar from './DrawerSidebar';
|
||||
import DrawerGestureContext from '../utils/DrawerGestureContext';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
import DrawerNavigatorItems from './DrawerNavigatorItems';
|
||||
import Drawer from './Drawer';
|
||||
import { Navigation } from '../types';
|
||||
import { PanGestureHandler } from 'react-native-gesture-handler';
|
||||
import {
|
||||
DrawerDescriptorMap,
|
||||
DrawerNavigationConfig,
|
||||
ContentComponentProps,
|
||||
} from '../types';
|
||||
|
||||
type DrawerOptions = {
|
||||
drawerBackgroundColor?: string;
|
||||
overlayColor?: string;
|
||||
minSwipeDistance?: number;
|
||||
drawerPosition: 'left' | 'right';
|
||||
drawerType: 'front' | 'back' | 'slide';
|
||||
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open';
|
||||
keyboardDismissMode?: 'on-drag' | 'none';
|
||||
drawerWidth: number | (() => number);
|
||||
statusBarAnimation: 'slide' | 'none' | 'fade';
|
||||
onDrawerClose?: () => void;
|
||||
onDrawerOpen?: () => void;
|
||||
contentContainerStyle?: ViewStyle;
|
||||
edgeWidth: number;
|
||||
hideStatusBar?: boolean;
|
||||
style?: ViewStyle;
|
||||
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
lazy: boolean;
|
||||
navigation: Navigation;
|
||||
descriptors: {
|
||||
[key: string]: {
|
||||
navigation: {};
|
||||
getComponent: () => React.ComponentType<{}>;
|
||||
options: DrawerOptions;
|
||||
};
|
||||
};
|
||||
navigationConfig: DrawerOptions & {
|
||||
contentComponent?: React.ComponentType<ContentComponentProps>;
|
||||
unmountInactiveRoutes?: boolean;
|
||||
contentOptions?: object;
|
||||
};
|
||||
screenProps: unknown;
|
||||
type Props = DrawerNavigationConfig & {
|
||||
state: DrawerNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: DrawerDescriptorMap;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -53,16 +29,46 @@ type State = {
|
||||
drawerWidth: number;
|
||||
};
|
||||
|
||||
const DefaultContentComponent = (props: ContentComponentProps) => (
|
||||
<ScrollView alwaysBounceVertical={false}>
|
||||
<SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
|
||||
<DrawerNavigatorItems {...props} />
|
||||
</SafeAreaView>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
/**
|
||||
* Component that renders the drawer.
|
||||
*/
|
||||
export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
lazy: true,
|
||||
drawerWidth: () => {
|
||||
/*
|
||||
* Default drawer width is screen width - header height
|
||||
* with a max width of 280 on mobile and 320 on tablet
|
||||
* https://material.io/guidelines/patterns/navigation-drawer.html
|
||||
*/
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const smallerAxisSize = Math.min(height, width);
|
||||
const isLandscape = width > height;
|
||||
const isTablet = smallerAxisSize >= 600;
|
||||
const appBarHeight = Platform.OS === 'ios' ? (isLandscape ? 32 : 44) : 56;
|
||||
const maxWidth = isTablet ? 320 : 280;
|
||||
|
||||
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
|
||||
},
|
||||
contentComponent: DefaultContentComponent,
|
||||
drawerPosition: I18nManager.isRTL ? 'right' : 'left',
|
||||
keyboardDismissMode: 'on-drag',
|
||||
drawerBackgroundColor: 'white',
|
||||
drawerType: 'front',
|
||||
hideStatusBar: false,
|
||||
statusBarAnimation: 'slide',
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const { index } = nextProps.navigation.state;
|
||||
const { index } = nextProps.state;
|
||||
|
||||
return {
|
||||
// Set the current tab to be loaded if it was not loaded before
|
||||
@@ -73,11 +79,11 @@ export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
state: State = {
|
||||
loaded: [this.props.navigation.state.index],
|
||||
loaded: [this.props.state.index],
|
||||
drawerWidth:
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth,
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@@ -91,30 +97,32 @@ export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
private drawerGestureRef = React.createRef<PanGestureHandler>();
|
||||
|
||||
private handleDrawerOpen = () => {
|
||||
const { navigation } = this.props;
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
navigation.dispatch(
|
||||
DrawerActions.openDrawer({
|
||||
key: navigation.state.key,
|
||||
})
|
||||
);
|
||||
navigation.dispatch({
|
||||
...DrawerActions.openDrawer(),
|
||||
target: state.key,
|
||||
});
|
||||
|
||||
navigation.emit({ type: 'drawerOpen' });
|
||||
};
|
||||
|
||||
private handleDrawerClose = () => {
|
||||
const { navigation } = this.props;
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
navigation.dispatch(
|
||||
DrawerActions.closeDrawer({
|
||||
key: navigation.state.key,
|
||||
})
|
||||
);
|
||||
navigation.dispatch({
|
||||
...DrawerActions.closeDrawer(),
|
||||
target: state.key,
|
||||
});
|
||||
|
||||
navigation.emit({ type: 'drawerClose' });
|
||||
};
|
||||
|
||||
private updateWidth = () => {
|
||||
const drawerWidth =
|
||||
typeof this.props.navigationConfig.drawerWidth === 'function'
|
||||
? this.props.navigationConfig.drawerWidth()
|
||||
: this.props.navigationConfig.drawerWidth;
|
||||
typeof this.props.drawerWidth === 'function'
|
||||
? this.props.drawerWidth()
|
||||
: this.props.drawerWidth;
|
||||
|
||||
if (this.state.drawerWidth !== drawerWidth) {
|
||||
this.setState({ drawerWidth });
|
||||
@@ -122,63 +130,42 @@ export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
private renderNavigationView = ({ progress }: any) => {
|
||||
return (
|
||||
<DrawerSidebar
|
||||
screenProps={this.props.screenProps}
|
||||
drawerOpenProgress={progress}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
contentComponent={this.props.navigationConfig.contentComponent}
|
||||
contentOptions={this.props.navigationConfig.contentOptions}
|
||||
drawerPosition={this.props.navigationConfig.drawerPosition}
|
||||
style={this.props.navigationConfig.style}
|
||||
{...this.props.navigationConfig}
|
||||
/>
|
||||
);
|
||||
return <DrawerSidebar drawerOpenProgress={progress} {...this.props} />;
|
||||
};
|
||||
|
||||
private renderContent = () => {
|
||||
let { lazy, navigation } = this.props;
|
||||
let { loaded } = this.state;
|
||||
let { routes } = navigation.state;
|
||||
let { lazy, state, descriptors, unmountInactiveRoutes } = this.props;
|
||||
|
||||
if (this.props.navigationConfig.unmountInactiveRoutes) {
|
||||
let activeKey = navigation.state.routes[navigation.state.index].key;
|
||||
let descriptor = this.props.descriptors[activeKey];
|
||||
const { loaded } = this.state;
|
||||
|
||||
return (
|
||||
<SceneView
|
||||
navigation={descriptor.navigation}
|
||||
screenProps={this.props.screenProps}
|
||||
component={descriptor.getComponent()}
|
||||
/>
|
||||
);
|
||||
if (unmountInactiveRoutes) {
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = descriptors[activeKey];
|
||||
|
||||
return descriptor.render();
|
||||
} else {
|
||||
return (
|
||||
<ScreenContainer style={styles.content}>
|
||||
{routes.map((route, index) => {
|
||||
{state.routes.map((route, index) => {
|
||||
if (lazy && !loaded.includes(index)) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
let isFocused = navigation.state.index === index;
|
||||
let descriptor = this.props.descriptors[route.key];
|
||||
const isFocused = state.index === index;
|
||||
const descriptor = descriptors[route.key];
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={[
|
||||
StyleSheet.absoluteFill,
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
{ opacity: isFocused ? 1 : 0 },
|
||||
]}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
<SceneView
|
||||
navigation={descriptor.navigation}
|
||||
screenProps={this.props.screenProps}
|
||||
component={descriptor.getComponent()}
|
||||
/>
|
||||
{descriptor.render()}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
@@ -193,9 +180,11 @@ export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
const {
|
||||
state,
|
||||
descriptors,
|
||||
drawerType,
|
||||
drawerPosition,
|
||||
drawerBackgroundColor,
|
||||
overlayColor,
|
||||
contentContainerStyle,
|
||||
@@ -204,16 +193,17 @@ export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
hideStatusBar,
|
||||
statusBarAnimation,
|
||||
gestureHandlerProps,
|
||||
} = this.props.navigationConfig;
|
||||
const activeKey = navigation.state.routes[navigation.state.index].key;
|
||||
const { drawerLockMode } = this.props.descriptors[activeKey].options;
|
||||
} = this.props;
|
||||
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const { drawerLockMode } = descriptors[activeKey].options;
|
||||
|
||||
const isOpen =
|
||||
drawerLockMode === 'locked-closed'
|
||||
? false
|
||||
: drawerLockMode === 'locked-open'
|
||||
? true
|
||||
: this.props.navigation.state.isDrawerOpen;
|
||||
: state.isDrawerOpen;
|
||||
|
||||
return (
|
||||
<DrawerGestureContext.Provider value={this.drawerGestureRef}>
|
||||
@@ -228,7 +218,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
|
||||
onGestureRef={this.setDrawerGestureRef}
|
||||
gestureHandlerProps={gestureHandlerProps}
|
||||
drawerType={drawerType}
|
||||
drawerPosition={this.props.navigationConfig.drawerPosition}
|
||||
drawerPosition={drawerPosition}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
drawerStyle={{
|
||||
backgroundColor: drawerBackgroundColor || 'white',
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* @flow */
|
||||
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { Screen, screensEnabled } from 'react-native-screens';
|
||||
|
||||
type Props = {
|
||||
|
||||
25
packages/material-bottom-tabs/package.json
Normal file
25
packages/material-bottom-tabs/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@navigation-ex/material-bottom-tabs",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@navigation-ex/core": "^0.0.1",
|
||||
"@navigation-ex/routers": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.8.24",
|
||||
"@types/react-native": "^0.60.2",
|
||||
"@types/react-native-vector-icons": "^6.4.1",
|
||||
"react": "16.8.3",
|
||||
"react-native": "^0.59.8",
|
||||
"react-native-paper": "^3.0.0-alpha.3",
|
||||
"react-native-vector-icons": "^6.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-paper": "^3.0.0-alpha.3",
|
||||
"react-native-vector-icons": "^6.0.0"
|
||||
}
|
||||
}
|
||||
14
packages/material-bottom-tabs/src/index.tsx
Normal file
14
packages/material-bottom-tabs/src/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Navigators
|
||||
*/
|
||||
export {
|
||||
default as createMaterialBottomTabNavigator,
|
||||
} from './navigators/createMaterialBottomTabNavigator';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export {
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationProp,
|
||||
} from './types';
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useNavigationBuilder,
|
||||
createNavigator,
|
||||
DefaultNavigatorOptions,
|
||||
} from '@navigation-ex/core';
|
||||
import {
|
||||
TabRouter,
|
||||
TabRouterOptions,
|
||||
TabNavigationState,
|
||||
} from '@navigation-ex/routers';
|
||||
|
||||
import MaterialBottomTabView from '../views/MaterialBottomTabView';
|
||||
import {
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
} from '../types';
|
||||
|
||||
type Props = DefaultNavigatorOptions<MaterialBottomTabNavigationOptions> &
|
||||
TabRouterOptions &
|
||||
MaterialBottomTabNavigationConfig;
|
||||
|
||||
function MaterialBottomTabNavigator({
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
TabNavigationState,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
TabRouterOptions
|
||||
>(TabRouter, {
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<MaterialBottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default createNavigator<
|
||||
MaterialBottomTabNavigationOptions,
|
||||
typeof MaterialBottomTabNavigator
|
||||
>(MaterialBottomTabNavigator);
|
||||
89
packages/material-bottom-tabs/src/types.tsx
Normal file
89
packages/material-bottom-tabs/src/types.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { BottomNavigation } from 'react-native-paper';
|
||||
import { ParamListBase, Descriptor, NavigationProp } from '@navigation-ex/core';
|
||||
import { TabNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
export type MaterialBottomTabNavigationEventMap = {
|
||||
refocus: undefined;
|
||||
tabPress: undefined;
|
||||
};
|
||||
|
||||
export type MaterialBottomTabNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
TabNavigationState,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Jump to an existing tab.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
};
|
||||
|
||||
export type MaterialBottomTabNavigationOptions = {
|
||||
/**
|
||||
* Title text for the screen.
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* Color of the tab bar when this tab is active. Only used when `shifting` is `true`.
|
||||
*/
|
||||
tabBarColor?: string;
|
||||
|
||||
/**
|
||||
* Label text of the tab displayed in the navigation bar. When undefined, scene title is used.
|
||||
*/
|
||||
tabBarLabel?: string;
|
||||
|
||||
/**
|
||||
* String referring to an icon in the `MaterialCommunityIcons` set, or a
|
||||
* function that given { focused: boolean, tintColor: string } returns a React.Node to display in the navigation bar.
|
||||
*/
|
||||
tabBarIcon?:
|
||||
| string
|
||||
| ((props: { focused: boolean; color: string }) => React.ReactNode);
|
||||
|
||||
/**
|
||||
* Badge to show on the tab icon, can be `true` to show a dot, `string` or `number` to show text.
|
||||
*/
|
||||
tabBarBadge?: boolean | number | string;
|
||||
|
||||
/**
|
||||
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
||||
*/
|
||||
tabBarAccessibilityLabel?: string;
|
||||
|
||||
/**
|
||||
* ID to locate this tab button in tests.
|
||||
*/
|
||||
tabBarTestID?: string;
|
||||
};
|
||||
|
||||
export type MaterialBottomTabDescriptor = Descriptor<
|
||||
ParamListBase,
|
||||
string,
|
||||
TabNavigationState,
|
||||
MaterialBottomTabNavigationOptions
|
||||
>;
|
||||
|
||||
export type MaterialBottomTabDescriptorMap = {
|
||||
[key: string]: MaterialBottomTabDescriptor;
|
||||
};
|
||||
|
||||
export type MaterialBottomTabNavigationConfig = Partial<
|
||||
Omit<
|
||||
React.ComponentProps<typeof BottomNavigation>,
|
||||
'navigationState' | 'onIndexChange' | 'renderScene'
|
||||
>
|
||||
>;
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { BottomNavigation } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { NavigationHelpers, ParamListBase, Route } from '@navigation-ex/core';
|
||||
import { TabNavigationState, TabActions } from '@navigation-ex/routers';
|
||||
|
||||
import {
|
||||
MaterialBottomTabDescriptorMap,
|
||||
MaterialBottomTabNavigationConfig,
|
||||
} from '../types';
|
||||
|
||||
type Props = MaterialBottomTabNavigationConfig & {
|
||||
state: TabNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: MaterialBottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
type Scene = { route: Route<string> };
|
||||
|
||||
export default class MaterialBottomTabView extends React.PureComponent<Props> {
|
||||
private getColor = ({ route }: Scene) => {
|
||||
return this.props.descriptors[route.key].options.tabBarColor;
|
||||
};
|
||||
|
||||
private getBadge = ({ route }: Scene) => {
|
||||
return this.props.descriptors[route.key].options.tabBarBadge;
|
||||
};
|
||||
|
||||
private getLabelText = ({ route }: Scene) => {
|
||||
const { options } = this.props.descriptors[route.key];
|
||||
|
||||
return options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel
|
||||
: typeof options.title === 'string'
|
||||
? options.title
|
||||
: route.name;
|
||||
};
|
||||
|
||||
private getAccessibilityLabel = ({ route }: Scene) => {
|
||||
const { descriptors, state } = this.props;
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
if (typeof options.tabBarAccessibilityLabel !== 'undefined') {
|
||||
return options.tabBarAccessibilityLabel;
|
||||
}
|
||||
|
||||
const label = this.getLabelText({ route });
|
||||
|
||||
if (typeof label === 'string') {
|
||||
return `${label}, tab, ${state.routes.indexOf(route) + 1} of ${
|
||||
state.routes.length
|
||||
}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private getTestID = ({ route }: Scene) => {
|
||||
return this.props.descriptors[route.key].options.tabBarTestID;
|
||||
};
|
||||
|
||||
private handleTabPress = ({ route }: Scene) => {
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
if (state.routes[state.index].key === route.key) {
|
||||
navigation.emit({
|
||||
type: 'refocus',
|
||||
target: route.key,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private renderIcon = ({
|
||||
route,
|
||||
focused,
|
||||
color,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
color: string;
|
||||
}) => {
|
||||
const { options } = this.props.descriptors[route.key];
|
||||
|
||||
if (typeof options.tabBarIcon === 'string') {
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={options.tabBarIcon}
|
||||
color={color}
|
||||
size={24}
|
||||
style={styles.icon}
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
accessibilityElementsHidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof options.tabBarIcon === 'function') {
|
||||
return options.tabBarIcon({ focused, color });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, navigation, descriptors, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<BottomNavigation
|
||||
{...rest}
|
||||
navigationState={state}
|
||||
onIndexChange={(index: number) =>
|
||||
navigation.dispatch({
|
||||
...TabActions.jumpTo(state.routes[index].name),
|
||||
target: state.key,
|
||||
})
|
||||
}
|
||||
renderScene={({ route }: Scene) => descriptors[route.key].render()}
|
||||
renderIcon={this.renderIcon}
|
||||
getLabelText={this.getLabelText}
|
||||
getColor={this.getColor}
|
||||
getBadge={this.getBadge}
|
||||
getAccessibilityLabel={this.getAccessibilityLabel}
|
||||
getTestID={this.getTestID}
|
||||
onTabPress={this.handleTabPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
25
packages/stack/package.json
Normal file
25
packages/stack/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@navigation-ex/stack",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@navigation-ex/core": "^0.0.1",
|
||||
"@navigation-ex/routers": "^0.0.1",
|
||||
"react-native-safe-area-view": "^0.14.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-community/masked-view": "^0.1.1",
|
||||
"@types/react": "^16.8.24",
|
||||
"@types/react-native": "^0.60.2",
|
||||
"react": "16.8.3",
|
||||
"react-native": "^0.59.8",
|
||||
"react-native-gesture-handler": "^1.3.0",
|
||||
"react-native-reanimated": "^1.1.0",
|
||||
"react-native-screens": "^1.0.0-alpha.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,9 @@ export { CardStyleInterpolators, HeaderStyleInterpolators, TransitionPresets };
|
||||
/**
|
||||
* Utilities
|
||||
*/
|
||||
|
||||
export { default as StackGestureContext } from './utils/StackGestureContext';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
export { StackNavigationOptions, StackNavigationProp } from './types';
|
||||
|
||||
@@ -1,45 +1,70 @@
|
||||
import * as React from 'react';
|
||||
import { StackRouter, createNavigator } from '@react-navigation/core';
|
||||
import { Platform } from 'react-native';
|
||||
import StackView from '../views/Stack/StackView';
|
||||
import {
|
||||
NavigationStackConfig,
|
||||
NavigationStackOptions,
|
||||
NavigationProp,
|
||||
Screen,
|
||||
} from '../types';
|
||||
useNavigationBuilder,
|
||||
createNavigator,
|
||||
DefaultNavigatorOptions,
|
||||
EventArg,
|
||||
} from '@navigation-ex/core';
|
||||
import {
|
||||
StackRouter,
|
||||
StackRouterOptions,
|
||||
StackNavigationState,
|
||||
StackActions,
|
||||
} from '@navigation-ex/routers';
|
||||
import KeyboardManager from '../views/KeyboardManager';
|
||||
import StackView from '../views/Stack/StackView';
|
||||
import { StackNavigationConfig, StackNavigationOptions } from '../types';
|
||||
|
||||
function createStackNavigator(
|
||||
routeConfigMap: {
|
||||
[key: string]:
|
||||
| Screen
|
||||
| ({ screen: Screen } | { getScreen(): Screen }) & {
|
||||
path?: string;
|
||||
navigationOptions?:
|
||||
| NavigationStackOptions
|
||||
| ((options: {
|
||||
navigation: NavigationProp;
|
||||
}) => NavigationStackOptions);
|
||||
};
|
||||
},
|
||||
stackConfig: NavigationStackConfig = {}
|
||||
) {
|
||||
const router = StackRouter(routeConfigMap, stackConfig);
|
||||
type Props = DefaultNavigatorOptions<StackNavigationOptions> &
|
||||
StackRouterOptions &
|
||||
StackNavigationConfig;
|
||||
|
||||
if (stackConfig.disableKeyboardHandling || Platform.OS === 'web') {
|
||||
return createNavigator(StackView, router, stackConfig);
|
||||
}
|
||||
function StackNavigator({
|
||||
keyboardHandlingEnabled,
|
||||
initialRouteName,
|
||||
children,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
StackNavigationState,
|
||||
StackNavigationOptions,
|
||||
StackRouterOptions
|
||||
>(StackRouter, {
|
||||
initialRouteName,
|
||||
children,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
return createNavigator(
|
||||
navigatorProps => (
|
||||
<KeyboardManager>
|
||||
{props => <StackView {...props} {...navigatorProps} />}
|
||||
</KeyboardManager>
|
||||
),
|
||||
router,
|
||||
stackConfig
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener &&
|
||||
navigation.addListener('refocus', (e: EventArg<'refocus', undefined>) => {
|
||||
if (state.index > 0 && !e.defaultPrevented) {
|
||||
navigation.dispatch({
|
||||
...StackActions.popToTop(),
|
||||
target: state.key,
|
||||
});
|
||||
}
|
||||
}),
|
||||
[navigation, state.index, state.key]
|
||||
);
|
||||
|
||||
return (
|
||||
<KeyboardManager enabled={keyboardHandlingEnabled !== false}>
|
||||
{props => (
|
||||
<StackView
|
||||
state={state}
|
||||
descriptors={descriptors}
|
||||
navigation={navigation}
|
||||
{...rest}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</KeyboardManager>
|
||||
);
|
||||
}
|
||||
|
||||
export default createStackNavigator;
|
||||
export default createNavigator<StackNavigationOptions, typeof StackNavigator>(
|
||||
StackNavigator
|
||||
);
|
||||
|
||||
@@ -5,44 +5,50 @@ import {
|
||||
LayoutChangeEvent,
|
||||
} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
Descriptor,
|
||||
Route,
|
||||
} from '@navigation-ex/core';
|
||||
import { StackNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
export type Route = {
|
||||
key: string;
|
||||
routeName: string;
|
||||
export type StackNavigationEventMap = {
|
||||
transitionStart: { closing: boolean };
|
||||
transitionEnd: { closing: boolean };
|
||||
};
|
||||
|
||||
export type NavigationEventName =
|
||||
| 'willFocus'
|
||||
| 'didFocus'
|
||||
| 'willBlur'
|
||||
| 'didBlur';
|
||||
export type StackNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
StackNavigationState,
|
||||
StackNavigationOptions,
|
||||
StackNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Push a new screen onto the stack.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
push<RouteName extends keyof ParamList>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
|
||||
export type NavigationState = {
|
||||
key: string;
|
||||
index: number;
|
||||
routes: Route[];
|
||||
transitions: {
|
||||
pushing: string[];
|
||||
popping: string[];
|
||||
};
|
||||
params?: { [key: string]: unknown };
|
||||
};
|
||||
/**
|
||||
* Pop a screen from the stack.
|
||||
*/
|
||||
pop(count?: number): void;
|
||||
|
||||
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;
|
||||
isFirstRouteInParent(): boolean;
|
||||
dangerouslyGetParent(): NavigationProp | undefined;
|
||||
/**
|
||||
* Pop to the first route in the stack, dismissing all other screens.
|
||||
*/
|
||||
popToTop(): void;
|
||||
};
|
||||
|
||||
export type Layout = { width: number; height: number };
|
||||
@@ -53,7 +59,7 @@ export type HeaderMode = 'float' | 'screen' | 'none';
|
||||
|
||||
export type HeaderScene<T> = {
|
||||
route: T;
|
||||
descriptor: SceneDescriptor;
|
||||
descriptor: StackDescriptor;
|
||||
progress: {
|
||||
current: Animated.Node<number>;
|
||||
next?: Animated.Node<number>;
|
||||
@@ -87,17 +93,28 @@ export type HeaderOptions = {
|
||||
export type HeaderProps = {
|
||||
mode: 'float' | 'screen';
|
||||
layout: Layout;
|
||||
scene: HeaderScene<Route>;
|
||||
previous?: HeaderScene<Route>;
|
||||
navigation: NavigationProp;
|
||||
scene: HeaderScene<Route<string>>;
|
||||
previous?: HeaderScene<Route<string>>;
|
||||
navigation: StackNavigationProp<ParamListBase>;
|
||||
styleInterpolator: HeaderStyleInterpolator;
|
||||
};
|
||||
|
||||
export type StackDescriptor = Descriptor<
|
||||
ParamListBase,
|
||||
string,
|
||||
StackNavigationState,
|
||||
StackNavigationOptions
|
||||
>;
|
||||
|
||||
export type StackDescriptorMap = {
|
||||
[key: string]: StackDescriptor;
|
||||
};
|
||||
|
||||
export type TransitionCallbackProps = {
|
||||
closing: boolean;
|
||||
};
|
||||
|
||||
export type NavigationStackOptions = HeaderOptions &
|
||||
export type StackNavigationOptions = HeaderOptions &
|
||||
Partial<TransitionPreset> & {
|
||||
title?: string;
|
||||
header?: null | ((props: HeaderProps) => React.ReactNode);
|
||||
@@ -111,25 +128,14 @@ export type NavigationStackOptions = HeaderOptions &
|
||||
vertical?: number;
|
||||
horizontal?: number;
|
||||
};
|
||||
onTransitionStart?: (props: TransitionCallbackProps) => void;
|
||||
onTransitionEnd?: (props: TransitionCallbackProps) => void;
|
||||
};
|
||||
|
||||
export type NavigationStackConfig = {
|
||||
export type StackNavigationConfig = {
|
||||
mode?: 'card' | 'modal';
|
||||
headerMode?: HeaderMode;
|
||||
disableKeyboardHandling?: boolean;
|
||||
keyboardHandlingEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type SceneDescriptor = {
|
||||
key: string;
|
||||
options: NavigationStackOptions;
|
||||
navigation: NavigationProp;
|
||||
getComponent(): React.ComponentType;
|
||||
};
|
||||
|
||||
export type SceneDescriptorMap = { [key: string]: SceneDescriptor | undefined };
|
||||
|
||||
export type HeaderBackButtonProps = {
|
||||
disabled?: boolean;
|
||||
onPress?: () => void;
|
||||
@@ -155,7 +161,7 @@ export type HeaderTitleProps = {
|
||||
};
|
||||
|
||||
export type Screen = React.ComponentType<any> & {
|
||||
navigationOptions?: NavigationStackOptions & {
|
||||
navigationOptions?: StackNavigationOptions & {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { StackActions } from '@react-navigation/core';
|
||||
import { StackActions } from '@navigation-ex/routers';
|
||||
import HeaderSegment from './HeaderSegment';
|
||||
import { HeaderProps, HeaderTitleProps } from '../../types';
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
@@ -20,7 +20,7 @@ export default class Header extends React.PureComponent<HeaderProps> {
|
||||
? options.headerTitle
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: scene.route.routeName;
|
||||
: scene.route.name;
|
||||
|
||||
let leftLabel;
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class Header extends React.PureComponent<HeaderProps> {
|
||||
? o.headerTitle
|
||||
: o.title !== undefined
|
||||
? o.title
|
||||
: previous.route.routeName;
|
||||
: previous.route.name;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -54,7 +54,10 @@ export default class Header extends React.PureComponent<HeaderProps> {
|
||||
onGoBack={
|
||||
previous
|
||||
? () =>
|
||||
navigation.dispatch(StackActions.pop({ key: scene.route.key }))
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: scene.route.key,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
styleInterpolator={styleInterpolator}
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import {
|
||||
Layout,
|
||||
Route,
|
||||
HeaderScene,
|
||||
NavigationProp,
|
||||
HeaderStyleInterpolator,
|
||||
} from '../../types';
|
||||
import { Route, ParamListBase } from '@navigation-ex/core';
|
||||
import { StackNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
import Header from './Header';
|
||||
import { forStatic } from '../../TransitionConfigs/HeaderStyleInterpolators';
|
||||
import {
|
||||
Layout,
|
||||
HeaderScene,
|
||||
HeaderStyleInterpolator,
|
||||
StackNavigationProp,
|
||||
} from '../../types';
|
||||
|
||||
export type Props = {
|
||||
mode: 'float' | 'screen';
|
||||
layout: Layout;
|
||||
scenes: Array<HeaderScene<Route> | undefined>;
|
||||
navigation: NavigationProp;
|
||||
getPreviousRoute: (props: { route: Route }) => Route | undefined;
|
||||
onContentHeightChange?: (props: { route: Route; height: number }) => void;
|
||||
scenes: Array<HeaderScene<Route<string>> | undefined>;
|
||||
state: StackNavigationState;
|
||||
getPreviousRoute: (props: {
|
||||
route: Route<string>;
|
||||
}) => Route<string> | undefined;
|
||||
onContentHeightChange?: (props: {
|
||||
route: Route<string>;
|
||||
height: number;
|
||||
}) => void;
|
||||
styleInterpolator: HeaderStyleInterpolator;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
@@ -25,13 +32,13 @@ export default function HeaderContainer({
|
||||
mode,
|
||||
scenes,
|
||||
layout,
|
||||
navigation,
|
||||
state,
|
||||
getPreviousRoute,
|
||||
onContentHeightChange,
|
||||
styleInterpolator,
|
||||
style,
|
||||
}: Props) {
|
||||
const focusedRoute = navigation.state.routes[navigation.state.index];
|
||||
const focusedRoute = state.routes[state.index];
|
||||
|
||||
return (
|
||||
<View pointerEvents="box-none" style={style}>
|
||||
@@ -78,7 +85,9 @@ export default function HeaderContainer({
|
||||
layout,
|
||||
scene,
|
||||
previous,
|
||||
navigation: scene.descriptor.navigation,
|
||||
navigation: scene.descriptor.navigation as StackNavigationProp<
|
||||
ParamListBase
|
||||
>,
|
||||
styleInterpolator: isHeaderStatic ? forStatic : styleInterpolator,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import { getStatusBarHeight } from 'react-native-safe-area-view';
|
||||
import { Route } from '@navigation-ex/core';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import HeaderBackground from './HeaderBackground';
|
||||
import memoize from '../../utils/memoize';
|
||||
import {
|
||||
Layout,
|
||||
HeaderStyleInterpolator,
|
||||
Route,
|
||||
HeaderBackButtonProps,
|
||||
HeaderTitleProps,
|
||||
HeaderOptions,
|
||||
@@ -32,7 +32,7 @@ type Props = HeaderOptions & {
|
||||
onGoBack?: () => void;
|
||||
title?: string;
|
||||
leftLabel?: string;
|
||||
scene: HeaderScene<Route>;
|
||||
scene: HeaderScene<Route<string>>;
|
||||
styleInterpolator: HeaderStyleInterpolator;
|
||||
};
|
||||
|
||||
@@ -322,7 +322,7 @@ export default class HeaderSegment extends React.Component<Props, State> {
|
||||
style={[
|
||||
Platform.select({
|
||||
ios: null,
|
||||
default: { left: onGoBack ? 72 : 16 },
|
||||
default: { left: left ? 72 : 16 },
|
||||
}),
|
||||
styles.title,
|
||||
titleStyle,
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { TextInput, Keyboard } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
enabled: boolean;
|
||||
children: (props: {
|
||||
onPageChangeStart: () => void;
|
||||
onPageChangeConfirm: () => void;
|
||||
@@ -15,6 +16,10 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
private previouslyFocusedTextInput: number | null = null;
|
||||
|
||||
private handlePageChangeStart = () => {
|
||||
if (!this.props.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = TextInput.State.currentlyFocusedField();
|
||||
|
||||
// When a page change begins, blur the currently focused input
|
||||
@@ -25,6 +30,10 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
};
|
||||
|
||||
private handlePageChangeConfirm = () => {
|
||||
if (!this.props.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
|
||||
// Cleanup the ID on successful page change
|
||||
@@ -32,6 +41,10 @@ export default class KeyboardManager extends React.Component<Props> {
|
||||
};
|
||||
|
||||
private handlePageChangeCancel = () => {
|
||||
if (!this.props.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The page didn't change, we should restore the focus of text input
|
||||
const input = this.previouslyFocusedTextInput;
|
||||
|
||||
|
||||
@@ -126,6 +126,19 @@ export default class Card extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// It might sometimes happen than animation will be unmounted
|
||||
// during running. However, we need to invoke listener onClose
|
||||
// manually in this case
|
||||
if (this.isRunningAnimation || this.noAnimationStartedSoFar) {
|
||||
if (this.isVisibleValue) {
|
||||
this.props.onOpen(false);
|
||||
} else {
|
||||
this.props.onClose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isVisible = new Value<Binary>(TRUE);
|
||||
private isVisibleValue: Binary = TRUE;
|
||||
private nextIsVisible = new Value<Binary | -1>(UNSET);
|
||||
@@ -389,19 +402,6 @@ export default class Card extends React.Component<Props> {
|
||||
},
|
||||
]);
|
||||
|
||||
componentWillUnmount(): void {
|
||||
// It might sometimes happen than animation will be unmounted
|
||||
// during running. However, we need to invoke listener onClose
|
||||
// manually in this case
|
||||
if (this.isRunningAnimation || this.noAnimationStartedSoFar) {
|
||||
if (this.isVisibleValue) {
|
||||
this.props.onOpen(false);
|
||||
} else {
|
||||
this.props.onClose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to ensure that this style doesn't change unless absolutely needs to
|
||||
// Changing it too often will result in huge frame drops due to detaching and attaching
|
||||
// Changing it during an animations can result in unexpected results
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import * as Screens from 'react-native-screens'; // Import with * as to prevent getters being called
|
||||
import { Route, NavigationHelpers, ParamListBase } from '@navigation-ex/core';
|
||||
import { StackNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
import { getDefaultHeaderHeight } from '../Header/HeaderSegment';
|
||||
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
||||
import StackItem from './StackItem';
|
||||
@@ -18,13 +22,11 @@ import {
|
||||
} from '../../TransitionConfigs/TransitionPresets';
|
||||
import { forNoAnimation } from '../../TransitionConfigs/HeaderStyleInterpolators';
|
||||
import {
|
||||
Route,
|
||||
Layout,
|
||||
HeaderMode,
|
||||
NavigationProp,
|
||||
HeaderScene,
|
||||
SceneDescriptorMap,
|
||||
NavigationStackOptions,
|
||||
StackDescriptorMap,
|
||||
StackNavigationOptions,
|
||||
} from '../../types';
|
||||
|
||||
type ProgressValues = {
|
||||
@@ -33,18 +35,21 @@ type ProgressValues = {
|
||||
|
||||
type Props = {
|
||||
mode: 'card' | 'modal';
|
||||
navigation: NavigationProp;
|
||||
descriptors: SceneDescriptorMap;
|
||||
routes: Route[];
|
||||
state: StackNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: StackDescriptorMap;
|
||||
routes: Route<string>[];
|
||||
openingRoutes: string[];
|
||||
closingRoutes: string[];
|
||||
onGoBack: (props: { route: Route }) => void;
|
||||
onOpenRoute: (props: { route: Route }) => void;
|
||||
onCloseRoute: (props: { route: Route }) => void;
|
||||
getPreviousRoute: (props: { route: Route }) => Route | undefined;
|
||||
getGesturesEnabled: (props: { route: Route }) => boolean;
|
||||
onGoBack: (props: { route: Route<string> }) => void;
|
||||
onOpenRoute: (props: { route: Route<string> }) => void;
|
||||
onCloseRoute: (props: { route: Route<string> }) => void;
|
||||
getPreviousRoute: (props: {
|
||||
route: Route<string>;
|
||||
}) => Route<string> | undefined;
|
||||
getGesturesEnabled: (props: { route: Route<string> }) => boolean;
|
||||
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
||||
renderScene: (props: { route: Route }) => React.ReactNode;
|
||||
renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
||||
headerMode: HeaderMode;
|
||||
onPageChangeStart?: () => void;
|
||||
onPageChangeConfirm?: () => void;
|
||||
@@ -52,9 +57,9 @@ type Props = {
|
||||
};
|
||||
|
||||
type State = {
|
||||
routes: Route[];
|
||||
descriptors: SceneDescriptorMap;
|
||||
scenes: HeaderScene<Route>[];
|
||||
routes: Route<string>[];
|
||||
descriptors: StackDescriptorMap;
|
||||
scenes: HeaderScene<Route<string>>[];
|
||||
progress: ProgressValues;
|
||||
layout: Layout;
|
||||
floatingHeaderHeights: { [key: string]: number };
|
||||
@@ -105,7 +110,7 @@ const { cond, eq } = Animated;
|
||||
const ANIMATED_ONE = new Animated.Value(1);
|
||||
|
||||
const getFloatingHeaderHeights = (
|
||||
routes: Route[],
|
||||
routes: Route<string>[],
|
||||
layout: Layout,
|
||||
previous: { [key: string]: number }
|
||||
) => {
|
||||
@@ -164,7 +169,8 @@ export default class Stack extends React.Component<Props, State> {
|
||||
const scene = {
|
||||
route,
|
||||
previous: previousRoute,
|
||||
descriptor: props.descriptors[route.key],
|
||||
descriptor:
|
||||
props.descriptors[route.key] || state.descriptors[route.key],
|
||||
progress: {
|
||||
current,
|
||||
next,
|
||||
@@ -237,7 +243,7 @@ export default class Stack extends React.Component<Props, State> {
|
||||
route,
|
||||
height,
|
||||
}: {
|
||||
route: Route;
|
||||
route: Route<string>;
|
||||
height: number;
|
||||
}) => {
|
||||
const previousHeight = this.state.floatingHeaderHeights[route.key];
|
||||
@@ -255,32 +261,30 @@ export default class Stack extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
private handleTransitionStart = (
|
||||
{ route }: { route: Route },
|
||||
{ route }: { route: Route<string> },
|
||||
closing: boolean
|
||||
) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
|
||||
descriptor &&
|
||||
descriptor.options.onTransitionStart &&
|
||||
descriptor.options.onTransitionStart({ closing });
|
||||
};
|
||||
) =>
|
||||
this.props.navigation.emit({
|
||||
type: 'transitionStart',
|
||||
data: { closing },
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
private handleTransitionEnd = (
|
||||
{ route }: { route: Route },
|
||||
{ route }: { route: Route<string> },
|
||||
closing: boolean
|
||||
) => {
|
||||
const descriptor = this.props.descriptors[route.key];
|
||||
|
||||
descriptor &&
|
||||
descriptor.options.onTransitionEnd &&
|
||||
descriptor.options.onTransitionEnd({ closing });
|
||||
};
|
||||
) =>
|
||||
this.props.navigation.emit({
|
||||
type: 'transitionEnd',
|
||||
data: { closing },
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
mode,
|
||||
descriptors,
|
||||
state,
|
||||
navigation,
|
||||
routes,
|
||||
closingRoutes,
|
||||
@@ -299,7 +303,7 @@ export default class Stack extends React.Component<Props, State> {
|
||||
|
||||
const { scenes, layout, progress, floatingHeaderHeights } = this.state;
|
||||
|
||||
const focusedRoute = navigation.state.routes[navigation.state.index];
|
||||
const focusedRoute = state.routes[state.index];
|
||||
const focusedDescriptor = descriptors[focusedRoute.key];
|
||||
const focusedOptions = focusedDescriptor ? focusedDescriptor.options : {};
|
||||
|
||||
@@ -353,7 +357,7 @@ export default class Stack extends React.Component<Props, State> {
|
||||
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
|
||||
} = descriptor
|
||||
? descriptor.options
|
||||
: ({} as NavigationStackOptions);
|
||||
: ({} as StackNavigationOptions);
|
||||
|
||||
return (
|
||||
<MaybeScreen
|
||||
@@ -373,6 +377,7 @@ export default class Stack extends React.Component<Props, State> {
|
||||
scene={scene}
|
||||
previousScene={scenes[index - 1]}
|
||||
navigation={navigation}
|
||||
state={state}
|
||||
cardTransparent={cardTransparent}
|
||||
cardOverlayEnabled={cardOverlayEnabled}
|
||||
cardShadowEnabled={cardShadowEnabled}
|
||||
@@ -408,7 +413,7 @@ export default class Stack extends React.Component<Props, State> {
|
||||
mode: 'float',
|
||||
layout,
|
||||
scenes,
|
||||
navigation,
|
||||
state,
|
||||
getPreviousRoute,
|
||||
onContentHeightChange: this.handleFloatingHeaderLayout,
|
||||
styleInterpolator:
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { StyleSheet, Platform, StyleProp, ViewStyle } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import { StackNavigationState } from '@navigation-ex/routers';
|
||||
import { Route, NavigationHelpers, ParamListBase } from '@navigation-ex/core';
|
||||
import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
|
||||
import Card from './Card';
|
||||
import {
|
||||
Route,
|
||||
HeaderScene,
|
||||
Layout,
|
||||
HeaderMode,
|
||||
NavigationProp,
|
||||
TransitionPreset,
|
||||
} from '../../types';
|
||||
import { HeaderScene, Layout, HeaderMode, TransitionPreset } from '../../types';
|
||||
|
||||
type Props = TransitionPreset & {
|
||||
index: number;
|
||||
@@ -19,22 +14,28 @@ type Props = TransitionPreset & {
|
||||
closing: boolean;
|
||||
layout: Layout;
|
||||
current: Animated.Value<number>;
|
||||
previousScene?: HeaderScene<Route>;
|
||||
scene: HeaderScene<Route>;
|
||||
navigation: NavigationProp;
|
||||
previousScene?: HeaderScene<Route<string>>;
|
||||
scene: HeaderScene<Route<string>>;
|
||||
state: StackNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
cardTransparent?: boolean;
|
||||
cardOverlayEnabled?: boolean;
|
||||
cardShadowEnabled?: boolean;
|
||||
cardStyle?: StyleProp<ViewStyle>;
|
||||
gestureEnabled?: boolean;
|
||||
getPreviousRoute: (props: { route: Route }) => Route | undefined;
|
||||
getPreviousRoute: (props: {
|
||||
route: Route<string>;
|
||||
}) => Route<string> | undefined;
|
||||
renderHeader: (props: HeaderContainerProps) => React.ReactNode;
|
||||
renderScene: (props: { route: Route }) => React.ReactNode;
|
||||
onOpenRoute: (props: { route: Route }) => void;
|
||||
onCloseRoute: (props: { route: Route }) => void;
|
||||
onGoBack: (props: { route: Route }) => void;
|
||||
onTransitionStart?: (props: { route: Route }, closing: boolean) => void;
|
||||
onTransitionEnd?: (props: { route: Route }, closing: boolean) => void;
|
||||
renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
||||
onOpenRoute: (props: { route: Route<string> }) => void;
|
||||
onCloseRoute: (props: { route: Route<string> }) => void;
|
||||
onGoBack: (props: { route: Route<string> }) => void;
|
||||
onTransitionStart?: (
|
||||
props: { route: Route<string> },
|
||||
closing: boolean
|
||||
) => void;
|
||||
onTransitionEnd?: (props: { route: Route<string> }, closing: boolean) => void;
|
||||
onPageChangeStart?: () => void;
|
||||
onPageChangeConfirm?: () => void;
|
||||
onPageChangeCancel?: () => void;
|
||||
@@ -90,7 +91,7 @@ export default class StackItem extends React.PureComponent<Props> {
|
||||
focused,
|
||||
closing,
|
||||
current,
|
||||
navigation,
|
||||
state,
|
||||
scene,
|
||||
previousScene,
|
||||
cardTransparent,
|
||||
@@ -151,7 +152,7 @@ export default class StackItem extends React.PureComponent<Props> {
|
||||
mode: 'screen',
|
||||
layout,
|
||||
scenes: [previousScene, scene],
|
||||
navigation,
|
||||
state,
|
||||
getPreviousRoute,
|
||||
styleInterpolator: headerStyleInterpolator,
|
||||
style: styles.header,
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { SceneView, StackActions } from '@react-navigation/core';
|
||||
import { ParamListBase, Route, NavigationHelpers } from '@navigation-ex/core';
|
||||
import { StackActions, StackNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
import Stack from './Stack';
|
||||
import HeaderContainer, {
|
||||
Props as HeaderContainerProps,
|
||||
} from '../Header/HeaderContainer';
|
||||
import {
|
||||
NavigationProp,
|
||||
NavigationStackConfig,
|
||||
Route,
|
||||
SceneDescriptorMap,
|
||||
} from '../../types';
|
||||
import { StackNavigationConfig, StackDescriptorMap } from '../../types';
|
||||
|
||||
type Props = {
|
||||
navigation: NavigationProp;
|
||||
descriptors: SceneDescriptorMap;
|
||||
navigationConfig: NavigationStackConfig;
|
||||
type Props = StackNavigationConfig & {
|
||||
state: StackNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: StackDescriptorMap;
|
||||
onPageChangeStart?: () => void;
|
||||
onPageChangeConfirm?: () => void;
|
||||
onPageChangeCancel?: () => void;
|
||||
screenProps?: unknown;
|
||||
};
|
||||
|
||||
type State = {
|
||||
// Local copy of the routes which are actually rendered
|
||||
routes: Route[];
|
||||
routes: Route<string>[];
|
||||
// List of routes being opened, we need to animate pushing of these new routes
|
||||
opening: string[];
|
||||
// List of routes being closed, we need to animate popping of these routes
|
||||
@@ -33,7 +29,7 @@ type State = {
|
||||
replacing: string[];
|
||||
// Since the local routes can vary from the routes from props, we need to keep the descriptors for old routes
|
||||
// Otherwise we won't be able to access the options for routes that were removed
|
||||
descriptors: SceneDescriptorMap;
|
||||
descriptors: StackDescriptorMap;
|
||||
};
|
||||
|
||||
class StackView extends React.Component<Props, State> {
|
||||
@@ -44,16 +40,14 @@ class StackView extends React.Component<Props, State> {
|
||||
// Here we determine which routes were added or removed to animate them
|
||||
// We keep a copy of the route being removed in local state to be able to animate it
|
||||
|
||||
const { navigation } = props;
|
||||
|
||||
let routes =
|
||||
navigation.state.index < navigation.state.routes.length - 1
|
||||
props.state.index < props.state.routes.length - 1
|
||||
? // Remove any extra routes from the state
|
||||
// The last visible route should be the focused route, i.e. at current index
|
||||
navigation.state.routes.slice(0, navigation.state.index + 1)
|
||||
: navigation.state.routes;
|
||||
props.state.routes.slice(0, props.state.index + 1)
|
||||
: props.state.routes;
|
||||
|
||||
if (navigation.state.index < navigation.state.routes.length - 1) {
|
||||
if (props.state.index < props.state.routes.length - 1) {
|
||||
console.warn(
|
||||
'StackRouter provided invalid state, index should always be the last route in the stack.'
|
||||
);
|
||||
@@ -85,7 +79,7 @@ class StackView extends React.Component<Props, State> {
|
||||
// We only need to animate routes if the focused route changed
|
||||
// Animating previous routes won't be visible coz the focused route is on top of everything
|
||||
|
||||
const isAnimationEnabled = (route: Route) => {
|
||||
const isAnimationEnabled = (route: Route<string>) => {
|
||||
const descriptor =
|
||||
props.descriptors[route.key] || state.descriptors[route.key];
|
||||
|
||||
@@ -155,7 +149,7 @@ class StackView extends React.Component<Props, State> {
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as SceneDescriptorMap
|
||||
{} as StackDescriptorMap
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -175,7 +169,7 @@ class StackView extends React.Component<Props, State> {
|
||||
descriptors: {},
|
||||
};
|
||||
|
||||
private getGesturesEnabled = ({ route }: { route: Route }) => {
|
||||
private getGesturesEnabled = ({ route }: { route: Route<string> }) => {
|
||||
const descriptor = this.state.descriptors[route.key];
|
||||
|
||||
if (descriptor) {
|
||||
@@ -195,7 +189,7 @@ class StackView extends React.Component<Props, State> {
|
||||
return false;
|
||||
};
|
||||
|
||||
private getPreviousRoute = ({ route }: { route: Route }) => {
|
||||
private getPreviousRoute = ({ route }: { route: Route<string> }) => {
|
||||
const { closing, replacing } = this.state;
|
||||
const routes = this.state.routes.filter(
|
||||
r =>
|
||||
@@ -207,7 +201,7 @@ class StackView extends React.Component<Props, State> {
|
||||
return routes[index - 1];
|
||||
};
|
||||
|
||||
private renderScene = ({ route }: { route: Route }) => {
|
||||
private renderScene = ({ route }: { route: Route<string> }) => {
|
||||
const descriptor =
|
||||
this.state.descriptors[route.key] || this.props.descriptors[route.key];
|
||||
|
||||
@@ -215,48 +209,37 @@ class StackView extends React.Component<Props, State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { navigation, getComponent } = descriptor;
|
||||
const SceneComponent = getComponent();
|
||||
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={this.props.screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
);
|
||||
return descriptor.render();
|
||||
};
|
||||
|
||||
private renderHeader = (props: HeaderContainerProps) => {
|
||||
return <HeaderContainer {...props} />;
|
||||
};
|
||||
|
||||
private handleTransitionComplete = () => {
|
||||
// TODO: remove when the new event system lands
|
||||
this.props.navigation.dispatch(StackActions.completeTransition());
|
||||
};
|
||||
private handleGoBack = ({ route }: { route: Route<string> }) => {
|
||||
const { state, navigation } = this.props;
|
||||
|
||||
private handleGoBack = ({ route }: { route: Route }) => {
|
||||
// This event will trigger when a gesture ends
|
||||
// We need to perform the transition before removing the route completely
|
||||
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
|
||||
navigation.dispatch({
|
||||
...StackActions.pop(),
|
||||
source: route.key,
|
||||
target: state.key,
|
||||
});
|
||||
};
|
||||
|
||||
private handleOpenRoute = ({ route }: { route: Route }) => {
|
||||
this.handleTransitionComplete();
|
||||
private handleOpenRoute = ({ route }: { route: Route<string> }) => {
|
||||
this.setState(state => ({
|
||||
routes: state.replacing.length
|
||||
? state.routes.filter(r => !state.replacing.includes(r.key))
|
||||
: state.routes,
|
||||
opening: state.opening.filter(key => key !== route.key),
|
||||
replacing: [],
|
||||
closing: state.closing.filter(key => key !== route.key),
|
||||
replacing: [],
|
||||
}));
|
||||
};
|
||||
|
||||
private handleCloseRoute = ({ route }: { route: Route }) => {
|
||||
this.handleTransitionComplete();
|
||||
|
||||
private handleCloseRoute = ({ route }: { route: Route<string> }) => {
|
||||
// This event will trigger when the animation for closing the route ends
|
||||
// In this case, we need to clean up any state tracking the route and pop it immediately
|
||||
|
||||
@@ -270,14 +253,15 @@ class StackView extends React.Component<Props, State> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
state,
|
||||
navigation,
|
||||
navigationConfig,
|
||||
onPageChangeStart,
|
||||
onPageChangeConfirm,
|
||||
onPageChangeCancel,
|
||||
mode = 'card',
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const { mode = 'card', ...config } = navigationConfig;
|
||||
const { routes, descriptors, opening, closing } = this.state;
|
||||
|
||||
const headerMode =
|
||||
@@ -300,9 +284,10 @@ class StackView extends React.Component<Props, State> {
|
||||
renderHeader={this.renderHeader}
|
||||
renderScene={this.renderScene}
|
||||
headerMode={headerMode}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
{...config}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
39
yarn.lock
39
yarn.lock
@@ -1742,11 +1742,6 @@
|
||||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/history@4.6.2":
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
|
||||
integrity sha512-eVAb52MJ4lfPLiO9VvTgv8KaZDEIqCwhv+lXOMLlt4C1YHTShgmMULEg0RrCbnqfYd6QKfHsMp0MiX0vWISpSw==
|
||||
|
||||
"@types/hoist-non-react-statics@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
@@ -6867,18 +6862,6 @@ hex-color-regex@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||
|
||||
history@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca"
|
||||
integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
loose-envify "^1.2.0"
|
||||
resolve-pathname "^2.2.0"
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
value-equal "^0.4.0"
|
||||
|
||||
hmac-drbg@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
@@ -8796,7 +8779,7 @@ loglevel@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
|
||||
integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA==
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@@ -12106,11 +12089,6 @@ resolve-global@1.0.0, resolve-global@^1.0.0:
|
||||
dependencies:
|
||||
global-dirs "^0.1.1"
|
||||
|
||||
resolve-pathname@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879"
|
||||
integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==
|
||||
|
||||
resolve-pkg@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-2.0.0.tgz#ac06991418a7623edc119084edc98b0e6bf05a41"
|
||||
@@ -13415,21 +13393,11 @@ timsort@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||
|
||||
tiny-invariant@^1.0.2:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
|
||||
integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
|
||||
|
||||
tiny-queue@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046"
|
||||
integrity sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY=
|
||||
|
||||
tiny-warning@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
@@ -14085,11 +14053,6 @@ validator@11.0.0:
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-11.0.0.tgz#fb10128bfb1fd14ce4ed36b79fc94289eae70667"
|
||||
integrity sha512-+wnGLYqaKV2++nUv60uGzUJyJQwYVOin6pn1tgEiFCeCQO60yeu3Og9/yPccbBX574kxIcEJicogkzx6s6eyag==
|
||||
|
||||
value-equal@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
|
||||
integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
|
||||
Reference in New Issue
Block a user