Skip to content

Commit 843bd70

Browse files
doublefacedoubleface
authored andcommitted
fix: Keep outside of screen selection on mouse wheel movements
1 parent 3224a62 commit 843bd70

1 file changed

Lines changed: 54 additions & 18 deletions

File tree

src/modules/selection/RectangularSelection.jsx

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,16 @@ const getSelectedFileIdsFromSelectoEvent = (e, getFileFromElement) => {
5353
const accumulateSelectedItemsDuringDrag = (
5454
selectedDuringDragRef,
5555
selectedFileIds,
56-
visibleFileIds
56+
visibleFileIds,
57+
preserveAll
5758
) => {
5859
const newAccumulated = new Set()
5960
for (const fileId of selectedDuringDragRef.current) {
60-
if (!visibleFileIds.has(fileId) || selectedFileIds.has(fileId)) {
61+
if (
62+
preserveAll ||
63+
!visibleFileIds.has(fileId) ||
64+
selectedFileIds.has(fileId)
65+
) {
6166
newAccumulated.add(fileId)
6267
}
6368
}
@@ -91,8 +96,10 @@ const RectangularSelection = ({
9196
const [isContainerReady, setIsContainerReady] = useState(false)
9297
const { setSelectedItems, selectedItems, setIsSelectAll } =
9398
useSelectionContext()
99+
const [resolvedScrollContainer, setResolvedScrollContainer] = useState(null)
94100
const isDraggingRef = useRef(false)
95101
const dragStartPosRef = useRef(null)
102+
const wheelScrolledDuringDragRef = useRef(false)
96103
const mutationObserverRef = useRef(null)
97104
const selectedDuringDragRef = useRef(new Set())
98105

@@ -108,6 +115,12 @@ const RectangularSelection = ({
108115
}
109116
}, [])
110117

118+
useEffect(() => {
119+
setResolvedScrollContainer(
120+
scrollElement || scrollContainerRef?.current || null
121+
)
122+
}, [scrollElement, scrollContainerRef, isContainerReady])
123+
111124
/**
112125
* Extracts file data from a DOM element using the data-file-id attribute.
113126
* Uses a Map for O(1) lookups instead of O(n) array.find().
@@ -149,10 +162,14 @@ const RectangularSelection = ({
149162
e,
150163
getFileFromElement
151164
)
165+
// After a wheel scroll, items may still be in the DOM but outside
166+
// the selection rectangle (content shifted, not rectangle shrunk).
167+
// In that case, preserve all accumulated items to avoid losing them.
152168
const newAccumulated = accumulateSelectedItemsDuringDrag(
153169
selectedDuringDragRef,
154170
selectedFileIds,
155-
visibleFileIds
171+
visibleFileIds,
172+
wheelScrolledDuringDragRef.current
156173
)
157174
selectedDuringDragRef.current = newAccumulated
158175

@@ -248,6 +265,7 @@ const RectangularSelection = ({
248265
*/
249266
const handleDragEnd = useCallback(() => {
250267
dragStartPosRef.current = null
268+
wheelScrolledDuringDragRef.current = false
251269
selectedDuringDragRef.current.clear()
252270
}, [])
253271

@@ -258,8 +276,7 @@ const RectangularSelection = ({
258276
* re-discover selectable targets so newly rendered elements can be selected.
259277
*/
260278
useEffect(() => {
261-
const container = scrollElement || scrollContainerRef?.current
262-
if (!container) return
279+
if (!resolvedScrollContainer) return
263280

264281
if (mutationObserverRef.current) {
265282
mutationObserverRef.current.disconnect()
@@ -271,11 +288,36 @@ const RectangularSelection = ({
271288
}
272289
})
273290

274-
observer.observe(container, { childList: true, subtree: true })
291+
observer.observe(resolvedScrollContainer, {
292+
childList: true,
293+
subtree: true
294+
})
275295
mutationObserverRef.current = observer
276296

277297
return () => observer.disconnect()
278-
}, [scrollElement, scrollContainerRef])
298+
}, [resolvedScrollContainer])
299+
300+
/**
301+
* Listens for mouse wheel scroll during a drag selection.
302+
* Marks that a wheel scroll occurred so the accumulator preserves
303+
* all previously selected items instead of dropping those that
304+
* are still in the DOM but scrolled out of the selection rectangle.
305+
*/
306+
useEffect(() => {
307+
if (!resolvedScrollContainer) return
308+
309+
const handleWheel = () => {
310+
if (!isDraggingRef.current) return
311+
wheelScrolledDuringDragRef.current = true
312+
}
313+
314+
resolvedScrollContainer.addEventListener('wheel', handleWheel, {
315+
passive: true
316+
})
317+
318+
return () =>
319+
resolvedScrollContainer.removeEventListener('wheel', handleWheel)
320+
}, [resolvedScrollContainer])
279321

280322
/**
281323
* Handles scroll events from react-selecto during drag selection.
@@ -290,15 +332,14 @@ const RectangularSelection = ({
290332
*/
291333
const handleScroll = useCallback(
292334
e => {
293-
const container = scrollElement || scrollContainerRef?.current
294-
if (!container) return
335+
if (!resolvedScrollContainer) return
295336

296-
container.scrollBy(
337+
resolvedScrollContainer.scrollBy(
297338
e.direction[0] * SCROLL_STEP_IN_PIXELS,
298339
e.direction[1] * SCROLL_STEP_IN_PIXELS
299340
)
300341
},
301-
[scrollElement, scrollContainerRef]
342+
[resolvedScrollContainer]
302343
)
303344

304345
/**
@@ -335,11 +376,6 @@ const RectangularSelection = ({
335376
[setSelectedItems, setIsSelectAll]
336377
)
337378

338-
// Use the directly provided scrollElement (from virtuoso's scrollerRef),
339-
// or fall back to scrollContainerRef.current for non-virtualized containers
340-
// eslint-disable-next-line react-hooks/refs
341-
const scrollContainer = scrollElement || scrollContainerRef?.current
342-
343379
return (
344380
<div
345381
ref={containerRef}
@@ -368,9 +404,9 @@ const RectangularSelection = ({
368404
onSelect={handleSelect}
369405
onScroll={handleScroll}
370406
scrollOptions={
371-
scrollContainer
407+
resolvedScrollContainer
372408
? {
373-
container: scrollContainer,
409+
container: resolvedScrollContainer,
374410
throttleTime: 30,
375411
threshold: 30
376412
}

0 commit comments

Comments
 (0)