mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-12 16:13:58 -07:00
Refactor Artist index to use react-window
(cherry picked from commit d022679b7dcbce3cec98e6a1fd0879e3c0d92523) Fixed: Restoring scroll position when going back/forward to artist list (cherry picked from commit 5aad84dba453c42b4b5a9eac43deecf91a98f4f6)
This commit is contained in:
parent
01e21c09db
commit
f509ca0f72
117 changed files with 4704 additions and 5511 deletions
|
@ -23,6 +23,8 @@ function SpinnerIconButton(props) {
|
|||
}
|
||||
|
||||
SpinnerIconButton.propTypes = {
|
||||
...IconButton.propTypes,
|
||||
className: PropTypes.string,
|
||||
name: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
|
||||
function SortMenu(props) {
|
||||
const {
|
||||
|
@ -30,7 +30,8 @@ function SortMenu(props) {
|
|||
SortMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT])
|
||||
};
|
||||
|
||||
SortMenu.defaultProps = {
|
||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
|
||||
function ViewMenu(props) {
|
||||
const {
|
||||
|
@ -27,7 +27,8 @@ function ViewMenu(props) {
|
|||
|
||||
ViewMenu.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT])
|
||||
};
|
||||
|
||||
ViewMenu.defaultProps = {
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScroll = (props) => {
|
||||
const { onScroll } = this.props;
|
||||
|
||||
if (this.props.onScroll && !isLocked()) {
|
||||
onScroll(props);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
innerClassName,
|
||||
children,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.VERTICAL}
|
||||
{...otherProps}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
</div>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageContentBody.propTypes = {
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
PageContentBody.defaultProps = {
|
||||
className: styles.contentBody,
|
||||
innerClassName: styles.innerContentBody
|
||||
};
|
||||
|
||||
export default PageContentBody;
|
51
frontend/src/Components/Page/PageContentBody.tsx
Normal file
51
frontend/src/Components/Page/PageContentBody.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React, { forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
interface PageContentBodyProps {
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
children: ReactNode;
|
||||
initialScrollTop?: number;
|
||||
onScroll?: (payload) => void;
|
||||
}
|
||||
|
||||
const PageContentBody = forwardRef(
|
||||
(
|
||||
props: PageContentBodyProps,
|
||||
ref: React.MutableRefObject<HTMLDivElement>
|
||||
) => {
|
||||
const {
|
||||
className = styles.contentBody,
|
||||
innerClassName = styles.innerContentBody,
|
||||
children,
|
||||
onScroll,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const onScrollWrapper = useCallback(
|
||||
(payload) => {
|
||||
if (onScroll && !isLocked()) {
|
||||
onScroll(payload);
|
||||
}
|
||||
},
|
||||
[onScroll]
|
||||
);
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
className={className}
|
||||
scrollDirection={ScrollDirection.Vertical}
|
||||
onScroll={onScrollWrapper}
|
||||
>
|
||||
<div className={innerClassName}>{children}</div>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default PageContentBody;
|
|
@ -45,7 +45,8 @@ PageToolbarButton.propTypes = {
|
|||
iconName: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object,
|
||||
isSpinning: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool
|
||||
isDisabled: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
PageToolbarButton.defaultProps = {
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
class Scroller extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._scroller = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
scrollDirection,
|
||||
autoFocus,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
if (this.props.scrollTop != null) {
|
||||
this._scroller.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
if (autoFocus && scrollDirection !== scrollDirections.NONE) {
|
||||
this._scroller.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_setScrollerRef = (ref) => {
|
||||
this._scroller = ref;
|
||||
|
||||
this.props.registerScroller(ref);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
scrollDirection,
|
||||
autoScroll,
|
||||
children,
|
||||
scrollTop,
|
||||
onScroll,
|
||||
registerScroller,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this._setScrollerRef}
|
||||
className={classNames(
|
||||
className,
|
||||
styles.scroller,
|
||||
styles[scrollDirection],
|
||||
autoScroll && styles.autoScroll
|
||||
)}
|
||||
tabIndex={-1}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Scroller.propTypes = {
|
||||
className: PropTypes.string,
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
|
||||
autoFocus: PropTypes.bool.isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func,
|
||||
registerScroller: PropTypes.func
|
||||
};
|
||||
|
||||
Scroller.defaultProps = {
|
||||
scrollDirection: scrollDirections.VERTICAL,
|
||||
autoFocus: true,
|
||||
autoScroll: true,
|
||||
registerScroller: () => { /* no-op */ }
|
||||
};
|
||||
|
||||
export default Scroller;
|
90
frontend/src/Components/Scroller/Scroller.tsx
Normal file
90
frontend/src/Components/Scroller/Scroller.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
interface ScrollerProps {
|
||||
className?: string;
|
||||
scrollDirection?: ScrollDirection;
|
||||
autoFocus?: boolean;
|
||||
autoScroll?: boolean;
|
||||
scrollTop?: number;
|
||||
initialScrollTop?: number;
|
||||
children?: ReactNode;
|
||||
onScroll?: (payload) => void;
|
||||
}
|
||||
|
||||
const Scroller = forwardRef(
|
||||
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
|
||||
const {
|
||||
className,
|
||||
autoFocus = false,
|
||||
autoScroll = true,
|
||||
scrollDirection = ScrollDirection.Vertical,
|
||||
children,
|
||||
scrollTop,
|
||||
initialScrollTop,
|
||||
onScroll,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const internalRef = useRef();
|
||||
const currentRef = ref ?? internalRef;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (initialScrollTop != null) {
|
||||
currentRef.current.scrollTop = initialScrollTop;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollTop != null) {
|
||||
currentRef.current.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
if (autoFocus && scrollDirection !== ScrollDirection.None) {
|
||||
currentRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
}, [autoFocus, currentRef, scrollDirection, scrollTop]);
|
||||
|
||||
useEffect(() => {
|
||||
const div = currentRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const scrollLeft = div.scrollLeft;
|
||||
const scrollTop = div.scrollTop;
|
||||
|
||||
onScroll?.({ scrollLeft, scrollTop });
|
||||
}, 10);
|
||||
|
||||
div.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
div.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [currentRef, onScroll]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
ref={currentRef}
|
||||
className={classNames(
|
||||
className,
|
||||
styles.scroller,
|
||||
styles[scrollDirection],
|
||||
autoScroll && styles.autoScroll
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Scroller;
|
|
@ -1,30 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
const {
|
||||
history
|
||||
} = props;
|
||||
|
||||
const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ?
|
||||
scrollPositions[scrollPositionKey] :
|
||||
0;
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
scrollTop={scrollTop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
export default withScrollPosition;
|
25
frontend/src/Components/withScrollPosition.tsx
Normal file
25
frontend/src/Components/withScrollPosition.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
const { history } = props;
|
||||
|
||||
const initialScrollTop =
|
||||
history.action === 'POP' ||
|
||||
(history.location.state && history.location.state.restoreScrollPosition)
|
||||
? scrollPositions[scrollPositionKey]
|
||||
: 0;
|
||||
|
||||
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
export default withScrollPosition;
|
Loading…
Add table
Add a link
Reference in a new issue