refactor: rewrite drawer layout with reanimated (#60)

This commit is contained in:
Satyajit Sahoo
2019-05-01 23:58:06 +02:00
parent 8c8cb8e758
commit f4fada9041
22 changed files with 1777 additions and 558 deletions

View File

@@ -55,6 +55,12 @@ jobs:
- store_artifacts:
path: coverage
destination: coverage
build:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: yarn prepare
workflows:
version: 2
@@ -70,3 +76,6 @@ workflows:
- unit-tests:
requires:
- install-dependencies
- build:
requires:
- install-dependencies

View File

@@ -0,0 +1,5 @@
/* eslint-disable import/no-commonjs */
module.exports = {
extends: ['@commitlint/config-conventional'],
};

View File

@@ -1,5 +1,12 @@
{
"presets": [
"expo"
],
"plugins": [
["module-resolver", {
"alias": {
"react-navigation-drawer": "../src/index"
}
}]
]
}

View File

@@ -1,7 +1,14 @@
{
"extends": '../.eslintrc',
'extends': '../.eslintrc',
"settings": {
"import/core-modules": [ "react-navigation-drawer", "react-native-gesture-handler", "react-native-vector-icons" ]
}
'settings':
{
'import/core-modules':
[
'react-navigation-drawer',
'react-native-gesture-handler',
'react-native-reanimated',
'react-native-vector-icons',
],
},
}

View File

@@ -10,8 +10,8 @@
"eject": "expo eject"
},
"dependencies": {
"@react-navigation/core": "^3.3.0",
"@react-navigation/native": "^3.3.0",
"@react-navigation/core": "^3.4.0",
"@react-navigation/native": "^3.4.1",
"expo": "32.0.6",
"hoist-non-react-statics": "^3.3.0",
"react": "16.5.0",

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import {
Animated,
Button,
Dimensions,
TextInput,
@@ -17,6 +16,7 @@ import { createStackNavigator } from 'react-navigation-stack';
import { SafeAreaView } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { createDrawerNavigator } from 'react-navigation-drawer';
import Animated from 'react-native-reanimated';
import { KeepAwake } from 'expo';
const SampleText = ({ children }) => <Text>{children}</Text>;
@@ -168,9 +168,9 @@ const DraftsStack = createStackNavigator(
const DrawerContents = ({ drawerOpenProgress, navigation }) => {
// `contentComponent` is passed an Animated.Value called drawerOpenProgress
// that can be used to do interesting things like a simple parallax drawe
const translateX = drawerOpenProgress.interpolate({
const translateX = Animated.interpolate(drawerOpenProgress, {
inputRange: [0, 1],
outputRange: [-50, 0],
outputRange: [-100, 0],
});
return (
@@ -210,7 +210,6 @@ function createDrawerExample(options = {}) {
{
overlayColor: 'rgba(0,0,0,0)',
drawerType: 'back',
useNativeAnimations: true,
contentContainerStyle: {
shadowColor: '#000000',
shadowOpacity: 0.4,

View File

@@ -176,7 +176,7 @@ function createDrawerExample(options = {}) {
},
{
initialRouteName: 'Drafts',
drawerWidth: 210,
drawerWidth: '60%',
navigationOptions: {
header: null,
},

View File

@@ -88,7 +88,7 @@ const DrawerExample = createDrawerNavigator(
activeTintColor: '#e91e63',
},
drawerType: 'back',
overlayColor: '#00000000',
overlayColor: 'rgba(233, 30, 99, 0.5)',
hideStatusBar: true,
}
);

View File

@@ -848,20 +848,20 @@
pouchdb-collections "^1.0.1"
tiny-queue "^0.2.1"
"@react-navigation/core@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-3.3.0.tgz#a8fa76e1c2a0da588da3d94ec9ea0956b7df753e"
integrity sha512-jCtvNnJu6CBctIvaGzL82xedWG0IQv+URwZfKQSkoUgiFViSsUhoDWHgnoRXAlWvR8Js7au3hrC/Cwshwhi9/w==
"@react-navigation/core@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-3.4.0.tgz#776845f9d4f8b2b9cb99c5d2d4433ebcef290d92"
integrity sha512-YAnx9mK6P/zYkvn4YxZL6thaNdouSmD7FUaftFrOAbE7y7cCfH8hmk7BOLoOet6Sh2+UnrpkWX7Kg54cT2Jw+g==
dependencies:
hoist-non-react-statics "^2.5.5"
hoist-non-react-statics "^3.3.0"
path-to-regexp "^1.7.0"
query-string "^6.2.0"
react-is "^16.6.3"
query-string "^6.4.2"
react-is "^16.8.6"
"@react-navigation/native@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-3.3.0.tgz#def7a94ef17581a404a3de2a3200f986e999dac1"
integrity sha512-w/+2B0qX441BpNkYb5QoPY8+Q4Q18adGTahVpc6o8Juj6odAxyIJ2RozXk7dCpN/w0dz4B+5ggqMKHVniE6K7w==
"@react-navigation/native@^3.4.1":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-3.4.1.tgz#e1fbf334ac834a9f10dd7d9c3af3e36939486089"
integrity sha512-pMAPQfvwC4DvhQfsrXKAf+FiU+A5XAh216v17rEePSFcbeOEt7cvewmWxCxydN/vFjJChFiPV+xnjJyJBdPLOg==
dependencies:
hoist-non-react-statics "^3.0.1"
react-native-safe-area-view "^0.13.0"
@@ -3157,7 +3157,7 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
hoist-non-react-statics@2.5.0, hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react-statics@^2.5.5, hoist-non-react-statics@^3.0.1, hoist-non-react-statics@^3.1.0:
hoist-non-react-statics@2.5.0, hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react-statics@^3.0.1, hoist-non-react-statics@^3.1.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
integrity sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w==
@@ -4831,7 +4831,7 @@ qs@^6.5.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
query-string@^6.2.0:
query-string@^6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.4.2.tgz#8be1dbd105306aebf86022144f575a29d516b713"
integrity sha512-DfJqAen17LfLA3rQ+H5S4uXphrF+ANU1lT2ijds4V/Tj4gZxA3gx5/tg1bz7kYCmwna7LyJNCYqO7jNRzo3aLw==
@@ -4882,7 +4882,7 @@ react-devtools-core@3.3.4:
shell-quote "^1.6.1"
ws "^3.3.1"
react-is@^16.6.3, react-is@^16.7.0:
react-is@^16.7.0, react-is@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
@@ -4906,7 +4906,7 @@ react-native-gesture-handler@~1.0.14:
invariant "^2.2.2"
prop-types "^15.5.10"
"react-native-maps@github:expo/react-native-maps#v0.22.1-exp.0":
react-native-maps@expo/react-native-maps#v0.22.1-exp.0:
version "0.22.1"
resolved "https://codeload.github.com/expo/react-native-maps/tar.gz/e6f98ff7272e5d0a7fe974a41f28593af2d77bb2"

View File

@@ -1,16 +1,64 @@
/* eslint-env jest */
jest.mock('react-native-gesture-handler/DrawerLayout', () => {
const React = require('react');
const View = require.requireActual('View');
const DrawerLayout = React.forwardRef((props, ref) => (
<View {...props} ref={ref} />
));
import NativeModules from 'NativeModules';
DrawerLayout.positions = {
Left: 'left',
Right: 'right',
};
return DrawerLayout;
Object.assign(NativeModules, {
RNGestureHandlerModule: {
attachGestureHandler: jest.fn(),
createGestureHandler: jest.fn(),
dropGestureHandler: jest.fn(),
updateGestureHandler: jest.fn(),
State: {},
Directions: {},
},
ReanimatedModule: {
createNode: jest.fn(),
configureProps: jest.fn(),
configureNativeProps: jest.fn(),
connectNodes: jest.fn(),
disconnectNodes: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn(),
},
PlatformConstants: {
forceTouchAvailable: false,
},
});
jest.mock('react-native-reanimated', () => ({
__esModule: true,
default: {
View: require('react-native').Animated.View,
Text: require('react-native').Animated.Text,
Clock: jest.fn(),
Value: jest.fn(),
onChange: jest.fn(),
interpolate: jest.fn(),
abs: jest.fn(),
add: jest.fn(),
sub: jest.fn(),
and: jest.fn(),
block: jest.fn(),
call: jest.fn(),
clockRunning: jest.fn(),
cond: jest.fn(),
divide: jest.fn(),
eq: jest.fn(),
event: jest.fn(),
greaterThan: jest.fn(),
lessThan: jest.fn(),
max: jest.fn(),
min: jest.fn(),
multiply: jest.fn(),
neq: jest.fn(),
or: jest.fn(),
set: jest.fn(),
spring: jest.fn(),
startClock: jest.fn(),
stopClock: jest.fn(),
timing: jest.fn(),
},
Easing: {
out: jest.fn(),
},
}));

View File

@@ -14,7 +14,8 @@
"test": "jest",
"lint": "eslint --ext .js,.ts,.tsx .",
"typescript": "tsc --noEmit",
"bootstrap": "yarn && yarn --cwd example",
"example": "yarn --cwd example",
"bootstrap": "yarn && yarn example",
"prepare": "bob build"
},
"keywords": [
@@ -38,30 +39,34 @@
"homepage": "https://github.com/react-navigation/react-navigation-drawer#readme",
"devDependencies": {
"@babel/core": "^7.4.3",
"@commitlint/config-conventional": "^7.5.0",
"@expo/vector-icons": "^10.0.1",
"@react-native-community/bob": "^0.3.3",
"@react-navigation/core": "^3.3.0",
"@react-navigation/native": "^3.3.0",
"@react-native-community/bob": "^0.3.4",
"@react-navigation/core": "^3.4.0",
"@react-navigation/native": "^3.4.1",
"@types/jest": "^24.0.11",
"@types/react": "^16.8.13",
"@types/react-native": "^0.57.43",
"@types/react-native": "^0.57.49",
"@types/react-test-renderer": "^16.8.1",
"babel-jest": "^24.7.1",
"commitlint": "^7.5.2",
"escape-string-regexp": "^1.0.5",
"eslint": "^5.16.0",
"eslint-config-satya164": "^2.4.1",
"eslint-plugin-react-native-globals": "^0.1.0",
"husky": "^1.3.1",
"jest": "^24.7.1",
"prettier": "^1.16.4",
"prettier": "^1.17.0",
"react": "16.5.0",
"react-dom": "16.5.0",
"react-lifecycles-compat": "^3.0.4",
"react-native": "~0.57.1",
"react-native-gesture-handler": "^1.1.0",
"react-native-reanimated": "^1.0.1",
"react-native-screens": "^1.0.0-alpha.22",
"react-native-testing-library": "^1.7.0",
"react-test-renderer": "16.8.6",
"typescript": "^3.4.3"
"typescript": "^3.4.5"
},
"peerDependencies": {
"@react-navigation/core": "^3.0.0",
@@ -69,6 +74,7 @@
"react": "*",
"react-native": "*",
"react-native-gesture-handler": "^1.0.12",
"react-native-reanimated": "^1.0.0",
"react-native-screens": "^1.0.0 || ^1.0.0-alpha"
},
"jest": {
@@ -97,7 +103,8 @@
},
"husky": {
"hooks": {
"pre-commit": "yarn lint && yarn typescript && yarn test"
"pre-commit": "yarn lint && yarn typescript && yarn test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"@react-native-community/bob": {
@@ -105,7 +112,8 @@
"output": "lib",
"targets": [
"commonjs",
"module"
"module",
"typescript"
]
}
}

View File

@@ -1,15 +1,15 @@
import * as DrawerAcions from './routers/DrawerActions';
/**
* Navigators
*/
/**
* Router
*/
import * as DrawerAcions from './routers/DrawerActions';
export {
default as createDrawerNavigator,
} from './navigators/createDrawerNavigator';
/**
* Router
*/
export { DrawerAcions };
export { default as DrawerRouter } from './routers/DrawerRouter';

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { View } from 'react-native';
import renderer from 'react-test-renderer';
import { render } from 'react-native-testing-library';
import { createAppContainer } from '@react-navigation/native';
import createDrawerNavigator from '../createDrawerNavigator';
@@ -21,62 +21,63 @@ class HomeScreen extends React.Component {
it('renders successfully', () => {
const MyDrawerNavigator = createDrawerNavigator({ Home: HomeScreen });
const App = createAppContainer(MyDrawerNavigator);
const rendered = renderer.create(<App />).toJSON();
const rendered = render(<App />).toJSON();
expect(rendered).toMatchInlineSnapshot(`
<RCTView
drawerBackgroundColor="white"
drawerPosition="left"
drawerType="front"
drawerWidth={320}
hideStatusBar={false}
keyboardDismissMode="on-drag"
onDrawerClose={[Function]}
onDrawerOpen={[Function]}
onDrawerStateChanged={[Function]}
onGestureRef={[Function]}
overlayColor="black"
renderNavigationView={[Function]}
statusBarAnimation="slide"
useNativeAnimations={true}
<View
collapsable={false}
onGestureHandlerEvent={[Function]}
onGestureHandlerStateChange={[Function]}
onLayout={[Function]}
style={
Object {
"flex": 1,
"overflow": "hidden",
}
}
>
<View
style={
Object {
"flex": 1,
"transform": Array [
Object {
"translateX": 0,
},
],
}
}
>
<View
collapsable={false}
pointerEvents="auto"
removeClippedSubviews={false}
style={
Array [
Object {
"flex": 1,
"overflow": "hidden",
},
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
Object {
"opacity": 1,
},
],
]
Object {
"flex": 1,
}
}
>
<View
collapsable={false}
pointerEvents="auto"
removeClippedSubviews={false}
style={
Object {
"flex": 1,
}
Array [
Object {
"flex": 1,
"overflow": "hidden",
},
Array [
Object {
"bottom": 0,
"left": 0,
"position": "absolute",
"right": 0,
"top": 0,
},
Object {
"opacity": 1,
},
],
]
}
>
<View
@@ -85,10 +86,150 @@ it('renders successfully', () => {
"flex": 1,
}
}
/>
>
<View
style={
Object {
"flex": 1,
}
}
/>
</View>
</View>
</View>
<View
collapsable={false}
onGestureHandlerEvent={[Function]}
onGestureHandlerStateChange={[Function]}
style={
Object {
"backgroundColor": "rgba(0, 0, 0, 0.5)",
"bottom": 0,
"left": 0,
"opacity": undefined,
"position": "absolute",
"right": 0,
"top": 0,
"zIndex": undefined,
}
}
/>
</View>
</RCTView>
<View
accessibilityViewIsModal={false}
onLayout={[Function]}
removeClippedSubviews={false}
style={
Object {
"backgroundColor": "white",
"bottom": 0,
"left": undefined,
"maxWidth": "100%",
"opacity": Object {},
"position": "absolute",
"top": 0,
"transform": Array [
Object {
"translateX": undefined,
},
],
"width": 320,
"zIndex": 0,
}
}
>
<View
style={
Array [
Object {
"flex": 1,
},
undefined,
]
}
>
<RCTScrollView
alwaysBounceVertical={false}
>
<View>
<View
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 20,
}
}
>
<View
style={
Array [
Object {
"paddingVertical": 4,
},
undefined,
]
}
>
<View
accessibilityLabel="Welcome anonymous"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Object {
"opacity": 1,
}
}
>
<View
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"alignItems": "center",
"backgroundColor": "rgba(0, 0, 0, .04)",
"flexDirection": "row",
"paddingBottom": 0,
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 0,
}
}
>
<Text
style={
Array [
Object {
"fontWeight": "bold",
"margin": 16,
},
Object {
"color": "#2196f3",
},
undefined,
undefined,
]
}
>
Welcome anonymous
</Text>
</View>
</View>
</View>
</View>
</View>
</RCTScrollView>
</View>
</View>
</View>
`);
});

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Dimensions, Platform, ScrollView } from 'react-native';
import { Dimensions, Platform, ScrollView, I18nManager } from 'react-native';
import { createNavigator } from '@react-navigation/core';
import { SafeAreaView } from '@react-navigation/native';
import DrawerRouter from '../routers/DrawerRouter';
@@ -35,14 +35,12 @@ const DefaultDrawerConfig = {
return Math.min(smallerAxisSize - appBarHeight, maxWidth);
},
contentComponent: defaultContentComponent,
drawerPosition: 'left',
drawerPosition: I18nManager.isRTL ? 'right' : 'left',
keyboardDismissMode: 'on-drag',
drawerBackgroundColor: 'white',
useNativeAnimations: true,
drawerType: 'front',
hideStatusBar: false,
statusBarAnimation: 'slide',
overlayColor: 'black',
};
const DrawerNavigator = (routeConfigs: object, config: any = {}) => {

View File

@@ -10,12 +10,7 @@ export const MARK_DRAWER_IDLE = 'Navigation/MARK_DRAWER_IDLE';
export type DrawerActionType =
| typeof OPEN_DRAWER
| typeof CLOSE_DRAWER
| typeof TOGGLE_DRAWER
| typeof DRAWER_OPENED
| typeof DRAWER_CLOSED
| typeof MARK_DRAWER_ACTIVE
| typeof MARK_DRAWER_SETTLING
| typeof MARK_DRAWER_IDLE;
| typeof TOGGLE_DRAWER;
export const openDrawer = (payload?: any) => ({
type: OPEN_DRAWER,
@@ -27,21 +22,6 @@ export const closeDrawer = (payload?: any) => ({
...payload,
});
export const markDrawerActive = (payload?: any) => ({
type: MARK_DRAWER_ACTIVE,
...payload,
});
export const markDrawerIdle = (payload?: any) => ({
type: MARK_DRAWER_IDLE,
...payload,
});
export const markDrawerSettling = (payload?: any) => ({
type: MARK_DRAWER_SETTLING,
...payload,
});
export const toggleDrawer = (payload?: any) => ({
type: TOGGLE_DRAWER,
...payload,

View File

@@ -15,8 +15,6 @@ type Action = {
type State = Route & {
isDrawerOpen?: any;
isDrawerIdle?: any;
drawerMovementDirection?: any;
};
function withDefaultValue(obj: object, key: string, defaultValue: any): any {
@@ -60,12 +58,6 @@ export default (
const switchRouter = SwitchRouter(routeConfigs, config);
let __id = -1;
const genId = () => {
__id++;
return __id;
};
return {
...switchRouter,
@@ -84,11 +76,6 @@ export default (
return {
...switchRouter.getStateForAction(action, undefined),
isDrawerOpen: false,
isDrawerIdle: true,
drawerMovementDirection: null,
openId: genId(),
closeId: genId(),
toggleId: genId(),
};
}
@@ -96,78 +83,27 @@ export default (
if (isRouterTargeted) {
// Only handle actions that are meant for this drawer, as specified by action.key.
if (action.type === DrawerActions.DRAWER_CLOSED) {
return {
...state,
isDrawerOpen: false,
isDrawerIdle: true,
drawerMovementDirection: null,
};
}
if (action.type === DrawerActions.DRAWER_OPENED) {
return {
...state,
isDrawerOpen: true,
isDrawerIdle: true,
drawerMovementDirection: null,
};
}
if (action.type === DrawerActions.CLOSE_DRAWER) {
return {
...state,
closeId: genId(),
};
}
if (action.type === DrawerActions.MARK_DRAWER_SETTLING) {
return {
...state,
isDrawerIdle: false,
drawerMovementDirection: action.willShow ? 'opening' : 'closing',
};
}
if (action.type === DrawerActions.MARK_DRAWER_ACTIVE) {
return {
...state,
isDrawerIdle: false,
drawerMovementDirection: null,
};
}
if (action.type === DrawerActions.MARK_DRAWER_IDLE) {
return {
...state,
isDrawerIdle: true,
drawerMovementDirection: null,
};
}
if (
action.type === NavigationActions.BACK &&
(state.isDrawerOpen || !state.isDrawerIdle) &&
state.drawerMovementDirection !== 'closing'
action.type === DrawerActions.CLOSE_DRAWER ||
(action.type === NavigationActions.BACK && state.isDrawerOpen)
) {
return {
...state,
closeId: genId(),
isDrawerOpen: false,
};
}
if (action.type === DrawerActions.OPEN_DRAWER) {
return {
...state,
openId: genId(),
isDrawerOpen: true,
};
}
if (action.type === DrawerActions.TOGGLE_DRAWER) {
return {
...state,
toggleId: genId(),
isDrawerOpen: !state.isDrawerOpen,
};
}
}
@@ -185,11 +121,11 @@ export default (
// If any navigation has happened, and the drawer is maybe open, make sure to close it
if (
getActiveRouteKey(switchedState) !== getActiveRouteKey(state) &&
(state.isDrawerOpen || state.drawerMovementDirection !== 'closing')
state.isDrawerOpen
) {
return {
...switchedState,
closeId: genId(),
isDrawerOpen: false,
};
}

View File

@@ -12,143 +12,136 @@ import * as DrawerActions from '../../routers/DrawerActions';
const INIT_ACTION = { type: NavigationActions.INIT };
describe('DrawerRouter', () => {
it('Handles basic drawer logic and fires close on switch', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
isDrawerIdle: true,
drawerMovementDirection: null,
openId: 0,
closeId: 1,
toggleId: 2,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
state
);
const expectedState2 = {
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
isDrawerIdle: true,
drawerMovementDirection: null,
openId: 0,
closeId: 3,
toggleId: 2,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
it('handles basic drawer logic and fires close on switch', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const expectedState = {
index: 0,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state).toEqual(expectedState);
const state2 = router.getStateForAction(
{ type: NavigationActions.NAVIGATE, routeName: 'Bar' },
state
);
const expectedState2 = {
index: 1,
isTransitioning: false,
routes: [
{ key: 'Foo', routeName: 'Foo', params: undefined },
{ key: 'Bar', routeName: 'Bar', params: undefined },
],
isDrawerOpen: false,
};
expect(state2).toEqual(expectedState2);
expect(router.getComponentForState(expectedState)).toEqual(ScreenA);
expect(router.getComponentForState(expectedState2)).toEqual(ScreenB);
});
it('Handles initial route navigation', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
const router = DrawerRouter(
{
Foo: {
screen: FooScreen,
},
Bar: {
screen: BarScreen,
},
it('handles initial route navigation', () => {
const FooScreen = () => <div />;
const BarScreen = () => <div />;
const router = DrawerRouter(
{
Foo: {
screen: FooScreen,
},
{ initialRouteName: 'Bar' }
);
const state = router.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
});
expect(state).toEqual({
index: 0,
isDrawerOpen: false,
isDrawerIdle: true,
drawerMovementDirection: null,
isTransitioning: false,
openId: 0,
closeId: 1,
toggleId: 2,
routes: [
{
key: 'Foo',
params: undefined,
routeName: 'Foo',
},
{
key: 'Bar',
params: undefined,
routeName: 'Bar',
},
],
});
Bar: {
screen: BarScreen,
},
},
{ initialRouteName: 'Bar' }
);
const state = router.getStateForAction({
type: NavigationActions.NAVIGATE,
routeName: 'Foo',
});
it('Drawer opens closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.toggleId).toEqual(2);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER },
state
);
expect(state2.openId).toEqual(3);
const state3 = router.getStateForAction(
{ type: DrawerActions.CLOSE_DRAWER },
state2
);
expect(state3.closeId).toEqual(4);
const state4 = router.getStateForAction(
{ type: DrawerActions.TOGGLE_DRAWER },
state3
);
expect(state4.toggleId).toEqual(5);
});
it('Drawer opens closes with key targeted', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
state
);
expect(state2.openId).toEqual(0);
const state3 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
state2
);
expect(state3.openId).toEqual(3);
expect(state).toEqual({
index: 0,
isDrawerOpen: false,
isTransitioning: false,
routes: [
{
key: 'Foo',
params: undefined,
routeName: 'Foo',
},
{
key: 'Bar',
params: undefined,
routeName: 'Bar',
},
],
});
});
it('Nested routers bubble up blocked actions', () => {
it('drawer opens, closes and toggles', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER },
state
);
expect(state2.isDrawerOpen).toEqual(true);
const state3 = router.getStateForAction(
{ type: DrawerActions.CLOSE_DRAWER },
state2
);
expect(state3.isDrawerOpen).toEqual(false);
const state4 = router.getStateForAction(
{ type: DrawerActions.TOGGLE_DRAWER },
state3
);
expect(state4.isDrawerOpen).toEqual(true);
});
it('drawer opens, closes with key targeted', () => {
const ScreenA = () => <div />;
const ScreenB = () => <div />;
const router = DrawerRouter({
Foo: { screen: ScreenA },
Bar: { screen: ScreenB },
});
const state = router.getStateForAction(INIT_ACTION);
const state2 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: 'wrong' },
state
);
expect(state2.isDrawerOpen).toEqual(false);
const state3 = router.getStateForAction(
{ type: DrawerActions.OPEN_DRAWER, key: state.key },
state2
);
expect(state3.isDrawerOpen).toEqual(true);
});
it('nested routers bubble up blocked actions', () => {
const ScreenA = () => <div />;
ScreenA.router = {
getStateForAction(action: { type: string }, lastState: any) {
@@ -167,7 +160,7 @@ it('Nested routers bubble up blocked actions', () => {
expect(state2).toEqual(null);
});
it('Drawer does not fire close when child routers return new state', () => {
it('drawer does not fire close when child routers return new state', () => {
const ScreenA = () => <div />;
ScreenA.router = {
getStateForAction(
@@ -184,14 +177,14 @@ it('Drawer does not fire close when child routers return new state', () => {
});
const state = router.getStateForAction(INIT_ACTION);
expect(state.closeId).toEqual(1);
expect(state.isDrawerOpen).toEqual(false);
const state2 = router.getStateForAction({ type: 'CHILD_ACTION' }, state);
expect(state2.closeId).toEqual(1);
expect(state2.isDrawerOpen).toEqual(false);
expect(state2.routes[0].changed).toEqual(true);
});
it('DrawerRouter will close drawer on child navigaton, not on child param changes', () => {
it('drawerRouter will close drawer on child navigaton, not on child param changes', () => {
class FooView extends React.Component {
render() {
return <div />;
@@ -217,13 +210,13 @@ it('DrawerRouter will close drawer on child navigaton, not on child param change
DrawerActions.openDrawer(),
emptyState
);
expect(initState.openId).toBe(3);
expect(initState.isDrawerOpen).toBe(true);
const state0 = router.getStateForAction(
NavigationActions.navigate({ routeName: 'Quo' }),
initState
);
expect(state0.closeId).toBe(4);
expect(state0.isDrawerOpen).toBe(false);
const initSwitchState = initState.routes[initState.index];
const initQuxState = initSwitchState.routes[initSwitchState.index];
@@ -237,7 +230,7 @@ it('DrawerRouter will close drawer on child navigaton, not on child param change
);
const state1switchState = state1.routes[state1.index];
const state1quxState = state1switchState.routes[state1switchState.index];
expect(state1.closeId).toBe(1); // don't fire close
expect(state1.isDrawerOpen).toBe(true); // don't fire close
expect(state1quxState.params.foo).toEqual('bar');
});
@@ -266,12 +259,6 @@ it('goBack closes drawer when inside of stack', () => {
);
expect(state3.index).toEqual(1);
expect(state3.routes[1].isDrawerOpen).toEqual(true);
expect(state3.routes[1].closeId).toEqual(1); // changed
const state4 = router.getStateForAction(NavigationActions.back(), state3);
expect(state4.routes[1].closeId).toEqual(4);
const state5 = router.getStateForAction(
{ type: DrawerActions.DRAWER_CLOSED },
state4
);
expect(state5.routes[1].isDrawerOpen).toEqual(false);
expect(state4.routes[1].isDrawerOpen).toEqual(false);
});

View File

@@ -17,10 +17,6 @@ export type Navigation = {
key: string;
index: number;
routes: Route[];
openId: string;
closeId: string;
toggleId: string;
isDrawerIdle: boolean;
isDrawerOpen: boolean;
};
openDrawer: () => void;

View File

@@ -0,0 +1,586 @@
import * as React from 'react';
import {
StyleSheet,
ViewStyle,
LayoutChangeEvent,
I18nManager,
Platform,
Keyboard,
StatusBar,
} from 'react-native';
import {
PanGestureHandler,
TapGestureHandler,
State,
TapGestureHandlerStateChangeEvent,
} from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
const {
Clock,
Value,
onChange,
clockRunning,
startClock,
stopClock,
interpolate,
spring,
abs,
add,
and,
block,
call,
cond,
divide,
eq,
event,
greaterThan,
lessThan,
max,
min,
multiply,
neq,
or,
set,
sub,
} = Animated;
const TRUE = 1;
const FALSE = 0;
const NOOP = 0;
const UNSET = -1;
const PROGRESS_EPSILON = 0.05;
const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;
const SWIPE_DISTANCE_THRESHOLD_DEFAULT = 60;
const SWIPE_DISTANCE_MINIMUM = 5;
const SPRING_CONFIG = {
damping: 30,
mass: 0.5,
stiffness: 150,
overshootClamping: true,
restSpeedThreshold: 0.001,
restDisplacementThreshold: 0.001,
};
type Binary = 0 | 1;
type Renderer = (props: { progress: Animated.Node<number> }) => React.ReactNode;
type Props = {
open: boolean;
onOpen: () => void;
onClose: () => void;
onGestureRef?: (ref: PanGestureHandler | null) => void;
locked: boolean;
drawerPosition: 'left' | 'right';
drawerType: 'front' | 'back' | 'slide';
keyboardDismissMode: 'none' | 'on-drag';
swipeEdgeWidth: number;
swipeDistanceThreshold?: number;
swipeVelocityThreshold: number;
hideStatusBar: boolean;
statusBarAnimation: 'slide' | 'none' | 'fade';
overlayStyle?: ViewStyle;
drawerStyle?: ViewStyle;
contentContainerStyle?: ViewStyle;
renderDrawerContent: Renderer;
renderSceneContent: Renderer;
};
export default class DrawerView extends React.PureComponent<Props> {
static defaultProps = {
locked: false,
drawerPostion: I18nManager.isRTL ? 'left' : 'right',
drawerType: 'front',
swipeEdgeWidth: 32,
swipeVelocityThreshold: 500,
keyboardDismissMode: 'on-drag',
hideStatusBar: false,
statusBarAnimation: 'slide',
};
componentDidUpdate(prevProps: Props) {
const {
open,
drawerPosition,
drawerType,
swipeDistanceThreshold,
swipeVelocityThreshold,
hideStatusBar,
} = this.props;
if (
// If we're not in the middle of a transition, sync the drawer's open state
typeof this.pendingOpenValue !== 'boolean' ||
open !== this.pendingOpenValue
) {
this.toggleDrawer(open);
}
this.pendingOpenValue = undefined;
if (open !== prevProps.open && hideStatusBar) {
this.toggleStatusBar(open);
}
if (prevProps.drawerPosition !== drawerPosition) {
this.drawerPosition.setValue(
drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT
);
}
if (prevProps.drawerType !== drawerType) {
this.isDrawerTypeFront.setValue(drawerType === 'front' ? TRUE : FALSE);
}
if (prevProps.swipeDistanceThreshold !== swipeDistanceThreshold) {
this.swipeDistanceThreshold.setValue(
swipeDistanceThreshold !== undefined
? swipeDistanceThreshold
: SWIPE_DISTANCE_THRESHOLD_DEFAULT
);
}
if (prevProps.swipeVelocityThreshold !== swipeVelocityThreshold) {
this.swipeVelocityThreshold.setValue(swipeVelocityThreshold);
}
}
componentWillUnmount() {
this.toggleStatusBar(false);
}
private clock = new Clock();
private isDrawerTypeFront = new Value<Binary>(
this.props.drawerType === 'front' ? TRUE : FALSE
);
private isOpen = new Value<Binary>(this.props.open ? TRUE : FALSE);
private nextIsOpen = new Value<Binary | -1>(UNSET);
private isSwiping = new Value<Binary>(FALSE);
private gestureState = new Value<number>(State.UNDETERMINED);
private touchX = new Value<number>(0);
private velocityX = new Value<number>(0);
private gestureX = new Value<number>(0);
private offsetX = new Value<number>(0);
private position = new Value<number>(0);
private containerWidth = new Value<number>(0);
private drawerWidth = new Value<number>(0);
private drawerOpacity = new Value<number>(0);
private drawerPosition = new Value<number>(
this.props.drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT
);
// Comment stolen from react-native-gesture-handler/DrawerLayout
//
// While closing the drawer when user starts gesture outside of its area (in greyed
// out part of the window), we want the drawer to follow only once finger reaches the
// edge of the drawer.
// E.g. on the diagram below drawer is illustrate by X signs and the greyed out area by
// dots. The touch gesture starts at '*' and moves left, touch path is indicated by
// an arrow pointing left
// 1) +---------------+ 2) +---------------+ 3) +---------------+ 4) +---------------+
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|.<-*..| |XXXXXXXX|<--*..| |XXXXX|<-----*..|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// +---------------+ +---------------+ +---------------+ +---------------+
//
// For the above to work properly we define animated value that will keep start position
// of the gesture. Then we use that value to calculate how much we need to subtract from
// the dragX. If the gesture started on the greyed out area we take the distance from the
// edge of the drawer to the start position. Otherwise we don't subtract at all and the
// drawer be pulled back as soon as you start the pan.
//
// This is used only when drawerType is "front"
private touchDistanceFromDrawer = cond(
this.isDrawerTypeFront,
cond(
eq(this.drawerPosition, DIRECTION_LEFT),
max(
// Distance of touch start from left screen edge - Drawer width
sub(sub(this.touchX, this.gestureX), this.drawerWidth),
0
),
min(
multiply(
// Distance of drawer from left screen edge - Touch start point
sub(
sub(this.containerWidth, this.drawerWidth),
sub(this.touchX, this.gestureX)
),
DIRECTION_RIGHT
),
0
)
),
0
);
private swipeDistanceThreshold = new Value<number>(
this.props.swipeDistanceThreshold !== undefined
? this.props.swipeDistanceThreshold
: SWIPE_DISTANCE_THRESHOLD_DEFAULT
);
private swipeVelocityThreshold = new Value<number>(
this.props.swipeVelocityThreshold
);
private currentOpenValue: boolean = this.props.open;
private pendingOpenValue: boolean | undefined;
private isStatusBarHidden: boolean = false;
private transitionTo = (isOpen: number | Animated.Node<number>) => {
const toValue = new Value(0);
const frameTime = new Value(0);
const state = {
position: this.position,
time: new Value(0),
finished: new Value(FALSE),
};
return block([
cond(clockRunning(this.clock), NOOP, [
// Animation wasn't running before
// Set the initial values and start the clock
set(toValue, multiply(isOpen, this.drawerWidth, this.drawerPosition)),
set(frameTime, 0),
set(state.time, 0),
set(state.finished, FALSE),
set(this.isOpen, isOpen),
startClock(this.clock),
]),
spring(
this.clock,
{ ...state, velocity: this.velocityX },
{ ...SPRING_CONFIG, toValue }
),
cond(state.finished, [
// Reset gesture and velocity from previous gesture
set(this.touchX, 0),
set(this.gestureX, 0),
set(this.velocityX, 0),
set(this.offsetX, 0),
// When the animation finishes, stop the clock
stopClock(this.clock),
call([this.isOpen], ([value]: ReadonlyArray<Binary>) => {
const open = Boolean(value);
if (open !== this.props.open) {
// Sync drawer's state after animation finished
// This shouldn't be necessary, but there seems to be an issue on iOS
this.toggleDrawer(this.props.open);
}
}),
]),
]);
};
private dragX = block([
onChange(
this.isOpen,
call([this.isOpen], ([value]: ReadonlyArray<Binary>) => {
const open = Boolean(value);
this.currentOpenValue = open;
// Without this check, the drawer can go to an infinite update <-> animate loop for sync updates
if (open !== this.props.open) {
// If the mode changed, update state
if (open) {
this.props.onOpen();
} else {
this.props.onClose();
}
this.pendingOpenValue = open;
// Force componentDidUpdate to fire, whether user does a setState or not
// This allows us to detect when the user drops the update and revert back
// It's necessary to make sure that the state stays in sync
this.forceUpdate();
}
})
),
onChange(
this.nextIsOpen,
cond(neq(this.nextIsOpen, UNSET), [
// Stop any running animations
cond(clockRunning(this.clock), stopClock(this.clock)),
// Update the open value to trigger the transition
set(this.isOpen, this.nextIsOpen),
set(this.nextIsOpen, UNSET),
])
),
// This block must be after the this.isOpen listener since we check for current value
onChange(
this.isSwiping,
// Listen to updates for this value only when it changes
// Without `onChange`, this will fire even if the value didn't change
// We don't want to call the listeners if the value didn't change
call([this.isSwiping], ([value]: ReadonlyArray<Binary>) => {
const { keyboardDismissMode } = this.props;
if (value === TRUE) {
if (keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
this.toggleStatusBar(true);
} else {
this.toggleStatusBar(this.currentOpenValue);
}
})
),
cond(
eq(this.gestureState, State.ACTIVE),
[
cond(this.isSwiping, NOOP, [
// We weren't dragging before, set it to true
set(this.isSwiping, TRUE),
// Also update the drag offset to the last position
set(this.offsetX, this.position),
]),
// Update position with previous offset + gesture distance
set(
this.position,
add(this.offsetX, this.gestureX, this.touchDistanceFromDrawer)
),
// Stop animations while we're dragging
stopClock(this.clock),
],
[
set(this.isSwiping, FALSE),
set(this.touchX, 0),
this.transitionTo(
cond(
or(
and(
greaterThan(abs(this.gestureX), SWIPE_DISTANCE_MINIMUM),
greaterThan(abs(this.velocityX), this.swipeVelocityThreshold)
),
greaterThan(abs(this.gestureX), this.swipeDistanceThreshold)
),
cond(
eq(this.drawerPosition, DIRECTION_LEFT),
// If swiped to right, open the drawer, otherwise close it
greaterThan(
cond(eq(this.velocityX, 0), this.gestureX, this.velocityX),
0
),
// If swiped to left, open the drawer, otherwise close it
lessThan(
cond(eq(this.velocityX, 0), this.gestureX, this.velocityX),
0
)
),
this.isOpen
)
),
]
),
this.position,
]);
private translateX = cond(
eq(this.drawerPosition, DIRECTION_RIGHT),
min(max(multiply(this.drawerWidth, -1), this.dragX), 0),
max(min(this.drawerWidth, this.dragX), 0)
);
private progress = cond(
// Check if the drawer width is available to avoid division by zero
eq(this.drawerWidth, 0),
0,
abs(divide(this.translateX, this.drawerWidth))
);
private handleGestureEvent = event([
{
nativeEvent: {
x: this.touchX,
translationX: this.gestureX,
velocityX: this.velocityX,
state: this.gestureState,
},
},
]);
private handleTapStateChange = ({
nativeEvent,
}: TapGestureHandlerStateChangeEvent) => {
if (nativeEvent.oldState === State.ACTIVE && !this.props.locked) {
this.toggleDrawer(false);
}
};
private handleContainerLayout = (e: LayoutChangeEvent) =>
this.containerWidth.setValue(e.nativeEvent.layout.width);
private handleDrawerLayout = (e: LayoutChangeEvent) => {
this.drawerWidth.setValue(e.nativeEvent.layout.width);
this.toggleDrawer(this.props.open);
// Until layout is available, drawer is hidden with opacity: 0 by default
// Show it in the next frame when layout is available
// If we don't delay it until the next frame, there's a visible flicker
requestAnimationFrame(() => this.drawerOpacity.setValue(1));
};
private toggleDrawer = (open: boolean) => {
this.nextIsOpen.setValue(open ? TRUE : FALSE);
// This value will also be set shortly after as changing this.nextIsOpen changes this.isOpen
// However, there's a race condition on Android, so we need to set a bit earlier
this.currentOpenValue = open;
};
private toggleStatusBar = (hidden: boolean) => {
const { hideStatusBar, statusBarAnimation } = this.props;
if (hideStatusBar && this.isStatusBarHidden !== hidden) {
this.isStatusBarHidden = hidden;
StatusBar.setHidden(hidden, statusBarAnimation);
}
};
render() {
const {
open,
locked,
drawerPosition,
drawerType,
swipeEdgeWidth,
contentContainerStyle,
drawerStyle,
overlayStyle,
onGestureRef,
renderDrawerContent,
renderSceneContent,
} = this.props;
const right = drawerPosition === 'right';
const contentTranslateX = drawerType === 'front' ? 0 : this.translateX;
const drawerTranslateX =
drawerType === 'back'
? I18nManager.isRTL
? multiply(this.drawerWidth, DIRECTION_RIGHT)
: this.drawerWidth
: this.translateX;
const offset = I18nManager.isRTL ? '100%' : multiply(this.drawerWidth, -1);
// FIXME: Currently hitSlop is broken when on Android when drawer is on right
// https://github.com/kmagiera/react-native-gesture-handler/issues/569
const hitSlop = right
? // Extend hitSlop to the side of the screen when drawer is closed
// This lets the user drag the drawer from the side of the screen
{ right: 0, width: open ? undefined : swipeEdgeWidth }
: { left: 0, width: open ? undefined : swipeEdgeWidth };
return (
<PanGestureHandler
ref={onGestureRef}
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
onGestureEvent={this.handleGestureEvent}
onHandlerStateChange={this.handleGestureEvent}
hitSlop={hitSlop}
enabled={!locked}
>
<Animated.View
onLayout={this.handleContainerLayout}
style={styles.main}
>
<Animated.View
style={[
styles.content,
{
transform: [{ translateX: contentTranslateX }],
},
contentContainerStyle as any,
]}
>
{renderSceneContent({ progress: this.progress })}
<TapGestureHandler onHandlerStateChange={this.handleTapStateChange}>
<Animated.View
style={[
styles.overlay,
{
opacity: interpolate(this.progress, {
inputRange: [PROGRESS_EPSILON, 1],
outputRange: [0, 1],
}),
// We don't want the user to be able to press through the overlay when drawer is open
// One approach is to adjust the pointerEvents based on the progress
// But we can also send the overlay behind the screen, which works, and is much less code
zIndex: cond(
greaterThan(this.progress, PROGRESS_EPSILON),
0,
-1
),
},
overlayStyle,
]}
/>
</TapGestureHandler>
</Animated.View>
<Animated.View
accessibilityViewIsModal={open}
removeClippedSubviews={Platform.OS !== 'ios'}
onLayout={this.handleDrawerLayout}
style={[
styles.container,
right ? { right: offset } : { left: offset },
{
transform: [{ translateX: drawerTranslateX }],
opacity: this.drawerOpacity,
zIndex: drawerType === 'back' ? -1 : 0,
},
drawerStyle as any,
]}
>
{renderDrawerContent({ progress: this.progress })}
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
position: 'absolute',
top: 0,
bottom: 0,
width: '80%',
maxWidth: '100%',
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
content: {
flex: 1,
},
main: {
flex: 1,
overflow: 'hidden',
},
});

View File

@@ -1,30 +1,28 @@
import * as React from 'react';
import { Dimensions, StyleSheet, ViewStyle, Animated } from 'react-native';
import { Dimensions, StyleSheet, ViewStyle } from 'react-native';
import { SceneView } from '@react-navigation/core';
import DrawerLayout from 'react-native-gesture-handler/DrawerLayout';
import { ScreenContainer } from 'react-native-screens';
import * as DrawerActions from '../routers/DrawerActions';
import DrawerSidebar, { ContentComponentProps } from './DrawerSidebar';
import DrawerGestureContext from '../utils/DrawerGestureContext';
import ResourceSavingScene from '../views/ResourceSavingScene';
import ResourceSavingScene from './ResourceSavingScene';
import Drawer from './Drawer';
import { Navigation } from '../types';
import { PanGestureHandler } from 'react-native-gesture-handler';
type DrawerOptions = {
drawerBackgroundColor?: string;
overlayColor?: string;
minSwipeDistance?: number;
drawerPosition: 'left' | 'right';
drawerType: 'front' | 'back' | 'slide';
drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open';
keyboardDismissMode?: 'on-drag' | 'none';
drawerType: 'front' | 'back' | 'slide';
drawerWidth: number | (() => number);
statusBarAnimation: 'slide' | 'none' | 'fade';
useNativeAnimations?: boolean;
onDrawerClose?: () => void;
onDrawerOpen?: () => void;
onDrawerStateChanged?: () => void;
drawerContainerStyle?: ViewStyle;
contentContainerStyle?: ViewStyle;
edgeWidth: number;
hideStatusBar?: boolean;
@@ -82,90 +80,36 @@ export default class DrawerView extends React.PureComponent<Props, State> {
};
componentDidMount() {
Dimensions.addEventListener('change', this._updateWidth);
}
componentDidUpdate(prevProps: Props) {
const {
openId,
closeId,
toggleId,
isDrawerOpen,
} = this.props.navigation.state;
const {
openId: prevOpenId,
closeId: prevCloseId,
toggleId: prevToggleId,
} = prevProps.navigation.state;
let prevIds = [prevOpenId, prevCloseId, prevToggleId];
let changedIds = [openId, closeId, toggleId]
.filter(id => !prevIds.includes(id))
// @ts-ignore
.sort((a, b) => a > b);
changedIds.forEach(id => {
if (id === openId) {
this._drawer.openDrawer();
} else if (id === closeId) {
this._drawer.closeDrawer();
} else if (id === toggleId) {
if (isDrawerOpen) {
this._drawer.closeDrawer();
} else {
this._drawer.openDrawer();
}
}
});
Dimensions.addEventListener('change', this.updateWidth);
}
componentWillUnmount() {
Dimensions.removeEventListener('change', this._updateWidth);
Dimensions.removeEventListener('change', this.updateWidth);
}
_drawer: typeof DrawerLayout;
private drawerGestureRef = React.createRef<PanGestureHandler>();
drawerGestureRef = React.createRef();
private handleDrawerOpen = () => {
const { navigation } = this.props;
_handleDrawerStateChange = (newState: string, willShow: boolean) => {
if (newState === 'Idle') {
if (!this.props.navigation.state.isDrawerIdle) {
this.props.navigation.dispatch({
type: DrawerActions.MARK_DRAWER_IDLE,
key: this.props.navigation.state.key,
});
}
} else if (newState === 'Settling') {
this.props.navigation.dispatch({
type: DrawerActions.MARK_DRAWER_SETTLING,
key: this.props.navigation.state.key,
willShow,
});
} else {
if (this.props.navigation.state.isDrawerIdle) {
this.props.navigation.dispatch({
type: DrawerActions.MARK_DRAWER_ACTIVE,
key: this.props.navigation.state.key,
});
}
}
navigation.dispatch(
DrawerActions.openDrawer({
key: navigation.state.key,
})
);
};
_handleDrawerOpen = () => {
this.props.navigation.dispatch({
type: DrawerActions.DRAWER_OPENED,
key: this.props.navigation.state.key,
});
private handleDrawerClose = () => {
const { navigation } = this.props;
navigation.dispatch(
DrawerActions.closeDrawer({
key: navigation.state.key,
})
);
};
_handleDrawerClose = () => {
this.props.navigation.dispatch({
type: DrawerActions.DRAWER_CLOSED,
key: this.props.navigation.state.key,
});
};
_updateWidth = () => {
private updateWidth = () => {
const drawerWidth =
typeof this.props.navigationConfig.drawerWidth === 'function'
? this.props.navigationConfig.drawerWidth()
@@ -176,27 +120,23 @@ export default class DrawerView extends React.PureComponent<Props, State> {
}
};
_renderNavigationView = (
drawerOpenProgress: Animated.AnimatedInterpolation
) => {
private renderNavigationView = ({ progress }: any) => {
return (
<DrawerGestureContext.Provider value={this.drawerGestureRef}>
<DrawerSidebar
screenProps={this.props.screenProps}
drawerOpenProgress={drawerOpenProgress}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
contentComponent={this.props.navigationConfig.contentComponent}
contentOptions={this.props.navigationConfig.contentOptions}
drawerPosition={this.props.navigationConfig.drawerPosition}
style={this.props.navigationConfig.style}
{...this.props.navigationConfig}
/>
</DrawerGestureContext.Provider>
<DrawerSidebar
screenProps={this.props.screenProps}
drawerOpenProgress={progress}
navigation={this.props.navigation}
descriptors={this.props.descriptors}
contentComponent={this.props.navigationConfig.contentComponent}
contentOptions={this.props.navigationConfig.contentOptions}
drawerPosition={this.props.navigationConfig.drawerPosition}
style={this.props.navigationConfig.style}
{...this.props.navigationConfig}
/>
);
};
_renderContent = () => {
private renderContent = () => {
let { lazy, navigation } = this.props;
let { loaded } = this.state;
let { routes } = navigation.state;
@@ -214,7 +154,7 @@ export default class DrawerView extends React.PureComponent<Props, State> {
);
} else {
return (
<ScreenContainer style={styles.pages}>
<ScreenContainer style={styles.content}>
{routes.map((route, index) => {
if (lazy && !loaded.includes(index)) {
// Don't render a screen if we've never navigated to it
@@ -246,67 +186,68 @@ export default class DrawerView extends React.PureComponent<Props, State> {
}
};
_setDrawerGestureRef = (ref: any) => {
private setDrawerGestureRef = (ref: PanGestureHandler | null) => {
// @ts-ignore
this.drawerGestureRef.current = ref;
};
render() {
const { navigation, screenProps } = this.props;
const { navigation } = this.props;
const {
drawerType,
drawerBackgroundColor,
overlayColor,
contentContainerStyle,
edgeWidth,
minSwipeDistance,
hideStatusBar,
statusBarAnimation,
} = this.props.navigationConfig;
const activeKey = navigation.state.routes[navigation.state.index].key;
const { drawerLockMode } = this.props.descriptors[activeKey].options;
const isOpen =
drawerLockMode === 'locked-closed'
? false
: drawerLockMode === 'locked-open'
? true
: this.props.navigation.state.isDrawerOpen;
return (
<DrawerLayout
ref={(c: any) => {
this._drawer = c;
}}
onGestureRef={this._setDrawerGestureRef}
drawerLockMode={
drawerLockMode ||
(typeof screenProps === 'object' &&
screenProps != null &&
// @ts-ignore
screenProps.drawerLockMode) ||
this.props.navigationConfig.drawerLockMode
}
drawerBackgroundColor={
this.props.navigationConfig.drawerBackgroundColor
}
keyboardDismissMode={this.props.navigationConfig.keyboardDismissMode}
drawerWidth={this.state.drawerWidth}
onDrawerOpen={this._handleDrawerOpen}
onDrawerClose={this._handleDrawerClose}
onDrawerStateChanged={this._handleDrawerStateChange}
useNativeAnimations={this.props.navigationConfig.useNativeAnimations}
renderNavigationView={this._renderNavigationView}
drawerPosition={
this.props.navigationConfig.drawerPosition === 'right'
? DrawerLayout.positions.Right
: DrawerLayout.positions.Left
}
/* props specific to react-native-gesture-handler/DrawerLayout */
drawerType={this.props.navigationConfig.drawerType}
edgeWidth={this.props.navigationConfig.edgeWidth}
hideStatusBar={this.props.navigationConfig.hideStatusBar}
statusBarAnimation={this.props.navigationConfig.statusBarAnimation}
minSwipeDistance={this.props.navigationConfig.minSwipeDistance}
overlayColor={this.props.navigationConfig.overlayColor}
drawerContainerStyle={this.props.navigationConfig.drawerContainerStyle}
contentContainerStyle={
this.props.navigationConfig.contentContainerStyle
}
>
<DrawerGestureContext.Provider value={this.drawerGestureRef}>
{this._renderContent()}
</DrawerGestureContext.Provider>
</DrawerLayout>
<DrawerGestureContext.Provider value={this.drawerGestureRef}>
<Drawer
open={isOpen}
locked={
drawerLockMode === 'locked-open' ||
drawerLockMode === 'locked-closed'
}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
onGestureRef={this.setDrawerGestureRef}
drawerType={drawerType}
drawerPosition={this.props.navigationConfig.drawerPosition}
contentContainerStyle={contentContainerStyle}
drawerStyle={{
backgroundColor: drawerBackgroundColor || 'white',
width: this.state.drawerWidth,
}}
overlayStyle={{
backgroundColor: overlayColor || 'rgba(0, 0, 0, 0.5)',
}}
swipeEdgeWidth={edgeWidth}
swipeDistanceThreshold={minSwipeDistance}
hideStatusBar={hideStatusBar}
statusBarAnimation={statusBarAnimation}
renderDrawerContent={this.renderNavigationView}
renderSceneContent={this.renderContent}
/>
</DrawerGestureContext.Provider>
);
}
}
const styles = StyleSheet.create({
pages: {
content: {
flex: 1,
},
});

View File

@@ -1 +0,0 @@
declare module 'react-native-gesture-handler/DrawerLayout';

File diff suppressed because it is too large Load Diff