From ac01af496ac33ef9c9025d7dea5641a9f5a70e6d Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Sat, 14 Nov 2020 22:19:07 +0800 Subject: [PATCH] TableHeading, Textbox: replace setRef with forwardRef --- .../filesystem/FilesystemBrowserTextbox.tsx | 14 +- .../general/form-elements/TagSelect.tsx | 10 +- .../components/torrent-list/TableHeading.tsx | 240 ++++++++---------- .../components/torrent-list/TorrentList.tsx | 10 +- .../javascript/ui/components/ContextMenu.tsx | 8 +- .../src/javascript/ui/components/Select.tsx | 16 +- .../src/javascript/ui/components/Textbox.tsx | 64 +++-- 7 files changed, 165 insertions(+), 197 deletions(-) diff --git a/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx b/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx index dc697efe..0714269c 100644 --- a/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx +++ b/client/src/javascript/components/general/filesystem/FilesystemBrowserTextbox.tsx @@ -26,7 +26,7 @@ interface FilesystemBrowserTextboxStates { class FilesystemBrowserTextbox extends React.Component { formRowRef = React.createRef(); - textboxRef: HTMLInputElement | null = null; + textboxRef = React.createRef(); constructor(props: FilesystemBrowserTextboxProps) { super(props); @@ -77,11 +77,11 @@ class FilesystemBrowserTextbox extends React.Component { - if (this.textboxRef == null) { + if (this.textboxRef.current == null) { return; } - const destination = this.textboxRef.value; + const destination = this.textboxRef.current.value; if (this.props.onChange) { this.props.onChange(destination); @@ -105,8 +105,8 @@ class FilesystemBrowserTextbox extends React.Component { - if (this.textboxRef != null) { - this.textboxRef.value = destination; + if (this.textboxRef.current != null) { + this.textboxRef.current.value = destination; } this.setState({destination, isDirectoryListOpen: isDirectory}); }; @@ -178,9 +178,7 @@ class FilesystemBrowserTextbox extends React.Component { - this.textboxRef = ref; - }}> + ref={this.textboxRef}> diff --git a/client/src/javascript/components/general/form-elements/TagSelect.tsx b/client/src/javascript/components/general/form-elements/TagSelect.tsx index e6d8c781..b1287463 100644 --- a/client/src/javascript/components/general/form-elements/TagSelect.tsx +++ b/client/src/javascript/components/general/form-elements/TagSelect.tsx @@ -23,7 +23,7 @@ interface TagSelectStates { export default class TagSelect extends React.Component { formRowRef = React.createRef(); menuRef = React.createRef(); - textboxRef: HTMLInputElement | null = null; + textboxRef = React.createRef(); tagMenuItems = Object.keys(TorrentFilterStore.taxonomy.tagCounts).reduce((accumulator: React.ReactNodeArray, tag) => { if (tag === '') { @@ -115,8 +115,8 @@ export default class TagSelect extends React.Component { - if (this.textboxRef != null) { - this.textboxRef.value = selectedTags.join(); + if (this.textboxRef.current != null) { + this.textboxRef.current.value = selectedTags.join(); } }); }; @@ -161,9 +161,7 @@ export default class TagSelect extends React.Component { - this.textboxRef = ref; - }}> + ref={this.textboxRef}> diff --git a/client/src/javascript/components/torrent-list/TableHeading.tsx b/client/src/javascript/components/torrent-list/TableHeading.tsx index 1e72e549..10259294 100644 --- a/client/src/javascript/components/torrent-list/TableHeading.tsx +++ b/client/src/javascript/components/torrent-list/TableHeading.tsx @@ -1,7 +1,8 @@ import classnames from 'classnames'; -import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl'; +import {forwardRef, MutableRefObject, ReactNodeArray, useRef, useState} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; import {observer} from 'mobx-react'; -import * as React from 'react'; +import {useEnsuredForwardedRef} from 'react-use'; import TorrentListColumns, {TorrentListColumn} from '../../constants/TorrentListColumns'; import SettingStore from '../../stores/SettingStore'; @@ -12,153 +13,132 @@ const pointerDownStyles = ` * { cursor: col-resize !important; } `; -interface TableHeadingProps extends WrappedComponentProps { +interface TableHeadingProps { onCellClick: (column: TorrentListColumn) => void; onWidthsChange: (column: TorrentListColumn, width: number) => void; - setRef?: React.RefCallback; } -@observer -class TableHeading extends React.Component { - focusedCell: TorrentListColumn | null = null; - focusedCellWidth: number | null = null; - isPointerDown = false; - lastPointerX: number | null = null; - tableHeading: HTMLDivElement | null = null; - resizeLine: HTMLDivElement | null = null; +const TableHeading = observer( + forwardRef(({onCellClick, onWidthsChange}: TableHeadingProps, ref) => { + const [isPointerDown, setIsPointerDown] = useState(false); - getHeadingElements() { - const {intl, onCellClick} = this.props; + const focusedCell = useRef(); + const focusedCellWidth = useRef(); + const lastPointerX = useRef(); + const tableHeading = useEnsuredForwardedRef(ref as MutableRefObject); + const resizeLine = useRef(null); - return SettingStore.floodSettings.torrentListColumns.reduce((accumulator: React.ReactNodeArray, {id, visible}) => { - if (!visible) { - return accumulator; + const intl = useIntl(); + + const handlePointerMove = (event: PointerEvent) => { + let widthDelta = 0; + if (lastPointerX.current != null) { + widthDelta = event.clientX - lastPointerX.current; } - const labelID = TorrentListColumns[id]?.id; - if (labelID == null) { - return accumulator; + let nextCellWidth = 20; + if (focusedCellWidth.current != null) { + nextCellWidth = focusedCellWidth.current + widthDelta; } - let handle = null; - const width = SettingStore.floodSettings.torrentListColumnWidths[id] || 100; + if (nextCellWidth > 20) { + focusedCellWidth.current = nextCellWidth; + lastPointerX.current = event.clientX; + if (resizeLine.current != null && tableHeading.current != null) { + resizeLine.current.style.transform = `translate(${Math.max(0, event.clientX)}px, ${ + tableHeading.current.getBoundingClientRect().top + }px)`; + } + } + }; - if (!this.isPointerDown) { - handle = ( - { - this.handlePointerDown(event, id, width); - }} - /> - ); + const handlePointerUp = () => { + UIStore.removeGlobalStyle(pointerDownStyles); + window.removeEventListener('pointerup', handlePointerUp); + window.removeEventListener('pointermove', handlePointerMove); + + setIsPointerDown(false); + lastPointerX.current = undefined; + + if (resizeLine.current != null) { + resizeLine.current.style.opacity = '0'; } - const isSortActive = id === SettingStore.floodSettings.sortTorrents.property; - const classes = classnames('table__cell table__heading', { - 'table__heading--is-sorted': isSortActive, - [`table__heading--direction--${SettingStore.floodSettings.sortTorrents.direction}`]: isSortActive, - }); - - accumulator.push( -
onCellClick(id)} style={{width: `${width}px`}}> - - - - {handle} -
, - ); - - return accumulator; - }, []); - } - - handlePointerMove = (event: PointerEvent) => { - let widthDelta = 0; - if (this.lastPointerX != null) { - widthDelta = event.clientX - this.lastPointerX; - } - - let nextCellWidth = 20; - if (this.focusedCellWidth != null) { - nextCellWidth = this.focusedCellWidth + widthDelta; - } - - if (nextCellWidth > 20) { - this.focusedCellWidth = nextCellWidth; - this.lastPointerX = event.clientX; - if (this.resizeLine != null && this.tableHeading != null) { - this.resizeLine.style.transform = `translate(${Math.max(0, event.clientX)}px, ${ - this.tableHeading.getBoundingClientRect().top - }px)`; + if (focusedCell.current != null && focusedCellWidth.current != null) { + onWidthsChange(focusedCell.current, focusedCellWidth.current); } - } - }; - handlePointerUp = () => { - UIStore.removeGlobalStyle(pointerDownStyles); - global.document.removeEventListener('pointerup', this.handlePointerUp); - global.document.removeEventListener('pointermove', (e) => this.handlePointerMove(e)); - - this.isPointerDown = false; - this.lastPointerX = null; - - if (this.resizeLine != null) { - this.resizeLine.style.opacity = '0'; - } - - if (this.focusedCell != null && this.focusedCellWidth != null) { - this.props.onWidthsChange(this.focusedCell, this.focusedCellWidth); - } - - this.focusedCell = null; - this.focusedCellWidth = null; - }; - - handlePointerDown = (event: React.PointerEvent, slug: TorrentListColumn, width: number) => { - if (!this.isPointerDown && this.resizeLine != null && this.tableHeading != null) { - global.document.addEventListener('pointerup', this.handlePointerUp); - global.document.addEventListener('pointermove', this.handlePointerMove); - UIStore.addGlobalStyle(pointerDownStyles); - - this.focusedCell = slug; - this.focusedCellWidth = width; - this.isPointerDown = true; - this.lastPointerX = event.clientX; - 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; + focusedCell.current = undefined; + focusedCellWidth.current = undefined; + }; return ( -
{ - this.tableHeading = ref; - if (setRef != null) { - setRef(ref); +
+ {SettingStore.floodSettings.torrentListColumns.reduce((accumulator: ReactNodeArray, {id, visible}) => { + if (!visible) { + return accumulator; } - }}> - {this.getHeadingElements()} + + const labelID = TorrentListColumns[id]?.id; + if (labelID == null) { + return accumulator; + } + + let handle = null; + const width = SettingStore.floodSettings.torrentListColumnWidths[id] || 100; + + if (!isPointerDown) { + handle = ( + { + if (!isPointerDown && resizeLine.current != null && tableHeading.current != null) { + setIsPointerDown(true); + + focusedCell.current = id; + focusedCellWidth.current = width; + lastPointerX.current = event.clientX; + + window.addEventListener('pointerup', handlePointerUp); + window.addEventListener('pointermove', handlePointerMove); + UIStore.addGlobalStyle(pointerDownStyles); + + resizeLine.current.style.transform = `translate(${Math.max(0, event.clientX)}px, ${ + tableHeading.current.getBoundingClientRect().top + }px)`; + resizeLine.current.style.opacity = '1'; + } + }} + /> + ); + } + + const isSortActive = id === SettingStore.floodSettings.sortTorrents.property; + const classes = classnames('table__cell table__heading', { + 'table__heading--is-sorted': isSortActive, + [`table__heading--direction--${SettingStore.floodSettings.sortTorrents.direction}`]: isSortActive, + }); + + accumulator.push( +
onCellClick(id)} style={{width: `${width}px`}}> + + + + {handle} +
, + ); + + return accumulator; + }, [])}
-
{ - this.resizeLine = ref; - }} - /> +
); - } -} + }), +); -export default injectIntl(TableHeading); +export default TableHeading; diff --git a/client/src/javascript/components/torrent-list/TorrentList.tsx b/client/src/javascript/components/torrent-list/TorrentList.tsx index d074bc32..87273c25 100644 --- a/client/src/javascript/components/torrent-list/TorrentList.tsx +++ b/client/src/javascript/components/torrent-list/TorrentList.tsx @@ -93,7 +93,7 @@ const getEmptyTorrentListNotification = (): ReactNode => { @observer class TorrentList extends Component { - listHeaderRef: HTMLDivElement | null = null; + listHeaderRef = createRef(); listViewportRef = createRef(); torrentListViewportSize = observable.object<{width: number; height: number}>({ @@ -123,8 +123,8 @@ class TorrentList extends Component { }; handleViewportScroll = (scrollLeft: number) => { - if (this.listHeaderRef != null) { - this.listHeaderRef.scrollLeft = scrollLeft; + if (this.listHeaderRef.current != null) { + this.listHeaderRef.current.scrollLeft = scrollLeft; } }; @@ -168,9 +168,7 @@ class TorrentList extends Component { SettingActions.saveSetting('sortTorrents', sortBy); }} onWidthsChange={this.handleColumnWidthChange} - setRef={(ref) => { - this.listHeaderRef = ref; - }} + ref={this.listHeaderRef} /> ); } diff --git a/client/src/javascript/ui/components/ContextMenu.tsx b/client/src/javascript/ui/components/ContextMenu.tsx index c9fbfdb1..ea9a7c19 100644 --- a/client/src/javascript/ui/components/ContextMenu.tsx +++ b/client/src/javascript/ui/components/ContextMenu.tsx @@ -1,6 +1,6 @@ import CSSTransition from 'react-transition-group/CSSTransition'; import classnames from 'classnames'; -import {CSSProperties, forwardRef, MouseEvent, ReactNode} from 'react'; +import {CSSProperties, forwardRef, MouseEvent, ReactNode, RefObject} from 'react'; import Overlay from './Overlay'; import transitionTimeouts from '../constants/transitionTimeouts'; @@ -18,7 +18,7 @@ interface ContextMenuProps { x: number; y: number; }; - triggerRef?: Element | null; + triggerRef?: RefObject; matchTriggerWidth?: boolean; padding?: boolean; scrolling?: boolean; @@ -47,8 +47,8 @@ const ContextMenu = forwardRef( const dropdownStyle: CSSProperties = {}; let shouldRenderAbove = false; - if (triggerRef) { - const buttonBoundingRect = triggerRef.getBoundingClientRect(); + if (triggerRef?.current) { + const buttonBoundingRect = triggerRef.current.getBoundingClientRect(); const windowHeight = window.innerHeight; const spaceAbove = buttonBoundingRect.top; const spaceBelow = windowHeight - buttonBoundingRect.bottom; diff --git a/client/src/javascript/ui/components/Select.tsx b/client/src/javascript/ui/components/Select.tsx index 16a4d1d5..bf44159e 100644 --- a/client/src/javascript/ui/components/Select.tsx +++ b/client/src/javascript/ui/components/Select.tsx @@ -41,9 +41,9 @@ interface SelectStates { export default class Select extends Component { menuRef = createRef(); - inputRef: HTMLInputElement | null = null; + inputRef = createRef(); - triggerRef: HTMLButtonElement | null = null; + triggerRef = createRef(); static defaultProps = { persistentPlaceholder: false, @@ -167,9 +167,7 @@ export default class Select extends Component { return (