Skip to content

Commit

Permalink
fix: Fix virtualizer drag and drop with layout gaps
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Mar 7, 2025
1 parent b4695c8 commit 426b740
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 13 deletions.
42 changes: 41 additions & 1 deletion packages/@react-stately/layout/src/GridLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
// Compute the number of rows and columns needed to display the content
let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
let numColumns = Math.max(1, Math.min(maxColumns, columns));
this.numColumns = numColumns;

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

let key = this.virtualizer!.keyAtPoint(new Point(x, y));
// Find the closest item within on either side of the point using the gap width.
let key: Key | null = null;
if (this.numColumns === 1) {
let searchRect = new Rect(x, Math.max(0, y - this.gap.height), 1, this.gap.height * 2);
let candidates = this.getVisibleLayoutInfos(searchRect);
let minDistance = Infinity;
for (let candidate of candidates) {
// Ignore items outside the search rect, e.g. persisted keys.
if (!candidate.rect.intersects(searchRect)) {
continue;
}

let yDist = Math.abs(candidate.rect.y - x);
let maxYDist = Math.abs(candidate.rect.maxY - x);
let dist = Math.min(yDist, maxYDist);
if (dist < minDistance) {
minDistance = dist;
key = candidate.key;
}
}
} else {
let searchRect = new Rect(Math.max(0, x - this.gap.width), y, this.gap.width * 2, 1);
let candidates = this.getVisibleLayoutInfos(searchRect);
let minDistance = Infinity;
for (let candidate of candidates) {
// Ignore items outside the search rect, e.g. persisted keys.
if (!candidate.rect.intersects(searchRect)) {
continue;
}

let xDist = Math.abs(candidate.rect.x - x);
let maxXDist = Math.abs(candidate.rect.maxX - x);
let dist = Math.min(xDist, maxXDist);
if (dist < minDistance) {
minDistance = dist;
key = candidate.key;
}
}
}

let layoutInfo = key != null ? this.getLayoutInfo(key) : null;
if (!layoutInfo) {
return {type: 'root'};
Expand Down
21 changes: 20 additions & 1 deletion packages/@react-stately/layout/src/ListLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,26 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
x += this.virtualizer!.visibleRect.x;
y += this.virtualizer!.visibleRect.y;

let key = this.virtualizer!.keyAtPoint(new Point(x, y));
// Find the closest item within on either side of the point using the gap width.
let searchRect = new Rect(x, Math.max(0, y - this.gap), 1, this.gap * 2);
let candidates = this.getVisibleLayoutInfos(searchRect);
let key: Key | null = null;
let minDistance = Infinity;
for (let candidate of candidates) {
// Ignore items outside the search rect, e.g. persisted keys.
if (!candidate.rect.intersects(searchRect)) {
continue;
}

let yDist = Math.abs(candidate.rect.y - x);
let maxYDist = Math.abs(candidate.rect.maxY - x);
let dist = Math.min(yDist, maxYDist);
if (dist < minDistance) {
minDistance = dist;
key = candidate.key;
}
}

if (key == null || this.virtualizer!.collection.size === 0) {
return {type: 'root'};
}
Expand Down
26 changes: 16 additions & 10 deletions packages/@react-stately/layout/src/TableLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,17 +542,23 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
x += this.virtualizer!.visibleRect.x;
y += this.virtualizer!.visibleRect.y;

// Custom variation of this.virtualizer.keyAtPoint that ignores body
// Find the closest item within on either side of the point using the gap width.
let searchRect = new Rect(x, Math.max(0, y - this.gap), 1, this.gap * 2);
let candidates = this.getVisibleLayoutInfos(searchRect);
let key: Key | null = null;
let point = new Point(x, y);
let rectAtPoint = new Rect(point.x, point.y, 1, 1);
let layoutInfos = this.virtualizer!.layout.getVisibleLayoutInfos(rectAtPoint).filter(info => info.type === 'row');

// Layout may return multiple layout infos in the case of
// persisted keys, so find the first one that actually intersects.
for (let layoutInfo of layoutInfos) {
if (layoutInfo.rect.intersects(rectAtPoint)) {
key = layoutInfo.key;
let minDistance = Infinity;
for (let candidate of candidates) {
// Ignore items outside the search rect, e.g. persisted keys.
if (candidate.type !== 'row' || !candidate.rect.intersects(searchRect)) {
continue;
}

let yDist = Math.abs(candidate.rect.y - x);
let maxYDist = Math.abs(candidate.rect.maxY - x);
let dist = Math.min(yDist, maxYDist);
if (dist < minDistance) {
minDistance = dist;
key = candidate.key;
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria-components/stories/ListBox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ export function VirtualizedListBoxDnd() {
<Virtualizer
layout={ListLayout}
layoutOptions={{
rowHeight: 25
rowHeight: 25,
gap: 8
}}>
<ListBox
className={styles.menu}
Expand Down

0 comments on commit 426b740

Please sign in to comment.