diff --git a/client/src/javascript/components/general/CustomScrollbars.tsx b/client/src/javascript/components/general/CustomScrollbars.tsx deleted file mode 100644 index 5f6fce3c..00000000 --- a/client/src/javascript/components/general/CustomScrollbars.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import classnames from 'classnames'; -import React from 'react'; -import {positionValues, Scrollbars} from 'react-custom-scrollbars'; - -const horizontalThumb: React.StatelessComponent = (props) => { - return
; -}; - -const verticalThumb: React.StatelessComponent = (props) => { - return
; -}; - -const renderView: React.StatelessComponent = (props) => { - return ( -
- {props.children} -
- ); -}; - -interface CustomScrollbarsProps { - children: React.ReactNode; - className?: string; - style?: React.CSSProperties; - autoHeight?: boolean; - autoHeightMin?: number | string; - autoHeightMax?: number | string; - inverted?: boolean; - getHorizontalThumb?: React.StatelessComponent; - getVerticalThumb?: React.StatelessComponent; - nativeScrollHandler?: (event: React.UIEvent) => void; - scrollHandler?: (values: positionValues) => void; - onScrollStart?: () => void; - onScrollStop?: () => void; -} - -const CustomScrollbars = React.forwardRef((props: CustomScrollbarsProps, ref) => { - const { - children, - className, - style, - autoHeight, - autoHeightMin, - autoHeightMax, - inverted, - getHorizontalThumb, - getVerticalThumb, - nativeScrollHandler, - scrollHandler, - onScrollStart, - onScrollStop, - } = props; - const classes = classnames('scrollbars', className, { - 'is-inverted': inverted, - }); - - return ( - - {children} - - ); -}); - -CustomScrollbars.defaultProps = { - className: '', - style: undefined, - autoHeight: undefined, - autoHeightMin: undefined, - autoHeightMax: undefined, - inverted: undefined, - getHorizontalThumb: horizontalThumb, - getVerticalThumb: verticalThumb, - nativeScrollHandler: undefined, - scrollHandler: undefined, - onScrollStart: undefined, - onScrollStop: undefined, -}; - -export default CustomScrollbars; diff --git a/client/src/javascript/components/general/ListViewport.tsx b/client/src/javascript/components/general/ListViewport.tsx index 19ed8ac3..152609a2 100644 --- a/client/src/javascript/components/general/ListViewport.tsx +++ b/client/src/javascript/components/general/ListViewport.tsx @@ -1,282 +1,37 @@ -import debounce from 'lodash/debounce'; import React from 'react'; -import {positionValues, Scrollbars} from 'react-custom-scrollbars'; -import throttle from 'lodash/throttle'; - -import CustomScrollbars from './CustomScrollbars'; - -const METHODS_TO_BIND = [ - 'handleScroll', - 'handleScrollStart', - 'handleScrollStop', - 'measureItemHeight', - 'scrollToTop', - 'setScrollPosition', - 'setViewportHeight', -] as const; interface ListViewportProps { + children?: React.ReactNode; itemRenderer: (index: number) => React.ReactNode; listClass: string; listLength: number; - scrollContainerClass: string; - topSpacerClass?: string; - bottomSpacerClass?: string; - itemScrollOffset?: number; - getVerticalThumb?: React.StatelessComponent; + onScroll?: () => void; } -interface ListViewportStates { - itemHeight: number | null; - listVerticalPadding: number | null; - scrollTop: number; - viewportHeight: number | null; -} +// TODO: Implement windowing or infinite scrolling +const ListViewport = React.forwardRef((props: ListViewportProps, ref) => { + const {children, listClass, listLength, itemRenderer, onScroll} = props; -class ListViewport extends React.Component { - scrollbarRef: Scrollbars | null = null; - listRef: HTMLUListElement | null = null; - topSpacerRef: HTMLLIElement | null = null; - isScrolling = false; - lastScrollTop = 0; + const list = []; - static defaultProps = { - bottomSpacerClass: 'list__spacer list__spacer--bottom', - itemScrollOffset: 10, - topSpacerClass: 'list__spacer list__spacer--top', - }; - - constructor(props: ListViewportProps) { - super(props); - - this.state = { - itemHeight: null, - listVerticalPadding: null, - scrollTop: 0, - viewportHeight: null, - }; - - METHODS_TO_BIND.forEach((methodName: T) => { - this[methodName] = this[methodName].bind(this); - }); - - this.setViewportHeight = debounce(this.setViewportHeight, 250); - this.updateAfterScrolling = debounce(this.updateAfterScrolling, 500, { - leading: true, - trailing: true, - }); - this.setScrollPosition = throttle(this.setScrollPosition, 100); + // For loops are fast, and performance matters here. + for (let index = 0; index < listLength; index += 1) { + list.push(itemRenderer(index)); } - componentDidMount() { - global.addEventListener('resize', this.setViewportHeight); - this.setViewportHeight(); - } + const listContent =
    {list}
; - shouldComponentUpdate(_nextProps: ListViewportProps, nextState: ListViewportStates) { - const {scrollTop} = this.state; - const scrollDelta = Math.abs(scrollTop - nextState.scrollTop); + return ( +
+ {children} + {listContent} +
+ ); +}); - if (this.isScrolling && scrollDelta > 20) { - return false; - } - - return true; - } - - componentWillUnmount() { - global.removeEventListener('resize', this.setViewportHeight); - } - - getViewportLimits(scrollDelta: number) { - const {itemScrollOffset, listLength} = this.props; - const {itemHeight, listVerticalPadding, scrollTop, viewportHeight} = this.state; - - if ( - itemHeight == null || - itemHeight <= 0 || - itemScrollOffset == null || - viewportHeight == null || - viewportHeight <= 0 - ) { - return { - minItemIndex: 0, - maxItemIndex: Math.min(50, listLength), - }; - } - - // Calculate the number of items that should be rendered based on the height - // of the viewport. We offset this to render a few more outside of the - // container's dimensions, which looks nicer when the user scrolls. - const offsetBottom = scrollDelta > 0 ? itemScrollOffset * 2 : itemScrollOffset / 2; - const offsetTop = scrollDelta < 0 ? itemScrollOffset * 2 : itemScrollOffset / 2; - - let viewportHeightPadded = viewportHeight; - - if (listVerticalPadding) { - viewportHeightPadded -= listVerticalPadding; - } - - // The number of elements in view is the height of the viewport divided - // by the height of the elements. - const elementsInView = Math.ceil(viewportHeightPadded / itemHeight); - - // The minimum item index to render is the number of items above the - // viewport's current scroll position, minus the offset. - const minItemIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - offsetTop); - - // The maximum item index to render is the minimum item rendered, plus the - // number of items in view, plus double the offset. - const maxItemIndex = Math.min(listLength, minItemIndex + elementsInView + offsetBottom + offsetTop); - - return {minItemIndex, maxItemIndex}; - } - - getListPadding(minItemIndex: number, maxItemIndex: number, itemCount: number) { - const {itemHeight} = this.state; - - if (itemHeight == null) { - return {bottom: 0, top: 0}; - } - - const bottom = (itemCount - maxItemIndex) * itemHeight; - const top = minItemIndex * itemHeight; - - return {bottom, top}; - } - - setScrollPosition(scrollValues: positionValues) { - const {scrollTop} = this.state; - - this.lastScrollTop = scrollTop; - this.setState({scrollTop: scrollValues.scrollTop}); - } - - setViewportHeight() { - if (this.scrollbarRef) { - this.setState({ - viewportHeight: this.scrollbarRef.getClientHeight(), - }); - } - } - - scrollToTop() { - const {scrollTop} = this.state; - - if (scrollTop !== 0) { - if (this.scrollbarRef != null) { - this.scrollbarRef.scrollToTop(); - } - - this.lastScrollTop = 0; - this.setState({scrollTop: 0}); - } - } - - measureItemHeight() { - this.lastScrollTop = 0; - - this.setState( - { - scrollTop: 0, - itemHeight: null, - }, - () => { - if (this.scrollbarRef != null) { - this.scrollbarRef.scrollTop(0); - } - }, - ); - } - - handleScroll(scrollValues: positionValues) { - this.setScrollPosition(scrollValues); - } - - handleScrollStart() { - this.isScrolling = true; - } - - handleScrollStop() { - this.isScrolling = false; - this.updateAfterScrolling(); - } - - updateAfterScrolling() { - this.forceUpdate(); - } - - render() { - const { - children, - listClass, - topSpacerClass, - bottomSpacerClass, - scrollContainerClass, - listLength, - getVerticalThumb, - itemRenderer, - } = this.props; - const {itemHeight, scrollTop, listVerticalPadding} = this.state; - - const {minItemIndex, maxItemIndex} = this.getViewportLimits(scrollTop - this.lastScrollTop); - const listPadding = this.getListPadding(minItemIndex, maxItemIndex, listLength); - const list = []; - - // For loops are fast, and performance matters here. - for (let index = minItemIndex; index < maxItemIndex; index += 1) { - list.push(itemRenderer(index)); - } - - const listContent = ( -
    { - this.listRef = ref; - - if (listVerticalPadding == null && this.listRef != null) { - const listStyle = global.getComputedStyle(this.listRef); - const paddingBottom = Number(listStyle.getPropertyValue('padding-bottom').replace('px', '')); - const paddingTop = Number(listStyle.getPropertyValue('padding-top').replace('px', '')); - - this.setState({ - listVerticalPadding: paddingBottom + paddingTop, - }); - } - }}> -
  • { - this.topSpacerRef = ref; - - if (itemHeight == null && this.topSpacerRef?.nextSibling != null) { - this.setState({ - itemHeight: (this.topSpacerRef.nextSibling as HTMLLIElement).offsetHeight, - }); - } - }} - style={{height: `${listPadding.top}px`}} - /> - {list} -
  • -
- ); - - return ( - { - this.scrollbarRef = ref; - }} - scrollHandler={this.handleScroll}> - {children} - {listContent} - - ); - } -} +ListViewport.defaultProps = { + children: undefined, + onScroll: undefined, +}; export default ListViewport; diff --git a/client/src/javascript/components/general/filesystem/FilesystemBrowser.tsx b/client/src/javascript/components/general/filesystem/FilesystemBrowser.tsx index 790359eb..ea899aca 100644 --- a/client/src/javascript/components/general/filesystem/FilesystemBrowser.tsx +++ b/client/src/javascript/components/general/filesystem/FilesystemBrowser.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {defineMessages, WrappedComponentProps} from 'react-intl'; import ArrowIcon from '../../icons/ArrowIcon'; -import CustomScrollbars from '../CustomScrollbars'; import File from '../../icons/File'; import FolderClosedSolid from '../../icons/FolderClosedSolid'; import FloodActions from '../../../actions/FloodActions'; @@ -28,7 +27,6 @@ const MESSAGES = defineMessages({ interface FilesystemBrowserProps extends WrappedComponentProps { selectable?: 'files' | 'directories'; directory: string; - maxHeight?: number | string | null; onItemSelection?: (newDestination: string, isDirectory?: boolean) => void; } @@ -112,7 +110,7 @@ class FilesystemBrowser extends React.PureComponent -
- {parentDirectory} - {errorMessage} - {listItems} -
- +
+ {parentDirectory} + {errorMessage} + {listItems} +
); } } diff --git a/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx b/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx index 0fd3ebfa..a3762b91 100644 --- a/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx +++ b/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx @@ -198,16 +198,10 @@ class FilesystemBrowserTextbox extends React.Component { this.contextMenuNodeRef = ref; }} - scrolling={false} triggerRef={this.textboxRef}> diff --git a/client/src/javascript/components/sidebar/NotificationsButton.tsx b/client/src/javascript/components/sidebar/NotificationsButton.tsx index 8157c3fb..327c599f 100644 --- a/client/src/javascript/components/sidebar/NotificationsButton.tsx +++ b/client/src/javascript/components/sidebar/NotificationsButton.tsx @@ -9,7 +9,6 @@ import type {Notification} from '@shared/types/Notification'; import FloodActions from '../../actions/FloodActions'; import ChevronLeftIcon from '../icons/ChevronLeftIcon'; import ChevronRightIcon from '../icons/ChevronRightIcon'; -import CustomScrollbars from '../general/CustomScrollbars'; import LoadingIndicatorDots from '../icons/LoadingIndicatorDots'; import NotificationIcon from '../icons/NotificationIcon'; import NotificationStore from '../../stores/NotificationStore'; @@ -221,11 +220,9 @@ class NotificationsButton extends React.Component {this.getTopToolbar()}
{loadingIndicatorIcon}
- -
    - {notifications.map(this.getNotification)} -
-
+
    + {notifications.map(this.getNotification)} +
{this.getBottomToolbar()}
); diff --git a/client/src/javascript/components/sidebar/Sidebar.tsx b/client/src/javascript/components/sidebar/Sidebar.tsx index 5111fb75..748bc4d6 100644 --- a/client/src/javascript/components/sidebar/Sidebar.tsx +++ b/client/src/javascript/components/sidebar/Sidebar.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import CustomScrollbars from '../general/CustomScrollbars'; import FeedsButton from './FeedsButton'; import LogoutButton from './LogoutButton'; import NotificationsButton from './NotificationsButton'; @@ -16,7 +15,7 @@ import DiskUsage from './DiskUsage'; const Sidebar = () => { return ( - +
@@ -30,7 +29,7 @@ const Sidebar = () => { - +
); }; diff --git a/client/src/javascript/components/torrent-list/TableHeading.tsx b/client/src/javascript/components/torrent-list/TableHeading.tsx index cabbdb39..b7d8b3e9 100644 --- a/client/src/javascript/components/torrent-list/TableHeading.tsx +++ b/client/src/javascript/components/torrent-list/TableHeading.tsx @@ -13,9 +13,9 @@ const pointerDownStyles = ` `; interface TableHeadingProps extends WrappedComponentProps { - scrollOffset: number; onCellClick: (column: TorrentListColumn) => void; onWidthsChange: (column: TorrentListColumn, width: number) => void; + setRef?: React.RefCallback; } @observer @@ -26,7 +26,6 @@ class TableHeading extends React.PureComponent { lastPointerX: number | null = null; tableHeading: HTMLDivElement | null = null; resizeLine: HTMLDivElement | null = null; - tableHeadingX = 0; constructor(props: TableHeadingProps) { super(props); @@ -36,12 +35,6 @@ class TableHeading extends React.PureComponent { this.handlePointerMove = this.handlePointerMove.bind(this); } - componentDidMount() { - if (this.tableHeading != null) { - this.tableHeadingX = this.tableHeading.getBoundingClientRect().left; - } - } - getHeadingElements() { const {intl, onCellClick} = this.props; @@ -103,11 +96,10 @@ class TableHeading extends React.PureComponent { if (nextCellWidth > 20) { this.focusedCellWidth = nextCellWidth; this.lastPointerX = event.clientX; - if (this.resizeLine != null) { - this.resizeLine.style.transform = `translateX(${Math.max( - 0, - event.clientX - this.tableHeadingX + this.props.scrollOffset, - )}px)`; + if (this.resizeLine != null && this.tableHeading != null) { + this.resizeLine.style.transform = `translate(${Math.max(0, event.clientX)}px, ${ + this.tableHeading.getBoundingClientRect().top + }px)`; } } } @@ -133,7 +125,7 @@ class TableHeading extends React.PureComponent { } handlePointerDown(event: React.PointerEvent, slug: TorrentListColumn, width: number) { - if (!this.isPointerDown && this.resizeLine != null) { + if (!this.isPointerDown && this.resizeLine != null && this.tableHeading != null) { global.document.addEventListener('pointerup', this.handlePointerUp); global.document.addEventListener('pointermove', this.handlePointerMove); UIStore.addGlobalStyle(pointerDownStyles); @@ -142,20 +134,24 @@ class TableHeading extends React.PureComponent { this.focusedCellWidth = width; this.isPointerDown = true; this.lastPointerX = event.clientX; - this.resizeLine.style.transform = `translateX(${Math.max( - 0, - event.clientX - this.tableHeadingX + this.props.scrollOffset, - )}px)`; + this.resizeLine.style.transform = `translate(${Math.max(0, event.clientX)}px, ${ + this.tableHeading.getBoundingClientRect().top + }px)`; this.resizeLine.style.opacity = '1'; } } render() { + const {setRef} = this.props; + return (
{ this.tableHeading = ref; + if (setRef != null) { + setRef(ref); + } }}> {this.getHeadingElements()}
diff --git a/client/src/javascript/components/torrent-list/TorrentList.tsx b/client/src/javascript/components/torrent-list/TorrentList.tsx index 490e84ad..27f65956 100644 --- a/client/src/javascript/components/torrent-list/TorrentList.tsx +++ b/client/src/javascript/components/torrent-list/TorrentList.tsx @@ -1,9 +1,7 @@ import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl'; -import debounce from 'lodash/debounce'; import Dropzone from 'react-dropzone'; import {observer} from 'mobx-react'; -import {Scrollbars} from 'react-custom-scrollbars'; -import {observable, reaction, runInAction} from 'mobx'; +import {observable, reaction} from 'mobx'; import React from 'react'; import defaultFloodSettings from '@shared/constants/defaultFloodSettings'; @@ -13,7 +11,6 @@ import type {TorrentProperties} from '@shared/types/Torrent'; import {Button} from '../../ui'; import ClientStatusStore from '../../stores/ClientStatusStore'; -import CustomScrollbars from '../general/CustomScrollbars'; import Files from '../icons/Files'; import GlobalContextMenuMountPoint from '../general/GlobalContextMenuMountPoint'; import ListViewport from '../general/ListViewport'; @@ -59,18 +56,11 @@ const getEmptyTorrentListNotification = (): React.ReactNode => { const handleClick = (torrent: TorrentProperties, event: React.MouseEvent) => UIActions.handleTorrentClick({hash: torrent.hash, event}); const handleDoubleClick = (torrent: TorrentProperties) => TorrentListContextMenu.handleDetailsClick(torrent); - -interface TorrentListStates { - tableScrollLeft: number; -} - @observer -class TorrentList extends React.Component { +class TorrentList extends React.Component { listContainer: HTMLDivElement | null = null; - listViewportRef: ListViewport | null = null; - horizontalScrollRef: Scrollbars | null = null; - verticalScrollbarThumb: HTMLDivElement | null = null; - lastScrollLeft = 0; + listHeaderRef: HTMLDivElement | null = null; + listViewportRef = React.createRef(); torrentListViewportSize = observable.object<{width: number; height: number}>({ width: window.innerWidth, @@ -80,48 +70,17 @@ class TorrentList extends React.Component SettingStore.floodSettings.torrentListViewSize, - (currentTorrentListViewSize, prevTorrentListViewSize) => { - const isCondensed = currentTorrentListViewSize === 'condensed'; - const wasCondensed = prevTorrentListViewSize === 'condensed'; - - if (this.verticalScrollbarThumb != null) { - if (!isCondensed && wasCondensed) { - this.updateVerticalThumbPosition(0); - } else if (isCondensed && this.listContainer != null) { - this.updateVerticalThumbPosition( - (SettingStore.totalCellWidth - this.listContainer.clientWidth) * -1 + this.lastScrollLeft, - ); - } - } - - if (currentTorrentListViewSize !== prevTorrentListViewSize && this.listViewportRef != null) { - this.listViewportRef.measureItemHeight(); - } - }, - ); - reaction(() => TorrentFilterStore.filters, this.handleTorrentFilterChange); - - this.state = { - tableScrollLeft: 0, - }; } - componentDidMount() { - window.addEventListener('resize', this.updateTorrentListViewSize); - } + handleColumnWidthChange = (column: TorrentListColumn, width: number) => { + const {torrentListColumnWidths = defaultFloodSettings.torrentListColumnWidths} = SettingStore.floodSettings; - componentDidUpdate() { - if (this.horizontalScrollRef != null && this.torrentListViewportSize.width === 0) { - this.updateTorrentListViewSize(); - } - } - - componentWillUnmount() { - window.removeEventListener('resize', this.updateTorrentListViewSize); - } + SettingActions.saveSetting('torrentListColumnWidths', { + ...torrentListColumnWidths, + [column]: width, + }); + }; handleContextMenuClick = (torrent: TorrentProperties, event: React.MouseEvent | React.TouchEvent) => { if (event.cancelable === true) { @@ -184,70 +143,14 @@ class TorrentList extends React.Component { - if (this.listViewportRef != null) { - this.listViewportRef.scrollToTop(); + if (this.listViewportRef.current != null) { + this.listViewportRef.current.scrollTop = 0; } }; - getVerticalScrollbarThumb: React.StatelessComponent = (props) => { - return ( -
-
{ - this.verticalScrollbarThumb = ref; - }} - role="button" - tabIndex={0} - /> -
- ); - }; - - handleHorizontalScroll = (event: React.UIEvent) => { - if (this.verticalScrollbarThumb != null) { - const {clientWidth, scrollLeft, scrollWidth} = event.target as HTMLElement; - this.lastScrollLeft = scrollLeft; - this.updateVerticalThumbPosition((scrollWidth - clientWidth) * -1 + scrollLeft); - } - }; - - handleHorizontalScrollStop = () => { - this.setState({tableScrollLeft: this.lastScrollLeft}); - }; - - handleColumnWidthChange = (column: TorrentListColumn, width: number) => { - const {torrentListColumnWidths = defaultFloodSettings.torrentListColumnWidths} = SettingStore.floodSettings; - - SettingActions.saveSetting('torrentListColumnWidths', { - ...torrentListColumnWidths, - [column]: width, - }); - }; - - /* eslint-disable react/sort-comp */ - updateTorrentListViewSize = debounce( - () => { - runInAction(() => { - this.torrentListViewportSize.height = this.horizontalScrollRef?.getClientHeight() || window.innerHeight; - this.torrentListViewportSize.width = this.horizontalScrollRef?.getClientWidth() || window.innerWidth; - }); - if (SettingStore.floodSettings.torrentListViewSize === 'condensed') { - if (this.verticalScrollbarThumb != null && this.listContainer != null) { - this.updateVerticalThumbPosition( - (SettingStore.totalCellWidth - this.listContainer.clientWidth) * -1 + this.lastScrollLeft, - ); - } - } - }, - 100, - {trailing: true}, - ); - /* eslint-enable react/sort-comp */ - - updateVerticalThumbPosition = (offset: number) => { - if (this.verticalScrollbarThumb != null) { - this.verticalScrollbarThumb.style.transform = `translateX(${offset}px)`; + handleViewportScroll = () => { + if (this.listHeaderRef != null && this.listViewportRef.current != null) { + this.listHeaderRef.scrollLeft = this.listViewportRef.current.scrollLeft; } }; @@ -285,23 +188,9 @@ class TorrentList extends React.Component { - this.listViewportRef = ref; - }} - scrollContainerClass="torrent__list__scrollbars--vertical" - /> - ); - if (isCondensed) { torrentListHeading = ( { const currentSort = SettingStore.floodSettings.sortTorrents; @@ -319,12 +208,23 @@ class TorrentList extends React.Component { + this.listHeaderRef = ref; + }} /> ); } - } - const listViewportWidth = this.torrentListViewportSize.width; + content = ( + + ); + } return ( @@ -335,26 +235,11 @@ class TorrentList extends React.Component { this.listContainer = ref; }}> - { - this.horizontalScrollRef = ref; - }}> -
- - {torrentListHeading} - {content} -
-
- +
+ + {torrentListHeading} + {content} +
diff --git a/client/src/sass/base/_main.scss b/client/src/sass/base/_main.scss index 397f570f..9d3b8f5a 100644 --- a/client/src/sass/base/_main.scss +++ b/client/src/sass/base/_main.scss @@ -5,3 +5,30 @@ body { ul { list-style: none; } + +* { + scrollbar-width: thin; +} + +::-webkit-scrollbar { + height: 6px; + width: 6px; +} + +::-webkit-scrollbar-corner { + background: none; +} + +::-webkit-scrollbar-track { + opacity: 0; +} + +::-webkit-scrollbar-thumb { + border-radius: 4px; + background: #8d8d8d; +} + +::-webkit-scrollbar-thumb:hover { + border-radius: 4px; + background: lighten(#8d8d8d, 10%); +} diff --git a/client/src/sass/components/_sidebar.scss b/client/src/sass/components/_sidebar.scss index 546a3342..a2794004 100644 --- a/client/src/sass/components/_sidebar.scss +++ b/client/src/sass/components/_sidebar.scss @@ -6,7 +6,9 @@ flex: 1; min-width: 240px; max-width: 250px; - overflow: auto; + height: 100%; + overflow-x: hidden; + overflow-y: overlay; position: relative; z-index: 2; transition: transform 0.2s; diff --git a/client/src/sass/components/_table.scss b/client/src/sass/components/_table.scss index 644ebe9d..4ded0961 100644 --- a/client/src/sass/components/_table.scss +++ b/client/src/sass/components/_table.scss @@ -9,6 +9,7 @@ $table--heading--resize--indicator--width: 1px; @include theme('color', 'table--heading--color'); display: flex; height: 24px; + overflow: hidden; font-size: 12px; white-space: nowrap; z-index: 1; @@ -121,7 +122,7 @@ $table--heading--resize--indicator--width: 1px; bottom: 0; left: 0; opacity: 0; - position: absolute; + position: fixed; top: 0; transition: opacity 0.125s; will-change: opacity, transform; diff --git a/client/src/sass/components/_torrent.scss b/client/src/sass/components/_torrent.scss index e24fa267..a7bfd2ec 100644 --- a/client/src/sass/components/_torrent.scss +++ b/client/src/sass/components/_torrent.scss @@ -475,9 +475,11 @@ $more-info--border: $textbox-repeater--button--border; @include theme('border-top', 'torrent--border'); display: flex; height: 30px; + min-width: max-content; + max-width: 100%; padding: 0; - &:nth-child(0n + 2) { + &:nth-child(0n + 1) { border-top: none; } diff --git a/client/src/sass/components/_torrents.scss b/client/src/sass/components/_torrents.scss index d1ce37f2..6ce6340c 100644 --- a/client/src/sass/components/_torrents.scss +++ b/client/src/sass/components/_torrents.scss @@ -2,8 +2,9 @@ @include theme('background', 'torrent-list--background'); @include theme('box-shadow', 'torrent-list--border'); display: flex; - flex: 1 1 auto; + flex: 1 1 0px; flex-direction: column; + overflow: hidden; position: relative; .loading-indicator { @@ -52,11 +53,18 @@ } } + &__viewport { + overflow-x: auto; + overflow-y: overlay; + height: 100%; + } + &__wrapper { display: flex; flex: 1 1 auto; flex-direction: column; height: 100%; + width: 100%; justify-content: center; list-style: none; opacity: 1; @@ -101,7 +109,6 @@ box-shadow: -1px 0 $torrent-list--border; display: flex; flex-direction: column; - flex: 1; flex: 0 1 100%; } } diff --git a/client/src/sass/style.scss.d.ts b/client/src/sass/style.scss.d.ts index 666fa579..9835e44d 100644 --- a/client/src/sass/style.scss.d.ts +++ b/client/src/sass/style.scss.d.ts @@ -477,6 +477,7 @@ declare const styles: { readonly torrent__list: string; readonly 'torrent__list__scrollbars--horizontal': string; readonly 'torrent__list__scrollbars--vertical': string; + readonly torrent__list__viewport: string; readonly torrent__list__wrapper: string; readonly 'torrent__list--loading-enter': string; readonly 'torrent__list--loading-enter-active': string; diff --git a/package-lock.json b/package-lock.json index 10af718a..93faf156 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,6 @@ "@types/passport": "^1.0.4", "@types/passport-jwt": "^3.0.3", "@types/react": "^16.9.52", - "@types/react-custom-scrollbars": "^4.0.7", "@types/react-dnd-multi-backend": "^6.0.0", "@types/react-dom": "^16.9.8", "@types/react-measure": "^2.0.6", @@ -128,7 +127,6 @@ "prettier": "^2.1.2", "promise": "^8.1.0", "react": "^16.14.0", - "react-custom-scrollbars": "^4.2.1", "react-dev-utils": "^10.2.1", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", @@ -3105,15 +3103,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-custom-scrollbars": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@types/react-custom-scrollbars/-/react-custom-scrollbars-4.0.7.tgz", - "integrity": "sha512-4QPZdwd+wmzWq9TyNSA/4MZFYvlQn1GlEFFkpFx8VSs13gR/L+hQne0vFnbzwlQmGG7OksthkoVpYxWJjzz95w==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-dnd-multi-backend": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.0.tgz", @@ -3806,12 +3795,6 @@ "node": ">=0.4.0" } }, - "node_modules/add-px-to-style": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz", - "integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=", - "dev": true - }, "node_modules/address": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", @@ -7933,17 +7916,6 @@ "utila": "~0.4" } }, - "node_modules/dom-css": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz", - "integrity": "sha1-/bwtWgFdCj4YcuEUcrvQ57nmogI=", - "dev": true, - "dependencies": { - "add-px-to-style": "1.0.0", - "prefix-style": "2.0.1", - "to-camel-case": "1.0.0" - } - }, "node_modules/dom-helpers": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", @@ -20189,12 +20161,6 @@ "node": ">=0.10.0" } }, - "node_modules/prefix-style": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz", - "integrity": "sha1-ZrupqHDP2jCKXcIOhekSCTLJWgY=", - "dev": true - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -20470,15 +20436,6 @@ } ] }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -20558,21 +20515,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-custom-scrollbars": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz", - "integrity": "sha1-gw/ZUCkn6X6KeMIIaBOJmyqLZts=", - "dev": true, - "dependencies": { - "dom-css": "^2.0.0", - "prop-types": "^15.5.10", - "raf": "^3.1.0" - }, - "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0", - "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" - } - }, "node_modules/react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -25009,15 +24951,6 @@ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", "dev": true }, - "node_modules/to-camel-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", - "integrity": "sha1-GlYFSy+daWKYzmamCJcyK29CPkY=", - "dev": true, - "dependencies": { - "to-space-case": "^1.0.0" - } - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -25027,12 +24960,6 @@ "node": ">=4" } }, - "node_modules/to-no-case": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", - "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=", - "dev": true - }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -25084,15 +25011,6 @@ "node": ">=8.0" } }, - "node_modules/to-space-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", - "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=", - "dev": true, - "dependencies": { - "to-no-case": "^1.0.0" - } - }, "node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -30859,15 +30777,6 @@ "csstype": "^3.0.2" } }, - "@types/react-custom-scrollbars": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@types/react-custom-scrollbars/-/react-custom-scrollbars-4.0.7.tgz", - "integrity": "sha512-4QPZdwd+wmzWq9TyNSA/4MZFYvlQn1GlEFFkpFx8VSs13gR/L+hQne0vFnbzwlQmGG7OksthkoVpYxWJjzz95w==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, "@types/react-dnd-multi-backend": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.0.tgz", @@ -31456,12 +31365,6 @@ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true }, - "add-px-to-style": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz", - "integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=", - "dev": true - }, "address": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", @@ -34865,17 +34768,6 @@ "utila": "~0.4" } }, - "dom-css": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz", - "integrity": "sha1-/bwtWgFdCj4YcuEUcrvQ57nmogI=", - "dev": true, - "requires": { - "add-px-to-style": "1.0.0", - "prefix-style": "2.0.1", - "to-camel-case": "1.0.0" - } - }, "dom-helpers": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", @@ -44676,12 +44568,6 @@ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "dev": true }, - "prefix-style": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz", - "integrity": "sha1-ZrupqHDP2jCKXcIOhekSCTLJWgY=", - "dev": true - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -44909,15 +44795,6 @@ "integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w==", "dev": true }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dev": true, - "requires": { - "performance-now": "^2.1.0" - } - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -44987,17 +44864,6 @@ "prop-types": "^15.6.2" } }, - "react-custom-scrollbars": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz", - "integrity": "sha1-gw/ZUCkn6X6KeMIIaBOJmyqLZts=", - "dev": true, - "requires": { - "dom-css": "^2.0.0", - "prop-types": "^15.5.10", - "raf": "^3.1.0" - } - }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -48618,27 +48484,12 @@ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", "dev": true }, - "to-camel-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", - "integrity": "sha1-GlYFSy+daWKYzmamCJcyK29CPkY=", - "dev": true, - "requires": { - "to-space-case": "^1.0.0" - } - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, - "to-no-case": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", - "integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=", - "dev": true - }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -48680,15 +48531,6 @@ "is-number": "^7.0.0" } }, - "to-space-case": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", - "integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=", - "dev": true, - "requires": { - "to-no-case": "^1.0.0" - } - }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", diff --git a/package.json b/package.json index 1375d257..45251983 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,6 @@ "@types/passport": "^1.0.4", "@types/passport-jwt": "^3.0.3", "@types/react": "^16.9.52", - "@types/react-custom-scrollbars": "^4.0.7", "@types/react-dnd-multi-backend": "^6.0.0", "@types/react-dom": "^16.9.8", "@types/react-measure": "^2.0.6", @@ -167,7 +166,6 @@ "prettier": "^2.1.2", "promise": "^8.1.0", "react": "^16.14.0", - "react-custom-scrollbars": "^4.2.1", "react-dev-utils": "^10.2.1", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3",