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: use body-scroll-lock instead of no-scroll #455

Merged
merged 7 commits into from Nov 15, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 14 additions & 14 deletions react-responsive-modal/__tests__/index.test.tsx
Expand Up @@ -105,7 +105,7 @@ describe('modal', () => {
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');
});

it('should block the scroll when modal is rendered open', () => {
Expand All @@ -114,7 +114,7 @@ describe('modal', () => {
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');
});

it('should block scroll when prop open change to true', () => {
Expand All @@ -123,14 +123,14 @@ describe('modal', () => {
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');

rerender(
<Modal open={true} onClose={() => null}>
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');
});

it('should unblock scroll when prop open change to false', async () => {
Expand All @@ -139,7 +139,7 @@ describe('modal', () => {
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');

rerender(
<Modal open={false} onClose={() => null} animationDuration={0}>
Expand All @@ -155,7 +155,7 @@ describe('modal', () => {
{ timeout: 1 }
);

expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');
});

it('should unblock scroll when unmounted directly', async () => {
Expand All @@ -164,10 +164,10 @@ describe('modal', () => {
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');

unmount();
expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');
});

it('should unblock scroll when multiple modals are opened and then closed', async () => {
Expand All @@ -181,7 +181,7 @@ describe('modal', () => {
</Modal>
</React.Fragment>
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');

// We close one modal, the scroll should be locked
rerender(
Expand All @@ -202,7 +202,7 @@ describe('modal', () => {
},
{ timeout: 1 }
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');

// We close the second modal, the scroll should be unlocked
rerender(
Expand All @@ -223,7 +223,7 @@ describe('modal', () => {
},
{ timeout: 1 }
);
expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');
});

it('should unblock scroll when one modal is closed and the one still open has blockScroll set to false', async () => {
Expand All @@ -237,7 +237,7 @@ describe('modal', () => {
</Modal>
</React.Fragment>
);
expect(document.documentElement.style.position).toBe('fixed');
expect(document.body.style.overflow).toBe('hidden');

// We close one modal, the scroll should be unlocked as remaining modal is not locking the scroll
rerender(
Expand All @@ -258,7 +258,7 @@ describe('modal', () => {
},
{ timeout: 1 }
);
expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');
});
});

Expand Down Expand Up @@ -437,7 +437,7 @@ describe('modal', () => {
<div>modal content</div>
</Modal>
);
expect(document.documentElement.style.position).toBe('');
expect(document.body.style.overflow).toBe('');
});
});

Expand Down
12 changes: 6 additions & 6 deletions react-responsive-modal/cypress/integration/modal.spec.ts
Expand Up @@ -43,26 +43,26 @@ describe('simple modal', () => {

it('should block the scroll when modal is opened', () => {
cy.get('button').eq(0).click();
cy.get('html').should('have.css', 'position', 'fixed');
cy.get('body').should('have.css', 'overflow', 'hidden');
});

it('should unblock the scroll when modal is closed', () => {
cy.get('button').eq(0).click();
cy.get('html').should('have.css', 'position', 'fixed');
cy.get('body').should('have.css', 'overflow', 'hidden');
cy.get('body').type('{esc}');
cy.get('html').should('not.have.css', 'position', 'fixed');
cy.get('body').should('not.have.css', 'overflow', 'hidden');
});

it('should unblock scroll only after last modal is closed when multiple modals are opened', () => {
cy.get('button').eq(1).click();
cy.get('[data-testid=modal] button').eq(0).click();
cy.get('[data-testid=modal]').should('have.length', 2);
cy.get('html').should('have.css', 'position', 'fixed');
cy.get('body').should('have.css', 'overflow', 'hidden');
cy.get('body').type('{esc}');
cy.get('[data-testid=modal]').should('have.length', 1);
cy.get('html').should('have.css', 'position', 'fixed');
cy.get('body').should('have.css', 'overflow', 'hidden');
cy.get('body').type('{esc}');
cy.get('[data-testid=modal]').should('not.exist');
cy.get('html').should('not.have.css', 'position', 'fixed');
cy.get('body').should('not.have.css', 'overflow', 'hidden');
});
});
9 changes: 5 additions & 4 deletions react-responsive-modal/package.json
Expand Up @@ -47,16 +47,16 @@
"size-limit": [
{
"path": "dist/react-responsive-modal.cjs.production.min.js",
"limit": "3.3 KB"
"limit": "3.8 KB"
},
{
"path": "dist/react-responsive-modal.esm.js",
"limit": "3.3 KB"
"limit": "3.8 KB"
}
],
"dependencies": {
"classnames": "^2.2.6",
"no-scroll": "^2.1.1"
"body-scroll-lock": "^3.1.5",
"classnames": "^2.2.6"
},
"peerDependencies": {
"react": "^16.8.0 || ^17",
Expand All @@ -66,6 +66,7 @@
"@size-limit/preset-small-lib": "4.7.0",
"@testing-library/jest-dom": "5.11.6",
"@testing-library/react": "11.1.2",
"@types/body-scroll-lock": "2.6.1",
"@types/classnames": "2.2.11",
"@types/no-scroll": "2.1.0",
"@types/node": "14.14.7",
Expand Down
24 changes: 8 additions & 16 deletions react-responsive-modal/src/index.tsx
Expand Up @@ -4,7 +4,8 @@ import cx from 'classnames';
import CloseIcon from './CloseIcon';
import { FocusTrap } from './FocusTrap';
import { modalManager, useModalManager } from './modalManager';
import { isBrowser, blockNoScroll, unblockNoScroll } from './utils';
import { useScrollLock } from './useScrollLock';
import { isBrowser } from './utils';

const classes = {
root: 'react-responsive-modal-root',
Expand Down Expand Up @@ -183,33 +184,24 @@ export const Modal = ({
const [showPortal, setShowPortal] = useState(false);

// Hook used to manage multiple modals opened at the same time
useModalManager(refModal, open, blockScroll);
useModalManager(refModal, open);

const handleOpen = () => {
if (blockScroll) {
blockNoScroll();
}
// Hook used to manage the scroll
useScrollLock(refModal, open, showPortal, blockScroll);

const handleOpen = () => {
if (
refContainer.current &&
!container &&
!document.body.contains(refContainer.current)
) {
document.body.appendChild(refContainer.current);
}

document.addEventListener('keydown', handleKeydown);
};

const handleClose = () => {
// Restore the scroll only if there is no modal on the screen
// We filter the modals that are not affecting the scroll
if (
blockScroll &&
modalManager.modals().filter((modal) => modal.blockScroll).length === 0
) {
unblockNoScroll();
}

if (
refContainer.current &&
!container &&
Expand All @@ -235,8 +227,8 @@ export const Modal = ({

useEffect(() => {
return () => {
// When the component is unmounted directly we want to unblock the scroll
if (showPortal) {
// When the modal is closed or removed directly, cleanup the listeners
handleClose();
}
};
Expand Down
27 changes: 9 additions & 18 deletions react-responsive-modal/src/modalManager.ts
@@ -1,46 +1,37 @@
import { Ref, useEffect } from 'react';

let modals: { element: Ref<any>; blockScroll: boolean }[] = [];
let modals: Ref<Element>[] = [];

/**
* Handle the order of the modals.
* Inspired by the material-ui implementation.
*/
export const modalManager = {
/**
* Return the modals array
*/
modals: () => modals,

/**
* Register a new modal
*/
add: (newModal: Ref<any>, blockScroll: boolean) => {
modals.push({ element: newModal, blockScroll });
add: (newModal: Ref<Element>) => {
modals.push(newModal);
},

/**
* Remove a modal
*/
remove: (oldModal: Ref<any>) => {
modals = modals.filter((modal) => modal.element !== oldModal);
remove: (oldModal: Ref<Element>) => {
modals = modals.filter((modal) => modal !== oldModal);
},

/**
* When multiple modals are rendered will return true if current modal is the last one
*/
isTopModal: (modal: Ref<any>) =>
!!modals.length && modals[modals.length - 1].element === modal,
isTopModal: (modal: Ref<Element>) =>
!!modals.length && modals[modals.length - 1] === modal,
};

export function useModalManager(
ref: Ref<any>,
open: boolean,
blockScroll: boolean
) {
export function useModalManager(ref: Ref<Element>, open: boolean) {
useEffect(() => {
if (open) {
modalManager.add(ref, blockScroll);
modalManager.add(ref);
}
return () => {
modalManager.remove(ref);
Expand Down
24 changes: 24 additions & 0 deletions react-responsive-modal/src/useScrollLock.ts
@@ -0,0 +1,24 @@
import { useEffect, useRef } from 'react';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';

export const useScrollLock = (
refModal: React.RefObject<Element>,
open: boolean,
showPortal: boolean,
blockScroll: boolean
) => {
const oldRef = useRef<Element | null>(null);

useEffect(() => {
if (open && refModal.current && blockScroll) {
oldRef.current = refModal.current;
disableBodyScroll(refModal.current);
}
return () => {
if (oldRef.current) {
enableBodyScroll(oldRef.current);
oldRef.current = null;
}
};
}, [open, showPortal, refModal]);
};
10 changes: 0 additions & 10 deletions react-responsive-modal/src/utils.ts
@@ -1,11 +1 @@
import noScroll from 'no-scroll';

export const isBrowser = typeof window !== 'undefined';

export const blockNoScroll = () => {
noScroll.on();
};

export const unblockNoScroll = () => {
noScroll.off();
};
24 changes: 16 additions & 8 deletions yarn.lock
Expand Up @@ -2410,6 +2410,13 @@ __metadata:
languageName: node
linkType: hard

"@types/body-scroll-lock@npm:2.6.1":
version: 2.6.1
resolution: "@types/body-scroll-lock@npm:2.6.1"
checksum: 7cb4ed5ce6b9d927321e966970b67dfc6cc23836c2eb6b03e0ef6cc4713199ab0f9cbf5a9b56987939b63a0a9e8a2c5678c1d73808e04578446dc3ea24998e32
languageName: node
linkType: hard

"@types/classnames@npm:2.2.11":
version: 2.2.11
resolution: "@types/classnames@npm:2.2.11"
Expand Down Expand Up @@ -3913,6 +3920,13 @@ __metadata:
languageName: node
linkType: hard

"body-scroll-lock@npm:^3.1.5":
version: 3.1.5
resolution: "body-scroll-lock@npm:3.1.5"
checksum: e8de58edc0fd7d483e3971045ff83fe6d722592d5153e9bfc9da2962a179a6522b432a4b35344bc8ae522eec080cc87301ce4ded4878f26254bf42a4f26d27ed
languageName: node
linkType: hard

"boolbase@npm:^1.0.0, boolbase@npm:~1.0.0":
version: 1.0.0
resolution: "boolbase@npm:1.0.0"
Expand Down Expand Up @@ -10615,13 +10629,6 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"no-scroll@npm:^2.1.1":
version: 2.1.1
resolution: "no-scroll@npm:2.1.1"
checksum: a575d5d3b84164a3eab41f4854526bf66969428533fccba87bd9898a86123cdf625c7bfb26bf1b479698dfec9bcf2f17ae9c743f196ca9469b6296e2e13f4b8e
languageName: node
linkType: hard

"node-abi@npm:^2.7.0":
version: 2.19.1
resolution: "node-abi@npm:2.19.1"
Expand Down Expand Up @@ -12511,17 +12518,18 @@ fsevents@^1.2.7:
"@size-limit/preset-small-lib": 4.7.0
"@testing-library/jest-dom": 5.11.6
"@testing-library/react": 11.1.2
"@types/body-scroll-lock": 2.6.1
"@types/classnames": 2.2.11
"@types/no-scroll": 2.1.0
"@types/node": 14.14.7
"@types/react": 16.9.56
"@types/react-dom": 16.9.9
"@types/react-transition-group": 4.4.0
babel-jest: 26.6.3
body-scroll-lock: ^3.1.5
classnames: ^2.2.6
cypress: 5.6.0
husky: 4.3.0
no-scroll: ^2.1.1
prettier: 2.1.2
react: 17.0.1
react-dom: 17.0.1
Expand Down