-
-
Save gragland/f50690d2724aec1bd513de8596dcd9b9 to your computer and use it in GitHub Desktop.
import { useState, useLayoutEffect } from 'react'; | |
// Usage | |
function App(){ | |
// State for our modal | |
const [modalOpen, setModalOpen] = useState(false); | |
return ( | |
<div> | |
<button onClick={() => setModalOpen(true)}>Show Modal</button> | |
<Content /> | |
{modalOpen && ( | |
<Modal | |
title="Try scrolling" | |
content="I bet you you can't! Muahahaha 😈" | |
onClose={() => setModalOpen(false)} | |
/> | |
)} | |
</div> | |
); | |
} | |
function Modal({ title, content, onClose }){ | |
// Call hook to lock body scroll | |
useLockBodyScroll(); | |
return ( | |
<div className="modal-overlay" onClick={onClose}> | |
<div className="modal"> | |
<h2>{title}</h2> | |
<p>{content}</p> | |
</div> | |
</div> | |
); | |
} | |
// Hook | |
function useLockBodyScroll() { | |
useLayoutEffect(() => { | |
// Get original value of body overflow | |
const originalStyle = window.getComputedStyle(document.body).overflow; | |
// Prevent scrolling on mount | |
document.body.style.overflow = 'hidden'; | |
// Re-enable scrolling when component unmounts | |
return () => document.body.style.overflow = originalStyle; | |
}, []); // Empty array ensures effect is only run on mount and unmount | |
} |
@solutweb Ah good point. When I have some time will have to dig into what's going on there. Open to pull requests if you happen to figure it out!
in addition to width issue @anyexinglu pointed out about windows scrollbars, there is also an issue with with fixed backgrounds resizing when you overflow:hidden the scrollbars. material-ui tries to resolve this by adding padding to any element with the class mui-fixed.
Why does it use useLayoutEffect
? Seems unnecessary to me since we only use it on mount and unmount.
When using it in TypeScript, it complains that the first param doesn't match EffectCallback
, since it's returning () => string
.
Changing return () => (document.body.style.overflow = 'visible')
into return () => {document.body.style.overflow = 'visible'}
fixes this.
This currently won't work for iOS devices. You could rewrite it like this
// Create ref to bind to element that should prevent scrolling
const ref = useRef(null);
useEffect(() => {
// Add/Remove listeners for touchmove -> finding touchmove better, as it doesn't prevent other touch events like clicking links
// The checks for ref && ref.current made it more TS friendly for me
if (ref && ref.current) {
ref.current.addEventListener('touchmove', e => e.preventDefault());
}
return () => {
if (ref && ref.current) {
ref.current.removeEventListener('touchmove', e => e.preventDefault());
}
};
}, []);
useLayoutEffect(() => {
// Get original body overflow
const originalStyle = window.getComputedStyle(document.body).overflow;
// Prevent scrolling on mount
document.body.style.overflow = 'hidden';
// Re-enable scrolling when component unmounts
return () => {
return (document.body.style.overflow = originalStyle);
};
}, []); // Empty array ensures effect is only run on mount and unmount
// Return the ref, to bind it to the element;
return ref;
}````
And by binding the ref to the element that locks the scrolling, iOS devices will also respect the scroll lock
I took a shot at modifying this to make it work:
- On iOS
- When multiple "instances" of
lockBodyScroll
(modals in this case) are active
It seems to be working with my changes in my sandbox fork here: https://codesandbox.io/s/uselockbodyscroll-example-mq9rm.
Or test it on your iOS device here: https://mq9rm.csb.app/
But I'm pretty new to custom hooks (and programming, really) so I'm open to suggestions/critique 👍🏼
EDIT: First of all, thanks for this. 👍
Saddly doesn't work on mobile 😞.
I tried on my Moto G5s + Chrome and on iPhone 8 Plus + Chrome too 🤔: