mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 12:55:21 +08:00
feat: implement various navigators
This commit is contained in:
21
packages/bottom-tabs/package.json
Normal file
21
packages/bottom-tabs/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@navigation-ex/bottom-tabs",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@navigation-ex/core": "^0.0.1",
|
||||
"@navigation-ex/routers": "^0.0.1",
|
||||
"react-native-safe-area-view": "^0.14.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.8.24",
|
||||
"@types/react-native": "^0.60.2",
|
||||
"react": "16.8.3",
|
||||
"react-native": "^0.59.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,13 @@
|
||||
export {
|
||||
default as createBottomTabNavigator,
|
||||
} from './navigators/createBottomTabNavigator';
|
||||
export {
|
||||
default as createMaterialTopTabNavigator,
|
||||
} from './navigators/createMaterialTopTabNavigator';
|
||||
|
||||
/**
|
||||
* Views
|
||||
*/
|
||||
export { default as BottomTabBar } from './views/BottomTabBar';
|
||||
export { default as MaterialTopTabBar } from './views/MaterialTopTabBar';
|
||||
|
||||
/**
|
||||
* Utils
|
||||
* Types
|
||||
*/
|
||||
export { default as createTabNavigator } from './utils/createTabNavigator';
|
||||
export { BottomTabNavigationOptions, BottomTabNavigationProp } from './types';
|
||||
|
||||
@@ -1,181 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
AccessibilityRole,
|
||||
AccessibilityState,
|
||||
} from 'react-native';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
|
||||
import createTabNavigator, {
|
||||
NavigationViewProps,
|
||||
} from '../utils/createTabNavigator';
|
||||
import BottomTabBar from '../views/BottomTabBar';
|
||||
import ResourceSavingScene from '../views/ResourceSavingScene';
|
||||
useNavigationBuilder,
|
||||
createNavigator,
|
||||
DefaultNavigatorOptions,
|
||||
} from '@navigation-ex/core';
|
||||
import {
|
||||
NavigationProp,
|
||||
Route,
|
||||
SceneDescriptor,
|
||||
NavigationBottomTabOptions,
|
||||
BottomTabBarOptions,
|
||||
TabRouter,
|
||||
TabRouterOptions,
|
||||
TabNavigationState,
|
||||
} from '@navigation-ex/routers';
|
||||
import BottomTabView from '../views/BottomTabView';
|
||||
import {
|
||||
BottomTabNavigationConfig,
|
||||
BottomTabNavigationOptions,
|
||||
} from '../types';
|
||||
|
||||
type Props = NavigationViewProps & {
|
||||
getAccessibilityRole: (props: {
|
||||
route: Route;
|
||||
}) => AccessibilityRole | undefined;
|
||||
getAccessibilityStates: (props: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
}) => AccessibilityState[];
|
||||
lazy?: boolean;
|
||||
tabBarComponent?: React.ComponentType<any>;
|
||||
tabBarOptions?: BottomTabBarOptions;
|
||||
navigation: NavigationProp;
|
||||
descriptors: { [key: string]: SceneDescriptor<NavigationBottomTabOptions> };
|
||||
screenProps?: unknown;
|
||||
};
|
||||
type Props = DefaultNavigatorOptions<BottomTabNavigationOptions> &
|
||||
TabRouterOptions &
|
||||
BottomTabNavigationConfig;
|
||||
|
||||
type State = {
|
||||
loaded: number[];
|
||||
};
|
||||
function BottomTabNavigator({
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation } = useNavigationBuilder<
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions,
|
||||
TabRouterOptions
|
||||
>(TabRouter, {
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
class TabNavigationView extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
lazy: true,
|
||||
getAccessibilityRole: (): AccessibilityRole => 'button',
|
||||
getAccessibilityStates: ({
|
||||
focused,
|
||||
}: {
|
||||
focused: boolean;
|
||||
}): AccessibilityState[] => (focused ? ['selected'] : []),
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const { index } = nextProps.navigation.state;
|
||||
|
||||
return {
|
||||
// Set the current tab to be loaded if it was not loaded before
|
||||
loaded: prevState.loaded.includes(index)
|
||||
? prevState.loaded
|
||||
: [...prevState.loaded, index],
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
loaded: [this.props.navigation.state.index],
|
||||
};
|
||||
|
||||
_getButtonComponent = ({ route }: { route: Route }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarButtonComponent) {
|
||||
return options.tabBarButtonComponent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
_renderTabBar = () => {
|
||||
const {
|
||||
tabBarComponent: TabBarComponent = BottomTabBar,
|
||||
tabBarOptions,
|
||||
navigation,
|
||||
screenProps,
|
||||
getLabelText,
|
||||
getAccessibilityLabel,
|
||||
getAccessibilityRole,
|
||||
getAccessibilityStates,
|
||||
getTestID,
|
||||
renderIcon,
|
||||
onTabPress,
|
||||
onTabLongPress,
|
||||
} = this.props;
|
||||
|
||||
const { descriptors } = this.props;
|
||||
const { state } = this.props.navigation;
|
||||
const route = state.routes[state.index];
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarVisible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabBarComponent
|
||||
{...tabBarOptions}
|
||||
jumpTo={this._jumpTo}
|
||||
navigation={navigation}
|
||||
screenProps={screenProps}
|
||||
onTabPress={onTabPress}
|
||||
onTabLongPress={onTabLongPress}
|
||||
getLabelText={getLabelText}
|
||||
getButtonComponent={this._getButtonComponent}
|
||||
getAccessibilityLabel={getAccessibilityLabel}
|
||||
getAccessibilityRole={getAccessibilityRole}
|
||||
getAccessibilityStates={getAccessibilityStates}
|
||||
getTestID={getTestID}
|
||||
renderIcon={renderIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_jumpTo = (key: string) => {
|
||||
const { navigation, onIndexChange } = this.props;
|
||||
|
||||
const index = navigation.state.routes.findIndex(route => route.key === key);
|
||||
|
||||
onIndexChange(index);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navigation, renderScene, lazy } = this.props;
|
||||
const { routes } = navigation.state;
|
||||
const { loaded } = this.state;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
if (lazy && !loaded.includes(index)) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFocused = navigation.state.index === index;
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
{renderScene({ route })}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
{this._renderTabBar()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<BottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pages: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default createTabNavigator<NavigationBottomTabOptions, Props>(
|
||||
TabNavigationView
|
||||
);
|
||||
export default createNavigator<
|
||||
BottomTabNavigationOptions,
|
||||
typeof BottomTabNavigator
|
||||
>(BottomTabNavigator);
|
||||
|
||||
@@ -1,201 +1,174 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AccessibilityRole,
|
||||
AccessibilityState,
|
||||
AccessibilityStates,
|
||||
StyleProp,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
Descriptor,
|
||||
Route,
|
||||
} from '@navigation-ex/core';
|
||||
import { TabNavigationState } from '@navigation-ex/routers';
|
||||
|
||||
export type Route = {
|
||||
key: string;
|
||||
routeName: string;
|
||||
} & (NavigationState | undefined);
|
||||
|
||||
export type NavigationEventName =
|
||||
| 'willFocus'
|
||||
| 'didFocus'
|
||||
| 'willBlur'
|
||||
| 'didBlur';
|
||||
|
||||
export type NavigationState = {
|
||||
key: string;
|
||||
index: number;
|
||||
routes: Route[];
|
||||
isTransitioning?: boolean;
|
||||
params?: { [key: string]: unknown };
|
||||
};
|
||||
|
||||
export type NavigationProp<RouteName = string, Params = object> = {
|
||||
emit(eventName: string): void;
|
||||
navigate(routeName: RouteName): void;
|
||||
goBack(): void;
|
||||
goBack(key: string | null): void;
|
||||
addListener: (
|
||||
event: NavigationEventName,
|
||||
callback: () => void
|
||||
) => { remove: () => void };
|
||||
isFocused(): boolean;
|
||||
state: NavigationState;
|
||||
setParams(params: Params): void;
|
||||
getParam(): Params;
|
||||
dispatch(action: { type: string }): void;
|
||||
dangerouslyGetParent(): NavigationProp | undefined;
|
||||
export type BottomTabNavigationEventMap = {
|
||||
refocus: undefined;
|
||||
tabPress: undefined;
|
||||
tabLongPress: undefined;
|
||||
};
|
||||
|
||||
export type Orientation = 'horizontal' | 'vertical';
|
||||
|
||||
export type LabelPosition = 'beside-icon' | 'below-icon';
|
||||
|
||||
export type BottomTabBarOptions = {
|
||||
keyboardHidesTabBar: boolean;
|
||||
activeTintColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
activeBackgroundColor?: string;
|
||||
inactiveBackgroundColor?: string;
|
||||
allowFontScaling: boolean;
|
||||
showLabel: boolean;
|
||||
showIcon: boolean;
|
||||
labelStyle: StyleProp<TextStyle>;
|
||||
tabStyle: StyleProp<ViewStyle>;
|
||||
labelPosition?:
|
||||
| LabelPosition
|
||||
| ((options: { deviceOrientation: Orientation }) => LabelPosition);
|
||||
adaptive?: boolean;
|
||||
style: StyleProp<ViewStyle>;
|
||||
export type BottomTabNavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string
|
||||
> = NavigationProp<
|
||||
ParamList,
|
||||
RouteName,
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions,
|
||||
BottomTabNavigationEventMap
|
||||
> & {
|
||||
/**
|
||||
* Jump to an existing tab.
|
||||
*
|
||||
* @param name Name of the route for the tab.
|
||||
* @param [params] Params object for the route.
|
||||
*/
|
||||
jumpTo<RouteName extends Extract<keyof ParamList, string>>(
|
||||
...args: ParamList[RouteName] extends void
|
||||
? [RouteName]
|
||||
: [RouteName, ParamList[RouteName]]
|
||||
): void;
|
||||
};
|
||||
|
||||
export type BottomTabBarProps = BottomTabBarOptions & {
|
||||
navigation: NavigationProp;
|
||||
onTabPress: (props: { route: Route }) => void;
|
||||
onTabLongPress: (props: { route: Route }) => void;
|
||||
getAccessibilityLabel: (props: { route: Route }) => string | undefined;
|
||||
getAccessibilityRole: (props: {
|
||||
route: Route;
|
||||
}) => AccessibilityRole | undefined;
|
||||
getAccessibilityStates: (props: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
}) => AccessibilityState[];
|
||||
getButtonComponent: (props: {
|
||||
route: Route;
|
||||
}) => React.ComponentType<any> | undefined;
|
||||
getLabelText: (props: {
|
||||
route: Route;
|
||||
}) =>
|
||||
| ((scene: {
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
}) => string | undefined)
|
||||
| string
|
||||
| undefined;
|
||||
getTestID: (props: { route: Route }) => string | undefined;
|
||||
renderIcon: (props: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
horizontal?: boolean;
|
||||
}) => React.ReactNode;
|
||||
dimensions: { width: number; height: number };
|
||||
isLandscape: boolean;
|
||||
safeAreaInset: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
};
|
||||
|
||||
export type MaterialTabBarOptions = {
|
||||
activeTintColor?: string;
|
||||
allowFontScaling?: boolean;
|
||||
bounces?: boolean;
|
||||
inactiveTintColor?: string;
|
||||
pressColor?: string;
|
||||
pressOpacity?: number;
|
||||
scrollEnabled?: boolean;
|
||||
showIcon?: boolean;
|
||||
showLabel?: boolean;
|
||||
upperCaseLabel?: boolean;
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
indicatorStyle?: StyleProp<ViewStyle>;
|
||||
iconStyle?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type MaterialTabBarProps = MaterialTabBarOptions & {
|
||||
layout: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
position: Animated.Node<number>;
|
||||
jumpTo: (key: string) => void;
|
||||
getLabelText: (scene: {
|
||||
route: Route;
|
||||
}) =>
|
||||
| ((scene: { focused: boolean; tintColor: string }) => string | undefined)
|
||||
| string
|
||||
| undefined;
|
||||
getAccessible?: (scene: { route: Route }) => boolean | undefined;
|
||||
getAccessibilityLabel: (scene: { route: Route }) => string | undefined;
|
||||
getTestID: (scene: { route: Route }) => string | undefined;
|
||||
renderIcon: (scene: {
|
||||
route: Route;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal?: boolean;
|
||||
}) => React.ReactNode;
|
||||
renderBadge?: (scene: { route: Route }) => React.ReactNode;
|
||||
onTabPress?: (scene: { route: Route }) => void;
|
||||
onTabLongPress?: (scene: { route: Route }) => void;
|
||||
tabBarPosition?: 'top' | 'bottom';
|
||||
screenProps: unknown;
|
||||
navigation: NavigationProp;
|
||||
};
|
||||
|
||||
export type NavigationCommonTabOptions = {
|
||||
export type BottomTabNavigationOptions = {
|
||||
/**
|
||||
* Title text for the screen.
|
||||
*/
|
||||
title?: string;
|
||||
tabBarLabel?: React.ReactNode;
|
||||
tabBarVisible?: boolean;
|
||||
tabBarAccessibilityLabel?: string;
|
||||
tabBarTestID?: string;
|
||||
|
||||
/**
|
||||
* Title string of a tab displayed in the tab bar or React Element
|
||||
* or a function that given { focused: boolean, tintColor: string } returns a React.Node, to display in tab bar.
|
||||
* When undefined, scene title is used. To hide, see tabBarOptions.showLabel in the previous section.
|
||||
*/
|
||||
tabBarLabel?:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode);
|
||||
|
||||
/**
|
||||
* React Element or a function that given { focused: boolean, tintColor: string } returns a React.Node, to display in the tab bar.
|
||||
*/
|
||||
tabBarIcon?:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
focused: boolean;
|
||||
tintColor?: string;
|
||||
horizontal?: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode);
|
||||
tabBarOnPress?: (props: {
|
||||
navigation: NavigationProp;
|
||||
defaultHandler: () => void;
|
||||
}) => void;
|
||||
tabBarOnLongPress?: (props: {
|
||||
navigation: NavigationProp;
|
||||
defaultHandler: () => void;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export type NavigationBottomTabOptions = NavigationCommonTabOptions & {
|
||||
tabBarButtonComponent?: React.ComponentType<BottomTabBarProps>;
|
||||
};
|
||||
/**
|
||||
* Accessibility label for the tab button. This is read by the screen reader when the user taps the tab.
|
||||
* It's recommended to set this if you don't have a label for the tab.
|
||||
*/
|
||||
tabBarAccessibilityLabel?: string;
|
||||
|
||||
export type NavigationMaterialTabOptions = NavigationCommonTabOptions & {
|
||||
/**
|
||||
* ID to locate this tab button in tests.
|
||||
*/
|
||||
tabBarTestID?: string;
|
||||
|
||||
/**
|
||||
* Boolean indicating whether the tab bar is visible when this screen is active.
|
||||
*/
|
||||
tabBarVisible?: boolean;
|
||||
|
||||
/**
|
||||
* Buttton component to render for the tab items instead of the default `TouchableWithoutFeedback`
|
||||
*/
|
||||
tabBarButtonComponent?: React.ComponentType<any>;
|
||||
swipeEnabled?: boolean | ((state: NavigationState) => boolean);
|
||||
};
|
||||
|
||||
export type SceneDescriptor<Options extends NavigationCommonTabOptions> = {
|
||||
key: string;
|
||||
options: Options;
|
||||
navigation: NavigationProp;
|
||||
getComponent(): React.ComponentType;
|
||||
export type BottomTabDescriptor = Descriptor<
|
||||
ParamListBase,
|
||||
string,
|
||||
TabNavigationState,
|
||||
BottomTabNavigationOptions
|
||||
>;
|
||||
|
||||
export type BottomTabDescriptorMap = {
|
||||
[key: string]: BottomTabDescriptor;
|
||||
};
|
||||
|
||||
export type Screen<
|
||||
Options extends NavigationCommonTabOptions
|
||||
> = React.ComponentType<any> & {
|
||||
navigationOptions?: Options & {
|
||||
[key: string]: any;
|
||||
};
|
||||
export type BottomTabNavigationConfig = {
|
||||
lazy?: boolean;
|
||||
tabBarComponent?: React.ComponentType<BottomTabBarProps>;
|
||||
tabBarOptions?: BottomTabBarOptions;
|
||||
};
|
||||
|
||||
export type BottomTabBarOptions = {
|
||||
keyboardHidesTabBar?: boolean;
|
||||
activeTintColor?: string;
|
||||
inactiveTintColor?: string;
|
||||
activeBackgroundColor?: string;
|
||||
inactiveBackgroundColor?: string;
|
||||
allowFontScaling?: boolean;
|
||||
showLabel?: boolean;
|
||||
showIcon?: boolean;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
tabStyle?: StyleProp<ViewStyle>;
|
||||
labelPosition?:
|
||||
| LabelPosition
|
||||
| ((options: { deviceOrientation: Orientation }) => LabelPosition);
|
||||
adaptive?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export type BottomTabBarProps = BottomTabBarOptions & {
|
||||
state: TabNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
onTabPress: (props: { route: Route<string> }) => void;
|
||||
onTabLongPress: (props: { route: Route<string> }) => void;
|
||||
getAccessibilityLabel: (props: {
|
||||
route: Route<string>;
|
||||
}) => string | undefined;
|
||||
getAccessibilityRole: (props: {
|
||||
route: Route<string>;
|
||||
}) => AccessibilityRole | undefined;
|
||||
getAccessibilityStates: (props: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => AccessibilityStates[];
|
||||
getButtonComponent: (props: {
|
||||
route: Route<string>;
|
||||
}) => React.ComponentType<any> | undefined;
|
||||
getLabelText: (props: {
|
||||
route: Route<string>;
|
||||
}) =>
|
||||
| ((scene: {
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
}) => React.ReactNode | undefined)
|
||||
| React.ReactNode;
|
||||
getTestID: (props: { route: Route<string> }) => string | undefined;
|
||||
renderIcon: (props: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode;
|
||||
safeAreaInset?: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
};
|
||||
|
||||
@@ -1,64 +1,40 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
TouchableWithoutFeedback,
|
||||
StyleSheet,
|
||||
View,
|
||||
Keyboard,
|
||||
Platform,
|
||||
LayoutChangeEvent,
|
||||
ScaledSize,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
import { Route } from '@navigation-ex/core';
|
||||
|
||||
import CrossFadeIcon from './CrossFadeIcon';
|
||||
import withDimensions from '../utils/withDimensions';
|
||||
import { Route, BottomTabBarProps } from '../types';
|
||||
import TabBarIcon from './TabBarIcon';
|
||||
import TouchableWithoutFeedbackWrapper from './TouchableWithoutFeedbackWrapper';
|
||||
import { BottomTabBarProps } from '../types';
|
||||
|
||||
type State = {
|
||||
dimensions: { height: number; width: number };
|
||||
layout: { height: number; width: number };
|
||||
keyboard: boolean;
|
||||
visible: Animated.Value;
|
||||
};
|
||||
|
||||
type Props = BottomTabBarProps & {
|
||||
activeTintColor: string;
|
||||
inactiveTintColor: string;
|
||||
safeAreaInset: React.ComponentProps<typeof SafeAreaView>['forceInset'];
|
||||
};
|
||||
|
||||
const majorVersion = parseInt(Platform.Version as string, 10);
|
||||
const isIos = Platform.OS === 'ios';
|
||||
const isIOS11 = majorVersion >= 11 && isIos;
|
||||
|
||||
const DEFAULT_MAX_TAB_ITEM_WIDTH = 125;
|
||||
|
||||
class TouchableWithoutFeedbackWrapper extends React.Component<
|
||||
React.ComponentProps<typeof TouchableWithoutFeedback> & {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
> {
|
||||
render() {
|
||||
const {
|
||||
onPress,
|
||||
onLongPress,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityRole,
|
||||
accessibilityStates,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
testID={testID}
|
||||
hitSlop={{ left: 15, right: 15, top: 0, bottom: 5 }}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityStates={accessibilityStates}
|
||||
>
|
||||
<View {...props} />
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
export default class TabBarBottom extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
keyboardHidesTabBar: true,
|
||||
activeTintColor: '#007AFF',
|
||||
@@ -75,32 +51,41 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
};
|
||||
|
||||
state = {
|
||||
dimensions: Dimensions.get('window'),
|
||||
layout: { height: 0, width: 0 },
|
||||
keyboard: false,
|
||||
visible: new Animated.Value(1),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this.handleOrientationChange);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
Keyboard.addListener('keyboardWillShow', this._handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardWillHide', this._handleKeyboardHide);
|
||||
Keyboard.addListener('keyboardWillShow', this.handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardWillHide', this.handleKeyboardHide);
|
||||
} else {
|
||||
Keyboard.addListener('keyboardDidShow', this._handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardDidHide', this._handleKeyboardHide);
|
||||
Keyboard.addListener('keyboardDidShow', this.handleKeyboardShow);
|
||||
Keyboard.addListener('keyboardDidHide', this.handleKeyboardHide);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this.handleOrientationChange);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
Keyboard.removeListener('keyboardWillShow', this._handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardWillHide', this._handleKeyboardHide);
|
||||
Keyboard.removeListener('keyboardWillShow', this.handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardWillHide', this.handleKeyboardHide);
|
||||
} else {
|
||||
Keyboard.removeListener('keyboardDidShow', this._handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardDidHide', this._handleKeyboardHide);
|
||||
Keyboard.removeListener('keyboardDidShow', this.handleKeyboardShow);
|
||||
Keyboard.removeListener('keyboardDidHide', this.handleKeyboardHide);
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyboardShow = () =>
|
||||
private handleOrientationChange = ({ window }: { window: ScaledSize }) => {
|
||||
this.setState({ dimensions: window });
|
||||
};
|
||||
|
||||
private handleKeyboardShow = () =>
|
||||
this.setState({ keyboard: true }, () =>
|
||||
Animated.timing(this.state.visible, {
|
||||
toValue: 0,
|
||||
@@ -109,7 +94,7 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
}).start()
|
||||
);
|
||||
|
||||
_handleKeyboardHide = () =>
|
||||
private handleKeyboardHide = () =>
|
||||
Animated.timing(this.state.visible, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
@@ -118,7 +103,7 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
this.setState({ keyboard: false });
|
||||
});
|
||||
|
||||
_handleLayout = (e: LayoutChangeEvent) => {
|
||||
private handleLayout = (e: LayoutChangeEvent) => {
|
||||
const { layout } = this.state;
|
||||
const { height, width } = e.nativeEvent.layout;
|
||||
|
||||
@@ -134,7 +119,13 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
});
|
||||
};
|
||||
|
||||
_renderLabel = ({ route, focused }: { route: Route; focused: boolean }) => {
|
||||
private renderLabel = ({
|
||||
route,
|
||||
focused,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => {
|
||||
const {
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
@@ -149,7 +140,7 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
}
|
||||
|
||||
const label = this.props.getLabelText({ route });
|
||||
const horizontal = this._shouldUseHorizontalLabels();
|
||||
const horizontal = this.shouldUseHorizontalLabels();
|
||||
const tintColor = focused ? activeTintColor : inactiveTintColor;
|
||||
|
||||
if (typeof label === 'string') {
|
||||
@@ -180,7 +171,13 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
return label;
|
||||
};
|
||||
|
||||
_renderIcon = ({ route, focused }: { route: Route; focused: boolean }) => {
|
||||
private renderIcon = ({
|
||||
route,
|
||||
focused,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
}) => {
|
||||
const {
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
@@ -188,17 +185,18 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
showIcon,
|
||||
showLabel,
|
||||
} = this.props;
|
||||
|
||||
if (showIcon === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const horizontal = this._shouldUseHorizontalLabels();
|
||||
const horizontal = this.shouldUseHorizontalLabels();
|
||||
|
||||
const activeOpacity = focused ? 1 : 0;
|
||||
const inactiveOpacity = focused ? 0 : 1;
|
||||
|
||||
return (
|
||||
<CrossFadeIcon
|
||||
<TabBarIcon
|
||||
route={route}
|
||||
horizontal={horizontal}
|
||||
activeOpacity={activeOpacity}
|
||||
@@ -215,15 +213,11 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
);
|
||||
};
|
||||
|
||||
_shouldUseHorizontalLabels = () => {
|
||||
const { routes } = this.props.navigation.state;
|
||||
const {
|
||||
isLandscape,
|
||||
dimensions,
|
||||
adaptive,
|
||||
tabStyle,
|
||||
labelPosition,
|
||||
} = this.props;
|
||||
private shouldUseHorizontalLabels = () => {
|
||||
const { state, adaptive, tabStyle, labelPosition } = this.props;
|
||||
const { dimensions } = this.state;
|
||||
const { routes } = state;
|
||||
const isLandscape = dimensions.width > dimensions.height;
|
||||
|
||||
if (labelPosition) {
|
||||
let position;
|
||||
@@ -266,23 +260,27 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
navigation,
|
||||
state,
|
||||
keyboardHidesTabBar,
|
||||
activeBackgroundColor,
|
||||
inactiveBackgroundColor,
|
||||
onTabPress,
|
||||
onTabLongPress,
|
||||
getAccessibilityLabel,
|
||||
getAccessibilityRole,
|
||||
getAccessibilityStates,
|
||||
getButtonComponent,
|
||||
getTestID,
|
||||
safeAreaInset,
|
||||
style,
|
||||
tabStyle,
|
||||
} = this.props;
|
||||
|
||||
const { routes } = navigation.state;
|
||||
const { routes } = state;
|
||||
|
||||
const tabBarStyle = [
|
||||
styles.tabBar,
|
||||
// @ts-ignore
|
||||
this._shouldUseHorizontalLabels() && !Platform.isPad
|
||||
this.shouldUseHorizontalLabels() && !Platform.isPad
|
||||
? styles.tabBarCompact
|
||||
: styles.tabBarRegular,
|
||||
style,
|
||||
@@ -313,33 +311,30 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
pointerEvents={
|
||||
keyboardHidesTabBar && this.state.keyboard ? 'none' : 'auto'
|
||||
}
|
||||
onLayout={this._handleLayout}
|
||||
onLayout={this.handleLayout}
|
||||
>
|
||||
<SafeAreaView style={tabBarStyle} forceInset={safeAreaInset}>
|
||||
{routes.map((route, index) => {
|
||||
const focused = index === navigation.state.index;
|
||||
const focused = index === state.index;
|
||||
const scene = { route, focused };
|
||||
const accessibilityLabel = this.props.getAccessibilityLabel({
|
||||
const accessibilityLabel = getAccessibilityLabel({
|
||||
route,
|
||||
});
|
||||
|
||||
const accessibilityRole = this.props.getAccessibilityRole({
|
||||
const accessibilityRole = getAccessibilityRole({
|
||||
route,
|
||||
});
|
||||
|
||||
const accessibilityStates = this.props.getAccessibilityStates(
|
||||
scene
|
||||
);
|
||||
const accessibilityStates = getAccessibilityStates(scene);
|
||||
|
||||
const testID = this.props.getTestID({ route });
|
||||
const testID = getTestID({ route });
|
||||
|
||||
const backgroundColor = focused
|
||||
? activeBackgroundColor
|
||||
: inactiveBackgroundColor;
|
||||
|
||||
const ButtonComponent =
|
||||
this.props.getButtonComponent({ route }) ||
|
||||
TouchableWithoutFeedbackWrapper;
|
||||
getButtonComponent({ route }) || TouchableWithoutFeedbackWrapper;
|
||||
|
||||
return (
|
||||
<ButtonComponent
|
||||
@@ -353,14 +348,14 @@ class TabBarBottom extends React.Component<BottomTabBarProps, State> {
|
||||
style={[
|
||||
styles.tab,
|
||||
{ backgroundColor },
|
||||
this._shouldUseHorizontalLabels()
|
||||
this.shouldUseHorizontalLabels()
|
||||
? styles.tabLandscape
|
||||
: styles.tabPortrait,
|
||||
tabStyle,
|
||||
]}
|
||||
>
|
||||
{this._renderIcon(scene)}
|
||||
{this._renderLabel(scene)}
|
||||
{this.renderIcon(scene)}
|
||||
{this.renderLabel(scene)}
|
||||
</ButtonComponent>
|
||||
);
|
||||
})}
|
||||
@@ -427,5 +422,3 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default withDimensions(TabBarBottom);
|
||||
|
||||
238
packages/bottom-tabs/src/views/BottomTabView.tsx
Normal file
238
packages/bottom-tabs/src/views/BottomTabView.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
AccessibilityRole,
|
||||
AccessibilityStates,
|
||||
} from 'react-native';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
ParamListBase,
|
||||
Route,
|
||||
BaseActions,
|
||||
} from '@navigation-ex/core';
|
||||
import { TabNavigationState } from '@navigation-ex/routers';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { ScreenContainer } from 'react-native-screens';
|
||||
|
||||
import BottomTabBar from './BottomTabBar';
|
||||
import { BottomTabNavigationConfig, BottomTabDescriptorMap } from '../types';
|
||||
import ResourceSavingScene from './ResourceSavingScene';
|
||||
|
||||
type Props = BottomTabNavigationConfig & {
|
||||
state: TabNavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
descriptors: BottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
type State = {
|
||||
loaded: number[];
|
||||
};
|
||||
|
||||
export default class BottomTabView extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
lazy: true,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const { index } = nextProps.state;
|
||||
|
||||
return {
|
||||
// Set the current tab to be loaded if it was not loaded before
|
||||
loaded: prevState.loaded.includes(index)
|
||||
? prevState.loaded
|
||||
: [...prevState.loaded, index],
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
loaded: [this.props.state.index],
|
||||
};
|
||||
|
||||
private getButtonComponent = ({ route }: { route: Route<string> }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarButtonComponent) {
|
||||
return options.tabBarButtonComponent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private renderIcon = ({
|
||||
route,
|
||||
focused,
|
||||
tintColor,
|
||||
horizontal,
|
||||
}: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarIcon) {
|
||||
return typeof options.tabBarIcon === 'function'
|
||||
? options.tabBarIcon({ focused, tintColor, horizontal })
|
||||
: options.tabBarIcon;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
private getLabelText = ({ route }: { route: Route<string> }) => {
|
||||
const { descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarLabel !== undefined) {
|
||||
return options.tabBarLabel;
|
||||
}
|
||||
|
||||
if (typeof options.title === 'string') {
|
||||
return options.title;
|
||||
}
|
||||
|
||||
return route.name;
|
||||
};
|
||||
|
||||
private getAccessibilityLabel = ({ route }: { route: Route<string> }) => {
|
||||
const { state, descriptors } = this.props;
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (typeof options.tabBarAccessibilityLabel !== 'undefined') {
|
||||
return options.tabBarAccessibilityLabel;
|
||||
}
|
||||
|
||||
const label = this.getLabelText({ route });
|
||||
|
||||
if (typeof label === 'string') {
|
||||
return `${label}, tab, ${state.routes.indexOf(route) + 1} of ${
|
||||
state.routes.length
|
||||
}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private getAccessibilityRole = (): AccessibilityRole => 'button';
|
||||
|
||||
private getAccessibilityStates = ({
|
||||
focused,
|
||||
}: {
|
||||
focused: boolean;
|
||||
}): AccessibilityStates[] => (focused ? ['selected'] : []);
|
||||
|
||||
private getTestID = ({ route }: { route: Route<string> }) =>
|
||||
this.props.descriptors[route.key].options.tabBarTestID;
|
||||
|
||||
private handleTabPress = ({ route }: { route: Route<string> }) => {
|
||||
const { state, navigation } = this.props;
|
||||
const event = this.props.navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
if (state.routes[state.index].key === route.key) {
|
||||
navigation.emit({
|
||||
type: 'refocus',
|
||||
target: route.key,
|
||||
});
|
||||
} else if (!event.defaultPrevented) {
|
||||
navigation.dispatch({
|
||||
...BaseActions.navigate(route.name),
|
||||
target: state.key,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleTabLongPress = ({ route }: { route: Route<string> }) => {
|
||||
this.props.navigation.emit({
|
||||
type: 'tabLongPress',
|
||||
target: route.key,
|
||||
});
|
||||
};
|
||||
|
||||
private renderTabBar = () => {
|
||||
const {
|
||||
tabBarComponent: TabBarComponent = BottomTabBar,
|
||||
tabBarOptions,
|
||||
state,
|
||||
navigation,
|
||||
} = this.props;
|
||||
|
||||
const { descriptors } = this.props;
|
||||
const route = state.routes[state.index];
|
||||
const descriptor = descriptors[route.key];
|
||||
const options = descriptor.options;
|
||||
|
||||
if (options.tabBarVisible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabBarComponent
|
||||
{...tabBarOptions}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
onTabPress={this.handleTabPress}
|
||||
onTabLongPress={this.handleTabLongPress}
|
||||
getLabelText={this.getLabelText}
|
||||
getButtonComponent={this.getButtonComponent}
|
||||
getAccessibilityLabel={this.getAccessibilityLabel}
|
||||
getAccessibilityRole={this.getAccessibilityRole}
|
||||
getAccessibilityStates={this.getAccessibilityStates}
|
||||
getTestID={this.getTestID}
|
||||
renderIcon={this.renderIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, descriptors, lazy } = this.props;
|
||||
const { routes } = state;
|
||||
const { loaded } = this.state;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScreenContainer style={styles.pages}>
|
||||
{routes.map((route, index) => {
|
||||
if (lazy && !loaded.includes(index)) {
|
||||
// Don't render a screen if we've never navigated to it
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFocused = state.index === index;
|
||||
|
||||
return (
|
||||
<ResourceSavingScene
|
||||
key={route.key}
|
||||
style={StyleSheet.absoluteFill}
|
||||
isVisible={isFocused}
|
||||
>
|
||||
{descriptors[route.key].render()}
|
||||
</ResourceSavingScene>
|
||||
);
|
||||
})}
|
||||
</ScreenContainer>
|
||||
{this.renderTabBar()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pages: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
69
packages/bottom-tabs/src/views/TabBarIcon.tsx
Normal file
69
packages/bottom-tabs/src/views/TabBarIcon.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
||||
import { Route } from '@navigation-ex/core';
|
||||
|
||||
type Props = {
|
||||
route: Route<string>;
|
||||
horizontal: boolean;
|
||||
activeOpacity: number;
|
||||
inactiveOpacity: number;
|
||||
activeTintColor: string;
|
||||
inactiveTintColor: string;
|
||||
renderIcon: (props: {
|
||||
route: Route<string>;
|
||||
focused: boolean;
|
||||
tintColor: string;
|
||||
horizontal: boolean;
|
||||
}) => React.ReactNode;
|
||||
style: StyleProp<ViewStyle>;
|
||||
};
|
||||
|
||||
export default function TabBarIcon({
|
||||
route,
|
||||
activeOpacity,
|
||||
inactiveOpacity,
|
||||
activeTintColor,
|
||||
inactiveTintColor,
|
||||
renderIcon,
|
||||
horizontal,
|
||||
style,
|
||||
}: Props) {
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them.
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={[styles.icon, { opacity: activeOpacity }]}>
|
||||
{renderIcon({
|
||||
route,
|
||||
focused: true,
|
||||
horizontal,
|
||||
tintColor: activeTintColor,
|
||||
})}
|
||||
</View>
|
||||
<View style={[styles.icon, { opacity: inactiveOpacity }]}>
|
||||
{renderIcon({
|
||||
route,
|
||||
focused: false,
|
||||
horizontal,
|
||||
tintColor: inactiveTintColor,
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
// We render the icon twice at the same position on top of each other:
|
||||
// active and inactive one, so we can fade between them:
|
||||
// Cover the whole iconContainer:
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
// Workaround for react-native >= 0.54 layout bug
|
||||
minWidth: 25,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { TouchableWithoutFeedback, View } from 'react-native';
|
||||
|
||||
export default function TouchableWithoutFeedbackWrapper({
|
||||
onPress,
|
||||
onLongPress,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityRole,
|
||||
accessibilityStates,
|
||||
...rest
|
||||
}: React.ComponentProps<typeof TouchableWithoutFeedback> & {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
testID={testID}
|
||||
hitSlop={{ left: 15, right: 15, top: 0, bottom: 5 }}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessibilityStates={accessibilityStates}
|
||||
>
|
||||
<View {...rest} />
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user