-
-
Save MartijnHols/e9f4f787efa9190885a708468f63c5bb to your computer and use it in GitHub Desktop.
The latest version is available at https://martijnhols.nl/gists/how-to-get-document-height-ios-safari-osk |
import { useEffect } from 'react' | |
const useOnScreenKeyboardScrollFix = () => { | |
useEffect(() => { | |
const handleScroll = () => { | |
window.scrollTo(0, 0) | |
} | |
window.addEventListener('scroll', handleScroll) | |
return () => { | |
window.removeEventListener('scroll', handleScroll) | |
} | |
}, []) | |
} | |
export default useOnScreenKeyboardScrollFix |
Your gist was really useful, thanks! I had to combine useDocumentHeight.ts
with this approach from SO to use in Next.js with SSR.
@MartijnHols can you give us a complete code example? currently is not working for me and I don't know why :(
Not at this time, but I can give you some pointers that may or may not be related. I had a few iterations on this solution, here's the latest version I use:
Global styling:
html,
body {
// Necessary for iOS when installed on homescreen, otherwise
// useDocumentHeight does not include the notch height.
// This must be vh not % due to a bug in iOS 15.1 where the address
// bar sometimes minimizes after the OSK closes. When this happens,
// only 100vh seems to have the correct value.
height: 100vh;
}
#root {
position: absolute;
top: 0;
left: 0;
width: 100%;
// This must be vh not % due to a bug in iOS 15.1 where the address
// bar sometimes minimizes after the OSK closes. When this happens,
// only 100vh seems to have the correct value.
height: 100vh;
overflow: hidden;
}
And then this is the primary sizing component (essentially the child of #root):
FullViewportContainer.tsx:
import { ElementType, HTMLAttributes } from 'react'
import useIsOnScreenKeyboardOpen from './useIsOnScreenKeyboardOpen'
import useOnScreenKeyboardScrollFix from './useOnScreenKeyboardScrollFix'
import usePreventOverScrolling from './usePreventOverscrolling'
import useViewportSize from './utils/useViewportSize'
interface Props extends HTMLAttributes<HTMLDivElement> {
element?: ElementType
}
const FullViewportContainer = ({
element: Element = 'div',
...others
}: Props) => {
const [, viewportHeight] = useViewportSize()
useOnScreenKeyboardScrollFix()
const isOnScreenKeyboardOpen = useIsOnScreenKeyboardOpen()
const ref = usePreventOverScrolling() // gist readers note: this solves a different problem and can be omitted
return (
<Element
{...others}
ref={ref}
style={{
height: viewportHeight,
padding: isOnScreenKeyboardOpen
? 'env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)'
: 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)',
transition: 'padding 100ms, height 100ms',
}}
/>
)
}
export default FullViewportContainer
useViewportSize.ts (IIRC in a later project I ran into issues with the useViewportSizeEffect and reworked it, but I don't exactly recall where and what that was):
https://martijnhols.nl/gists/how-to-get-document-height-ios-safari-osk
useIsOnScreenKeyboardOpen.ts (this is pretty ugly but it works for me 🤷 ):
https://martijnhols.nl/gists/how-to-detect-the-on-screen-keyboard-in-ios-safari
Can I get usePreventOverScrolling hook , please ?
@dylan0805 This is pretty hacky; at the limits of what is reasonable to do in a browser. I'd only use if you really need to.
import { useEffect, useRef } from 'react'
import findNearestScrollContainer from './utils/findNearestScrollContainer'
// On iOS in the browser you can pull on elements down / make the page bounce
// similar to pull to refresh. This causes weird behavior when trying to
// simulate a full screen app.
// This is a partial fix for edge cases where this is still possible (e.g. on
// iOS 15 when hiding the button bar via the Aa button on the address bar). Most
// regular cases are fixed properly by ensuring the page content height isn't
// larger than the viewport.
const usePreventOverScrolling = () => {
const container = useRef<HTMLDivElement>(null)
useEffect(() => {
const elem = container.current
if (!elem) {
return
}
let startTouch: Touch | undefined = undefined
const handleTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 1) {
return
}
startTouch = e.touches[0]
}
const handleTouchMove = (e: TouchEvent) => {
if (e.touches.length !== 1 || !startTouch) {
return
}
const deltaY = startTouch.pageY - e.targetTouches[0].pageY
const deltaX = startTouch.pageX - e.targetTouches[0].pageX
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal scroll probably
return
}
const target = e.target as HTMLElement
const nearestScrollContainer = findNearestScrollContainer(target)
if (!nearestScrollContainer) {
console.log('Preventing scroll: no nearest scroll container')
e.preventDefault()
return
}
const isScrollingUp = deltaY < 0
const isAtTop = nearestScrollContainer.scrollTop === 0
if (isScrollingUp && isAtTop) {
console.log(
'Preventing scroll: already at top of nearest scroll container',
)
e.preventDefault()
return
}
const isAtBottom =
nearestScrollContainer.scrollTop ===
nearestScrollContainer.scrollHeight -
nearestScrollContainer.clientHeight
if (!isScrollingUp && isAtBottom) {
console.log(
'Preventing scroll: already at bottom of nearest scroll container',
)
e.preventDefault()
return
}
}
elem.addEventListener('touchstart', handleTouchStart)
elem.addEventListener('touchmove', handleTouchMove)
return () => {
elem.removeEventListener('touchstart', handleTouchStart)
elem.removeEventListener('touchmove', handleTouchMove)
}
}, [container])
return container
}
export default usePreventOverScrolling
const findNearestScrollContainer = (
elem: HTMLElement,
): HTMLElement | undefined => {
if (elem.scrollHeight > elem.offsetHeight) {
return elem
}
const parent = elem.parentElement
if (!parent) {
return undefined
}
return findNearestScrollContainer(parent)
}
export default findNearestScrollContainer
This can be useful for fixed layouts like in PWAs that try to appear like regular apps. For example in a chat interface:
Both hooks are needed to achieve this effect.