Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add body scroll locking feature #4

Merged
merged 5 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ By analysing a few projects, most of the logic was identical in every single one

A concern that came from the proposed solution was that every single website has different designs for a navbar or a drawer. Although this is true, their behaviour is usually the same. So, the solution should only deal with logic and behaviours while giving total freedom of the content that is rendered.

⚠️ **Note:** If you are using this package on a `Layout` component that doesn't unmount/remount on each page change, you will probably need to listen to router events so that you make sure you close the drawer even if the page change is triggered by the browser history buttons. See [Router Events](#router-events) section to check how it can be done for Next.js projects.
PedroMiguelSS marked this conversation as resolved.
Show resolved Hide resolved

## Usage

```js
Expand All @@ -40,7 +42,7 @@ import { NavigationProvider, Navbar, Drawer, useNavigation } from '@moxy/react-n

const MyNavigationHelper = () => {
const { drawer } = useNavigation();

return (
<>
<span>{ `Is Drawer Open? ${drawer.isOpen}` }</span>
Expand Down Expand Up @@ -86,6 +88,50 @@ Import the styleguide base styles in the app's entry CSS file:
import '@moxy/react-navigation/dist/index.css';
```

### Router events
PedroMiguelSS marked this conversation as resolved.
Show resolved Hide resolved

Taking `MyNavigationHelper` component as example:

```js
import { useRouter } from 'next/router';

const MyNavigationHelper = () => {
const router = useRouter();

const {
drawer: {
open: openDrawer,
close: closeDrawer,
toggle: toggleDrawer,
isOpen: isDrawerOpen,
}
} = useNavigation();

useEffect(() => {
const handleRouteChange = () => {
closeDrawer();
};

router.events.on('routeChangeStart', handleRouteChange);

return () => {
router.events.off('routeChangeStart', handleRouteChange);
};
}, [closeDrawer, router.events]);

return (
<>
<span>{ `Is Drawer Open? ${isDrawerOpen}` }</span>
<button onClick={ openDrawer }>Open Drawer</button>
<button onClick={ closeDrawer }>Close Drawer</button>
<button onClick={ toggleDrawer }>Toggle Drawer</button>
</>
);
}
```

We are listening `routeChangeStart` event for the sake of this example. You can check [here](https://nextjs.org/docs/api-reference/next/router#routerevents) a complete list of the supported events for the Next.js Router.
PedroMiguelSS marked this conversation as resolved.
Show resolved Hide resolved

## Styling

Each provided component accepts classNames for styling. Although there might be a need to style conditionally, some auxiliary data attributes are provided and can be styled like so:
Expand Down Expand Up @@ -182,6 +228,12 @@ Type: `boolean` | Default: `true`

An overlay that renders together with the drawer. When clicked, closes the drawer.

#### lockBodyScroll

Type: `boolean` | Default: `true`

Disables body scroll whenever the drawer is open. It keeps the drawer scroll if needed.

#### className

Type: `string` | Required: `false`
Expand Down
1 change: 1 addition & 0 deletions demo/pages/index.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.home {
margin-top: 150px;
height: 200vh;
}
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-dom": "^16.8.0"
},
"dependencies": {
"body-scroll-lock": "^3.0.3",
"classnames": "^2.2.6",
"hoist-non-react-statics": "^3.3.2",
"prop-types": "^15.7.2"
Expand Down
35 changes: 33 additions & 2 deletions src/components/drawer/Drawer.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';

import { useNavigation } from '../../hooks';

const drawerClassName = 'react-navigation_drawer';

const Drawer = ({ placement, withOverlay, children, className, overlayClassName }) => {
const Drawer = ({
placement,
withOverlay,
lockBodyScroll,
children,
className,
overlayClassName,
}) => {
const drawerRef = useRef();
const { drawer } = useNavigation();

const drawerClassNames = classNames(drawerClassName, `${drawerClassName}-${placement}`, className);
const overlayClassNames = classNames(`${drawerClassName}-overlay`, overlayClassName);

useEffect(() => {
if (!lockBodyScroll) { return; }

if (drawer.isOpen) {
disableBodyScroll(drawerRef.current);
} else {
enableBodyScroll(drawerRef.current);
}
}, [drawer.isOpen, lockBodyScroll]);

useEffect(() => {
const drawer = drawerRef.current;

return () => {
enableBodyScroll(drawer);
};
}, []);

const handleOverlayClick = useCallback(() => {
drawer.close();
}, [drawer]);
Expand All @@ -25,6 +53,7 @@ const Drawer = ({ placement, withOverlay, children, className, overlayClassName
onClick={ handleOverlayClick } />
}
<div
ref={ drawerRef }
className={ drawerClassNames }
data-open={ drawer.isOpen }
data-placement={ placement }>
Expand All @@ -37,6 +66,7 @@ const Drawer = ({ placement, withOverlay, children, className, overlayClassName
Drawer.propTypes = {
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
withOverlay: PropTypes.bool,
lockBodyScroll: PropTypes.bool,
children: PropTypes.any,
className: PropTypes.string,
overlayClassName: PropTypes.string,
Expand All @@ -45,6 +75,7 @@ Drawer.propTypes = {
Drawer.defaultProps = {
placement: 'left',
withOverlay: true,
lockBodyScroll: true,
};

export default Drawer;
1 change: 1 addition & 0 deletions src/styles/drawer.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.react-navigation_drawer {
position: fixed;
transition: transform 0.3s ease;
overflow: auto;

&[data-open="true"] {
transform: translate(0, 0);
Expand Down
65 changes: 64 additions & 1 deletion tests/components/Drawer.test.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { NavigationProvider, Drawer } from '../../src';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import { NavigationProvider, Drawer, useNavigation } from '../../src';

jest.mock('body-scroll-lock', () => ({
disableBodyScroll: jest.fn(),
enableBodyScroll: jest.fn(),
clearAllBodyScrollLocks: jest.fn(),
}));

const TriggerButton = () => {
const { drawer } = useNavigation();

return (
<button onClick={ drawer.toggle }>Toggle</button> // eslint-disable-line
);
};

const defaultProps = {
children: <p>Custom Drawer.</p>,
};

const renderWithProps = (props = {}) => render(
<NavigationProvider>
<TriggerButton />
<Drawer { ...defaultProps } { ...props } />
</NavigationProvider>,
);

beforeEach(() => {
jest.clearAllMocks();
});

describe('Drawer Component', () => {
it('should render correctly', () => {
const { asFragment } = renderWithProps();
Expand All @@ -28,4 +48,47 @@ describe('Drawer Component', () => {

expect(asFragment()).toMatchSnapshot();
});

it('should disable/enable body scroll when drawer is toggled', () => {
PedroMiguelSS marked this conversation as resolved.
Show resolved Hide resolved
const { container, getByRole } = renderWithProps();

const triggerButton = getByRole('button');
const drawer = container.querySelector('.react-navigation_drawer');

expect(enableBodyScroll).toHaveBeenCalledWith(drawer);

fireEvent.click(triggerButton);

expect(disableBodyScroll).toHaveBeenCalledWith(drawer);

fireEvent.click(triggerButton);

expect(enableBodyScroll).toHaveBeenCalledWith(drawer);
});

it('should not disable body scroll when lockBodyScroll prop is false', () => {
const { getByRole } = renderWithProps({ lockBodyScroll: false });
const triggerButton = getByRole('button');

expect(enableBodyScroll).not.toHaveBeenCalled();

fireEvent.click(triggerButton);

expect(disableBodyScroll).not.toHaveBeenCalled();
});

it('should enable back body scroll whenever the component is unmounted', () => {
const { container, getByRole, unmount } = renderWithProps();

const triggerButton = getByRole('button');
const drawer = container.querySelector('.react-navigation_drawer');

fireEvent.click(triggerButton);

expect(disableBodyScroll).toHaveBeenCalledWith(drawer);

unmount();

expect(enableBodyScroll).toHaveBeenCalledWith(drawer);
});
});
6 changes: 6 additions & 0 deletions tests/components/__snapshots__/Drawer.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

exports[`Drawer Component should render correctly 1`] = `
<DocumentFragment>
<button>
Toggle
</button>
<div
class="react-navigation_drawer-overlay"
data-open="false"
Expand All @@ -21,6 +24,9 @@ exports[`Drawer Component should render correctly 1`] = `

exports[`Drawer Component should render correctly when overlay is clicked 1`] = `
<DocumentFragment>
<button>
Toggle
</button>
<div
class="react-navigation_drawer-overlay"
data-open="false"
Expand Down