Skip to content

Commit 033f901

Browse files
authored
feat: use body-scroll-lock instead of no-scroll (#455)
1 parent 5727913 commit 033f901

File tree

8 files changed

+82
-76
lines changed

8 files changed

+82
-76
lines changed

react-responsive-modal/__tests__/index.test.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('modal', () => {
105105
<div>modal content</div>
106106
</Modal>
107107
);
108-
expect(document.documentElement.style.position).toBe('');
108+
expect(document.body.style.overflow).toBe('');
109109
});
110110

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

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

128128
rerender(
129129
<Modal open={true} onClose={() => null}>
130130
<div>modal content</div>
131131
</Modal>
132132
);
133-
expect(document.documentElement.style.position).toBe('fixed');
133+
expect(document.body.style.overflow).toBe('hidden');
134134
});
135135

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

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

158-
expect(document.documentElement.style.position).toBe('');
158+
expect(document.body.style.overflow).toBe('');
159159
});
160160

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

169169
unmount();
170-
expect(document.documentElement.style.position).toBe('');
170+
expect(document.body.style.overflow).toBe('');
171171
});
172172

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

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

207207
// We close the second modal, the scroll should be unlocked
208208
rerender(
@@ -223,7 +223,7 @@ describe('modal', () => {
223223
},
224224
{ timeout: 1 }
225225
);
226-
expect(document.documentElement.style.position).toBe('');
226+
expect(document.body.style.overflow).toBe('');
227227
});
228228

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

242242
// We close one modal, the scroll should be unlocked as remaining modal is not locking the scroll
243243
rerender(
@@ -258,7 +258,7 @@ describe('modal', () => {
258258
},
259259
{ timeout: 1 }
260260
);
261-
expect(document.documentElement.style.position).toBe('');
261+
expect(document.body.style.overflow).toBe('');
262262
});
263263
});
264264

@@ -437,7 +437,7 @@ describe('modal', () => {
437437
<div>modal content</div>
438438
</Modal>
439439
);
440-
expect(document.documentElement.style.position).toBe('');
440+
expect(document.body.style.overflow).toBe('');
441441
});
442442
});
443443

react-responsive-modal/cypress/integration/modal.spec.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,26 @@ describe('simple modal', () => {
4343

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

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

5656
it('should unblock scroll only after last modal is closed when multiple modals are opened', () => {
5757
cy.get('button').eq(1).click();
5858
cy.get('[data-testid=modal] button').eq(0).click();
5959
cy.get('[data-testid=modal]').should('have.length', 2);
60-
cy.get('html').should('have.css', 'position', 'fixed');
60+
cy.get('body').should('have.css', 'overflow', 'hidden');
6161
cy.get('body').type('{esc}');
6262
cy.get('[data-testid=modal]').should('have.length', 1);
63-
cy.get('html').should('have.css', 'position', 'fixed');
63+
cy.get('body').should('have.css', 'overflow', 'hidden');
6464
cy.get('body').type('{esc}');
6565
cy.get('[data-testid=modal]').should('not.exist');
66-
cy.get('html').should('not.have.css', 'position', 'fixed');
66+
cy.get('body').should('not.have.css', 'overflow', 'hidden');
6767
});
6868
});

react-responsive-modal/package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@
4747
"size-limit": [
4848
{
4949
"path": "dist/react-responsive-modal.cjs.production.min.js",
50-
"limit": "3.3 KB"
50+
"limit": "3.8 KB"
5151
},
5252
{
5353
"path": "dist/react-responsive-modal.esm.js",
54-
"limit": "3.3 KB"
54+
"limit": "3.8 KB"
5555
}
5656
],
5757
"dependencies": {
58-
"classnames": "^2.2.6",
59-
"no-scroll": "^2.1.1"
58+
"body-scroll-lock": "^3.1.5",
59+
"classnames": "^2.2.6"
6060
},
6161
"peerDependencies": {
6262
"react": "^16.8.0 || ^17",
@@ -66,6 +66,7 @@
6666
"@size-limit/preset-small-lib": "4.7.0",
6767
"@testing-library/jest-dom": "5.11.6",
6868
"@testing-library/react": "11.1.2",
69+
"@types/body-scroll-lock": "2.6.1",
6970
"@types/classnames": "2.2.11",
7071
"@types/no-scroll": "2.1.0",
7172
"@types/node": "14.14.7",

react-responsive-modal/src/index.tsx

+8-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import cx from 'classnames';
44
import CloseIcon from './CloseIcon';
55
import { FocusTrap } from './FocusTrap';
66
import { modalManager, useModalManager } from './modalManager';
7-
import { isBrowser, blockNoScroll, unblockNoScroll } from './utils';
7+
import { useScrollLock } from './useScrollLock';
8+
import { isBrowser } from './utils';
89

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

185186
// Hook used to manage multiple modals opened at the same time
186-
useModalManager(refModal, open, blockScroll);
187+
useModalManager(refModal, open);
187188

188-
const handleOpen = () => {
189-
if (blockScroll) {
190-
blockNoScroll();
191-
}
189+
// Hook used to manage the scroll
190+
useScrollLock(refModal, open, showPortal, blockScroll);
192191

192+
const handleOpen = () => {
193193
if (
194194
refContainer.current &&
195195
!container &&
196196
!document.body.contains(refContainer.current)
197197
) {
198198
document.body.appendChild(refContainer.current);
199199
}
200+
200201
document.addEventListener('keydown', handleKeydown);
201202
};
202203

203204
const handleClose = () => {
204-
// Restore the scroll only if there is no modal on the screen
205-
// We filter the modals that are not affecting the scroll
206-
if (
207-
blockScroll &&
208-
modalManager.modals().filter((modal) => modal.blockScroll).length === 0
209-
) {
210-
unblockNoScroll();
211-
}
212-
213205
if (
214206
refContainer.current &&
215207
!container &&
@@ -235,8 +227,8 @@ export const Modal = ({
235227

236228
useEffect(() => {
237229
return () => {
238-
// When the component is unmounted directly we want to unblock the scroll
239230
if (showPortal) {
231+
// When the modal is closed or removed directly, cleanup the listeners
240232
handleClose();
241233
}
242234
};

react-responsive-modal/src/modalManager.ts

+9-18
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,37 @@
11
import { Ref, useEffect } from 'react';
22

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

55
/**
66
* Handle the order of the modals.
77
* Inspired by the material-ui implementation.
88
*/
99
export const modalManager = {
10-
/**
11-
* Return the modals array
12-
*/
13-
modals: () => modals,
14-
1510
/**
1611
* Register a new modal
1712
*/
18-
add: (newModal: Ref<any>, blockScroll: boolean) => {
19-
modals.push({ element: newModal, blockScroll });
13+
add: (newModal: Ref<Element>) => {
14+
modals.push(newModal);
2015
},
2116

2217
/**
2318
* Remove a modal
2419
*/
25-
remove: (oldModal: Ref<any>) => {
26-
modals = modals.filter((modal) => modal.element !== oldModal);
20+
remove: (oldModal: Ref<Element>) => {
21+
modals = modals.filter((modal) => modal !== oldModal);
2722
},
2823

2924
/**
3025
* When multiple modals are rendered will return true if current modal is the last one
3126
*/
32-
isTopModal: (modal: Ref<any>) =>
33-
!!modals.length && modals[modals.length - 1].element === modal,
27+
isTopModal: (modal: Ref<Element>) =>
28+
!!modals.length && modals[modals.length - 1] === modal,
3429
};
3530

36-
export function useModalManager(
37-
ref: Ref<any>,
38-
open: boolean,
39-
blockScroll: boolean
40-
) {
31+
export function useModalManager(ref: Ref<Element>, open: boolean) {
4132
useEffect(() => {
4233
if (open) {
43-
modalManager.add(ref, blockScroll);
34+
modalManager.add(ref);
4435
}
4536
return () => {
4637
modalManager.remove(ref);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useEffect, useRef } from 'react';
2+
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
3+
4+
export const useScrollLock = (
5+
refModal: React.RefObject<Element>,
6+
open: boolean,
7+
showPortal: boolean,
8+
blockScroll: boolean
9+
) => {
10+
const oldRef = useRef<Element | null>(null);
11+
12+
useEffect(() => {
13+
if (open && refModal.current && blockScroll) {
14+
oldRef.current = refModal.current;
15+
disableBodyScroll(refModal.current);
16+
}
17+
return () => {
18+
if (oldRef.current) {
19+
enableBodyScroll(oldRef.current);
20+
oldRef.current = null;
21+
}
22+
};
23+
}, [open, showPortal, refModal]);
24+
};

react-responsive-modal/src/utils.ts

-10
Original file line numberDiff line numberDiff line change
@@ -1,11 +1 @@
1-
import noScroll from 'no-scroll';
2-
31
export const isBrowser = typeof window !== 'undefined';
4-
5-
export const blockNoScroll = () => {
6-
noScroll.on();
7-
};
8-
9-
export const unblockNoScroll = () => {
10-
noScroll.off();
11-
};

yarn.lock

+16-8
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,13 @@ __metadata:
24102410
languageName: node
24112411
linkType: hard
24122412

2413+
"@types/body-scroll-lock@npm:2.6.1":
2414+
version: 2.6.1
2415+
resolution: "@types/body-scroll-lock@npm:2.6.1"
2416+
checksum: 7cb4ed5ce6b9d927321e966970b67dfc6cc23836c2eb6b03e0ef6cc4713199ab0f9cbf5a9b56987939b63a0a9e8a2c5678c1d73808e04578446dc3ea24998e32
2417+
languageName: node
2418+
linkType: hard
2419+
24132420
"@types/classnames@npm:2.2.11":
24142421
version: 2.2.11
24152422
resolution: "@types/classnames@npm:2.2.11"
@@ -3913,6 +3920,13 @@ __metadata:
39133920
languageName: node
39143921
linkType: hard
39153922

3923+
"body-scroll-lock@npm:^3.1.5":
3924+
version: 3.1.5
3925+
resolution: "body-scroll-lock@npm:3.1.5"
3926+
checksum: e8de58edc0fd7d483e3971045ff83fe6d722592d5153e9bfc9da2962a179a6522b432a4b35344bc8ae522eec080cc87301ce4ded4878f26254bf42a4f26d27ed
3927+
languageName: node
3928+
linkType: hard
3929+
39163930
"boolbase@npm:^1.0.0, boolbase@npm:~1.0.0":
39173931
version: 1.0.0
39183932
resolution: "boolbase@npm:1.0.0"
@@ -10615,13 +10629,6 @@ fsevents@^1.2.7:
1061510629
languageName: node
1061610630
linkType: hard
1061710631

10618-
"no-scroll@npm:^2.1.1":
10619-
version: 2.1.1
10620-
resolution: "no-scroll@npm:2.1.1"
10621-
checksum: a575d5d3b84164a3eab41f4854526bf66969428533fccba87bd9898a86123cdf625c7bfb26bf1b479698dfec9bcf2f17ae9c743f196ca9469b6296e2e13f4b8e
10622-
languageName: node
10623-
linkType: hard
10624-
1062510632
"node-abi@npm:^2.7.0":
1062610633
version: 2.19.1
1062710634
resolution: "node-abi@npm:2.19.1"
@@ -12511,17 +12518,18 @@ fsevents@^1.2.7:
1251112518
"@size-limit/preset-small-lib": 4.7.0
1251212519
"@testing-library/jest-dom": 5.11.6
1251312520
"@testing-library/react": 11.1.2
12521+
"@types/body-scroll-lock": 2.6.1
1251412522
"@types/classnames": 2.2.11
1251512523
"@types/no-scroll": 2.1.0
1251612524
"@types/node": 14.14.7
1251712525
"@types/react": 16.9.56
1251812526
"@types/react-dom": 16.9.9
1251912527
"@types/react-transition-group": 4.4.0
1252012528
babel-jest: 26.6.3
12529+
body-scroll-lock: ^3.1.5
1252112530
classnames: ^2.2.6
1252212531
cypress: 5.6.0
1252312532
husky: 4.3.0
12524-
no-scroll: ^2.1.1
1252512533
prettier: 2.1.2
1252612534
react: 17.0.1
1252712535
react-dom: 17.0.1

0 commit comments

Comments
 (0)