diff --git a/packages/react-native-drawer-layout/LICENSE b/packages/react-native-drawer-layout/LICENSE
new file mode 100644
index 0000000000..001d6a9bd1
--- /dev/null
+++ b/packages/react-native-drawer-layout/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 React Native Community
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/react-native-drawer-layout/README.md b/packages/react-native-drawer-layout/README.md
new file mode 100644
index 0000000000..4130b7d381
--- /dev/null
+++ b/packages/react-native-drawer-layout/README.md
@@ -0,0 +1,257 @@
+# React Native Drawer Layout
+
+A cross-platform Drawer component for React Native. Implemented using [`react-native-gesture-handler`](https://docs.swmansion.com/react-native-gesture-handler/) and [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/).
+
+Note that swipe gestures are only supported on iOS and Android.
+
+## Installation
+
+Open a Terminal in the project root and run:
+
+```sh
+npm install react-native-drawer-layout
+```
+
+Then, you need to install and configure the libraries that are required by the drawer:
+
+1. First, install [`react-native-gesture-handler`](https://docs.swmansion.com/react-native-gesture-handler/) and [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/).
+
+ If you have a Expo managed project, in your project directory, run:
+
+ ```sh
+ npx expo install react-native-gesture-handler react-native-reanimated
+ ```
+
+ If you have a bare React Native project, in your project directory, run:
+
+ ```bash npm2yarn
+ npm install react-native-gesture-handler react-native-reanimated
+ ```
+
+ The Drawer supports both Reanimated 1 and Reanimated 2. If you want to use Reanimated 2, make sure to configure it following the [installation guide](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation).
+
+2. To finalize installation of `react-native-gesture-handler`, add the following at the **top** (make sure it's at the top and there's nothing else before it) of your entry file, such as `index.js` or `App.js`:
+
+ ```js
+ import 'react-native-gesture-handler';
+ ```
+
+ > Note: If you are building for Android or iOS, do not skip this step, or your app may crash in production even if it works fine in development. This is not applicable to other platforms.
+
+3. If you're on a Mac and developing for iOS, you also need to install the pods (via [Cocoapods](https://cocoapods.org/)) to complete the linking.
+
+```sh
+npx pod-install ios
+```
+
+We're done! Now you can build and run the app on your device/simulator.
+
+## Quick Start
+
+```js
+import * as React from 'react';
+import { Button, Text } from 'react-native';
+import { Drawer } from 'react-native-drawer-layout';
+
+export default function DrawerExample() {
+ const [open, setOpen] = React.useState(false);
+
+ return (
+ setOpen(true)}
+ onClose={() => setOpen(false)}
+ renderDrawerContent={() => {
+ return Drawer content;
+ }}
+ >
+
+ );
+}
+```
+
+## API reference
+
+The package exports a `Drawer` component which is the one you'd use to render the drawer.
+
+### `Drawer`
+
+Component responsible for rendering a drawer with animations and gestures.
+
+#### Drawer Props
+
+##### `open`
+
+Whether the drawer is open or not.
+
+##### `onOpen`
+
+Callback which is called when the drawer is opened.
+
+##### `onClose`
+
+Callback which is called when the drawer is closed.
+
+##### `renderDrawerContent`
+
+Callback which returns a react element to render as the content of the drawer.
+
+##### `layout`
+
+Object containing the layout of the container. Defaults to the dimensions of the application's window.
+
+##### `drawerPosition`
+
+Position of the drawer on the screen. Defaults to `right` in RTL mode, otherwise `left`.
+
+##### `drawerType`
+
+Type of the drawer. It determines how the drawer looks and animates.
+
+- `front`: Traditional drawer which covers the screen with a overlay behind it.
+- `back`: The drawer is revealed behind the screen on swipe.
+- `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
+- `permanent`: A permanent drawer is shown as a sidebar.
+
+Defaults to `slide` on iOS and `front` on other platforms.
+
+##### `drawerStyle`
+
+Style object for the drawer. You can pass a custom background color for drawer or a custom width for the drawer.
+
+##### `overlayStyle`
+
+Style object for the overlay.
+
+##### `hideStatusBarOnOpen`
+
+Whether to hide the status bar when the drawer is open. Defaults to `false`.
+
+##### `keyboardDismissMode`
+
+Whether to dismiss the keyboard when the drawer is open. Supported values are:
+
+- `none`: The keyboard will not be dismissed when the drawer is open.
+- `on-drag`: The keyboard will be dismissed when the drawer is opened by a swipe gesture.
+
+Defaults to `on-drag`.
+
+##### `statusBarAnimation`
+
+Animation to use when the status bar is hidden. Supported values are:
+
+- `slide`: The status bar will slide out of view.
+- `fade`: The status bar will fade out of view.
+- `none`: The status bar will not animate.
+
+Use it in combination with `hideStatusBarOnOpen`.
+
+##### `swipeEnabled`
+
+Whether to enable swipe gestures to open the drawer. Defaults to `true`.
+
+This is not supported on web.
+
+##### `swipeEdgeWidth`
+
+How far from the edge of the screen the swipe gesture should activate. Defaults to `32`.
+
+This is not supported on web.
+
+##### `swipeMinDistance`
+
+Minimum swipe distance that should activate opening the drawer. Defaults to `60`.
+
+This is not supported on web.
+
+##### `swipeMinVelocity`
+
+Minimum swipe velocity that should activate opening the drawer. Defaults to `500`.
+
+This is not supported on web.
+
+##### `gestureHandlerProps`
+
+Props to pass to the underlying pan gesture handler.
+
+This is not supported on web.
+
+##### `children`
+
+Content that the drawer should wrap.
+
+### `useDrawerProgress`
+
+The `useDrawerProgress` hook returns a Reanimated SharedValue (with modern implementation) or Reanimated Node (with legacy implementation) which represents the progress of the drawer. It can be used to animate the content of the screen.
+
+Example with modern implementation:
+
+```js
+import { Animated } from 'react-native-reanimated';
+import { useDrawerProgress } from 'react-native-drawer-layout';
+
+// ...
+
+function MyComponent() {
+ const progress = useDrawerProgress();
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ transform: [
+ {
+ translateX: interpolate(progress, [0, 1], [-100, 0]),
+ },
+ ],
+ };
+ });
+
+ return {/* ... */};
+}
+```
+
+Example with legacy implementation:
+
+```js
+import { Animated } from 'react-native-reanimated';
+import { useDrawerProgress } from 'react-native-drawer-layout';
+
+// ...
+
+function MyComponent() {
+ const progress = useDrawerProgress();
+
+ // If you are on react-native-reanimated 1.x, use `Animated.interpolate` instead of `Animated.interpolateNode`
+ const translateX = Animated.interpolateNode(progress, {
+ inputRange: [0, 1],
+ outputRange: [-100, 0],
+ });
+
+ return (
+
+ {/* ... */}
+
+ );
+}
+```
+
+If you are using class components, you can use the `DrawerProgressContext` to get the progress value.
+
+```js
+import { DrawerProgressContext } from 'react-native-drawer-layout';
+
+// ...
+
+class MyComponent extends React.Component {
+ static contextType = DrawerProgressContext;
+
+ render() {
+ const progress = this.context;
+
+ // ...
+ }
+}
+```
diff --git a/packages/react-native-drawer-layout/package.json b/packages/react-native-drawer-layout/package.json
new file mode 100644
index 0000000000..2db9c03d03
--- /dev/null
+++ b/packages/react-native-drawer-layout/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "react-native-drawer-layout",
+ "description": "Drawer component for React Native",
+ "version": "3.0.0",
+ "keywords": [
+ "react-native-component",
+ "react-component",
+ "react-native",
+ "ios",
+ "android",
+ "drawer",
+ "swipe"
+ ],
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/react-navigation/react-navigation.git",
+ "directory": "packages/react-native-drawer-layout"
+ },
+ "bugs": {
+ "url": "https://github.com/react-navigation/react-navigation/issues"
+ },
+ "homepage": "https://github.com/react-navigation/react-navigation/tree/main/packages/react-native-drawer-layout#readme",
+ "main": "lib/commonjs/index.js",
+ "react-native": "src/index.tsx",
+ "source": "src/index.tsx",
+ "module": "lib/module/index.js",
+ "types": "lib/typescript/src/index.d.ts",
+ "files": [
+ "src",
+ "lib",
+ "!**/__tests__"
+ ],
+ "sideEffects": false,
+ "scripts": {
+ "prepack": "bob build",
+ "clean": "del lib"
+ },
+ "devDependencies": {
+ "del-cli": "^5.0.0",
+ "react": "18.1.0",
+ "react-native": "0.70.5",
+ "react-native-builder-bob": "^0.20.3",
+ "typescript": "^4.9.4"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-gesture-handler": ">= 1.0.0",
+ "react-native-reanimated": ">= 1.0.0"
+ },
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "lib",
+ "targets": [
+ "commonjs",
+ "module",
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ }
+}
diff --git a/packages/react-native-drawer-layout/src/constants.tsx b/packages/react-native-drawer-layout/src/constants.tsx
new file mode 100644
index 0000000000..908b05bc78
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/constants.tsx
@@ -0,0 +1,4 @@
+export const SWIPE_MIN_OFFSET = 5;
+export const SWIPE_MIN_DISTANCE = 60;
+export const SWIPE_MIN_VELOCITY = 500;
+export const DEFAULT_DRAWER_WIDTH = '80%';
diff --git a/packages/react-native-drawer-layout/src/index.tsx b/packages/react-native-drawer-layout/src/index.tsx
new file mode 100644
index 0000000000..2b93015d92
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/index.tsx
@@ -0,0 +1,4 @@
+export { default as DrawerGestureContext } from './utils/DrawerGestureContext';
+export { default as DrawerProgressContext } from './utils/DrawerProgressContext';
+export { default as useDrawerProgress } from './utils/useDrawerProgress';
+export { default as Drawer } from './views/Drawer';
diff --git a/packages/react-native-drawer-layout/src/types.tsx b/packages/react-native-drawer-layout/src/types.tsx
new file mode 100644
index 0000000000..8313623626
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/types.tsx
@@ -0,0 +1,116 @@
+import type { StyleProp, ViewStyle } from 'react-native';
+import type { PanGestureHandler } from 'react-native-gesture-handler';
+
+export type Layout = { width: number; height: number };
+
+export type DrawerProps = {
+ /**
+ * Whether the drawer is open or not.
+ */
+ open: boolean;
+
+ /**
+ * Callback which is called when the drawer is opened.
+ */
+ onOpen: () => void;
+
+ /**
+ * Callback which is called when the drawer is closed.
+ */
+ onClose: () => void;
+
+ /**
+ * Callback which returns a react element to render as the content of the drawer.
+ */
+ renderDrawerContent: () => React.ReactNode;
+
+ /**
+ * Object containing the layout of the container.
+ * Defaults to the dimensions of the application's window.
+ */
+ layout?: { width: number; height: number };
+
+ /**
+ * Position of the drawer on the screen.
+ * Defaults to `right` in RTL mode, otherwise `left`.
+ */
+ drawerPosition?: 'left' | 'right';
+
+ /**
+ * Type of the drawer. It determines how the drawer looks and animates.
+ * - `front`: Traditional drawer which covers the screen with a overlay behind it.
+ * - `back`: The drawer is revealed behind the screen on swipe.
+ * - `slide`: Both the screen and the drawer slide on swipe to reveal the drawer.
+ * - `permanent`: A permanent drawer is shown as a sidebar.
+ *
+ * Defaults to `slide` on iOS and `front` on other platforms.
+ */
+ drawerType?: 'front' | 'back' | 'slide' | 'permanent';
+
+ /**
+ * Style object for the drawer component.
+ * You can pass a custom background color for drawer or a custom width here.
+ */
+ drawerStyle?: StyleProp;
+
+ /**
+ * Style object for the drawer overlay.
+ */
+ overlayStyle?: StyleProp;
+
+ /**
+ * Whether the keyboard should be dismissed when the swipe gesture begins.
+ * Defaults to `'on-drag'`. Set to `'none'` to disable keyboard handling.
+ */
+ keyboardDismissMode?: 'none' | 'on-drag';
+
+ /**
+ * Whether the statusbar should be hidden when the drawer is pulled or opens.
+ * Defaults to `false`.
+ */
+ hideStatusBarOnOpen?: boolean;
+
+ /**
+ * Animation of the statusbar when hiding it. Use in combination with `hideStatusBarOnOpen`.
+ */
+ statusBarAnimation?: 'slide' | 'fade' | 'none';
+
+ /**
+ * Whether you can use swipe gestures to open or close the drawer.
+ * Defaults to `true`.
+ * This is not supported on Web.
+ */
+ swipeEnabled?: boolean;
+
+ /**
+ * How far from the edge of the screen the swipe gesture should activate.
+ * Defaults to `32`.
+ * This is not supported on Web.
+ */
+ swipeEdgeWidth?: number;
+
+ /**
+ * Minimum swipe distance that should activate opening the drawer.
+ * Defaults to `60`.
+ * This is not supported on Web.
+ */
+ swipeMinDistance?: number;
+
+ /**
+ * Minimum swipe velocity that should activate opening the drawer.
+ * Defaults to `500`.
+ * This is not supported on Web.
+ */
+ swipeMinVelocity?: number;
+
+ /**
+ * Props to pass to the underlying pan gesture handler.
+ * This is not supported on Web.
+ */
+ gestureHandlerProps?: React.ComponentProps;
+
+ /**
+ * Content that the drawer should wrap.
+ */
+ children: React.ReactNode;
+};
diff --git a/packages/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx b/packages/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
new file mode 100644
index 0000000000..b875d95fbc
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/utils/DrawerGestureContext.tsx
@@ -0,0 +1,3 @@
+import * as React from 'react';
+
+export default React.createContext | null>(null);
diff --git a/packages/react-native-drawer-layout/src/utils/DrawerProgressContext.tsx b/packages/react-native-drawer-layout/src/utils/DrawerProgressContext.tsx
new file mode 100644
index 0000000000..13203868db
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/utils/DrawerProgressContext.tsx
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import type Animated from 'react-native-reanimated';
+
+export default React.createContext<
+ Readonly> | Animated.Node | undefined
+>(undefined);
diff --git a/packages/react-native-drawer-layout/src/utils/useDrawerProgress.tsx b/packages/react-native-drawer-layout/src/utils/useDrawerProgress.tsx
new file mode 100644
index 0000000000..63eea88640
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/utils/useDrawerProgress.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import type Animated from 'react-native-reanimated';
+
+import DrawerProgressContext from './DrawerProgressContext';
+
+export default function useDrawerProgress():
+ | Readonly>
+ | Animated.Node {
+ const progress = React.useContext(DrawerProgressContext);
+
+ if (progress === undefined) {
+ throw new Error(
+ "Couldn't find a drawer. Is your component inside a drawer?"
+ );
+ }
+
+ return progress;
+}
diff --git a/packages/react-native-drawer-layout/src/views/Drawer.tsx b/packages/react-native-drawer-layout/src/views/Drawer.tsx
new file mode 100644
index 0000000000..96229993d6
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/Drawer.tsx
@@ -0,0 +1,122 @@
+import * as React from 'react';
+import {
+ I18nManager,
+ Platform,
+ StyleProp,
+ StyleSheet,
+ useWindowDimensions,
+ ViewStyle,
+} from 'react-native';
+import * as Reanimated from 'react-native-reanimated';
+
+import { SWIPE_MIN_DISTANCE, SWIPE_MIN_VELOCITY } from '../constants';
+import type { DrawerProps } from '../types';
+import { GestureHandlerRootView } from './GestureHandler';
+
+type Props = DrawerProps & {
+ /**
+ * Whether to use the legacy implementation of the drawer.
+ * The legacy implementation uses v1 of Reanimated.
+ * The modern implementation uses v2 of Reanimated.
+ *
+ * By default, the appropriate implementation is used based on whether Reanimated v2 is configured.
+ */
+ useLegacyImplementation?: boolean;
+
+ /**
+ * Style object for the wrapper view.
+ */
+ style?: StyleProp;
+};
+
+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/components/navigation-drawer
+ */
+ 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);
+};
+
+export default function Drawer({
+ // Reanimated 2 is not configured
+ // @ts-expect-error: the type definitions are incomplete
+ useLegacyImplementation = !Reanimated.isConfigured?.(),
+ layout: customLayout,
+ drawerType = Platform.select({ ios: 'slide', default: 'front' }),
+ drawerPosition = I18nManager.getConstants().isRTL ? 'right' : 'left',
+ drawerStyle,
+ swipeEnabled = Platform.OS !== 'web' &&
+ Platform.OS !== 'windows' &&
+ Platform.OS !== 'macos',
+ swipeEdgeWidth = 32,
+ swipeMinDistance = SWIPE_MIN_DISTANCE,
+ swipeMinVelocity = SWIPE_MIN_VELOCITY,
+ keyboardDismissMode = 'on-drag',
+ hideStatusBarOnOpen = false,
+ statusBarAnimation = 'slide',
+ style,
+ ...rest
+}: Props) {
+ // Reanimated v3 dropped legacy v1 API
+ const legacyImplemenationNotAvailable =
+ require('react-native-reanimated').abs === undefined;
+
+ if (useLegacyImplementation && legacyImplemenationNotAvailable) {
+ throw new Error(
+ 'The `useLegacyImplementation` prop is not available with Reanimated 3 as it no longer includes support for Reanimated 1 legacy API. Remove the `useLegacyImplementation` prop from `Drawer.Navigator` to be able to use it.'
+ );
+ }
+
+ const Drawer: typeof import('./modern/Drawer').default =
+ useLegacyImplementation
+ ? require('./legacy/Drawer').default
+ : require('./modern/Drawer').default;
+
+ const windowDimensions = useWindowDimensions();
+ const layout = customLayout ?? windowDimensions;
+
+ return (
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ drawer: {
+ backgroundColor: 'white',
+ },
+});
diff --git a/packages/react-native-drawer-layout/src/views/GestureHandler.android.tsx b/packages/react-native-drawer-layout/src/views/GestureHandler.android.tsx
new file mode 100644
index 0000000000..9fa1ac3c2f
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/GestureHandler.android.tsx
@@ -0,0 +1 @@
+export * from './GestureHandlerNative';
diff --git a/packages/react-native-drawer-layout/src/views/GestureHandler.ios.tsx b/packages/react-native-drawer-layout/src/views/GestureHandler.ios.tsx
new file mode 100644
index 0000000000..9fa1ac3c2f
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/GestureHandler.ios.tsx
@@ -0,0 +1 @@
+export * from './GestureHandlerNative';
diff --git a/packages/react-native-drawer-layout/src/views/GestureHandler.tsx b/packages/react-native-drawer-layout/src/views/GestureHandler.tsx
new file mode 100644
index 0000000000..610c86ab1e
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/GestureHandler.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import { View } from 'react-native';
+import type {
+ PanGestureHandlerProperties,
+ TapGestureHandlerProperties,
+} from 'react-native-gesture-handler';
+
+const Dummy: any = ({ children }: { children: React.ReactNode }) => (
+ <>{children}>
+);
+
+export const PanGestureHandler =
+ Dummy as React.ComponentType;
+
+export const TapGestureHandler =
+ Dummy as React.ComponentType;
+
+export const GestureHandlerRootView = View;
+
+export const enum GestureState {
+ UNDETERMINED = 0,
+ FAILED = 1,
+ BEGAN = 2,
+ CANCELLED = 3,
+ ACTIVE = 4,
+ END = 5,
+}
+
+export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
diff --git a/packages/react-native-drawer-layout/src/views/GestureHandlerNative.tsx b/packages/react-native-drawer-layout/src/views/GestureHandlerNative.tsx
new file mode 100644
index 0000000000..f0850d2180
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/GestureHandlerNative.tsx
@@ -0,0 +1,24 @@
+import * as React from 'react';
+import {
+ PanGestureHandler as PanGestureHandlerNative,
+ PanGestureHandlerProperties,
+} from 'react-native-gesture-handler';
+
+import DrawerGestureContext from '../utils/DrawerGestureContext';
+
+export function PanGestureHandler(props: PanGestureHandlerProperties) {
+ const gestureRef = React.useRef(null);
+
+ return (
+
+
+
+ );
+}
+
+export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
+export {
+ GestureHandlerRootView,
+ State as GestureState,
+ TapGestureHandler,
+} from 'react-native-gesture-handler';
diff --git a/packages/react-native-drawer-layout/src/views/legacy/Drawer.tsx b/packages/react-native-drawer-layout/src/views/legacy/Drawer.tsx
new file mode 100644
index 0000000000..28e1684c0b
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/legacy/Drawer.tsx
@@ -0,0 +1,681 @@
+import * as React from 'react';
+import {
+ I18nManager,
+ InteractionManager,
+ Keyboard,
+ LayoutChangeEvent,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ View,
+} from 'react-native';
+import Animated from 'react-native-reanimated';
+
+import {
+ DEFAULT_DRAWER_WIDTH,
+ SWIPE_MIN_DISTANCE,
+ SWIPE_MIN_OFFSET,
+ SWIPE_MIN_VELOCITY,
+} from '../../constants';
+import type { DrawerProps } from '../../types';
+import DrawerProgressContext from '../../utils/DrawerProgressContext';
+import { GestureState, PanGestureHandler } from '../GestureHandler';
+import Overlay from './Overlay';
+
+const {
+ Clock,
+ Value,
+ onChange,
+ clockRunning,
+ startClock,
+ stopClock,
+ 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 DIRECTION_LEFT = 1;
+const DIRECTION_RIGHT = -1;
+
+const SPRING_CONFIG = {
+ stiffness: 1000,
+ damping: 500,
+ mass: 3,
+ overshootClamping: true,
+ restDisplacementThreshold: 0.01,
+ restSpeedThreshold: 0.01,
+};
+
+const ANIMATED_ZERO = new Animated.Value(0);
+const ANIMATED_ONE = new Animated.Value(1);
+
+type Binary = 0 | 1;
+
+type Props = DrawerProps & {
+ layout: { width: number };
+};
+
+export default class Drawer extends React.Component {
+ componentDidUpdate(prevProps: Props) {
+ const {
+ open,
+ drawerPosition,
+ drawerType,
+ swipeMinDistance,
+ swipeMinVelocity,
+ hideStatusBarOnOpen,
+ } = 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 && hideStatusBarOnOpen) {
+ 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.swipeMinDistance !== swipeMinDistance) {
+ this.swipeDistanceThreshold.setValue(
+ swipeMinDistance ?? SWIPE_MIN_DISTANCE
+ );
+ }
+
+ if (prevProps.swipeMinVelocity !== swipeMinVelocity) {
+ this.swipeVelocityThreshold.setValue(
+ swipeMinVelocity ?? SWIPE_MIN_VELOCITY
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ this.toggleStatusBar(false);
+ this.handleEndInteraction();
+ }
+
+ private handleEndInteraction = () => {
+ if (this.interactionHandle !== undefined) {
+ InteractionManager.clearInteractionHandle(this.interactionHandle);
+ this.interactionHandle = undefined;
+ }
+ };
+
+ private handleStartInteraction = () => {
+ if (this.interactionHandle === undefined) {
+ this.interactionHandle = InteractionManager.createInteractionHandle();
+ }
+ };
+
+ private getDrawerWidth = (): number => {
+ const { drawerStyle, layout } = this.props;
+ const { width = DEFAULT_DRAWER_WIDTH } =
+ StyleSheet.flatten(drawerStyle) || {};
+
+ if (typeof width === 'string' && width.endsWith('%')) {
+ // Try to calculate width if a percentage is given
+ const percentage = Number(width.replace(/%$/, ''));
+
+ if (Number.isFinite(percentage)) {
+ return layout.width * (percentage / 100);
+ }
+ }
+
+ return typeof width === 'number' ? width : 0;
+ };
+
+ private clock = new Clock();
+ private interactionHandle: number | undefined;
+
+ private isDrawerTypeFront = new Value(
+ this.props.drawerType === 'front' ? TRUE : FALSE
+ );
+
+ private isOpen = new Value(this.props.open ? TRUE : FALSE);
+ private nextIsOpen = new Value(UNSET);
+ private isSwiping = new Value(FALSE);
+
+ private initialDrawerWidth = this.getDrawerWidth();
+
+ private gestureState = new Value(GestureState.UNDETERMINED);
+ private touchX = new Value(0);
+ private velocityX = new Value(0);
+ private gestureX = new Value(0);
+ private offsetX = new Value(0);
+ private position = new Value(
+ this.props.open
+ ? this.initialDrawerWidth *
+ (this.props.drawerPosition === 'right'
+ ? DIRECTION_RIGHT
+ : DIRECTION_LEFT)
+ : 0
+ );
+
+ private containerWidth = new Value(this.props.layout.width);
+ private drawerWidth = new Value(this.initialDrawerWidth);
+ private drawerOpacity = new Value(
+ this.props.drawerType === 'permanent' ? 1 : 0
+ );
+ private drawerPosition = new Value(
+ 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(
+ this.props.swipeMinDistance ?? SWIPE_MIN_DISTANCE
+ );
+ private swipeVelocityThreshold = new Value(
+ this.props.swipeMinVelocity ?? SWIPE_MIN_VELOCITY
+ );
+
+ private currentOpenValue: boolean = this.props.open;
+ private pendingOpenValue: boolean | undefined;
+
+ private isStatusBarHidden: boolean = false;
+
+ private manuallyTriggerSpring = new Value(FALSE);
+
+ private transitionTo = (isOpen: number | Animated.Node) => {
+ const toValue = new Value(0);
+ const frameTime = new Value(0);
+
+ const state = {
+ position: this.position,
+ time: new Value(0),
+ finished: new Value(FALSE),
+ velocity: new Value(0),
+ };
+
+ 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(state.velocity, this.velocityX),
+ set(this.isOpen, isOpen),
+ startClock(this.clock),
+ call([], this.handleStartInteraction),
+ set(this.manuallyTriggerSpring, FALSE),
+ ]),
+ spring(this.clock, state, { ...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]: readonly Binary[]) => {
+ const open = Boolean(value);
+ this.handleEndInteraction();
+
+ 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]: readonly 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.gestureX, 0),
+ 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]: readonly Binary[]) => {
+ const { keyboardDismissMode } = this.props;
+
+ if (value === TRUE) {
+ if (keyboardDismissMode === 'on-drag') {
+ Keyboard.dismiss();
+ }
+
+ this.toggleStatusBar(true);
+ } else {
+ this.toggleStatusBar(this.currentOpenValue);
+ }
+ })
+ ),
+ onChange(
+ this.gestureState,
+ cond(
+ eq(this.gestureState, GestureState.ACTIVE),
+ call([], this.handleStartInteraction)
+ )
+ ),
+ cond(
+ eq(this.gestureState, GestureState.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(
+ this.manuallyTriggerSpring,
+ this.isOpen,
+ cond(
+ or(
+ and(
+ greaterThan(abs(this.gestureX), SWIPE_MIN_OFFSET),
+ 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,
+ },
+ },
+ ]);
+
+ private handleGestureStateChange = event([
+ {
+ nativeEvent: {
+ state: (s: Animated.Value) => set(this.gestureState, s),
+ },
+ },
+ ]);
+
+ 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(() =>
+ requestAnimationFrame(() => this.drawerOpacity.setValue(1))
+ );
+ };
+
+ private toggleDrawer = (open: boolean) => {
+ if (this.currentOpenValue !== open) {
+ 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 { hideStatusBarOnOpen: hideStatusBar, statusBarAnimation } =
+ this.props;
+
+ if (hideStatusBar && this.isStatusBarHidden !== hidden) {
+ this.isStatusBarHidden = hidden;
+ StatusBar.setHidden(hidden, statusBarAnimation);
+ }
+ };
+
+ render() {
+ const {
+ open,
+ swipeEnabled,
+ drawerPosition,
+ drawerType,
+ swipeEdgeWidth,
+ drawerStyle,
+ overlayStyle,
+ renderDrawerContent,
+ children,
+ gestureHandlerProps,
+ } = this.props;
+
+ const isOpen = drawerType === 'permanent' ? true : open;
+ const isRight = drawerPosition === 'right';
+
+ const contentTranslateX =
+ drawerType === 'front' ? ANIMATED_ZERO : this.translateX;
+
+ const drawerTranslateX =
+ drawerType === 'back'
+ ? I18nManager.getConstants().isRTL
+ ? multiply(
+ sub(this.containerWidth, this.drawerWidth),
+ isRight ? 1 : -1
+ )
+ : ANIMATED_ZERO
+ : this.translateX;
+
+ const offset =
+ drawerType === 'back'
+ ? 0
+ : I18nManager.getConstants().isRTL
+ ? '100%'
+ : multiply(this.drawerWidth, -1);
+
+ // FIXME: Currently hitSlop is broken when on Android when drawer is on right
+ // https://github.com/software-mansion/react-native-gesture-handler/issues/569
+ const hitSlop = isRight
+ ? // 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: isOpen ? undefined : swipeEdgeWidth }
+ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
+
+ const progress = drawerType === 'permanent' ? ANIMATED_ONE : this.progress;
+
+ return (
+
+
+
+
+
+ {children}
+
+ {
+ // Disable overlay if sidebar is permanent
+ drawerType === 'permanent' ? null : (
+ this.toggleDrawer(false)}
+ style={overlayStyle as any}
+ accessibilityElementsHidden={!isOpen}
+ importantForAccessibility={
+ isOpen ? 'auto' : 'no-hide-descendants'
+ }
+ />
+ )
+ }
+
+
+ {drawerType === 'permanent' ? null : (
+ (this.currentOpenValue = false)),
+ ]),
+ ]),
+ ])}
+ />
+ )}
+
+ {renderDrawerContent()}
+
+
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: 'white',
+ maxWidth: '100%',
+ },
+ nonPermanent: {
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ width: DEFAULT_DRAWER_WIDTH,
+ },
+ content: {
+ flex: 1,
+ },
+ main: {
+ flex: 1,
+ ...Platform.select({
+ // FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
+ // But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
+ web: null,
+ default: { overflow: 'hidden' },
+ }),
+ },
+});
diff --git a/packages/react-native-drawer-layout/src/views/legacy/Overlay.tsx b/packages/react-native-drawer-layout/src/views/legacy/Overlay.tsx
new file mode 100644
index 0000000000..0267252061
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/legacy/Overlay.tsx
@@ -0,0 +1,75 @@
+import * as React from 'react';
+import { Platform, Pressable, StyleSheet } from 'react-native';
+import Animated from 'react-native-reanimated';
+
+const {
+ // @ts-expect-error: this is to support reanimated 1
+ interpolate: interpolateDeprecated,
+ interpolateNode,
+ cond,
+ greaterThan,
+} = Animated;
+
+const interpolate: typeof interpolateNode =
+ interpolateNode ?? interpolateDeprecated;
+
+const PROGRESS_EPSILON = 0.05;
+
+type Props = React.ComponentProps & {
+ progress: Animated.Node;
+ onPress: () => void;
+};
+
+const Overlay = React.forwardRef(function Overlay(
+ { progress, onPress, style, ...props }: Props,
+ ref: React.Ref
+) {
+ const animatedStyle = {
+ opacity: interpolate(progress, {
+ // Default input range is [PROGRESS_EPSILON, 1]
+ // On Windows, the output value is 1 when input value is out of range for some reason
+ // The default value 0 will be interpolated to 1 in this case, which is not what we want.
+ // Therefore changing input range on Windows to [0,1] instead.
+ inputRange:
+ Platform.OS === 'windows' || Platform.OS === 'macos'
+ ? [0, 1]
+ : [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(progress, PROGRESS_EPSILON), 0, -1),
+ };
+
+ return (
+
+
+
+ );
+});
+
+const overlayStyle = Platform.select>({
+ web: {
+ // Disable touch highlight on mobile Safari.
+ // WebkitTapHighlightColor must be used outside of StyleSheet.create because react-native-web will omit the property.
+ WebkitTapHighlightColor: 'transparent',
+ },
+ default: {},
+});
+
+const styles = StyleSheet.create({
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ pressable: {
+ flex: 1,
+ },
+});
+
+export default Overlay;
diff --git a/packages/react-native-drawer-layout/src/views/modern/Drawer.tsx b/packages/react-native-drawer-layout/src/views/modern/Drawer.tsx
new file mode 100644
index 0000000000..5527e75107
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/modern/Drawer.tsx
@@ -0,0 +1,412 @@
+import * as React from 'react';
+import {
+ I18nManager,
+ InteractionManager,
+ Keyboard,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ View,
+} from 'react-native';
+import Animated, {
+ interpolate,
+ runOnJS,
+ useAnimatedGestureHandler,
+ useAnimatedStyle,
+ useDerivedValue,
+ useSharedValue,
+ withSpring,
+} from 'react-native-reanimated';
+
+import {
+ DEFAULT_DRAWER_WIDTH,
+ SWIPE_MIN_DISTANCE,
+ SWIPE_MIN_OFFSET,
+ SWIPE_MIN_VELOCITY,
+} from '../../constants';
+import type { DrawerProps } from '../../types';
+import DrawerProgressContext from '../../utils/DrawerProgressContext';
+import {
+ GestureState,
+ PanGestureHandler,
+ PanGestureHandlerGestureEvent,
+} from '../GestureHandler';
+import Overlay from './Overlay';
+
+const minmax = (value: number, start: number, end: number) => {
+ 'worklet';
+
+ return Math.min(Math.max(value, start), end);
+};
+
+type Props = DrawerProps & {
+ layout: { width: number };
+};
+
+export default function Drawer({
+ layout,
+ drawerPosition,
+ drawerStyle,
+ drawerType,
+ gestureHandlerProps,
+ hideStatusBarOnOpen,
+ keyboardDismissMode,
+ onClose,
+ onOpen,
+ open,
+ overlayStyle,
+ statusBarAnimation,
+ swipeEnabled,
+ swipeEdgeWidth,
+ swipeMinDistance = SWIPE_MIN_DISTANCE,
+ swipeMinVelocity = SWIPE_MIN_VELOCITY,
+ renderDrawerContent,
+ children,
+}: Props) {
+ const getDrawerWidth = (): number => {
+ const { width = DEFAULT_DRAWER_WIDTH } =
+ StyleSheet.flatten(drawerStyle) || {};
+
+ if (typeof width === 'string' && width.endsWith('%')) {
+ // Try to calculate width if a percentage is given
+ const percentage = Number(width.replace(/%$/, ''));
+
+ if (Number.isFinite(percentage)) {
+ return layout.width * (percentage / 100);
+ }
+ }
+
+ return typeof width === 'number' ? width : 0;
+ };
+
+ const drawerWidth = getDrawerWidth();
+
+ const isOpen = drawerType === 'permanent' ? true : open;
+ const isRight = drawerPosition === 'right';
+
+ const getDrawerTranslationX = React.useCallback(
+ (open: boolean) => {
+ 'worklet';
+
+ if (drawerPosition === 'left') {
+ return open ? 0 : -drawerWidth;
+ }
+
+ return open ? 0 : drawerWidth;
+ },
+ [drawerPosition, drawerWidth]
+ );
+
+ const hideStatusBar = React.useCallback(
+ (hide: boolean) => {
+ if (hideStatusBarOnOpen) {
+ StatusBar.setHidden(hide, statusBarAnimation);
+ }
+ },
+ [hideStatusBarOnOpen, statusBarAnimation]
+ );
+
+ React.useEffect(() => {
+ hideStatusBar(isOpen);
+
+ return () => hideStatusBar(false);
+ }, [isOpen, hideStatusBarOnOpen, statusBarAnimation, hideStatusBar]);
+
+ const interactionHandleRef = React.useRef(null);
+
+ const startInteraction = () => {
+ interactionHandleRef.current = InteractionManager.createInteractionHandle();
+ };
+
+ const endInteraction = () => {
+ if (interactionHandleRef.current != null) {
+ InteractionManager.clearInteractionHandle(interactionHandleRef.current);
+ interactionHandleRef.current = null;
+ }
+ };
+
+ const hideKeyboard = () => {
+ if (keyboardDismissMode === 'on-drag') {
+ Keyboard.dismiss();
+ }
+ };
+
+ const onGestureStart = () => {
+ startInteraction();
+ hideKeyboard();
+ hideStatusBar(true);
+ };
+
+ const onGestureFinish = () => {
+ endInteraction();
+ };
+
+ // FIXME: Currently hitSlop is broken when on Android when drawer is on right
+ // https://github.com/software-mansion/react-native-gesture-handler/issues/569
+ const hitSlop = isRight
+ ? // 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: isOpen ? undefined : swipeEdgeWidth }
+ : { left: 0, width: isOpen ? undefined : swipeEdgeWidth };
+
+ const touchStartX = useSharedValue(0);
+ const touchX = useSharedValue(0);
+ const translationX = useSharedValue(getDrawerTranslationX(open));
+ const gestureState = useSharedValue(GestureState.UNDETERMINED);
+
+ const toggleDrawer = React.useCallback(
+ (open: boolean, velocity?: number) => {
+ 'worklet';
+
+ const translateX = getDrawerTranslationX(open);
+
+ touchStartX.value = 0;
+ touchX.value = 0;
+ translationX.value = withSpring(translateX, {
+ velocity,
+ stiffness: 1000,
+ damping: 500,
+ mass: 3,
+ overshootClamping: true,
+ restDisplacementThreshold: 0.01,
+ restSpeedThreshold: 0.01,
+ });
+
+ if (open) {
+ runOnJS(onOpen)();
+ } else {
+ runOnJS(onClose)();
+ }
+ },
+ [getDrawerTranslationX, onClose, onOpen, touchStartX, touchX, translationX]
+ );
+
+ React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
+
+ const onGestureEvent = useAnimatedGestureHandler<
+ PanGestureHandlerGestureEvent,
+ { startX: number; hasCalledOnStart: boolean }
+ >({
+ onStart: (event, ctx) => {
+ ctx.hasCalledOnStart = false;
+ ctx.startX = translationX.value;
+ gestureState.value = event.state;
+ touchStartX.value = event.x;
+ },
+ onActive: (event, ctx) => {
+ touchX.value = event.x;
+ translationX.value = ctx.startX + event.translationX;
+ gestureState.value = event.state;
+
+ // onStart will _always_ be called, even when the activation
+ // criteria isn't met yet. This makes sure onGestureStart is only
+ // called when the criteria is really met.
+ if (!ctx.hasCalledOnStart) {
+ ctx.hasCalledOnStart = true;
+ runOnJS(onGestureStart)();
+ }
+ },
+ onEnd: (event) => {
+ gestureState.value = event.state;
+
+ const nextOpen =
+ (Math.abs(event.translationX) > SWIPE_MIN_OFFSET &&
+ Math.abs(event.translationX) > swipeMinVelocity) ||
+ Math.abs(event.translationX) > swipeMinDistance
+ ? drawerPosition === 'left'
+ ? // If swiped to right, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) > 0
+ : // If swiped to left, open the drawer, otherwise close it
+ (event.velocityX === 0 ? event.translationX : event.velocityX) < 0
+ : open;
+
+ toggleDrawer(nextOpen, event.velocityX);
+ },
+ onFinish: () => {
+ runOnJS(onGestureFinish)();
+ },
+ });
+
+ const translateX = useDerivedValue(() => {
+ // 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 translationX. 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"
+ const touchDistance =
+ drawerType === 'front' && gestureState.value === GestureState.ACTIVE
+ ? minmax(
+ drawerPosition === 'left'
+ ? touchStartX.value - drawerWidth
+ : layout.width - drawerWidth - touchStartX.value,
+ 0,
+ layout.width
+ )
+ : 0;
+
+ const translateX =
+ drawerPosition === 'left'
+ ? minmax(translationX.value + touchDistance, -drawerWidth, 0)
+ : minmax(translationX.value - touchDistance, 0, drawerWidth);
+
+ return translateX;
+ });
+
+ const isRTL = I18nManager.getConstants().isRTL;
+ const drawerAnimatedStyle = useAnimatedStyle(() => {
+ const distanceFromEdge = layout.width - drawerWidth;
+
+ return {
+ transform:
+ drawerType === 'permanent'
+ ? // Reanimated needs the property to be present, but it results in Browser bug
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=20574
+ []
+ : [
+ {
+ translateX:
+ // The drawer stays in place when `drawerType` is `back`
+ (drawerType === 'back' ? 0 : translateX.value) +
+ (drawerPosition === 'left'
+ ? isRTL
+ ? -distanceFromEdge
+ : 0
+ : isRTL
+ ? 0
+ : distanceFromEdge),
+ },
+ ],
+ };
+ });
+
+ const contentAnimatedStyle = useAnimatedStyle(() => {
+ return {
+ transform:
+ drawerType === 'permanent'
+ ? // Reanimated needs the property to be present, but it results in Browser bug
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=20574
+ []
+ : [
+ {
+ translateX:
+ // The screen content stays in place when `drawerType` is `front`
+ drawerType === 'front'
+ ? 0
+ : translateX.value +
+ drawerWidth * (drawerPosition === 'left' ? 1 : -1),
+ },
+ ],
+ };
+ });
+
+ const progress = useDerivedValue(() => {
+ return drawerType === 'permanent'
+ ? 1
+ : interpolate(
+ translateX.value,
+ [getDrawerTranslationX(false), getDrawerTranslationX(true)],
+ [0, 1]
+ );
+ });
+
+ return (
+
+
+ {/* Immediate child of gesture handler needs to be an Animated.View */}
+
+
+
+ {children}
+
+ {drawerType !== 'permanent' ? (
+ toggleDrawer(false)}
+ style={overlayStyle}
+ />
+ ) : null}
+
+
+ {renderDrawerContent()}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ top: 0,
+ bottom: 0,
+ maxWidth: '100%',
+ width: DEFAULT_DRAWER_WIDTH,
+ },
+ content: {
+ flex: 1,
+ },
+ main: {
+ flex: 1,
+ ...Platform.select({
+ // FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
+ // But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
+ web: null,
+ default: { overflow: 'hidden' },
+ }),
+ },
+});
diff --git a/packages/react-native-drawer-layout/src/views/modern/Overlay.tsx b/packages/react-native-drawer-layout/src/views/modern/Overlay.tsx
new file mode 100644
index 0000000000..c91d08d706
--- /dev/null
+++ b/packages/react-native-drawer-layout/src/views/modern/Overlay.tsx
@@ -0,0 +1,70 @@
+import * as React from 'react';
+import { Platform, Pressable, StyleSheet } from 'react-native';
+import Animated, {
+ useAnimatedProps,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+
+const PROGRESS_EPSILON = 0.05;
+
+type Props = React.ComponentProps & {
+ progress: Animated.SharedValue;
+ onPress: () => void;
+};
+
+const Overlay = React.forwardRef(function Overlay(
+ { progress, onPress, style, ...props }: Props,
+ ref: React.Ref
+) {
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ opacity: progress.value,
+ // We don't want the user to be able to press through the overlay when drawer is open
+ // We can send the overlay behind the screen to avoid it
+ zIndex: progress.value > PROGRESS_EPSILON ? 0 : -1,
+ };
+ });
+
+ const animatedProps = useAnimatedProps(() => {
+ const active = progress.value > PROGRESS_EPSILON;
+
+ return {
+ pointerEvents: active ? 'auto' : 'none',
+ accessibilityElementsHidden: !active,
+ importantForAccessibility: active ? 'auto' : 'no-hide-descendants',
+ } as const;
+ });
+
+ return (
+
+
+
+ );
+});
+
+const overlayStyle = Platform.select>({
+ web: {
+ // Disable touch highlight on mobile Safari.
+ // WebkitTapHighlightColor must be used outside of StyleSheet.create because react-native-web will omit the property.
+ WebkitTapHighlightColor: 'transparent',
+ },
+ default: {},
+});
+
+const styles = StyleSheet.create({
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ },
+ pressable: {
+ flex: 1,
+ pointerEvents: 'auto',
+ },
+});
+
+export default Overlay;
diff --git a/packages/react-native-drawer-layout/tsconfig.build.json b/packages/react-native-drawer-layout/tsconfig.build.json
new file mode 100644
index 0000000000..543a78763b
--- /dev/null
+++ b/packages/react-native-drawer-layout/tsconfig.build.json
@@ -0,0 +1,6 @@
+{
+ "extends": "./tsconfig",
+ "compilerOptions": {
+ "paths": {}
+ }
+}
diff --git a/packages/react-native-drawer-layout/tsconfig.json b/packages/react-native-drawer-layout/tsconfig.json
new file mode 100644
index 0000000000..4e8e8216bd
--- /dev/null
+++ b/packages/react-native-drawer-layout/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig",
+ "references": [],
+ "compilerOptions": {
+ "outDir": "./lib/typescript"
+ }
+}