TableHeading: sync scroll with list on cell focus

This commit is contained in:
Jesse Chan
2021-03-12 11:24:26 +08:00
parent 76092ed734
commit a77c0acb07
2 changed files with 123 additions and 118 deletions
@@ -15,140 +15,144 @@ const pointerDownStyles = `
interface TableHeadingProps {
onCellClick: (column: TorrentListColumn) => void;
onCellFocus: () => void;
onWidthsChange: (column: TorrentListColumn, width: number) => void;
}
const TableHeading = observer(
forwardRef<HTMLDivElement, TableHeadingProps>(({onCellClick, onWidthsChange}: TableHeadingProps, ref) => {
const [isPointerDown, setIsPointerDown] = useState<boolean>(false);
forwardRef<HTMLDivElement, TableHeadingProps>(
({onCellClick, onCellFocus, onWidthsChange}: TableHeadingProps, ref) => {
const [isPointerDown, setIsPointerDown] = useState<boolean>(false);
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);
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);
const {i18n} = useLingui();
const {i18n} = useLingui();
const handlePointerMove = (event: PointerEvent) => {
let widthDelta = 0;
if (lastPointerX.current != null) {
widthDelta = event.clientX - lastPointerX.current;
}
let nextCellWidth = 20;
if (focusedCellWidth.current != null) {
nextCellWidth = focusedCellWidth.current + widthDelta;
}
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)`;
const handlePointerMove = (event: PointerEvent) => {
let widthDelta = 0;
if (lastPointerX.current != null) {
widthDelta = event.clientX - lastPointerX.current;
}
}
};
const handlePointerUp = () => {
UIStore.removeGlobalStyle(pointerDownStyles);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointermove', handlePointerMove);
let nextCellWidth = 20;
if (focusedCellWidth.current != null) {
nextCellWidth = focusedCellWidth.current + widthDelta;
}
setIsPointerDown(false);
lastPointerX.current = undefined;
if (resizeLine.current != null) {
resizeLine.current.style.opacity = '0';
}
if (focusedCell.current != null && focusedCellWidth.current != null) {
onWidthsChange(focusedCell.current, focusedCellWidth.current);
}
focusedCell.current = undefined;
focusedCellWidth.current = undefined;
};
return (
<div className="table__row table__row--heading" role="rowheader" ref={tableHeading}>
{SettingStore.floodSettings.torrentListColumns.reduce((accumulator: ReactNodeArray, {id, visible}) => {
if (!visible) {
return accumulator;
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)`;
}
}
};
const labelID = TorrentListColumns[id];
if (labelID == null) {
return accumulator;
}
const handlePointerUp = () => {
UIStore.removeGlobalStyle(pointerDownStyles);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointermove', handlePointerMove);
let handle = null;
const width = SettingStore.floodSettings.torrentListColumnWidths[id] || 100;
setIsPointerDown(false);
lastPointerX.current = undefined;
if (!isPointerDown) {
handle = (
<span
className="table__heading__handle"
onPointerDown={(event) => {
if (!isPointerDown && resizeLine.current != null && tableHeading.current != null) {
setIsPointerDown(true);
if (resizeLine.current != null) {
resizeLine.current.style.opacity = '0';
}
focusedCell.current = id;
focusedCellWidth.current = width;
lastPointerX.current = event.clientX;
if (focusedCell.current != null && focusedCellWidth.current != null) {
onWidthsChange(focusedCell.current, focusedCellWidth.current);
}
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointermove', handlePointerMove);
UIStore.addGlobalStyle(pointerDownStyles);
focusedCell.current = undefined;
focusedCellWidth.current = undefined;
};
resizeLine.current.style.transform = `translate(${Math.max(0, event.clientX)}px, ${
tableHeading.current.getBoundingClientRect().top
}px)`;
resizeLine.current.style.opacity = '1';
}
return (
<div className="table__row table__row--heading" role="rowheader" ref={tableHeading}>
{SettingStore.floodSettings.torrentListColumns.reduce((accumulator: ReactNodeArray, {id, visible}) => {
if (!visible) {
return accumulator;
}
const labelID = TorrentListColumns[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(
<button
className={classes}
css={{
textAlign: 'left',
':focus': {
outline: 'none',
WebkitTapHighlightColor: 'transparent',
},
}}
/>
type="button"
key={id}
onClick={() => onCellClick(id)}
onFocus={() => onCellFocus()}
style={{
width: `${width}px`,
}}>
<span className="table__heading__label" title={i18n._(labelID)}>
<Trans id={labelID} />
</span>
{handle}
</button>,
);
}
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(
<button
className={classes}
css={{
textAlign: 'left',
':focus': {
outline: 'none',
WebkitTapHighlightColor: 'transparent',
},
}}
type="button"
key={id}
onClick={() => onCellClick(id)}
style={{
width: `${width}px`,
}}>
<span className="table__heading__label" title={i18n._(labelID)}>
<Trans id={labelID} />
</span>
{handle}
</button>,
);
return accumulator;
}, [])}
<div className="table__cell table__heading table__heading--fill" />
<div className="table__heading__resize-line" ref={resizeLine} />
</div>
);
}),
return accumulator;
}, [])}
<div className="table__cell table__heading table__heading--fill" />
<div className="table__heading__resize-line" ref={resizeLine} />
</div>
);
},
),
);
export default TableHeading;
@@ -86,6 +86,11 @@ const TorrentList: FC = observer(() => {
if (isCondensed) {
torrentListHeading = (
<TableHeading
onCellFocus={() => {
if (listViewportOuterRef.current != null && listHeaderRef.current != null) {
listViewportOuterRef.current.scrollLeft = listHeaderRef.current.scrollLeft;
}
}}
onCellClick={(property: TorrentListColumn) => {
const currentSort = SettingStore.floodSettings.sortTorrents;
@@ -101,10 +106,6 @@ const TorrentList: FC = observer(() => {
};
SettingActions.saveSetting('sortTorrents', sortBy);
if (listViewportOuterRef.current != null && listHeaderRef.current != null) {
listViewportOuterRef.current.scrollLeft = listHeaderRef.current.scrollLeft;
}
}}
onWidthsChange={(column: TorrentListColumn, width: number) => {
const {torrentListColumnWidths = defaultFloodSettings.torrentListColumnWidths} = SettingStore.floodSettings;