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

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