diff --git a/web/package.json b/web/package.json index cbf8bd15..3b43b509 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,7 @@ "remark-unlink": "^3.1.0", "semver": "^7.3.8", "tinycolor2": "^1.4.2", + "ua-parser-js": "^1.0.32", "unified": "^9.2.1", "yaml": "2.0.1" }, @@ -76,7 +77,7 @@ "proxy": "http://localhost:8000", "scripts": { "copy:static": "shx rm -rf src/static && shx mkdir src/static && shx cp -r public/static/* src/static", - "start": "yarn copy:static && react-scripts start", + "start": "yarn copy:static && DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", "build": "yarn copy:static && INLINE_RUNTIME_CHUNK=false IMAGE_INLINE_SIZE_LIMIT=0 react-scripts build", "test": "sed -i -e 's/const FORCE_EXIT_DELAY = 500;/const FORCE_EXIT_DELAY = 1000;/g' ./node_modules/jest-worker/build/base/BaseWorkerPool.js && TZ=UTC react-scripts test # See https://github.com/facebook/jest/issues/11354", "test:coverage": "TZ=UTC react-scripts test --coverage --watchAll=false", diff --git a/web/src/layout/controlPanel/settings/orgSettings/profile/DeleteOrg.tsx b/web/src/layout/controlPanel/settings/orgSettings/profile/DeleteOrg.tsx index c7540404..6462f5e5 100644 --- a/web/src/layout/controlPanel/settings/orgSettings/profile/DeleteOrg.tsx +++ b/web/src/layout/controlPanel/settings/orgSettings/profile/DeleteOrg.tsx @@ -6,6 +6,7 @@ import API from '../../../../../api'; import { AppCtx, unselectOrg } from '../../../../../context/AppCtx'; import { AuthorizerAction, ErrorKind, Organization } from '../../../../../types'; import alertDispatcher from '../../../../../utils/alertDispatcher'; +import scrollToTop from '../../../../../utils/scrollToTop'; import InputField from '../../../../common/InputField'; import Modal from '../../../../common/Modal'; import ActionBtn from '../../../ActionBtn'; @@ -31,7 +32,7 @@ const DeleteOrganization = (props: Props) => { setIsDeleting(true); await API.deleteOrganization(props.organization.name); dispatch(unselectOrg()); - window.scrollTo(0, 0); // Scroll to top when org is deleted + scrollToTop(); // Scroll to top when org is deleted setIsDeleting(false); } catch (err: any) { setIsDeleting(false); diff --git a/web/src/layout/package/index.test.tsx b/web/src/layout/package/index.test.tsx index 80a44f68..a13f2798 100644 --- a/web/src/layout/package/index.test.tsx +++ b/web/src/layout/package/index.test.tsx @@ -14,6 +14,9 @@ jest.mock('react-markdown', () => (props: any) => { return <>{props.children}>; }); jest.mock('remark-gfm', () => () =>
); +jest.mock('../../utils/bannerDispatcher', () => ({ + getBanner: () => null, +})); const getMockPackage = (fixtureId: string): Package => { return require(`./__fixtures__/index/${fixtureId}.json`) as Package; diff --git a/web/src/layout/package/index.tsx b/web/src/layout/package/index.tsx index 163c5b73..47f58e3c 100644 --- a/web/src/layout/package/index.tsx +++ b/web/src/layout/package/index.tsx @@ -34,6 +34,7 @@ import bannerDispatcher from '../../utils/bannerDispatcher'; import isFuture from '../../utils/isFuture'; import isPackageOfficial from '../../utils/isPackageOfficial'; import { prepareQueryString } from '../../utils/prepareQueryString'; +import scrollToTop from '../../utils/scrollToTop'; import sortPackageVersions from '../../utils/sortPackageVersions'; import updateMetaIndex from '../../utils/updateMetaIndex'; import AnchorHeader from '../common/AnchorHeader'; @@ -240,7 +241,7 @@ const PackageView = (props: Props) => { setApiError(null); setCurrentPkgId(detailPkg.packageId); setRelatedPosition(undefined); - window.scrollTo(0, 0); // Scroll to top when a new version is loaded + scrollToTop(); // Scroll to top when a new version is loaded // Stop loading when readme is not defined or is the same than the previous one if ( isNull(detailPkg.readme) || @@ -508,7 +509,7 @@ const PackageView = (props: Props) => { if (props.hash !== currentHash) { setCurrentHash(props.hash); if (isUndefined(props.hash) || props.hash === '') { - window.scrollTo(0, 0); + scrollToTop(); } else { scrollIntoView(); } diff --git a/web/src/layout/search/index.tsx b/web/src/layout/search/index.tsx index 400cd77c..ec4bbf75 100644 --- a/web/src/layout/search/index.tsx +++ b/web/src/layout/search/index.tsx @@ -15,6 +15,7 @@ import { FacetOption, Facets, Package, RepositoryKind, SearchFiltersURL, SearchR import { TS_QUERY } from '../../utils/data'; import getSampleQueries from '../../utils/getSampleQueries'; import { prepareQueryString } from '../../utils/prepareQueryString'; +import scrollToTop from '../../utils/scrollToTop'; import Loading from '../common/Loading'; import NoData from '../common/NoData'; import Pagination from '../common/Pagination'; @@ -121,10 +122,6 @@ const SearchView = (props: Props) => { setScrollPosition(window.scrollY); }; - const updateWindowScrollPosition = (newPosition: number) => { - window.scrollTo(0, newPosition); - }; - const prepareSelectedFilters = (name: string, newFilters: string[], prevFilters: FiltersProp): FiltersProp => { let cleanFilters: FiltersProp = {}; switch (name) { @@ -260,7 +257,7 @@ const SearchView = (props: Props) => { }), }); setScrollPosition(0); - updateWindowScrollPosition(0); + scrollToTop(0); }; const onPaginationLimitChange = (newLimit: number): void => { @@ -272,7 +269,7 @@ const SearchView = (props: Props) => { }), }); setScrollPosition(0); - updateWindowScrollPosition(0); + scrollToTop(0); dispatch(updateLimit(newLimit)); }; @@ -322,14 +319,14 @@ const SearchView = (props: Props) => { if (history.action === 'PUSH') { // When search page is open from detail page if (props.fromDetail && !isUndefined(scrollPosition)) { - updateWindowScrollPosition(scrollPosition); + scrollToTop(scrollPosition); // When search has changed } else { - updateWindowScrollPosition(0); + scrollToTop(0); } // On pop action and when scroll position has been previously saved } else if (!isUndefined(scrollPosition)) { - updateWindowScrollPosition(scrollPosition); + scrollToTop(scrollPosition); } } } diff --git a/web/src/utils/browserDetect.ts b/web/src/utils/browserDetect.ts new file mode 100644 index 00000000..9fe3f074 --- /dev/null +++ b/web/src/utils/browserDetect.ts @@ -0,0 +1,20 @@ +const parser = require('ua-parser-js'); + +class BrowserDetect { + private ua: any = {}; + + public init() { + this.ua = parser(navigator.userAgent); + } + + public isSafari(): boolean { + if (this.ua.browser.name.includes('Safari')) { + return true; + } + return false; + } +} + +const browserDetect = new BrowserDetect(); +browserDetect.init(); +export default browserDetect; diff --git a/web/src/utils/scrollToTop.ts b/web/src/utils/scrollToTop.ts new file mode 100644 index 00000000..86c9c7c5 --- /dev/null +++ b/web/src/utils/scrollToTop.ts @@ -0,0 +1,12 @@ +import browserDetect from './browserDetect'; + +const scrollToTop = (position?: number): void => { + const isSafari = browserDetect.isSafari(); + window.scrollTo({ + top: position || 0, + // @ts-ignore: Unreachable code error + behavior: isSafari ? 'instant' : 'auto', + }); +}; + +export default scrollToTop; diff --git a/web/yarn.lock b/web/yarn.lock index b2be0cd3..c62becdf 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -10118,6 +10118,11 @@ typescript@^4.9.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== +ua-parser-js@^1.0.32: + version "1.0.32" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.32.tgz#786bf17df97de159d5b1c9d5e8e9e89806f8a030" + integrity sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"