Skip to content

Commit cbf6cb1

Browse files
committed
added use lock scroll hook
1 parent d68de0e commit cbf6cb1

File tree

3 files changed

+156
-1
lines changed

3 files changed

+156
-1
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,32 @@ const SomeComponent: React.VFC = ({ slug }) => {
535535
}
536536
```
537537
538+
### `useLockScroll`
539+
540+
When a modal / popover opens you often want to lock the scroll of the body to prevent double scrollsbars.
541+
This hook provides two types of lock solutions that are often used.
542+
543+
LockType.Overflow is clean and has less impact on code but is not supported in IOS / Safari
544+
LockType.Fixed uses position fixed but remembers your scroll position to prevent jumping to top of page. Use when you need all support, could have more impact on your styles.
545+
> More info can be found here: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
546+
547+
Usage
548+
```typescript jsx
549+
import useLockScroll, { LockType } from '@freshheads/javascript-essentials/build/react/hooks/useLockScroll';
550+
551+
552+
const Modal = () => {
553+
const { isOpen, setIsOpen } = useState<boolean>(false);
554+
useLockScroll(LockType.Overflow, isOpen);
555+
556+
// ...
557+
}
558+
```
559+
560+
> There are other solutions:
561+
> 1. https://github.com/willmcpo/body-scroll-lock -> prevents touch events on iOS in combination with overflow
562+
> 2. Use overscroll-behavior: contain; -> Css only but seems to have some drawbacks (good for research / first try)
563+
538564
## Routing
539565
540566
### `createPathFromRoute`
@@ -664,4 +690,3 @@ toJson({ value: new SomeClass() }); // = typescript error
664690
- [Tracking utilities](https://github.com/freshheads/013/blob/develop/assets/frontend/src/js/utility/trackingUtilities.ts) (misschien ook HOC oid. `withTrackingOnClick` oid.? Of een hook?)
665691
- [Routing: extract path with placeholders](https://github.com/freshheads/013/blob/develop/assets/frontend/src/js/routing/utility/urlGenerator.ts#L13)
666692
- [Group object array preserving order](https://github.com/freshheads/freshheads-data-2.0/blob/develop/assets/frontend/src/js/components/blueprintPeriodResultOverview/utilities/resultSortingUtilities.ts)
667-
- [LockScroll](https://github.com/freshheads/freshheads-kerstwens-2020/blob/main/src/hooks/useLockHtmlScroll.ts)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import useLockScroll, { LockType } from '../useLockScroll';
3+
4+
describe('useLockScroll', () => {
5+
it('should add overflow to html element', () => {
6+
renderHook(() => useLockScroll(LockType.Overflow, true));
7+
8+
expect(document.documentElement.style.overflow).toBe('hidden');
9+
});
10+
11+
it('should add position fixed to body element', () => {
12+
renderHook(() => useLockScroll(LockType.Fixed, true));
13+
14+
expect(document.body.style.position).toBe('fixed');
15+
});
16+
});

src/react/hooks/useLockScroll.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useEffect } from 'react';
2+
3+
export enum LockType {
4+
Overflow = 'overflow',
5+
Fixed = 'fixed',
6+
}
7+
8+
let counter = 0;
9+
let originalOverflow: string | null = null;
10+
let originalPosition: string | null = null;
11+
let scrollOffsetY = 0;
12+
let scrollOffsetX = 0;
13+
let useBodyScroll = false;
14+
let currentLockType: LockType | null = null;
15+
16+
// Supports two lock types eg. Overflow or Fixed
17+
// Overflow is clean and has less impact on code but is not supported in IOS / Safari
18+
// @see https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/
19+
// There are other solutions:
20+
// 1. https://github.com/willmcpo/body-scroll-lock -> prevents touch events on iOS in combination with overflow
21+
// 2. Use overscroll-behavior: contain; -> Css only but seems to have some drawbacks (good for research / first try)
22+
const lock = () => {
23+
switch (currentLockType) {
24+
case LockType.Overflow:
25+
originalOverflow = window.getComputedStyle(document.documentElement)
26+
.overflow;
27+
document.documentElement.style.overflow = 'hidden';
28+
29+
return;
30+
case LockType.Fixed:
31+
originalPosition = window.getComputedStyle(document.body).position;
32+
33+
// @todo move calculate position logic to utility which can be used separately
34+
scrollOffsetY =
35+
window.scrollY ||
36+
document.documentElement.scrollTop ||
37+
document.body.scrollTop;
38+
scrollOffsetX =
39+
window.scrollX ||
40+
document.documentElement.scrollLeft ||
41+
document.body.scrollLeft;
42+
useBodyScroll =
43+
document.body.scrollLeft > 0 &&
44+
document.documentElement.scrollLeft === 0;
45+
46+
document.body.style.position = 'fixed';
47+
document.body.style.top = `-${scrollOffsetY}px`;
48+
document.body.style.left = `-${scrollOffsetX}px`;
49+
50+
return;
51+
default:
52+
throw new Error('Lock type not supported');
53+
}
54+
};
55+
56+
const unlock = () => {
57+
switch (currentLockType) {
58+
case LockType.Overflow:
59+
if (originalOverflow) {
60+
document.documentElement.style.overflow = originalOverflow;
61+
}
62+
63+
originalOverflow = null;
64+
65+
return;
66+
case LockType.Fixed:
67+
if (originalPosition) {
68+
document.body.style.position = originalPosition;
69+
}
70+
71+
if (useBodyScroll) {
72+
window.scrollTo(0, scrollOffsetY);
73+
document.body.scrollTo(scrollOffsetX, 0);
74+
} else {
75+
window.scrollTo(scrollOffsetX, scrollOffsetY);
76+
}
77+
78+
originalPosition = null;
79+
80+
return;
81+
default:
82+
throw new Error('Lock type not supported');
83+
}
84+
};
85+
86+
const increment = () => {
87+
counter++;
88+
if (counter === 1) {
89+
lock();
90+
}
91+
};
92+
93+
const decrement = () => {
94+
counter--;
95+
if (counter === 0) {
96+
unlock();
97+
}
98+
};
99+
100+
const useLockScroll = (lockType: LockType, enabled = true) => {
101+
useEffect(() => {
102+
if (enabled) {
103+
currentLockType = lockType;
104+
105+
increment();
106+
107+
return () => decrement();
108+
}
109+
110+
return;
111+
}, [enabled, lockType]);
112+
};
113+
114+
export default useLockScroll;

0 commit comments

Comments
 (0)