@react-stately/layout 4.3.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/GridLayout.ts CHANGED
@@ -120,11 +120,21 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
120
120
  let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
121
121
  this.gap = new Size(horizontalSpacing, minSpace.height);
122
122
 
123
+ // If there is a skeleton loader within the last 2 items in the collection, increment the collection size
124
+ // so that an additional row is added for the skeletons.
123
125
  let collection = this.virtualizer!.collection;
124
- // Make sure to set rows to 0 if we performing a first time load or are rendering the empty state so that Virtualizer
125
- // won't try to render its body
126
- let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
127
- let rows = isEmptyOrLoading ? 0 : Math.ceil(collection.size / numColumns);
126
+ let collectionSize = collection.size;
127
+ let lastKey = collection.getLastKey();
128
+ for (let i = 0; i < 2 && lastKey != null; i++) {
129
+ let item = collection.getItem(lastKey);
130
+ if (item?.type === 'skeleton') {
131
+ collectionSize++;
132
+ break;
133
+ }
134
+ lastKey = collection.getKeyBefore(lastKey);
135
+ }
136
+
137
+ let rows = Math.ceil(collectionSize / numColumns);
128
138
  let iterator = collection[Symbol.iterator]();
129
139
  let y = rows > 0 ? minSpace.height : 0;
130
140
  let newLayoutInfos = new Map();
package/src/ListLayout.ts CHANGED
@@ -251,43 +251,49 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
251
251
  this.validRect = this.requestedRect.copy();
252
252
  }
253
253
 
254
- protected buildCollection(y = this.padding): LayoutNode[] {
254
+ protected buildCollection(y: number = this.padding): LayoutNode[] {
255
255
  let collection = this.virtualizer!.collection;
256
- let skipped = 0;
256
+ let collectionNodes = [...collection];
257
+ let loaderNodes = collectionNodes.filter(node => node.type === 'loader');
257
258
  let nodes: LayoutNode[] = [];
258
- let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
259
+ let isEmptyOrLoading = collection?.size === 0;
259
260
  if (isEmptyOrLoading) {
260
261
  y = 0;
261
262
  }
262
263
 
263
- for (let node of collection) {
264
+ for (let node of collectionNodes) {
264
265
  let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;
265
266
  // Skip rows before the valid rectangle unless they are already cached.
266
267
  if (node.type === 'item' && y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) {
267
268
  y += rowHeight;
268
- skipped++;
269
269
  continue;
270
270
  }
271
271
 
272
272
  let layoutNode = this.buildChild(node, this.padding, y, null);
273
273
  y = layoutNode.layoutInfo.rect.maxY + this.gap;
274
274
  nodes.push(layoutNode);
275
- if (node.type === 'item' && y > this.requestedRect.maxY) {
276
- let itemsAfterRect = collection.size - (nodes.length + skipped);
277
- let lastNode = collection.getItem(collection.getLastKey()!);
278
- if (lastNode?.type === 'loader') {
279
- itemsAfterRect--;
280
- }
281
-
282
- y += itemsAfterRect * rowHeight;
275
+ if (node.type === 'loader') {
276
+ let index = loaderNodes.indexOf(node);
277
+ loaderNodes.splice(index, 1);
278
+ }
283
279
 
284
- // Always add the loader sentinel if present. This assumes the loader is the last option/row
285
- // will need to refactor when handling multi section loading
286
- if (lastNode?.type === 'loader' && nodes.at(-1)?.layoutInfo.type !== 'loader') {
287
- let loader = this.buildChild(lastNode, this.padding, y, null);
280
+ // Build each loader that exists in the collection that is outside the visible rect so that they are persisted
281
+ // at the proper estimated location. If the node.type is "section" then we don't do this shortcut since we have to
282
+ // build the sections to see how tall they are.
283
+ if ((node.type === 'item' || node.type === 'loader') && y > this.requestedRect.maxY) {
284
+ let lastProcessedIndex = collectionNodes.indexOf(node);
285
+ for (let loaderNode of loaderNodes) {
286
+ let loaderNodeIndex = collectionNodes.indexOf(loaderNode);
287
+ // Subtract by an additional 1 since we've already added the current item's height to y
288
+ y += (loaderNodeIndex - lastProcessedIndex - 1) * rowHeight;
289
+ let loader = this.buildChild(loaderNode, this.padding, y, null);
288
290
  nodes.push(loader);
289
291
  y = loader.layoutInfo.rect.maxY;
292
+ lastProcessedIndex = loaderNodeIndex;
290
293
  }
294
+
295
+ // Account for the rest of the items after the last loader spinner, subtract by 1 since we've processed the current node's height already
296
+ y += (collectionNodes.length - lastProcessedIndex - 1) * rowHeight;
291
297
  break;
292
298
  }
293
299
  }
@@ -585,8 +591,24 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
585
591
  let layoutInfo = this.getLayoutInfo(target.key)!;
586
592
  let rect: Rect;
587
593
  if (target.dropPosition === 'before') {
588
- rect = new Rect(layoutInfo.rect.x, layoutInfo.rect.y - this.dropIndicatorThickness / 2, layoutInfo.rect.width, this.dropIndicatorThickness);
594
+ rect = new Rect(layoutInfo.rect.x, Math.max(0, layoutInfo.rect.y - this.dropIndicatorThickness / 2), layoutInfo.rect.width, this.dropIndicatorThickness);
589
595
  } else if (target.dropPosition === 'after') {
596
+ // Render after last visible descendant of the drop target.
597
+ let targetNode = this.collection.getItem(target.key);
598
+ if (targetNode) {
599
+ let targetLevel = targetNode.level ?? 0;
600
+ let currentKey = this.collection.getKeyAfter(target.key);
601
+
602
+ while (currentKey != null) {
603
+ let node = this.collection.getItem(currentKey);
604
+ if (!node || node.level <= targetLevel) {
605
+ break;
606
+ }
607
+
608
+ layoutInfo = this.getLayoutInfo(currentKey) || layoutInfo;
609
+ currentKey = this.collection.getKeyAfter(currentKey);
610
+ }
611
+ }
590
612
  rect = new Rect(layoutInfo.rect.x, layoutInfo.rect.maxY - this.dropIndicatorThickness / 2, layoutInfo.rect.width, this.dropIndicatorThickness);
591
613
  } else {
592
614
  rect = layoutInfo.rect;
@@ -68,9 +68,12 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
68
68
  // If columnWidths were provided via layoutOptions, update those.
69
69
  // Otherwise, calculate column widths ourselves.
70
70
  if (invalidationContext.layoutOptions?.columnWidths) {
71
- if (invalidationContext.layoutOptions.columnWidths !== this.columnWidths) {
72
- this.columnWidths = invalidationContext.layoutOptions.columnWidths;
73
- invalidationContext.sizeChanged = true;
71
+ for (const [key, val] of invalidationContext.layoutOptions.columnWidths) {
72
+ if (this.columnWidths.get(key) !== val) {
73
+ this.columnWidths = invalidationContext.layoutOptions.columnWidths;
74
+ invalidationContext.sizeChanged = true;
75
+ break;
76
+ }
74
77
  }
75
78
  } else if (invalidationContext.sizeChanged || this.columnsChanged(newCollection, this.lastCollection)) {
76
79
  let columnLayout = new TableColumnLayout({});
@@ -274,9 +277,6 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
274
277
  if (y > this.requestedRect.maxY) {
275
278
  let rowsAfterRect = collection.size - (children.length + skipped);
276
279
  let lastNode = getLastItem(childNodes);
277
- if (lastNode?.type === 'loader') {
278
- rowsAfterRect--;
279
- }
280
280
 
281
281
  // Estimate the remaining height for rows that we don't need to layout right now.
282
282
  y += rowsAfterRect * rowHeight;
@@ -296,7 +296,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
296
296
  }
297
297
 
298
298
  // Make sure that the table body gets a height if empty or performing initial load
299
- let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
299
+ let isEmptyOrLoading = collection?.size === 0;
300
300
  if (isEmptyOrLoading) {
301
301
  y = this.virtualizer!.visibleRect.maxY;
302
302
  } else {
@@ -169,8 +169,9 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
169
169
  newLayoutInfos.set(lastNode.key, layoutInfo);
170
170
  }
171
171
 
172
- // Reset all columns to the maximum for the next section. If loading, set to 0 so virtualizer doesn't render its body since there aren't items to render
173
- let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
172
+ // Reset all columns to the maximum for the next section. If loading, set to 0 so virtualizer doesn't render its body since there aren't items to render,
173
+ // except if we are performing skeleton loading
174
+ let isEmptyOrLoading = collection?.size === 0 && collection.getItem(collection.getFirstKey()!)?.type !== 'skeleton';
174
175
  let maxHeight = isEmptyOrLoading ? 0 : Math.max(...columnHeights);
175
176
  this.contentSize = new Size(this.virtualizer!.visibleRect.width, maxHeight);
176
177
  this.layoutInfos = newLayoutInfos;