feat: implement various navigators

This commit is contained in:
satyajit.happy
2019-08-19 00:52:09 +05:30
parent 4878d18abf
commit f0b80ce0f6
36 changed files with 1563 additions and 935 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,14 @@
/**
* Navigators
*/
export {
default as createMaterialBottomTabNavigator,
} from './navigators/createMaterialBottomTabNavigator';
/**
* Types
*/
export {
MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationProp,
} from './types';

View File

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

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

View File

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

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

View File

@@ -29,5 +29,9 @@ export { CardStyleInterpolators, HeaderStyleInterpolators, TransitionPresets };
/**
* Utilities
*/
export { default as StackGestureContext } from './utils/StackGestureContext';
/**
* Types
*/
export { StackNavigationOptions, StackNavigationProp } from './types';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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