Skip to content

Commit fb2ec29

Browse files
feat: useBodyScrollLock
1 parent 17256c6 commit fb2ec29

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

packages/vue-primitives/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { createContext } from './createContext.ts'
2+
export { useBodyScrollLock } from './useBodyScrollLock.ts'
23
export { useComposedElements } from './useComposedElements.ts'
34
export { useControllableState, useControllableStateV2 } from './useControllableState.ts'
45
export { useEscapeKeydown } from './useEscapeKeydown.ts'
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { isClient, isIOS } from '@vueuse/core'
2+
3+
export function useBodyScrollLock(): () => void {
4+
if (!isClient)
5+
return () => {}
6+
7+
const body = document.body
8+
if (body.hasAttribute('data-scroll-lock'))
9+
return () => {}
10+
11+
const html = document.documentElement
12+
const bodyStyle = body.style
13+
14+
const originalStyles = {
15+
overflow: bodyStyle.overflow,
16+
overflowX: bodyStyle.overflowX,
17+
overflowY: bodyStyle.overflowY,
18+
position: bodyStyle.position,
19+
top: bodyStyle.top,
20+
left: bodyStyle.left,
21+
right: bodyStyle.right,
22+
bottom: bodyStyle.bottom,
23+
scrollBehavior: html.style.scrollBehavior,
24+
}
25+
26+
const initialOverflow = bodyStyle.overflow
27+
const scrollY = window.scrollY
28+
29+
bodyStyle.top = `-${scrollY}px`
30+
bodyStyle.overflowX = 'hidden'
31+
html.style.scrollBehavior = 'auto'
32+
33+
bodyStyle.setProperty('overflow-y', 'scroll', 'important')
34+
bodyStyle.position = 'fixed'
35+
bodyStyle.left = '0'
36+
bodyStyle.right = '0'
37+
bodyStyle.bottom = '0'
38+
body.setAttribute('data-scroll-lock', 'true')
39+
40+
let stopTouchMoveListener: (() => void) | undefined
41+
42+
if (isIOS) {
43+
function onTouchmove(e: TouchEvent) {
44+
preventDefault(e)
45+
}
46+
47+
document.addEventListener('touchmove', onTouchmove, {
48+
passive: false,
49+
})
50+
51+
stopTouchMoveListener = () => {
52+
document.removeEventListener('touchmove', onTouchmove)
53+
}
54+
}
55+
56+
function onlock() {
57+
bodyStyle.overflow = initialOverflow ?? ''
58+
body.removeAttribute('data-scroll-lock')
59+
60+
bodyStyle.overflowY = originalStyles.overflowY
61+
bodyStyle.position = originalStyles.position
62+
bodyStyle.left = originalStyles.left
63+
bodyStyle.right = originalStyles.right
64+
bodyStyle.bottom = originalStyles.bottom
65+
66+
bodyStyle.top = originalStyles.top
67+
window.scrollTo(0, scrollY)
68+
html.style.scrollBehavior = originalStyles.scrollBehavior
69+
70+
stopTouchMoveListener?.()
71+
}
72+
73+
return onlock
74+
}
75+
76+
function preventDefault(event: TouchEvent): boolean {
77+
const _target = event.target as Element
78+
79+
// Do not prevent if element or parentNodes have overflow: scroll set.
80+
if (checkOverflowScroll(_target))
81+
return false
82+
83+
// Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
84+
if (event.touches.length > 1)
85+
return true
86+
87+
if (event.preventDefault)
88+
event.preventDefault()
89+
90+
return false
91+
}
92+
93+
function checkOverflowScroll(el: Element): boolean {
94+
const style = window.getComputedStyle(el)
95+
if (
96+
style.overflowX === 'scroll'
97+
|| style.overflowY === 'scroll'
98+
|| (style.overflowX === 'auto' && el.clientWidth < el.scrollWidth)
99+
|| (style.overflowY === 'auto' && el.clientHeight < el.scrollHeight)
100+
) {
101+
return true
102+
}
103+
else {
104+
const parent = el.parentNode as Element
105+
106+
if (!parent || parent.tagName === 'BODY')
107+
return false
108+
109+
return checkOverflowScroll(parent)
110+
}
111+
}

0 commit comments

Comments
 (0)