Skip to content

Commit e36312f

Browse files
authored
feat(web): improve torrent selection UX with unified click and escape behavior (#782)
1 parent e4c0556 commit e36312f

4 files changed

Lines changed: 51 additions & 39 deletions

File tree

web/src/components/torrents/TorrentDetailsPanel.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,23 +86,8 @@ export const TorrentDetailsPanel = memo(function TorrentDetailsPanel({ instanceI
8686
}
8787
}, [initialTab, onInitialTabConsumed, setActiveTab])
8888

89-
// Escape key handler to close the panel
90-
useEffect(() => {
91-
if (!onClose) return
92-
93-
const handleKeyDown = (e: KeyboardEvent) => {
94-
if (e.key === "Escape" && !e.defaultPrevented) {
95-
// Don't close if a dialog/modal is open
96-
const hasOpenDialog = document.querySelector("[role=\"dialog\"]")
97-
if (!hasOpenDialog) {
98-
onClose()
99-
}
100-
}
101-
}
102-
103-
window.addEventListener("keydown", handleKeyDown)
104-
return () => window.removeEventListener("keydown", handleKeyDown)
105-
}, [onClose])
89+
// Note: Escape key handling is now unified in Torrents.tsx
90+
// to close panel and clear selection atomically
10691

10792
const [showAddPeersDialog, setShowAddPeersDialog] = useState(false)
10893
const { formatTimestamp } = useDateTimeFormatters()

web/src/components/torrents/TorrentTableOptimized.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,12 +1912,7 @@ export const TorrentTableOptimized = memo(function TorrentTableOptimized({
19121912
}
19131913
}, [filters, effectiveSearch, instanceId, virtualizer, sortedTorrents.length, lastUserAction, resetSelectionState])
19141914

1915-
// Clear selection handler for keyboard navigation
1916-
const clearSelection = useCallback(() => {
1917-
resetSelectionState()
1918-
}, [resetSelectionState])
1919-
1920-
// Set up keyboard navigation with selection clearing
1915+
// Set up keyboard navigation (PageUp/Down, Home/End)
19211916
useKeyboardNavigation({
19221917
parentRef,
19231918
virtualizer,
@@ -1926,8 +1921,6 @@ export const TorrentTableOptimized = memo(function TorrentTableOptimized({
19261921
isLoadingMore,
19271922
loadMore,
19281923
estimatedRowHeight,
1929-
onClearSelection: clearSelection,
1930-
hasSelection: isAllSelected || selectedRowIds.length > 0,
19311924
})
19321925

19331926
// Apply Ctrl/Cmd+A shortcut to select all torrents
@@ -2609,6 +2602,17 @@ export const TorrentTableOptimized = memo(function TorrentTableOptimized({
26092602
handleRowSelection(torrent.hash, !isRowSelected, row.id)
26102603
lastSelectedIndexRef.current = currentIndex
26112604
} else {
2605+
// Plain click - open details panel
2606+
// If row is already selected, keep selection intact
2607+
// Otherwise, select only this torrent (replace selection)
2608+
if (!isRowSelected) {
2609+
const allRows = table.getRowModel().rows
2610+
const currentIndex = allRows.findIndex(r => r.id === row.id)
2611+
setIsAllSelected(false)
2612+
setExcludedFromSelectAll(new Set())
2613+
setRowSelection({ [row.id]: true })
2614+
lastSelectedIndexRef.current = currentIndex
2615+
}
26122616
onTorrentSelect?.(torrent)
26132617
}
26142618
}}
@@ -2722,7 +2726,17 @@ export const TorrentTableOptimized = memo(function TorrentTableOptimized({
27222726
handleRowSelection(torrent.hash, !isRowSelected, row.id)
27232727
lastSelectedIndexRef.current = currentIndex
27242728
} else {
2725-
// Plain click - open details without changing checkbox selection state
2729+
// Plain click - open details panel
2730+
// If row is already selected, keep selection intact
2731+
// Otherwise, select only this torrent (replace selection)
2732+
if (!isRowSelected) {
2733+
const allRows = table.getRowModel().rows
2734+
const currentIndex = allRows.findIndex(r => r.id === row.id)
2735+
setIsAllSelected(false)
2736+
setExcludedFromSelectAll(new Set())
2737+
setRowSelection({ [row.id]: true })
2738+
lastSelectedIndexRef.current = currentIndex
2739+
}
27262740
onTorrentSelect?.(torrent)
27272741
}
27282742
}

web/src/hooks/useKeyboardNavigation.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ interface UseKeyboardNavigationProps {
1717
isLoadingMore: boolean
1818
loadMore: () => void
1919
estimatedRowHeight?: number
20-
// Selection handling props (optional)
21-
onClearSelection?: () => void
22-
hasSelection?: boolean
2320
}
2421

2522
export function useKeyboardNavigation({
@@ -30,8 +27,6 @@ export function useKeyboardNavigation({
3027
isLoadingMore,
3128
loadMore,
3229
estimatedRowHeight = 40,
33-
onClearSelection,
34-
hasSelection = false,
3530
}: UseKeyboardNavigationProps) {
3631

3732
// Set up keyboard event listeners
@@ -54,12 +49,8 @@ export function useKeyboardNavigation({
5449

5550
if (!container) return
5651

57-
// Handle Escape key for clearing selection
58-
if (key === "Escape" && onClearSelection && hasSelection) {
59-
event.preventDefault()
60-
onClearSelection()
61-
return
62-
}
52+
// Note: Escape key handling is now unified in Torrents.tsx
53+
// to close panel and clear selection atomically
6354

6455
// Only handle standard navigation keys
6556
const navigationKeys = ["PageUp", "PageDown", "Home", "End"]
@@ -157,5 +148,5 @@ export function useKeyboardNavigation({
157148
return () => {
158149
window.removeEventListener("keydown", handleKeyDown)
159150
}
160-
}, [virtualizer, safeLoadedRows, hasLoadedAll, isLoadingMore, loadMore, estimatedRowHeight, parentRef, onClearSelection, hasSelection])
161-
}
151+
}, [virtualizer, safeLoadedRows, hasLoadedAll, isLoadingMore, loadMore, estimatedRowHeight, parentRef])
152+
}

web/src/pages/Torrents.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
1212
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
1313
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"
1414
import { VisuallyHidden } from "@/components/ui/visually-hidden"
15+
import { useTorrentSelection } from "@/contexts/TorrentSelectionContext"
1516
import { usePersistedCompactViewState } from "@/hooks/usePersistedCompactViewState"
1617
import { usePersistedFilters } from "@/hooks/usePersistedFilters"
1718
import { usePersistedFilterSidebarState } from "@/hooks/usePersistedFilterSidebarState"
@@ -31,6 +32,7 @@ export function Torrents({ instanceId, search, onSearchChange }: TorrentsProps)
3132
const [filters, setFilters] = usePersistedFilters(instanceId)
3233
const [filterSidebarCollapsed] = usePersistedFilterSidebarState(false)
3334
const { viewMode } = usePersistedCompactViewState("normal")
35+
const { clearSelection } = useTorrentSelection()
3436

3537
// Sidebar width: 320px normal, 260px dense
3638
const sidebarWidth = viewMode === "dense" ? "16.25rem" : "20rem"
@@ -220,6 +222,26 @@ export function Torrents({ instanceId, search, onSearchChange }: TorrentsProps)
220222
}
221223
}, [selectedTorrent, isMobile])
222224

225+
// Unified Escape handler: close panel and clear selection atomically
226+
useEffect(() => {
227+
const handleEscape = (e: KeyboardEvent) => {
228+
if (e.key !== "Escape") return
229+
if (e.defaultPrevented) return
230+
231+
// Skip if a dialog is open (dialogs handle their own Escape)
232+
if (document.querySelector("[role=\"dialog\"]")) return
233+
234+
e.preventDefault()
235+
236+
// Close panel and clear selection in one action
237+
setSelectedTorrent(null)
238+
clearSelection()
239+
}
240+
241+
window.addEventListener("keydown", handleEscape)
242+
return () => window.removeEventListener("keydown", handleEscape)
243+
}, [clearSelection])
244+
223245
// Close the mobile filters sheet when viewport switches to desktop layout
224246
useEffect(() => {
225247
const mediaQuery = window.matchMedia("(min-width: 768px)")

0 commit comments

Comments
 (0)