Merge pull request #1599 from Soxasora/feat_pwa_pull_to_refresh
feat: PWA pull to refresh
This commit is contained in:
commit
c3b7ad3fdd
|
@ -6,6 +6,7 @@ import Footer from './footer'
|
|||
import Seo, { SeoSearch } from './seo'
|
||||
import Search from './search'
|
||||
import styles from './layout.module.css'
|
||||
import PullToRefresh from './pull-to-refresh'
|
||||
|
||||
export default function Layout ({
|
||||
sub, contain = true, footer = true, footerLinks = true,
|
||||
|
@ -14,16 +15,18 @@ export default function Layout ({
|
|||
return (
|
||||
<>
|
||||
{seo && <Seo sub={sub} item={item} user={user} />}
|
||||
<Navigation sub={sub} />
|
||||
{contain
|
||||
? (
|
||||
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
|
||||
{children}
|
||||
</Container>
|
||||
)
|
||||
: children}
|
||||
{footer && <Footer links={footerLinks} />}
|
||||
<NavFooter sub={sub} />
|
||||
<PullToRefresh android> {/* android prop if true disables its native PTR */}
|
||||
<Navigation sub={sub} />
|
||||
{contain
|
||||
? (
|
||||
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
|
||||
{children}
|
||||
</Container>
|
||||
)
|
||||
: children}
|
||||
{footer && <Footer links={footerLinks} />}
|
||||
<NavFooter sub={sub} />
|
||||
</PullToRefresh>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import styles from './pull-to-refresh.module.css'
|
||||
|
||||
const REFRESH_THRESHOLD = 50
|
||||
|
||||
export default function PullToRefresh ({ children, android }) {
|
||||
const router = useRouter()
|
||||
const [pullDistance, setPullDistance] = useState(0)
|
||||
const [isPWA, setIsPWA] = useState(false)
|
||||
const [isAndroid, setIsAndroid] = useState(false)
|
||||
const touchStartY = useRef(0)
|
||||
const touchEndY = useRef(0)
|
||||
|
||||
const checkPWA = () => {
|
||||
const androidPWA = window.matchMedia('(display-mode: standalone)').matches
|
||||
const iosPWA = window.navigator.standalone === true
|
||||
setIsAndroid(androidPWA) // we need to know if the user is on Android to enable toggling its native PTR
|
||||
setIsPWA(androidPWA || iosPWA)
|
||||
}
|
||||
|
||||
useEffect(checkPWA, [])
|
||||
|
||||
const handleTouchStart = useCallback((e) => {
|
||||
// don't handle if the user is not scrolling from the top of the page, is not on a PWA or if we want Android's native PTR
|
||||
if (!isPWA || (isAndroid && !android) || window.scrollY > 0) return
|
||||
touchStartY.current = e.touches[0].clientY
|
||||
}, [isPWA, isAndroid, android])
|
||||
|
||||
const handleTouchMove = useCallback((e) => {
|
||||
if (touchStartY.current === 0) return
|
||||
if (!isPWA || (isAndroid && !android)) return
|
||||
touchEndY.current = e.touches[0].clientY
|
||||
const distance = touchEndY.current - touchStartY.current
|
||||
setPullDistance(distance)
|
||||
document.body.style.marginTop = `${Math.max(0, Math.min(distance / 2, 25))}px`
|
||||
}, [isPWA, isAndroid, android])
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
if (touchStartY.current === 0 || touchEndY.current === 0) return
|
||||
if (touchEndY.current - touchStartY.current > REFRESH_THRESHOLD) {
|
||||
router.push(router.asPath)
|
||||
}
|
||||
setPullDistance(0)
|
||||
document.body.style.marginTop = '0px'
|
||||
touchStartY.current = 0
|
||||
touchEndY.current = 0
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPWA || (isAndroid && !android)) return
|
||||
document.body.style.overscrollBehaviorY = 'contain'
|
||||
document.addEventListener('touchstart', handleTouchStart)
|
||||
document.addEventListener('touchmove', handleTouchMove)
|
||||
document.addEventListener('touchend', handleTouchEnd)
|
||||
return () => {
|
||||
document.body.style.overscrollBehaviorY = ''
|
||||
document.body.style.marginTop = '0px'
|
||||
document.removeEventListener('touchstart', handleTouchStart)
|
||||
document.removeEventListener('touchmove', handleTouchMove)
|
||||
document.removeEventListener('touchend', handleTouchEnd)
|
||||
}
|
||||
}, [isPWA, isAndroid, android, handleTouchStart, handleTouchMove, handleTouchEnd])
|
||||
|
||||
const pullMessage = useMemo(() => {
|
||||
if (pullDistance > REFRESH_THRESHOLD) return 'release to refresh'
|
||||
return 'pull down to refresh'
|
||||
}, [pullDistance])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<p className={`${styles.pullMessage}`} style={{ top: `${Math.max(-20, Math.min(-20 + pullDistance / 2, 5))}px` }}>
|
||||
{pullMessage}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
.pullMessage {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -20px;
|
||||
height: 15px;
|
||||
transform: translateX(-50%);
|
||||
font-size: small;
|
||||
color: #a5a5a5;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
text-align: center;
|
||||
opacity: 0.75;
|
||||
}
|
Loading…
Reference in New Issue