Fixed: Scroll position not reset when navigating on small screens

This commit is contained in:
Thibault HERVÉ 2026-04-18 17:39:13 +02:00
parent 4b85fab05b
commit 459a92335a
8 changed files with 60 additions and 89 deletions

View file

@ -224,8 +224,6 @@ class Collection extends Component {
view,
onSortSelect,
onFilterSelect,
initialScrollTop,
onScroll,
isRefreshingCollections,
isSaving,
isAdding,
@ -307,7 +305,7 @@ class Collection extends Component {
ref={this.scrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
onScroll={onScroll}
scrollPositionKey="movieCollections"
>
{
isFetching && !isPopulated &&
@ -336,7 +334,6 @@ class Collection extends Component {
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
scrollTop={initialScrollTop}
{...otherProps}
/>
</div>
@ -377,7 +374,6 @@ class Collection extends Component {
}
Collection.propTypes = {
initialScrollTop: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
@ -395,7 +391,6 @@ Collection.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired,
onRefreshMovieCollectionsPress: PropTypes.func.isRequired
};

View file

@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import {
fetchMovieCollections,
@ -12,7 +11,6 @@ import {
setMovieCollectionsSort
} from 'Store/Actions/movieCollectionActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import scrollPositions from 'Store/scrollPositions';
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -82,10 +80,6 @@ class CollectionConnector extends Component {
//
// Listeners
onScroll = ({ scrollTop }) => {
scrollPositions.movieCollections = scrollTop;
};
onUpdateSelectedPress = (payload) => {
this.props.onUpdateSelectedPress(payload);
};
@ -105,7 +99,6 @@ class CollectionConnector extends Component {
<Collection
{...otherProps}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onUpdateSelectedPress={this.onUpdateSelectedPress}
/>
);
@ -121,7 +114,4 @@ CollectionConnector.propTypes = {
dispatchClearQueueDetails: PropTypes.func.isRequired
};
export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector),
'movieCollections'
);
export default connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector);

View file

@ -1,5 +1,6 @@
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
import useScrollPosition from 'Helpers/Hooks/useScrollPosition';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
@ -7,7 +8,7 @@ interface PageContentBodyProps {
className?: string;
innerClassName?: string;
children: ReactNode;
initialScrollTop?: number;
scrollPositionKey?: string;
onScroll?: (payload: OnScroll) => void;
}
@ -17,26 +18,32 @@ const PageContentBody = forwardRef(
className = styles.contentBody,
innerClassName = styles.innerContentBody,
children,
scrollPositionKey,
onScroll,
...otherProps
} = props;
const onScrollWrapper = useCallback(
const { initialScrollTop, onScroll: onScrollMemo } =
useScrollPosition(scrollPositionKey);
const handleScroll = useCallback(
(payload: OnScroll) => {
if (onScroll && !isLocked()) {
onScroll(payload);
if (isLocked()) {
return;
}
onScrollMemo(payload);
onScroll?.(payload);
},
[onScroll]
[onScroll, onScrollMemo]
);
return (
<Scroller
ref={ref}
{...otherProps}
className={className}
scrollDirection="vertical"
onScroll={onScrollWrapper}
initialScrollTop={initialScrollTop}
onScroll={handleScroll}
>
<div className={innerClassName}>{children}</div>
</Scroller>

View file

@ -1,31 +0,0 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import scrollPositions from 'Store/scrollPositions';
interface WrappedComponentProps {
initialScrollTop: number;
}
interface ScrollPositionProps {
history: RouteComponentProps['history'];
location: RouteComponentProps['location'];
match: RouteComponentProps['match'];
}
function withScrollPosition(
WrappedComponent: React.FC<WrappedComponentProps>,
scrollPositionKey: string
) {
function ScrollPosition(props: ScrollPositionProps) {
const { history } = props;
const initialScrollTop =
history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
}
return ScrollPosition;
}
export default withScrollPosition;

View file

@ -259,8 +259,6 @@ class DiscoverMovie extends Component {
onSortSelect,
onFilterSelect,
onViewSelect,
initialScrollTop,
onScroll,
onAddMoviesPress,
isSyncingLists,
...otherProps
@ -370,7 +368,7 @@ class DiscoverMovie extends Component {
ref={this.scrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
onScroll={onScroll}
scrollPositionKey="discoverMovie"
>
{
isFetching && !isPopulated &&
@ -399,7 +397,6 @@ class DiscoverMovie extends Component {
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
scrollTop={initialScrollTop}
{...otherProps}
/>
</div>
@ -444,7 +441,6 @@ class DiscoverMovie extends Component {
}
DiscoverMovie.propTypes = {
initialScrollTop: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
@ -465,7 +461,6 @@ DiscoverMovie.propTypes = {
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onAddMoviesPress: PropTypes.func.isRequired,
onExcludeMoviesPress: PropTypes.func.isRequired,
onImportListSyncPress: PropTypes.func.isRequired,

View file

@ -3,11 +3,9 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { addImportListExclusions, addMovies, clearAddMovie, fetchDiscoverMovies, setListMovieFilter, setListMovieSort, setListMovieTableOption, setListMovieView } from 'Store/Actions/discoverMovieActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createDiscoverMovieClientSideCollectionItemsSelector from 'Store/Selectors/createDiscoverMovieClientSideCollectionItemsSelector';
@ -106,10 +104,6 @@ class DiscoverMovieConnector extends Component {
this.props.dispatchSetListMovieView(view);
};
onScroll = ({ scrollTop }) => {
scrollPositions.discoverMovie = scrollTop;
};
onAddMoviesPress = ({ ids, addOptions }) => {
this.props.dispatchAddMovies(ids, addOptions);
};
@ -126,7 +120,6 @@ class DiscoverMovieConnector extends Component {
<DiscoverMovie
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onAddMoviesPress={this.onAddMoviesPress}
onExcludeMoviesPress={this.onExcludeMoviesPress}
onSyncListsPress={this.onSyncListsPress}
@ -146,7 +139,4 @@ DiscoverMovieConnector.propTypes = {
dispatchAddImportListExclusions: PropTypes.func.isRequired
};
export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(DiscoverMovieConnector),
'discoverMovie'
);
export default connect(createMapStateToProps, createMapDispatchToProps)(DiscoverMovieConnector);

View file

@ -0,0 +1,35 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router';
import { OnScroll } from 'Components/Scroller/Scroller';
import scrollPositions from 'Store/scrollPositions';
function useScrollPosition(key?: string) {
const { pathname } = useLocation();
const { action } = useHistory();
// Reset window scroll on PUSH/REPLACE (mobile's scroll container).
// Skip POP so the browser (mobile) and memorized `initialScrollTop` (desktop inner) can restore.
useEffect(() => {
if (action !== 'POP') {
window.scrollTo(0, 0);
}
}, [pathname, action]);
const initialScrollTop = useMemo(
() => (key && action === 'POP' ? scrollPositions[key] ?? 0 : 0),
[key, action]
);
const onScroll = useCallback(
({ scrollTop }: OnScroll) => {
if (key) {
scrollPositions[key] = scrollTop;
}
},
[key]
);
return { initialScrollTop, onScroll };
}
export default useScrollPosition;

View file

@ -21,7 +21,6 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import withScrollPosition from 'Components/withScrollPosition';
import { align, icons, kinds } from 'Helpers/Props';
import { DESCENDING } from 'Helpers/Props/sortDirections';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
@ -35,7 +34,6 @@ import {
setMovieView,
} from 'Store/Actions/movieIndexActions';
import { fetchQueueDetails } from 'Store/Actions/queueActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
@ -72,11 +70,7 @@ function getViewComponent(view: string) {
return MovieIndexTable;
}
interface MovieIndexProps {
initialScrollTop?: number;
}
const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
function MovieIndex() {
const history = useHistory();
const {
@ -186,13 +180,9 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
[setJumpToCharacter]
);
const onScroll = useCallback(
({ scrollTop }: { scrollTop: number }) => {
setJumpToCharacter(undefined);
scrollPositions.movieIndex = scrollTop;
},
[setJumpToCharacter]
);
const onScroll = useCallback(() => {
setJumpToCharacter(undefined);
}, [setJumpToCharacter]);
const jumpBarItems: PageJumpBarItems = useMemo(() => {
// Reset if not sorting by sortTitle
@ -345,7 +335,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
scrollPositionKey="movieIndex"
onScroll={onScroll}
>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
@ -407,6 +397,6 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
</PageContent>
</SelectProvider>
);
}, 'movieIndex');
}
export default MovieIndex;