diff --git a/packages/vue-final-modal/src/components/VueFinalModal/useBodyScrollLock.ts b/packages/vue-final-modal/src/components/VueFinalModal/useBodyScrollLock.ts index dd710360..6c4b0711 100644 --- a/packages/vue-final-modal/src/components/VueFinalModal/useBodyScrollLock.ts +++ b/packages/vue-final-modal/src/components/VueFinalModal/useBodyScrollLock.ts @@ -12,6 +12,8 @@ type Lock = { options?: BodyScrollOptions } +type AxisType = 'x' | 'y' + // stolen from body-scroll-lock // Older browsers don't support event options, feature detect it. @@ -40,24 +42,34 @@ const isIosDevice let locks: Lock[] = [] let documentListenerAdded = false -let clientY = 0 -let initialClientY = -1 +let client: Record<AxisType, number> = { x: 0, y: 0 } +let initialClient: Record<AxisType, number> = { x: -1, y: -1 } let previousBodyOverflowSetting: undefined | string let previousBodyPaddingRight: undefined | string +let axis: AxisType | null = null -const hasScrollbar = (el: HTMLElement) => { +const hasScrollbar = (el: HTMLElement, axis: AxisType) => { if (!el || el.nodeType !== Node.ELEMENT_NODE) return false const style = window.getComputedStyle(el) - return ['auto', 'scroll'].includes(style.overflowY) && el.scrollHeight > el.clientHeight + const overflow = style[`overflow${axis === 'y' ? 'Y' : 'X'}`] + const totalScroll = el[`scroll${axis === 'y' ? 'Height' : 'Width'}`] + const clientSize = el[`client${axis === 'y' ? 'Height' : 'Width'}`] + + return ['auto', 'scroll'].includes(overflow) && totalScroll > clientSize } -const shouldScroll = (el: HTMLElement, delta: number) => { - if (el.scrollTop === 0 && delta < 0) +const shouldScroll = (el: HTMLElement, delta: number, axis: AxisType) => { + const totalScroll = el[`scroll${axis === 'y' ? 'Height' : 'Width'}`] + const scrolled = el[`scroll${axis === 'y' ? 'Top' : 'Left'}`] + const clientSize = el[`client${axis === 'y' ? 'Height' : 'Width'}`] + + if (scrolled === 0 && delta < 0) return false - if (el.scrollTop + el.clientHeight + delta >= el.scrollHeight && delta > 0) + if (scrolled + clientSize + delta >= totalScroll && delta > 0) return false + return true } @@ -72,18 +84,20 @@ const composedPath = (el: null | HTMLElement) => { return path } -const hasAnyScrollableEl = (el: HTMLElement | null, delta: number) => { - let hasAnyScrollableEl = false +const hasAnyScrollableEl = (el: HTMLElement | null) => { const path = composedPath(el) - path.forEach((el) => { - if (hasScrollbar(el) && shouldScroll(el, delta)) - hasAnyScrollableEl = true - }) - return hasAnyScrollableEl + for (const el of path) { + if (hasScrollbar(el, 'y') && shouldScroll(el, -client.y, 'y')) + return true + + if (hasScrollbar(el, 'x') && shouldScroll(el, -client.x, 'x')) + return true + } + return false } // returns true if `el` should be allowed to receive touchmove events. -const allowTouchMove = (el: HTMLElement | null) => locks.some(() => hasAnyScrollableEl(el, -clientY)) +const allowTouchMove = (el: HTMLElement | null) => locks.some(() => hasAnyScrollableEl(el)) const preventDefault = (rawEvent: TouchEvent) => { const e = rawEvent || window.event @@ -142,21 +156,35 @@ const restoreOverflowSetting = () => { } } // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions -const isTargetElementTotallyScrolled = (targetElement: HTMLElement) => - targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false +const isTargetElementTotallyScrolled = (targetElement: any, axis: AxisType): boolean => { + if (targetElement) { + const totalScroll = targetElement[`scroll${axis === 'y' ? 'Height' : 'Width'}`] + const scrolled = targetElement[`scroll${axis === 'y' ? 'Top' : 'Left'}`] + const clientSize = targetElement[`client${axis === 'y' ? 'Height' : 'Width'}`] + return totalScroll - scrolled <= clientSize + } + return false +} -const handleScroll = (event: TouchEvent, targetElement: HTMLElement) => { - clientY = event.targetTouches[0].clientY - initialClientY +const handleScroll = (event: TouchEvent, targetElement: HTMLElement, axis: AxisType) => { + const touch = event.targetTouches[0] + client = { + x: touch.clientX - initialClient.x, + y: touch.clientY - initialClient.y, + } + const initialPos = initialClient[axis] + const scrollPos = targetElement && targetElement[`scroll${axis === 'y' ? 'Top' : 'Left'}`] + const clientPos = (axis === 'y' ? touch.clientY : touch.clientX) - initialPos if (allowTouchMove(event.target as HTMLElement | null)) return false - if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { + if (targetElement && scrollPos === 0 && clientPos > 0) { // element is at the top of its scroll. return preventDefault(event) } - if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { + if (isTargetElementTotallyScrolled(targetElement, axis) && clientPos < 0) { // element is at the bottom of its scroll. return preventDefault(event) } @@ -189,13 +217,21 @@ export const disableBodyScroll = (targetElement?: HTMLElement, options?: BodyScr targetElement.ontouchstart = (event: TouchEvent) => { if (event.targetTouches.length === 1) { // detect single touch. - initialClientY = event.targetTouches[0].clientY + initialClient = { + x: event.targetTouches[0].clientX, + y: event.targetTouches[0].clientY, + } } } targetElement.ontouchmove = (event: TouchEvent) => { if (event.targetTouches.length === 1) { // detect single touch. - handleScroll(event, targetElement) + if (!axis) { + const distX = Math.abs(initialClient.x - event.targetTouches[0].clientX) + const distY = Math.abs(initialClient.y - event.targetTouches[0].clientY) + axis = distX > distY ? 'x' : 'y' + } + handleScroll(event, targetElement, axis) } }