Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix horizontal scroll in child element on IOS #434

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}

Expand Down