refactor: pull to refresh (#3391)

* refactor: decoupled PTR by removing import and creating new touch logic

* fix: overscroll behavior on mobile is now prevented on the y axis

* feat: added shadow effects to icon

* fix: modified cypress test

* fix: added better scroll lock functionality

* fix: hide icon if scroll value is negative

* fix: changed to allow usage on all touch devices
pull/3395/head
Brandon Cohen 12 months ago committed by GitHub
parent cd1cacad55
commit c1e10338c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,7 +13,7 @@ describe('Pull To Refresh', () => {
url: '/api/v1/*',
}).as('apiCall');
cy.get('.searchbar').swipe('bottom', [190, 400]);
cy.get('.searchbar').swipe('bottom', [190, 500]);
cy.wait('@apiCall').then((interception) => {
assert.isNotNull(

@ -68,7 +68,6 @@
"openpgp": "5.7.0",
"plex-api": "5.3.2",
"pug": "3.0.2",
"pulltorefreshjs": "0.1.22",
"react": "18.2.0",
"react-ace": "10.1.0",
"react-animate-height": "2.1.2",
@ -121,7 +120,6 @@
"@types/node": "17.0.36",
"@types/node-schedule": "2.1.0",
"@types/nodemailer": "6.4.7",
"@types/pulltorefreshjs": "0.1.5",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react-transition-group": "4.4.5",

@ -0,0 +1,118 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
const PullToRefresh = () => {
const router = useRouter();
const [pullStartPoint, setPullStartPoint] = useState(0);
const [pullChange, setPullChange] = useState(0);
const refreshDiv = useRef<HTMLDivElement>(null);
// Various pull down thresholds that determine icon location
const pullDownInitThreshold = pullChange > 20;
const pullDownStopThreshold = 120;
const pullDownReloadThreshold = pullChange > 340;
const pullDownIconLocation = pullChange / 3;
useEffect(() => {
// Reload function that is called when reload threshold has been hit
// Add loading class to determine when to add spin animation
const forceReload = () => {
refreshDiv.current?.classList.add('loading');
setTimeout(() => {
router.reload();
}, 1000);
};
const html = document.querySelector('html');
// Determines if we are at the top of the page
// Locks or unlocks page when pulling down to refresh
const pullStart = (e: TouchEvent) => {
setPullStartPoint(e.targetTouches[0].screenY);
if (window.scrollY === 0 && window.scrollX === 0) {
refreshDiv.current?.classList.add('block');
refreshDiv.current?.classList.remove('hidden');
document.body.style.touchAction = 'none';
document.body.style.overscrollBehavior = 'none';
if (html) {
html.style.overscrollBehaviorY = 'none';
}
} else {
refreshDiv.current?.classList.remove('block');
refreshDiv.current?.classList.add('hidden');
}
};
// Tracks how far we have pulled down the refresh icon
const pullDown = async (e: TouchEvent) => {
const screenY = e.targetTouches[0].screenY;
const pullLength =
pullStartPoint < screenY ? Math.abs(screenY - pullStartPoint) : 0;
setPullChange(pullLength);
};
// Will reload the page if we are past the threshold
// Otherwise, we reset the pull
const pullFinish = () => {
setPullStartPoint(0);
if (pullDownReloadThreshold) {
forceReload();
} else {
setPullChange(0);
}
document.body.style.touchAction = 'auto';
document.body.style.overscrollBehaviorY = 'auto';
if (html) {
html.style.overscrollBehaviorY = 'auto';
}
};
window.addEventListener('touchstart', pullStart, { passive: false });
window.addEventListener('touchmove', pullDown, { passive: false });
window.addEventListener('touchend', pullFinish, { passive: false });
return () => {
window.removeEventListener('touchstart', pullStart);
window.removeEventListener('touchmove', pullDown);
window.removeEventListener('touchend', pullFinish);
};
}, [pullDownInitThreshold, pullDownReloadThreshold, pullStartPoint, router]);
return (
<div
ref={refreshDiv}
className="absolute left-0 right-0 top-0 z-50 m-auto w-fit transition-all ease-out"
id="refreshIcon"
style={{
top:
pullDownIconLocation < pullDownStopThreshold && pullDownInitThreshold
? pullDownIconLocation
: pullDownInitThreshold
? pullDownStopThreshold
: '',
}}
>
<div
className={`${
refreshDiv.current?.classList.contains('loading') && 'animate-spin'
} relative -top-24 h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 shadow-md shadow-black ring-1 ring-gray-700`}
style={{ animationDirection: 'reverse' }}
>
<ArrowPathIcon
className={`rounded-full ${
pullDownReloadThreshold && 'rotate-180'
} text-indigo-500 transition-all duration-300`}
/>
</div>
</div>
);
};
export default PullToRefresh;

@ -1,8 +1,8 @@
import MobileMenu from '@app/components/Layout/MobileMenu';
import PullToRefresh from '@app/components/Layout/PullToRefresh';
import SearchInput from '@app/components/Layout/SearchInput';
import Sidebar from '@app/components/Layout/Sidebar';
import UserDropdown from '@app/components/Layout/UserDropdown';
import PullToRefresh from '@app/components/PullToRefresh';
import type { AvailableLocale } from '@app/context/LanguageContext';
import useLocale from '@app/hooks/useLocale';
import useSettings from '@app/hooks/useSettings';

@ -1,45 +0,0 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useRouter } from 'next/router';
import PR from 'pulltorefreshjs';
import { useEffect } from 'react';
import ReactDOMServer from 'react-dom/server';
const PullToRefresh = () => {
const router = useRouter();
useEffect(() => {
PR.init({
mainElement: '#pull-to-refresh',
onRefresh() {
router.reload();
},
iconArrow: ReactDOMServer.renderToString(
<div className="p-2">
<ArrowPathIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
</div>
),
iconRefreshing: ReactDOMServer.renderToString(
<div
className="animate-spin p-2"
style={{ animationDirection: 'reverse' }}
>
<ArrowPathIcon className="z-50 m-auto h-9 w-9 rounded-full border-4 border-gray-800 bg-gray-800 text-indigo-500 ring-1 ring-gray-700" />
</div>
),
instructionsPullToRefresh: ReactDOMServer.renderToString(<div />),
instructionsReleaseToRefresh: ReactDOMServer.renderToString(<div />),
instructionsRefreshing: ReactDOMServer.renderToString(<div />),
distReload: 60,
distIgnore: 15,
shouldPullToRefresh: () =>
!window.scrollY && document.body.style.overflow !== 'hidden',
});
return () => {
PR.destroyAll();
};
}, [router]);
return <div id="pull-to-refresh"></div>;
};
export default PullToRefresh;

@ -17,7 +17,7 @@
body {
@apply bg-gray-900;
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;
}
code {

@ -3954,11 +3954,6 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/pulltorefreshjs@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/pulltorefreshjs/-/pulltorefreshjs-0.1.5.tgz#f15c9dbc91b8fdd8135093d81ece9e9d4d2324d7"
integrity sha512-/VRTgBettvBg1KI8mGnA9oeWs359tTXQ7qsxLuXnksL88jvK6ZNMStG5T9x9vUO9O7jLsgREB0cElz/BWFfdew==
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
@ -11434,11 +11429,6 @@ pug@3.0.2, pug@^3.0.2:
pug-runtime "^3.0.1"
pug-strip-comments "^2.0.0"
pulltorefreshjs@0.1.22:
version "0.1.22"
resolved "https://registry.yarnpkg.com/pulltorefreshjs/-/pulltorefreshjs-0.1.22.tgz#ddb5e3feee0b2a49fd46e1b18e84fffef2c47ac0"
integrity sha512-haxNVEHnS4NCQA7NeG7TSV69z4uqy/N7nfPRuc4dPWe8H6ygUrMjdNeohE+6v0lVVX/ukSjbLYwPUGUYtFKfvQ==
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"

Loading…
Cancel
Save