TableHeading, Textbox: replace setRef with forwardRef

This commit is contained in:
Jesse Chan
2020-11-14 22:19:07 +08:00
parent 7f91d3f244
commit ac01af496a
7 changed files with 165 additions and 197 deletions
@@ -26,7 +26,7 @@ interface FilesystemBrowserTextboxStates {
class FilesystemBrowserTextbox extends React.Component<FilesystemBrowserTextboxProps, FilesystemBrowserTextboxStates> {
formRowRef = React.createRef<HTMLDivElement>();
textboxRef: HTMLInputElement | null = null;
textboxRef = React.createRef<HTMLInputElement>();
constructor(props: FilesystemBrowserTextboxProps) {
super(props);
@@ -77,11 +77,11 @@ class FilesystemBrowserTextbox extends React.Component<FilesystemBrowserTextboxP
/* eslint-disable react/sort-comp */
handleDestinationInputChange = debounce(
() => {
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<FilesystemBrowserTextboxP
};
handleItemSelection = (destination: string, isDirectory = true) => {
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<FilesystemBrowserTextboxP
placeholder={intl.formatMessage({
id: 'torrents.add.destination.placeholder',
})}
setRef={(ref) => {
this.textboxRef = ref;
}}>
ref={this.textboxRef}>
<FormElementAddon onClick={this.handleDirectoryListButtonClick}>
<Search />
</FormElementAddon>
@@ -23,7 +23,7 @@ interface TagSelectStates {
export default class TagSelect extends React.Component<TagSelectProps, TagSelectStates> {
formRowRef = React.createRef<HTMLDivElement>();
menuRef = React.createRef<HTMLDivElement>();
textboxRef: HTMLInputElement | null = null;
textboxRef = React.createRef<HTMLInputElement>();
tagMenuItems = Object.keys(TorrentFilterStore.taxonomy.tagCounts).reduce((accumulator: React.ReactNodeArray, tag) => {
if (tag === '') {
@@ -115,8 +115,8 @@ export default class TagSelect extends React.Component<TagSelectProps, TagSelect
}
this.setState({selectedTags}, () => {
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<TagSelectProps, TagSelect
addonPlacement="after"
defaultValue={defaultValue}
placeholder={placeholder}
setRef={(ref) => {
this.textboxRef = ref;
}}>
ref={this.textboxRef}>
<FormElementAddon onClick={this.toggleOpenState} className="select__indicator">
<Chevron />
</FormElementAddon>
@@ -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<HTMLDivElement>;
}
@observer
class TableHeading extends React.Component<TableHeadingProps> {
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<HTMLDivElement, TableHeadingProps>(({onCellClick, onWidthsChange}: TableHeadingProps, ref) => {
const [isPointerDown, setIsPointerDown] = useState<boolean>(false);
getHeadingElements() {
const {intl, onCellClick} = this.props;
const focusedCell = useRef<TorrentListColumn>();
const focusedCellWidth = useRef<number>();
const lastPointerX = useRef<number>();
const tableHeading = useEnsuredForwardedRef<HTMLDivElement>(ref as MutableRefObject<HTMLDivElement>);
const resizeLine = useRef<HTMLDivElement>(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 = (
<span
className="table__heading__handle"
onPointerDown={(event) => {
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(
<div className={classes} key={id} onClick={() => onCellClick(id)} style={{width: `${width}px`}}>
<span
className="table__heading__label"
title={intl.formatMessage({
id: labelID,
})}>
<FormattedMessage id={labelID} />
</span>
{handle}
</div>,
);
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 (
<div
className="table__row table__row--heading"
ref={(ref) => {
this.tableHeading = ref;
if (setRef != null) {
setRef(ref);
<div className="table__row table__row--heading" ref={tableHeading}>
{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 = (
<span
className="table__heading__handle"
onPointerDown={(event) => {
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(
<div className={classes} key={id} onClick={() => onCellClick(id)} style={{width: `${width}px`}}>
<span
className="table__heading__label"
title={intl.formatMessage({
id: labelID,
})}>
<FormattedMessage id={labelID} />
</span>
{handle}
</div>,
);
return accumulator;
}, [])}
<div className="table__cell table__heading table__heading--fill" />
<div
className="table__heading__resize-line"
ref={(ref) => {
this.resizeLine = ref;
}}
/>
<div className="table__heading__resize-line" ref={resizeLine} />
</div>
);
}
}
}),
);
export default injectIntl(TableHeading);
export default TableHeading;
@@ -93,7 +93,7 @@ const getEmptyTorrentListNotification = (): ReactNode => {
@observer
class TorrentList extends Component<WrappedComponentProps> {
listHeaderRef: HTMLDivElement | null = null;
listHeaderRef = createRef<HTMLDivElement>();
listViewportRef = createRef<FixedSizeList>();
torrentListViewportSize = observable.object<{width: number; height: number}>({
@@ -123,8 +123,8 @@ class TorrentList extends Component<WrappedComponentProps> {
};
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<WrappedComponentProps> {
SettingActions.saveSetting('sortTorrents', sortBy);
}}
onWidthsChange={this.handleColumnWidthChange}
setRef={(ref) => {
this.listHeaderRef = ref;
}}
ref={this.listHeaderRef}
/>
);
}
@@ -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<Element>;
matchTriggerWidth?: boolean;
padding?: boolean;
scrolling?: boolean;
@@ -47,8 +47,8 @@ const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(
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;
+6 -10
View File
@@ -41,9 +41,9 @@ interface SelectStates {
export default class Select extends Component<SelectProps, SelectStates> {
menuRef = createRef<HTMLDivElement>();
inputRef: HTMLInputElement | null = null;
inputRef = createRef<HTMLInputElement>();
triggerRef: HTMLButtonElement | null = null;
triggerRef = createRef<HTMLButtonElement>();
static defaultProps = {
persistentPlaceholder: false,
@@ -167,9 +167,7 @@ export default class Select extends Component<SelectProps, SelectStates> {
return (
<Button
additionalClassNames="select__button"
buttonRef={(ref) => {
this.triggerRef = ref;
}}
buttonRef={this.triggerRef}
addonPlacement="after"
onClick={this.handleTriggerClick}
priority={priority}
@@ -194,8 +192,8 @@ export default class Select extends Component<SelectProps, SelectStates> {
this.props.onSelect(id);
}
if (this.inputRef) {
dispatchChangeEvent(this.inputRef);
if (this.inputRef.current) {
dispatchChangeEvent(this.inputRef.current);
}
});
};
@@ -258,9 +256,7 @@ export default class Select extends Component<SelectProps, SelectStates> {
name={`${id}`}
onChange={noop}
tabIndex={-1}
ref={(ref) => {
this.inputRef = ref;
}}
ref={this.inputRef}
type="text"
value={selectedID}
/>
+31 -33
View File
@@ -1,5 +1,5 @@
import classnames from 'classnames';
import * as React from 'react';
import {Children, cloneElement, forwardRef, ReactElement} from 'react';
import FormElementAddon from './FormElementAddon';
import FormRowItem from './FormRowItem';
@@ -8,40 +8,23 @@ import type {FormRowItemProps} from './FormRowItem';
type TextboxProps = Pick<
React.InputHTMLAttributes<HTMLInputElement>,
'defaultValue' | 'placeholder' | 'onChange' | 'onClick' | 'autoComplete'
'children' | 'defaultValue' | 'placeholder' | 'onChange' | 'onClick' | 'autoComplete'
> & {
id: string;
label?: React.ReactNode;
type?: 'text' | 'password';
width?: FormRowItemProps['width'];
setRef?: React.Ref<HTMLInputElement>;
addonPlacement?: 'before' | 'after';
labelOffset?: boolean;
wrapperClassName?: string;
};
export default class Textbox extends React.Component<TextboxProps> {
static defaultProps = {
type: 'text',
};
getLabel(): React.ReactNode {
const {id, label} = this.props;
if (label) {
return (
<label className="form__element__label" htmlFor={id}>
{label}
</label>
);
}
return undefined;
}
render() {
const {
const Textbox = forwardRef<HTMLInputElement, TextboxProps>(
(
{
children,
id,
label,
addonPlacement,
labelOffset,
wrapperClassName,
@@ -50,17 +33,17 @@ export default class Textbox extends React.Component<TextboxProps> {
placeholder,
autoComplete,
type,
setRef,
onChange,
onClick,
} = this.props;
}: TextboxProps,
ref,
) => {
let addonCount = 0;
const childElements = React.Children.map(children, (child) => {
const childAsElement = child as React.ReactElement;
const childElements = Children.map(children, (child) => {
const childAsElement = child as ReactElement;
if (childAsElement && childAsElement.type === FormElementAddon) {
addonCount += 1;
return React.cloneElement(childAsElement, {
return cloneElement(childAsElement, {
addonIndex: addonCount,
addonPlacement,
});
@@ -78,7 +61,11 @@ export default class Textbox extends React.Component<TextboxProps> {
return (
<FormRowItem width={width}>
{this.getLabel()}
{label ?? (
<label className="form__element__label" htmlFor={id}>
{label}
</label>
)}
<div className={wrapperClasses}>
<input
className={inputClasses}
@@ -87,7 +74,7 @@ export default class Textbox extends React.Component<TextboxProps> {
name={id}
onChange={onChange}
onClick={onClick}
ref={setRef}
ref={ref}
tabIndex={0}
type={type}
autoComplete={autoComplete}
@@ -96,5 +83,16 @@ export default class Textbox extends React.Component<TextboxProps> {
</div>
</FormRowItem>
);
}
}
},
);
Textbox.defaultProps = {
label: undefined,
type: 'text',
width: undefined,
addonPlacement: undefined,
labelOffset: undefined,
wrapperClassName: undefined,
};
export default Textbox;