fix(table): harden queue and row interaction lifecycles

- fence column resize and queue lost-capture handlers to the active pointer

- keep staged bulk actions and RTL submenu keyboard navigation truthful

- add deterministic selection, resize, action-count, and keyboard regressions
This commit is contained in:
NimBold
2026-08-22 01:05:36 +03:30
parent 1fe12b1fea
commit 1672dce803
10 changed files with 216 additions and 35 deletions
+28 -28
View File
@@ -53,7 +53,8 @@ import {
import {
moveSelectedBlockToIndex
} from '../utils/queueOrdering';
import { updateDownloadSelection } from '../utils/downloadSelection';
import { selectContextMenuTarget, updateDownloadSelection } from '../utils/downloadSelection';
import { createColumnResizeSession } from '../utils/columnResize';
import { clampFloatingPosition } from '../utils/floatingPosition';
import { FloatingQueueSubmenu } from './FloatingQueueSubmenu';
import { openPropertiesWindow } from '../propertiesBridge';
@@ -671,33 +672,27 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter, onSummaryC
const startX = event.clientX;
const startWidth = columnWidthsRef.current[index];
const handlePointerMove = (moveEvent: PointerEvent) => {
const nextWidth = Math.max(COLUMN_MINIMUMS[index], startWidth + moveEvent.clientX - startX);
const cleanup = createColumnResizeSession({
windowTarget: window,
documentTarget: document,
body: document.body,
pointerId: event.pointerId,
startX,
startWidth,
minWidth: COLUMN_MINIMUMS[index],
onWidth: nextWidth => {
const nextWidths = columnWidthsRef.current.map((width, columnIndex) =>
columnIndex === index ? nextWidth : width
);
columnWidthsRef.current = nextWidths;
setColumnWidths(nextWidths);
};
const handlePointerUp = () => {
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointercancel', handlePointerUp);
window.removeEventListener('blur', handlePointerUp);
document.removeEventListener('visibilitychange', handlePointerUp);
persistColumnWidths(columnWidthsRef.current);
document.body.classList.remove('is-column-resizing');
resizeCleanupRef.current = null;
};
resizeCleanupRef.current = handlePointerUp;
document.body.classList.add('is-column-resizing');
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointercancel', handlePointerUp);
window.addEventListener('blur', handlePointerUp);
document.addEventListener('visibilitychange', handlePointerUp);
},
onEnd: () => {
persistColumnWidths(columnWidthsRef.current);
resizeCleanupRef.current = null;
},
});
resizeCleanupRef.current = cleanup;
};
const clampMenuPosition = useCallback((x: number, y: number, menuWidth: number, menuHeight: number) => {
@@ -1335,7 +1330,9 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter, onSummaryC
finishQueueDrag(true);
}
};
const lostPointerCapture = () => finishQueueDrag(true);
const lostPointerCapture = (event: Event) => {
if ((event as PointerEvent).pointerId === pointerId) finishQueueDrag(true);
};
const cancel = () => finishQueueDrag(true);
window.addEventListener('pointermove', pointerMove);
window.addEventListener('pointerup', pointerUp);
@@ -1779,10 +1776,13 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter, onSummaryC
}, [clearQueueClickSuppression, handleDownloadDoubleClick]);
const handleContextMenu = useCallback((menu: { x: number; y: number; id: string }) => {
if (!selectedIdsRef.current.has(menu.id)) {
setSelectedIds(new Set([menu.id]));
setLastSelectedId(menu.id);
}
const nextSelection = selectContextMenuTarget({
selectedIds: selectedIdsRef.current,
lastSelectedId: lastSelectedIdRef.current,
targetId: menu.id,
});
setSelectedIds(nextSelection.selectedIds);
setLastSelectedId(nextSelection.lastSelectedId);
setColumnMenu(null);
const position = clampMenuPosition(menu.x, menu.y, 200, 300);
setContextMenuPosition(position);
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ChevronRight } from 'lucide-react';
import type { Queue } from '../store/useDownloadStore';
import { positionFloatingSubmenu, type FloatingSubmenuPosition } from '../utils/floatingPosition';
import { isFloatingSubmenuCloseKey, positionFloatingSubmenu, type FloatingSubmenuPosition } from '../utils/floatingPosition';
interface FloatingQueueSubmenuProps {
label: React.ReactNode;
@@ -167,7 +167,7 @@ export const FloatingQueueSubmenu: React.FC<FloatingQueueSubmenuProps> = ({ labe
}
}}
onKeyDown={event => {
if (event.key !== 'Escape' && event.key !== 'ArrowLeft') return;
if (!isFloatingSubmenuCloseKey(event.key, isRtl)) return;
event.preventDefault();
event.stopPropagation();
closeMenu();