Skip to content

Commit 73d96f2

Browse files
committed
temp: remove scroll-lock, add body-scroll-lock back
1 parent 9d355ed commit 73d96f2

File tree

6 files changed

+287
-76
lines changed

6 files changed

+287
-76
lines changed

packages/vue-final-modal/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,18 @@
3232
"devDependencies": {
3333
"@cypress/vue": "^5.0.5",
3434
"@release-it/conventional-changelog": "^5.1.1",
35-
"@types/scroll-lock": "^2.1.3",
3635
"@vueuse/core": "^10.7.1",
3736
"@vueuse/integrations": "^10.7.1",
3837
"cypress": "^13.6.0",
3938
"focus-trap": "^7.5.4",
4039
"release-it": "^16.1.3",
41-
"scroll-lock": "^2.1.5",
4240
"vite-plugin-dts": "^3.6.3",
4341
"vue": "3.3.7"
4442
},
4543
"peerDependencies": {
4644
"@vueuse/core": ">=10.0.0",
4745
"@vueuse/integrations": ">=10.0.0",
4846
"focus-trap": ">=7.2.0",
49-
"scroll-lock": ">=2.1.5",
5047
"vue": ">=3.0.0"
5148
},
5249
"homepage": "https://vue-final-modal.org/",

packages/vue-final-modal/src/components/VueFinalModal/VueFinalModal.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useTransition } from './useTransition'
55
import { useToClose } from './useToClose'
66
import { useModelValue } from './useModelValue'
77
import { useFocusTrap } from './useFocusTrap'
8-
import { useScrollLock } from './useScrollLock'
8+
import { useLockScroll } from './useBodyScrollLock'
99
import { useZIndex } from './useZIndex'
1010
import { vVisible } from './vVisible'
1111
import { useInternalExposed } from './useInternalExposed'
@@ -46,7 +46,7 @@ const vfmContentEl = ref<HTMLDivElement>()
4646
4747
const { focus, blur } = useFocusTrap(props, { focusEl: vfmRootEl })
4848
const { modelValueLocal } = useModelValue(props, emit, { open, close })
49-
const { disablePageScroll, enablePageScroll } = useScrollLock(props, {
49+
const { disableBodyScroll, enableBodyScroll } = useLockScroll(props, {
5050
lockScrollEl: vfmRootEl,
5151
modelValueLocal,
5252
})
@@ -77,7 +77,7 @@ onMounted(() => {
7777
})
7878
7979
onBeforeUnmount(() => {
80-
enablePageScroll()
80+
enableBodyScroll()
8181
arrayRemoveItem(modals, modalExposed)
8282
arrayRemoveItem(openedModals, modalExposed)
8383
blur()
@@ -86,7 +86,7 @@ onBeforeUnmount(() => {
8686
8787
function onEntering() {
8888
nextTick(() => {
89-
disablePageScroll()
89+
disableBodyScroll()
9090
focus()
9191
})
9292
}
@@ -100,7 +100,7 @@ function onEnter() {
100100
function onLeave() {
101101
arrayRemoveItem(openedModals, modalExposed)
102102
resetZIndex()
103-
enablePageScroll()
103+
enableBodyScroll()
104104
emit('closed')
105105
// eslint-disable-next-line vue/custom-event-name-casing
106106
emit('_closed')
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import type { Ref } from 'vue'
2+
import { onBeforeUnmount, watch } from 'vue'
3+
import type VueFinalModal from './VueFinalModal.vue'
4+
5+
type BodyScrollOptions = {
6+
reserveScrollBarGap?: boolean
7+
allowTouchMove?: (el?: null | HTMLElement) => boolean
8+
}
9+
10+
type Lock = {
11+
targetElement: HTMLElement
12+
options?: BodyScrollOptions
13+
}
14+
15+
// stolen from body-scroll-lock
16+
17+
// Older browsers don't support event options, feature detect it.
18+
let hasPassiveEvents = false
19+
if (typeof window !== 'undefined') {
20+
const passiveTestOptions = {
21+
get passive() {
22+
hasPassiveEvents = true
23+
return undefined
24+
},
25+
}
26+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
27+
// @ts-expect-error
28+
window.addEventListener('testPassive', null, passiveTestOptions)
29+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
30+
// @ts-expect-error
31+
window.removeEventListener('testPassive', null, passiveTestOptions)
32+
}
33+
34+
const isIosDevice
35+
= typeof window !== 'undefined'
36+
&& window.navigator
37+
&& window.navigator.platform
38+
&& (/iP(ad|hone|od)/.test(window.navigator.platform)
39+
|| (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1))
40+
41+
let locks: Lock[] = []
42+
let documentListenerAdded = false
43+
let clientY = 0
44+
let initialClientY = -1
45+
let previousBodyOverflowSetting: undefined | string
46+
let previousBodyPaddingRight: undefined | string
47+
48+
const hasScrollbar = (el: HTMLElement) => {
49+
if (!el || el.nodeType !== Node.ELEMENT_NODE)
50+
return false
51+
52+
const style = window.getComputedStyle(el)
53+
return ['auto', 'scroll'].includes(style.overflowY) && el.scrollHeight > el.clientHeight
54+
}
55+
56+
const shouldScroll = (el: HTMLElement, delta: number) => {
57+
if (el.scrollTop === 0 && delta < 0)
58+
return false
59+
if (el.scrollTop + el.clientHeight + delta >= el.scrollHeight && delta > 0)
60+
return false
61+
return true
62+
}
63+
64+
const composedPath = (el: null | HTMLElement) => {
65+
const path = []
66+
while (el) {
67+
path.push(el)
68+
if (el.classList.contains('vfm'))
69+
return path
70+
el = el.parentElement
71+
}
72+
return path
73+
}
74+
75+
const hasAnyScrollableEl = (el: HTMLElement | null, delta: number) => {
76+
let hasAnyScrollableEl = false
77+
const path = composedPath(el)
78+
path.forEach((el) => {
79+
if (hasScrollbar(el) && shouldScroll(el, delta))
80+
hasAnyScrollableEl = true
81+
})
82+
return hasAnyScrollableEl
83+
}
84+
85+
// returns true if `el` should be allowed to receive touchmove events.
86+
const allowTouchMove = (el: HTMLElement | null) => locks.some(() => hasAnyScrollableEl(el, -clientY))
87+
88+
const preventDefault = (rawEvent: TouchEvent) => {
89+
const e = rawEvent || window.event
90+
91+
// For the case whereby consumers adds a touchmove event listener to document.
92+
// Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })
93+
// in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then
94+
// the touchmove event on document will break.
95+
if (allowTouchMove(e.target as HTMLElement | null))
96+
return true
97+
98+
// Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).
99+
if (e.touches.length > 1)
100+
return true
101+
102+
if (e.preventDefault)
103+
e.preventDefault()
104+
105+
return false
106+
}
107+
108+
const setOverflowHidden = (options?: BodyScrollOptions) => {
109+
// If previousBodyPaddingRight is already set, don't set it again.
110+
if (previousBodyPaddingRight === undefined) {
111+
const reserveScrollBarGap = !!options && options.reserveScrollBarGap === true
112+
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
113+
114+
if (reserveScrollBarGap && scrollBarGap > 0) {
115+
const computedBodyPaddingRight = parseInt(getComputedStyle(document.body).getPropertyValue('padding-right'), 10)
116+
previousBodyPaddingRight = document.body.style.paddingRight
117+
document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`
118+
}
119+
}
120+
// If previousBodyOverflowSetting is already set, don't set it again.
121+
if (previousBodyOverflowSetting === undefined) {
122+
previousBodyOverflowSetting = document.body.style.overflow
123+
document.body.style.overflow = 'hidden'
124+
}
125+
}
126+
127+
const restoreOverflowSetting = () => {
128+
if (previousBodyPaddingRight !== undefined) {
129+
document.body.style.paddingRight = previousBodyPaddingRight
130+
131+
// Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it
132+
// can be set again.
133+
previousBodyPaddingRight = undefined
134+
}
135+
136+
if (previousBodyOverflowSetting !== undefined) {
137+
document.body.style.overflow = previousBodyOverflowSetting
138+
139+
// Restore previousBodyOverflowSetting to undefined
140+
// so setOverflowHidden knows it can be set again.
141+
previousBodyOverflowSetting = undefined
142+
}
143+
}
144+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
145+
const isTargetElementTotallyScrolled = (targetElement: HTMLElement) =>
146+
targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false
147+
148+
const handleScroll = (event: TouchEvent, targetElement: HTMLElement) => {
149+
clientY = event.targetTouches[0].clientY - initialClientY
150+
151+
if (allowTouchMove(event.target as HTMLElement | null))
152+
return false
153+
154+
if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
155+
// element is at the top of its scroll.
156+
return preventDefault(event)
157+
}
158+
159+
if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
160+
// element is at the bottom of its scroll.
161+
return preventDefault(event)
162+
}
163+
164+
event.stopPropagation()
165+
return true
166+
}
167+
168+
export const disableBodyScroll = (targetElement?: HTMLElement, options?: BodyScrollOptions) => {
169+
// targetElement must be provided
170+
if (!targetElement) {
171+
console.error(
172+
'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.',
173+
)
174+
return
175+
}
176+
177+
// disableBodyScroll must not have been called on this targetElement before
178+
if (locks.some(lock => lock.targetElement === targetElement))
179+
return
180+
181+
const lock = {
182+
targetElement,
183+
options: options || {},
184+
}
185+
186+
locks = [...locks, lock]
187+
188+
if (isIosDevice) {
189+
targetElement.ontouchstart = (event: TouchEvent) => {
190+
if (event.targetTouches.length === 1) {
191+
// detect single touch.
192+
initialClientY = event.targetTouches[0].clientY
193+
}
194+
}
195+
targetElement.ontouchmove = (event: TouchEvent) => {
196+
if (event.targetTouches.length === 1) {
197+
// detect single touch.
198+
handleScroll(event, targetElement)
199+
}
200+
}
201+
202+
if (!documentListenerAdded) {
203+
document.addEventListener('touchmove', preventDefault, hasPassiveEvents ? { passive: false } : undefined)
204+
documentListenerAdded = true
205+
}
206+
}
207+
else {
208+
setOverflowHidden(options)
209+
}
210+
}
211+
212+
export const enableBodyScroll = (targetElement?: HTMLElement) => {
213+
if (!targetElement) {
214+
console.error(
215+
'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.',
216+
)
217+
return
218+
}
219+
220+
locks = locks.filter(lock => lock.targetElement !== targetElement)
221+
222+
if (isIosDevice) {
223+
targetElement.ontouchstart = null
224+
targetElement.ontouchmove = null
225+
226+
if (documentListenerAdded && locks.length === 0) {
227+
document.removeEventListener('touchmove', preventDefault, (hasPassiveEvents ? { passive: false } : undefined) as any)
228+
documentListenerAdded = false
229+
}
230+
}
231+
else if (!locks.length) {
232+
restoreOverflowSetting()
233+
}
234+
}
235+
236+
export function useLockScroll(props: InstanceType<typeof VueFinalModal>['$props'], options: {
237+
lockScrollEl: Ref<undefined | HTMLElement>
238+
modelValueLocal: Ref<boolean>
239+
}) {
240+
const { lockScrollEl, modelValueLocal } = options
241+
242+
let _lockScrollEl: HTMLElement
243+
watch(lockScrollEl, (val) => {
244+
if (val)
245+
_lockScrollEl = val
246+
}, { immediate: true })
247+
248+
watch(() => props.lockScroll, (val) => {
249+
val ? _disableBodyScroll() : _enableBodyScroll()
250+
})
251+
252+
onBeforeUnmount(() => {
253+
_enableBodyScroll()
254+
})
255+
256+
function _enableBodyScroll() {
257+
_lockScrollEl && enableBodyScroll(_lockScrollEl)
258+
}
259+
260+
function _disableBodyScroll() {
261+
if (!modelValueLocal.value)
262+
return
263+
props.lockScroll && _lockScrollEl
264+
&& disableBodyScroll(_lockScrollEl, {
265+
reserveScrollBarGap: props.reserveScrollBarGap,
266+
allowTouchMove: (el) => {
267+
while (el && el !== document.body) {
268+
if (el.getAttribute('vfm-scroll-lock-ignore') !== null)
269+
return true
270+
271+
el = el.parentElement
272+
}
273+
return false
274+
},
275+
})
276+
}
277+
278+
return {
279+
enableBodyScroll: _enableBodyScroll,
280+
disableBodyScroll: _disableBodyScroll,
281+
}
282+
}

0 commit comments

Comments
 (0)