Skip to content

Commit 426b740

Browse files
committed
fix: Fix virtualizer drag and drop with layout gaps
1 parent b4695c8 commit 426b740

File tree

4 files changed

+79
-13
lines changed

4 files changed

+79
-13
lines changed

packages/@react-stately/layout/src/GridLayout.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
102102
// Compute the number of rows and columns needed to display the content
103103
let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
104104
let numColumns = Math.max(1, Math.min(maxColumns, columns));
105+
this.numColumns = numColumns;
105106

106107
// Compute the available width (minus the space between items)
107108
let width = visibleWidth - (minSpace.width * Math.max(0, numColumns));
@@ -223,7 +224,46 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
223224
x += this.virtualizer!.visibleRect.x;
224225
y += this.virtualizer!.visibleRect.y;
225226

226-
let key = this.virtualizer!.keyAtPoint(new Point(x, y));
227+
// Find the closest item within on either side of the point using the gap width.
228+
let key: Key | null = null;
229+
if (this.numColumns === 1) {
230+
let searchRect = new Rect(x, Math.max(0, y - this.gap.height), 1, this.gap.height * 2);
231+
let candidates = this.getVisibleLayoutInfos(searchRect);
232+
let minDistance = Infinity;
233+
for (let candidate of candidates) {
234+
// Ignore items outside the search rect, e.g. persisted keys.
235+
if (!candidate.rect.intersects(searchRect)) {
236+
continue;
237+
}
238+
239+
let yDist = Math.abs(candidate.rect.y - x);
240+
let maxYDist = Math.abs(candidate.rect.maxY - x);
241+
let dist = Math.min(yDist, maxYDist);
242+
if (dist < minDistance) {
243+
minDistance = dist;
244+
key = candidate.key;
245+
}
246+
}
247+
} else {
248+
let searchRect = new Rect(Math.max(0, x - this.gap.width), y, this.gap.width * 2, 1);
249+
let candidates = this.getVisibleLayoutInfos(searchRect);
250+
let minDistance = Infinity;
251+
for (let candidate of candidates) {
252+
// Ignore items outside the search rect, e.g. persisted keys.
253+
if (!candidate.rect.intersects(searchRect)) {
254+
continue;
255+
}
256+
257+
let xDist = Math.abs(candidate.rect.x - x);
258+
let maxXDist = Math.abs(candidate.rect.maxX - x);
259+
let dist = Math.min(xDist, maxXDist);
260+
if (dist < minDistance) {
261+
minDistance = dist;
262+
key = candidate.key;
263+
}
264+
}
265+
}
266+
227267
let layoutInfo = key != null ? this.getLayoutInfo(key) : null;
228268
if (!layoutInfo) {
229269
return {type: 'root'};

packages/@react-stately/layout/src/ListLayout.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,26 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
505505
x += this.virtualizer!.visibleRect.x;
506506
y += this.virtualizer!.visibleRect.y;
507507

508-
let key = this.virtualizer!.keyAtPoint(new Point(x, y));
508+
// Find the closest item within on either side of the point using the gap width.
509+
let searchRect = new Rect(x, Math.max(0, y - this.gap), 1, this.gap * 2);
510+
let candidates = this.getVisibleLayoutInfos(searchRect);
511+
let key: Key | null = null;
512+
let minDistance = Infinity;
513+
for (let candidate of candidates) {
514+
// Ignore items outside the search rect, e.g. persisted keys.
515+
if (!candidate.rect.intersects(searchRect)) {
516+
continue;
517+
}
518+
519+
let yDist = Math.abs(candidate.rect.y - x);
520+
let maxYDist = Math.abs(candidate.rect.maxY - x);
521+
let dist = Math.min(yDist, maxYDist);
522+
if (dist < minDistance) {
523+
minDistance = dist;
524+
key = candidate.key;
525+
}
526+
}
527+
509528
if (key == null || this.virtualizer!.collection.size === 0) {
510529
return {type: 'root'};
511530
}

packages/@react-stately/layout/src/TableLayout.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -542,17 +542,23 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
542542
x += this.virtualizer!.visibleRect.x;
543543
y += this.virtualizer!.visibleRect.y;
544544

545-
// Custom variation of this.virtualizer.keyAtPoint that ignores body
545+
// Find the closest item within on either side of the point using the gap width.
546+
let searchRect = new Rect(x, Math.max(0, y - this.gap), 1, this.gap * 2);
547+
let candidates = this.getVisibleLayoutInfos(searchRect);
546548
let key: Key | null = null;
547-
let point = new Point(x, y);
548-
let rectAtPoint = new Rect(point.x, point.y, 1, 1);
549-
let layoutInfos = this.virtualizer!.layout.getVisibleLayoutInfos(rectAtPoint).filter(info => info.type === 'row');
550-
551-
// Layout may return multiple layout infos in the case of
552-
// persisted keys, so find the first one that actually intersects.
553-
for (let layoutInfo of layoutInfos) {
554-
if (layoutInfo.rect.intersects(rectAtPoint)) {
555-
key = layoutInfo.key;
549+
let minDistance = Infinity;
550+
for (let candidate of candidates) {
551+
// Ignore items outside the search rect, e.g. persisted keys.
552+
if (candidate.type !== 'row' || !candidate.rect.intersects(searchRect)) {
553+
continue;
554+
}
555+
556+
let yDist = Math.abs(candidate.rect.y - x);
557+
let maxYDist = Math.abs(candidate.rect.maxY - x);
558+
let dist = Math.min(yDist, maxYDist);
559+
if (dist < minDistance) {
560+
minDistance = dist;
561+
key = candidate.key;
556562
}
557563
}
558564

packages/react-aria-components/stories/ListBox.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ export function VirtualizedListBoxDnd() {
317317
<Virtualizer
318318
layout={ListLayout}
319319
layoutOptions={{
320-
rowHeight: 25
320+
rowHeight: 25,
321+
gap: 8
321322
}}>
322323
<ListBox
323324
className={styles.menu}

0 commit comments

Comments
 (0)