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/*',
|
url: '/api/v1/*',
|
||||||
}).as('apiCall');
|
}).as('apiCall');
|
||||||
|
|
||||||
cy.get('.searchbar').swipe('bottom', [190, 400]);
|
cy.get('.searchbar').swipe('bottom', [190, 500]);
|
||||||
|
|
||||||
cy.wait('@apiCall').then((interception) => {
|
cy.wait('@apiCall').then((interception) => {
|
||||||
assert.isNotNull(
|
assert.isNotNull(
|
||||||
|
@@ -68,7 +68,6 @@
|
|||||||
"openpgp": "5.7.0",
|
"openpgp": "5.7.0",
|
||||||
"plex-api": "5.3.2",
|
"plex-api": "5.3.2",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
"pulltorefreshjs": "0.1.22",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
"react-animate-height": "2.1.2",
|
"react-animate-height": "2.1.2",
|
||||||
@@ -121,7 +120,6 @@
|
|||||||
"@types/node": "17.0.36",
|
"@types/node": "17.0.36",
|
||||||
"@types/node-schedule": "2.1.0",
|
"@types/node-schedule": "2.1.0",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/pulltorefreshjs": "0.1.5",
|
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-transition-group": "4.4.5",
|
"@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 MobileMenu from '@app/components/Layout/MobileMenu';
|
||||||
|
import PullToRefresh from '@app/components/Layout/PullToRefresh';
|
||||||
import SearchInput from '@app/components/Layout/SearchInput';
|
import SearchInput from '@app/components/Layout/SearchInput';
|
||||||
import Sidebar from '@app/components/Layout/Sidebar';
|
import Sidebar from '@app/components/Layout/Sidebar';
|
||||||
import UserDropdown from '@app/components/Layout/UserDropdown';
|
import UserDropdown from '@app/components/Layout/UserDropdown';
|
||||||
import PullToRefresh from '@app/components/PullToRefresh';
|
|
||||||
import type { AvailableLocale } from '@app/context/LanguageContext';
|
import type { AvailableLocale } from '@app/context/LanguageContext';
|
||||||
import useLocale from '@app/hooks/useLocale';
|
import useLocale from '@app/hooks/useLocale';
|
||||||
import useSettings from '@app/hooks/useSettings';
|
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 {
|
body {
|
||||||
@apply bg-gray-900;
|
@apply bg-gray-900;
|
||||||
overscroll-behavior-y: contain;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
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"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
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@*":
|
"@types/qs@*":
|
||||||
version "6.9.7"
|
version "6.9.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
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-runtime "^3.0.1"
|
||||||
pug-strip-comments "^2.0.0"
|
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:
|
pump@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||||
|
Reference in New Issue
Block a user