refactor: refresh drawer according to latest material design guidelines

This commit is contained in:
satyajit.happy
2019-10-07 22:29:57 +02:00
parent d685e78fa9
commit 9ce8ec59fb
9 changed files with 321 additions and 358 deletions

View File

@@ -8,8 +8,9 @@ export {
/**
* Views
*/
export { default as DrawerNavigatorItems } from './views/DrawerNavigatorItems';
export { default as DrawerSidebar } from './views/DrawerSidebar';
export { default as DrawerItem } from './views/DrawerItem';
export { default as DrawerItemList } from './views/DrawerItemList';
export { default as DrawerContent } from './views/DrawerContent';
export { default as DrawerView } from './views/DrawerView';
/**
@@ -23,5 +24,5 @@ export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
export {
DrawerNavigationOptions,
DrawerNavigationProp,
DrawerNavigationItemsProps,
DrawerContentOptions,
} from './types';

View File

@@ -12,16 +12,11 @@ import { PanGestureHandler } from 'react-native-gesture-handler';
export type Scene = {
route: Route<string>;
index: number;
focused: boolean;
color?: string;
};
export type DrawerNavigationConfig = {
/**
* Custom background color for the drawer. Defaults to `white`.
*/
drawerBackgroundColor: string;
export type DrawerNavigationConfig<T = DrawerContentOptions> = {
/**
* Position of the drawer on the screen. Defaults to `left`.
*/
@@ -33,11 +28,6 @@ export type DrawerNavigationConfig = {
* - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
*/
drawerType: 'front' | 'back' | 'slide';
/**
* Number or a function which returns the width of the drawer.
* If a function is provided, it'll be called again when the screen's dimensions change.
*/
drawerWidth: number | (() => number);
/**
* How far from the edge of the screen the swipe gesture should activate.
*/
@@ -82,49 +72,51 @@ export type DrawerNavigationConfig = {
* Custom component used to render as the content of the drawer, for example, navigation items.
* Defaults to `DrawerItems`.
*/
contentComponent: React.ComponentType<ContentComponentProps>;
contentComponent: React.ComponentType<DrawerContentComponentProps<T>>;
/**
* Options for the content component which will be passed as props.
*/
contentOptions?: object;
contentOptions?: T;
/**
* Style object for the component wrapping the screen content.
*/
sceneContainerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
/**
* Style object for the drawer component.
* You can pass a custom background color for a drawer or a custom width here.
*/
drawerStyle?: StyleProp<ViewStyle>;
};
export type DrawerNavigationOptions = {
title?: string;
drawerLabel?:
| string
| ((props: { color?: string; focused: boolean }) => React.ReactElement);
| ((props: { color: string; focused: boolean }) => React.ReactNode);
drawerIcon?: (props: {
color?: string;
color: string;
size: number;
focused: boolean;
}) => React.ReactElement;
}) => React.ReactNode;
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open';
};
export type ContentComponentProps = DrawerNavigationItemsProps & {
export type DrawerContentComponentProps<T = DrawerContentOptions> = T & {
state: DrawerNavigationState;
navigation: NavigationHelpers<ParamListBase>;
descriptors: { [key: string]: any };
descriptors: DrawerDescriptorMap;
/**
* Animated node which represents the current progress of the drawer's open state.
* `0` is closed, `1` is open.
*/
drawerOpenProgress: Animated.Node<number>;
progress: Animated.Node<number>;
/**
* Position of the drawer on the screen.
*/
drawerPosition: 'left' | 'right';
};
export type DrawerNavigationItemsProps = {
/**
* The array of routes, can be modified or overridden to control what's shown in the drawer.
*/
items: Route<string>[];
/**
* Route key identifying the currently active route.
*/
activeItemKey?: string | null;
export type DrawerContentOptions = {
/**
* Color for the icon and label in the active item in the drawer.
*/
@@ -144,7 +136,7 @@ export type DrawerNavigationItemsProps = {
/**
* Style object for the content section.
*/
itemsContainerStyle?: ViewStyle;
contentContainerStyle?: ViewStyle;
/**
* Style object for the single item, which can contain an icon and/or a label.
*/
@@ -161,17 +153,6 @@ export type DrawerNavigationItemsProps = {
* Style object to overwrite `Text` style of the inactive label.
*/
inactiveLabelStyle?: StyleProp<TextStyle>;
/**
* Style object for the wrapper `View` of the icon.
*/
iconContainerStyle?: StyleProp<ViewStyle>;
/**
* Position of the drawer on the screen.
*/
drawerPosition: 'left' | 'right';
getLabel: (scene: Scene) => React.ReactNode;
renderIcon: (scene: Scene) => React.ReactNode;
onItemPress: (scene: { route: Route<string>; focused: boolean }) => void;
};
export type DrawerNavigationEventMap = {

View File

@@ -0,0 +1,36 @@
import * as React from 'react';
import { ScrollView } from 'react-native';
import { useSafeArea } from 'react-native-safe-area-context';
import DrawerItemList from './DrawerItemList';
import { DrawerContentComponentProps } from '../types';
export default function DrawerContent({
state,
navigation,
descriptors,
contentContainerStyle,
drawerPosition,
...rest
}: DrawerContentComponentProps) {
const insets = useSafeArea();
return (
<ScrollView
contentContainerStyle={[
{
paddingTop: insets.top + 4,
paddingLeft: drawerPosition === 'left' ? insets.left : 0,
paddingRight: drawerPosition === 'right' ? insets.right : 0,
},
contentContainerStyle,
]}
>
<DrawerItemList
state={state}
navigation={navigation}
descriptors={descriptors}
{...rest}
/>
</ScrollView>
);
}

View File

@@ -0,0 +1,116 @@
import * as React from 'react';
import {
Text,
View,
StyleSheet,
StyleProp,
ViewStyle,
TextStyle,
} from 'react-native';
import TouchableItem from './TouchableItem';
type Props = {
/**
* The label text of the item.
*/
label:
| string
| ((props: { focused: boolean; color: string }) => React.ReactNode);
/**
* Icon to display for the `DrawerItem`.
*/
icon?: (props: {
focused: boolean;
size: number;
color: string;
}) => React.ReactNode;
/**
* Whether to highlight the drawer item as active.
*/
focused: boolean;
/**
* Function to execute on press.
*/
onPress?: () => void;
/**
* Color for the icon and label.
*/
color: string;
/**
* Style object for the label element.
*/
labelStyle?: StyleProp<TextStyle>;
/**
* Style object for the wrapper element.
*/
style?: StyleProp<ViewStyle>;
};
/**
* A component used to show an action item with an icon and a label in a navigation drawer.
*/
export default function DrawerItem({
icon,
label,
focused,
color,
style,
onPress,
...rest
}: Props) {
const { borderRadius = 4 } = StyleSheet.flatten(style || {});
const iconNode = icon ? icon({ size: 24, focused, color }) : null;
return (
<View {...rest} style={[styles.container, { borderRadius }, style]}>
<TouchableItem
borderless
delayPressIn={0}
onPress={onPress}
style={[styles.wrapper, { borderRadius }]}
accessibilityTraits={focused ? ['button', 'selected'] : 'button'}
accessibilityComponentType="button"
accessibilityRole="button"
accessibilityStates={focused ? ['selected'] : []}
>
<React.Fragment>
{iconNode}
{typeof label === 'function' ? (
label({ color, focused })
) : (
<Text
numberOfLines={1}
style={[
styles.label,
{
color,
fontWeight: '500',
marginLeft: iconNode ? 32 : 0,
marginVertical: 5,
},
]}
>
{label}
</Text>
)}
</React.Fragment>
</TouchableItem>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginHorizontal: 10,
marginVertical: 4,
},
wrapper: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
label: {
marginRight: 32,
},
});

View File

@@ -0,0 +1,77 @@
import * as React from 'react';
import { CommonActions } from '@react-navigation/core';
import {
DrawerActions,
DrawerNavigationState,
} from '@react-navigation/routers';
import DrawerItem from './DrawerItem';
import {
DrawerNavigationHelpers,
DrawerDescriptorMap,
DrawerContentOptions,
} from '../types';
type Props = DrawerContentOptions & {
state: DrawerNavigationState;
navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap;
};
/**
* Component that renders the navigation list in the drawer.
*/
export default function DrawerItemList({
state,
navigation,
descriptors,
activeTintColor = '#6200ee',
inactiveTintColor = 'rgba(0, 0, 0, .68)',
activeBackgroundColor = 'rgba(98, 0, 238, 0.12)',
inactiveBackgroundColor = 'transparent',
itemStyle,
labelStyle,
activeLabelStyle,
inactiveLabelStyle,
}: Props) {
return (state.routes.map((route, i) => {
const focused = i === state.index;
const color = focused ? activeTintColor : inactiveTintColor;
const { title, drawerLabel, drawerIcon } = descriptors[route.key].options;
return (
<DrawerItem
key={route.key}
label={
drawerLabel !== undefined
? drawerLabel
: title !== undefined
? title
: route.name
}
icon={drawerIcon}
focused={focused}
color={color}
style={[
{
backgroundColor: focused
? activeBackgroundColor
: inactiveBackgroundColor,
},
itemStyle,
]}
labelStyle={[
labelStyle,
focused ? activeLabelStyle : inactiveLabelStyle,
]}
onPress={() => {
navigation.dispatch({
...(focused
? DrawerActions.closeDrawer()
: CommonActions.navigate(route.name)),
target: state.key,
});
}}
/>
);
}) as React.ReactNode) as React.ReactElement;
}

View File

@@ -1,129 +0,0 @@
import * as React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useSafeArea } from 'react-native-safe-area-context';
import TouchableItem from './TouchableItem';
import { DrawerNavigationItemsProps } from '../types';
/**
* Component that renders the navigation list in the drawer.
*/
const DrawerNavigatorItems = ({
items,
activeItemKey,
activeTintColor,
activeBackgroundColor,
inactiveTintColor,
inactiveBackgroundColor,
getLabel,
renderIcon,
onItemPress,
itemsContainerStyle,
itemStyle,
labelStyle,
activeLabelStyle,
inactiveLabelStyle,
iconContainerStyle,
drawerPosition,
}: DrawerNavigationItemsProps) => {
const insets = useSafeArea();
return (
<View style={[styles.container, itemsContainerStyle]}>
{items.map((route, index: number) => {
const focused = activeItemKey === route.key;
const color = focused ? activeTintColor : inactiveTintColor;
const backgroundColor = focused
? activeBackgroundColor
: inactiveBackgroundColor;
const scene = { route, index, focused, color };
const icon = renderIcon(scene);
const label = getLabel(scene);
const accessibilityLabel =
typeof label === 'string' ? label : undefined;
const extraLabelStyle = focused ? activeLabelStyle : inactiveLabelStyle;
return (
<TouchableItem
key={route.key}
accessible
accessibilityLabel={accessibilityLabel}
onPress={() => {
onItemPress({ route, focused });
}}
delayPressIn={0}
>
<View
style={[
{
backgroundColor,
marginLeft: drawerPosition === 'left' ? insets.left : 0,
marginRight: drawerPosition === 'right' ? insets.right : 0,
},
styles.item,
itemStyle,
]}
>
{icon ? (
<View
style={[
styles.icon,
focused ? null : styles.inactiveIcon,
iconContainerStyle,
]}
>
{icon}
</View>
) : null}
{typeof label === 'string' ? (
<Text
style={[styles.label, { color }, labelStyle, extraLabelStyle]}
>
{label}
</Text>
) : (
label
)}
</View>
</TouchableItem>
);
})}
</View>
);
};
/* Material design specs - https://material.io/guidelines/patterns/navigation-drawer.html#navigation-drawer-specs */
DrawerNavigatorItems.defaultProps = {
activeTintColor: '#2196f3',
activeBackgroundColor: 'rgba(0, 0, 0, .04)',
inactiveTintColor: 'rgba(0, 0, 0, .87)',
inactiveBackgroundColor: 'transparent',
};
const styles = StyleSheet.create({
container: {
paddingVertical: 4,
},
item: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginHorizontal: 16,
width: 24,
alignItems: 'center',
},
inactiveIcon: {
/*
* Icons have 0.54 opacity according to guidelines
* 100/87 * 54 ~= 62
*/
opacity: 0.62,
},
label: {
margin: 16,
fontWeight: 'bold',
},
});
export default DrawerNavigatorItems;

View File

@@ -1,128 +0,0 @@
import * as React from 'react';
import { StyleSheet, View, ViewStyle, StyleProp } from 'react-native';
import Animated from 'react-native-reanimated';
import { Route, CommonActions } from '@react-navigation/core';
import {
DrawerActions,
DrawerNavigationState,
} from '@react-navigation/routers';
import {
Scene,
ContentComponentProps,
DrawerDescriptorMap,
DrawerNavigationHelpers,
} from '../types';
type Props = {
contentComponent?: React.ComponentType<ContentComponentProps>;
contentOptions?: object;
state: DrawerNavigationState;
navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap;
drawerOpenProgress: Animated.Node<number>;
drawerPosition: 'left' | 'right';
style?: StyleProp<ViewStyle>;
};
/**
* Component that renders the sidebar screen of the drawer.
*/
class DrawerSidebar extends React.PureComponent<Props> {
private getScreenOptions = (routeKey: string) => {
const descriptor = this.props.descriptors[routeKey];
if (!descriptor.options) {
throw new Error(
'Cannot access screen descriptor options from drawer sidebar'
);
}
return descriptor.options;
};
private getLabel = ({ focused, color, route }: Scene) => {
const { drawerLabel, title } = this.getScreenOptions(route.key);
if (drawerLabel) {
return typeof drawerLabel === 'function'
? drawerLabel({ color, focused })
: drawerLabel;
}
if (typeof title === 'string') {
return title;
}
return route.name;
};
private renderIcon = ({ focused, color, route }: Scene) => {
const { drawerIcon } = this.getScreenOptions(route.key);
if (drawerIcon) {
return typeof drawerIcon === 'function'
? drawerIcon({ color, focused })
: drawerIcon;
}
return null;
};
private handleItemPress = ({
route,
focused,
}: {
route: Route<string>;
focused: boolean;
}) => {
const { state, navigation } = this.props;
navigation.dispatch({
...(focused
? DrawerActions.closeDrawer()
: CommonActions.navigate(route.name)),
target: state.key,
});
};
render() {
const ContentComponent = this.props.contentComponent;
if (!ContentComponent) {
return null;
}
const { state } = this.props;
if (typeof state.index !== 'number') {
throw new Error(
'The index of the route should be state in the navigation state'
);
}
return (
<View style={[styles.container, this.props.style]}>
<ContentComponent
{...this.props.contentOptions}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
drawerOpenProgress={this.props.drawerOpenProgress}
items={state.routes}
activeItemKey={
state.routes[state.index] ? state.routes[state.index].key : null
}
getLabel={this.getLabel}
renderIcon={this.renderIcon}
onItemPress={this.handleItemPress}
drawerPosition={this.props.drawerPosition}
/>
</View>
);
}
}
export default DrawerSidebar;
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -1,30 +1,35 @@
import * as React from 'react';
import { Dimensions, StyleSheet, I18nManager, Platform } from 'react-native';
import { SafeAreaProvider, useSafeArea } from 'react-native-safe-area-context';
import {
Dimensions,
StyleSheet,
I18nManager,
Platform,
ScaledSize,
} from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
// eslint-disable-next-line import/no-unresolved
import { ScreenContainer } from 'react-native-screens';
import { PanGestureHandler, ScrollView } from 'react-native-gesture-handler';
import { PanGestureHandler } from 'react-native-gesture-handler';
import {
DrawerNavigationState,
DrawerActions,
} from '@react-navigation/routers';
import DrawerSidebar from './DrawerSidebar';
import DrawerGestureContext from '../utils/DrawerGestureContext';
import ResourceSavingScene from './ResourceSavingScene';
import DrawerNavigatorItems from './DrawerNavigatorItems';
import DrawerContent from './DrawerContent';
import Drawer from './Drawer';
import {
DrawerDescriptorMap,
DrawerNavigationConfig,
ContentComponentProps,
DrawerNavigationHelpers,
} from '../types';
type Props = DrawerNavigationConfig & {
type Props = Omit<DrawerNavigationConfig, 'overlayColor'> & {
state: DrawerNavigationState;
navigation: DrawerNavigationHelpers;
descriptors: DrawerDescriptorMap;
overlayColor: string;
};
type State = {
@@ -32,17 +37,25 @@ type State = {
drawerWidth: number;
};
const DefaultContentComponent = (props: ContentComponentProps) => {
const insets = useSafeArea();
const getDefaultDrawerWidth = ({
height,
width,
}: {
height: number;
width: number;
}) => {
/*
* 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 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 (
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={{ marginTop: insets.top }}
>
<DrawerNavigatorItems {...props} />
</ScrollView>
);
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
};
/**
@@ -51,25 +64,10 @@ const DefaultContentComponent = (props: ContentComponentProps) => {
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,
contentComponent: DrawerContent,
drawerPosition: I18nManager.isRTL ? 'right' : 'left',
keyboardDismissMode: 'on-drag',
drawerBackgroundColor: 'white',
overlayColor: 'rgba(0, 0, 0, 0.5)',
drawerType: 'front',
hideStatusBar: false,
statusBarAnimation: 'slide',
@@ -88,10 +86,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
state: State = {
loaded: [this.props.state.index],
drawerWidth:
typeof this.props.drawerWidth === 'function'
? this.props.drawerWidth()
: this.props.drawerWidth,
drawerWidth: getDefaultDrawerWidth(Dimensions.get('window')),
};
componentDidMount() {
@@ -126,11 +121,8 @@ export default class DrawerView extends React.PureComponent<Props, State> {
navigation.emit({ type: 'drawerClose' });
};
private updateWidth = () => {
const drawerWidth =
typeof this.props.drawerWidth === 'function'
? this.props.drawerWidth()
: this.props.drawerWidth;
private updateWidth = ({ window }: { window: ScaledSize }) => {
const drawerWidth = getDefaultDrawerWidth(window);
if (this.state.drawerWidth !== drawerWidth) {
this.setState({ drawerWidth });
@@ -138,7 +130,17 @@ export default class DrawerView extends React.PureComponent<Props, State> {
};
private renderNavigationView = ({ progress }: any) => {
return <DrawerSidebar drawerOpenProgress={progress} {...this.props} />;
const { state, navigation, descriptors, drawerPosition } = this.props;
return (
<DrawerContent
progress={progress}
state={state}
navigation={navigation}
descriptors={descriptors}
drawerPosition={drawerPosition}
/>
);
};
private renderContent = () => {
@@ -192,9 +194,9 @@ export default class DrawerView extends React.PureComponent<Props, State> {
descriptors,
drawerType,
drawerPosition,
drawerBackgroundColor,
overlayColor,
sceneContainerStyle,
drawerStyle,
edgeWidth,
minSwipeDistance,
hideStatusBar,
@@ -202,6 +204,8 @@ export default class DrawerView extends React.PureComponent<Props, State> {
gestureHandlerProps,
} = this.props;
const { drawerWidth } = this.state;
const activeKey = state.routes[state.index].key;
const { drawerLockMode } = descriptors[activeKey].options;
@@ -228,13 +232,8 @@ export default class DrawerView extends React.PureComponent<Props, State> {
drawerType={drawerType}
drawerPosition={drawerPosition}
sceneContainerStyle={sceneContainerStyle}
drawerStyle={{
backgroundColor: drawerBackgroundColor || 'white',
width: this.state.drawerWidth,
}}
overlayStyle={{
backgroundColor: overlayColor || 'rgba(0, 0, 0, 0.5)',
}}
drawerStyle={[{ width: drawerWidth }, drawerStyle]}
overlayStyle={{ backgroundColor: overlayColor }}
swipeEdgeWidth={edgeWidth}
swipeDistanceThreshold={minSwipeDistance}
hideStatusBar={hideStatusBar}

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { ScrollView, AsyncStorage, YellowBox } from 'react-native';
import LinkingPrefixes from './LinkingPrefixes';
import { MaterialIcons } from '@expo/vector-icons';
import { Appbar, List } from 'react-native-paper';
import { Asset } from 'expo-asset';
import {
@@ -22,6 +22,7 @@ import {
StackNavigationProp,
} from '@react-navigation/stack';
import LinkingPrefixes from './LinkingPrefixes';
import SimpleStack from './Screens/SimpleStack';
import NativeStack from './Screens/NativeStack';
import ModalPresentationStack from './Screens/ModalPresentationStack';
@@ -34,11 +35,12 @@ import CompatAPI from './Screens/CompatAPI';
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
type RootDrawerParamList = {
root: undefined;
Root: undefined;
Another: undefined;
};
type RootStackParamList = {
home: undefined;
Home: undefined;
} & {
[P in keyof typeof SCREENS]: undefined;
};
@@ -141,7 +143,15 @@ export default function App() {
}
>
<Drawer.Navigator>
<Drawer.Screen name="root" options={{ title: 'Examples' }}>
<Drawer.Screen
name="Root"
options={{
title: 'Examples',
drawerIcon: ({ size, color }) => (
<MaterialIcons size={size} color={color} name="folder" />
),
}}
>
{({
navigation,
}: {
@@ -149,7 +159,7 @@ export default function App() {
}) => (
<Stack.Navigator>
<Stack.Screen
name="home"
name="Home"
options={{
title: 'Examples',
headerLeft: () => (