-
-
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 | |
| } |
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 ๐๐ผ
@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!