mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
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
This commit is contained in:
@@ -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",
|
||||
|
118
src/components/Layout/PullToRefresh/index.tsx
Normal file
118
src/components/Layout/PullToRefresh/index.tsx
Normal file
@@ -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 {
|
||||
|
10
yarn.lock
10
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user