@react-stately/layout 4.2.1 → 4.3.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/dist/GridLayout.main.js +18 -5
- package/dist/GridLayout.main.js.map +1 -1
- package/dist/GridLayout.mjs +18 -5
- package/dist/GridLayout.module.js +18 -5
- package/dist/GridLayout.module.js.map +1 -1
- package/dist/ListLayout.main.js +25 -6
- package/dist/ListLayout.main.js.map +1 -1
- package/dist/ListLayout.mjs +25 -6
- package/dist/ListLayout.module.js +25 -6
- package/dist/ListLayout.module.js.map +1 -1
- package/dist/TableLayout.main.js +26 -4
- package/dist/TableLayout.main.js.map +1 -1
- package/dist/TableLayout.mjs +27 -5
- package/dist/TableLayout.module.js +27 -5
- package/dist/TableLayout.module.js.map +1 -1
- package/dist/WaterfallLayout.main.js +30 -18
- package/dist/WaterfallLayout.main.js.map +1 -1
- package/dist/WaterfallLayout.mjs +30 -18
- package/dist/WaterfallLayout.module.js +30 -18
- package/dist/WaterfallLayout.module.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/GridLayout.ts +28 -11
- package/src/ListLayout.ts +40 -18
- package/src/TableLayout.ts +36 -6
- package/src/WaterfallLayout.ts +17 -6
package/src/ListLayout.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ListLayoutOptions {
|
|
|
29
29
|
headingHeight?: number,
|
|
30
30
|
/** The estimated height of a section header, when the height is variable. */
|
|
31
31
|
estimatedHeadingHeight?: number,
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
33
|
* The fixed height of a loader element in px. This loader is specifically for
|
|
34
34
|
* "load more" elements rendered when loading more rows at the root level or inside nested row/sections.
|
|
35
35
|
* @default 48
|
|
@@ -115,12 +115,12 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
115
115
|
return this.virtualizer!.collection;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
getLayoutInfo(key: Key) {
|
|
118
|
+
getLayoutInfo(key: Key): LayoutInfo | null {
|
|
119
119
|
this.ensureLayoutInfo(key);
|
|
120
120
|
return this.layoutNodes.get(key)?.layoutInfo || null;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
getVisibleLayoutInfos(rect: Rect) {
|
|
123
|
+
getVisibleLayoutInfos(rect: Rect): LayoutInfo[] {
|
|
124
124
|
// Adjust rect to keep number of visible rows consistent.
|
|
125
125
|
// (only if height > 1 for getDropTargetFromPoint)
|
|
126
126
|
if (rect.height > 1) {
|
|
@@ -151,7 +151,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
151
151
|
return res;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
protected layoutIfNeeded(rect: Rect) {
|
|
154
|
+
protected layoutIfNeeded(rect: Rect): void {
|
|
155
155
|
if (!this.lastCollection) {
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
@@ -160,7 +160,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
160
160
|
this.requestedRect = this.requestedRect.union(rect);
|
|
161
161
|
this.rootNodes = this.buildCollection();
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
// Ensure all of the persisted keys are available.
|
|
165
165
|
for (let key of this.virtualizer!.persistedKeys) {
|
|
166
166
|
if (this.ensureLayoutInfo(key)) {
|
|
@@ -183,8 +183,8 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
183
183
|
return false;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
protected isVisible(node: LayoutNode, rect: Rect) {
|
|
187
|
-
return node.layoutInfo.rect.intersects(rect) || node.layoutInfo.isSticky || node.layoutInfo.type === 'header' || this.virtualizer!.isPersistedKey(node.layoutInfo.key);
|
|
186
|
+
protected isVisible(node: LayoutNode, rect: Rect): boolean {
|
|
187
|
+
return node.layoutInfo.rect.intersects(rect) || node.layoutInfo.isSticky || node.layoutInfo.type === 'header' || node.layoutInfo.type === 'loader' || this.virtualizer!.isPersistedKey(node.layoutInfo.key);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
protected shouldInvalidateEverything(invalidationContext: InvalidationContext<O>): boolean {
|
|
@@ -211,7 +211,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
211
211
|
|| newOptions.padding !== oldOptions.padding;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
update(invalidationContext: InvalidationContext<O>) {
|
|
214
|
+
update(invalidationContext: InvalidationContext<O>): void {
|
|
215
215
|
let collection = this.virtualizer!.collection;
|
|
216
216
|
|
|
217
217
|
// Reset valid rect if we will have to invalidate everything.
|
|
@@ -255,9 +255,13 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
255
255
|
let collection = this.virtualizer!.collection;
|
|
256
256
|
let skipped = 0;
|
|
257
257
|
let nodes: LayoutNode[] = [];
|
|
258
|
+
let isEmptyOrLoading = collection?.size === 0 || (collection.size === 1 && collection.getItem(collection.getFirstKey()!)!.type === 'loader');
|
|
259
|
+
if (isEmptyOrLoading) {
|
|
260
|
+
y = 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
258
263
|
for (let node of collection) {
|
|
259
264
|
let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;
|
|
260
|
-
|
|
261
265
|
// Skip rows before the valid rectangle unless they are already cached.
|
|
262
266
|
if (node.type === 'item' && y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) {
|
|
263
267
|
y += rowHeight;
|
|
@@ -268,24 +272,37 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
268
272
|
let layoutNode = this.buildChild(node, this.padding, y, null);
|
|
269
273
|
y = layoutNode.layoutInfo.rect.maxY + this.gap;
|
|
270
274
|
nodes.push(layoutNode);
|
|
271
|
-
|
|
272
275
|
if (node.type === 'item' && y > this.requestedRect.maxY) {
|
|
273
|
-
|
|
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;
|
|
283
|
+
|
|
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);
|
|
288
|
+
nodes.push(loader);
|
|
289
|
+
y = loader.layoutInfo.rect.maxY;
|
|
290
|
+
}
|
|
274
291
|
break;
|
|
275
292
|
}
|
|
276
293
|
}
|
|
277
294
|
|
|
278
295
|
y -= this.gap;
|
|
279
|
-
y += this.padding;
|
|
296
|
+
y += isEmptyOrLoading ? 0 : this.padding;
|
|
280
297
|
this.contentSize = new Size(this.virtualizer!.visibleRect.width, y);
|
|
281
298
|
return nodes;
|
|
282
299
|
}
|
|
283
300
|
|
|
284
|
-
protected isValid(node: Node<T>, y: number) {
|
|
301
|
+
protected isValid(node: Node<T>, y: number): boolean {
|
|
285
302
|
let cached = this.layoutNodes.get(node.key);
|
|
286
303
|
return (
|
|
287
304
|
!this.invalidateEverything &&
|
|
288
|
-
cached &&
|
|
305
|
+
!!cached &&
|
|
289
306
|
cached.node === node &&
|
|
290
307
|
y === cached.layoutInfo.rect.y &&
|
|
291
308
|
cached.layoutInfo.rect.intersects(this.validRect) &&
|
|
@@ -301,6 +318,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
301
318
|
let layoutNode = this.buildNode(node, x, y);
|
|
302
319
|
|
|
303
320
|
layoutNode.layoutInfo.parentKey = parentKey ?? null;
|
|
321
|
+
layoutNode.layoutInfo.allowOverflow = true;
|
|
304
322
|
this.layoutNodes.set(node.key, layoutNode);
|
|
305
323
|
return layoutNode;
|
|
306
324
|
}
|
|
@@ -315,6 +333,8 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
315
333
|
return this.buildSectionHeader(node, x, y);
|
|
316
334
|
case 'loader':
|
|
317
335
|
return this.buildLoader(node, x, y);
|
|
336
|
+
case 'separator':
|
|
337
|
+
return this.buildItem(node, x, y);
|
|
318
338
|
default:
|
|
319
339
|
throw new Error('Unsupported node type: ' + node.type);
|
|
320
340
|
}
|
|
@@ -324,7 +344,9 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
324
344
|
let rect = new Rect(x, y, this.padding, 0);
|
|
325
345
|
let layoutInfo = new LayoutInfo('loader', node.key, rect);
|
|
326
346
|
rect.width = this.virtualizer!.contentSize.width - this.padding - x;
|
|
327
|
-
|
|
347
|
+
// Note that if the user provides isLoading to their sentinel during a case where they only want to render the emptyState, this will reserve
|
|
348
|
+
// room for the loader alongside rendering the emptyState
|
|
349
|
+
rect.height = node.props.isLoading ? this.loaderHeight ?? this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT : 0;
|
|
328
350
|
|
|
329
351
|
return {
|
|
330
352
|
layoutInfo,
|
|
@@ -446,7 +468,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
446
468
|
};
|
|
447
469
|
}
|
|
448
470
|
|
|
449
|
-
updateItemSize(key: Key, size: Size) {
|
|
471
|
+
updateItemSize(key: Key, size: Size): boolean {
|
|
450
472
|
let layoutNode = this.layoutNodes.get(key);
|
|
451
473
|
// If no layoutInfo, item has been deleted/removed.
|
|
452
474
|
if (!layoutNode) {
|
|
@@ -497,7 +519,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
497
519
|
}
|
|
498
520
|
}
|
|
499
521
|
|
|
500
|
-
getContentSize() {
|
|
522
|
+
getContentSize(): Size {
|
|
501
523
|
return this.contentSize;
|
|
502
524
|
}
|
|
503
525
|
|
|
@@ -506,7 +528,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
|
|
|
506
528
|
y += this.virtualizer!.visibleRect.y;
|
|
507
529
|
|
|
508
530
|
// 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);
|
|
531
|
+
let searchRect = new Rect(x, Math.max(0, y - this.gap), 1, Math.max(1, this.gap * 2));
|
|
510
532
|
let candidates = this.getVisibleLayoutInfos(searchRect);
|
|
511
533
|
let key: Key | null = null;
|
|
512
534
|
let minDistance = Infinity;
|
package/src/TableLayout.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {DropTarget, ItemDropTarget, Key} from '@react-types/shared';
|
|
14
|
-
import {getChildNodes} from '@react-stately/collections';
|
|
14
|
+
import {getChildNodes, getLastItem} from '@react-stately/collections';
|
|
15
15
|
import {GridNode} from '@react-types/grid';
|
|
16
16
|
import {InvalidationContext, LayoutInfo, Point, Rect, Size} from '@react-stately/virtualizer';
|
|
17
17
|
import {LayoutNode, ListLayout, ListLayoutOptions} from './ListLayout';
|
|
@@ -85,6 +85,10 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
|
|
|
85
85
|
this.stickyColumnIndices = [];
|
|
86
86
|
|
|
87
87
|
let collection = this.virtualizer!.collection as TableCollection<T>;
|
|
88
|
+
if (collection.head?.key === -1) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
88
92
|
for (let column of collection.columns) {
|
|
89
93
|
// The selection cell and any other sticky columns always need to be visible.
|
|
90
94
|
// In addition, row headers need to be in the DOM for accessibility labeling.
|
|
@@ -251,7 +255,8 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
|
|
|
251
255
|
let width = 0;
|
|
252
256
|
let children: LayoutNode[] = [];
|
|
253
257
|
let rowHeight = this.getEstimatedRowHeight() + this.gap;
|
|
254
|
-
|
|
258
|
+
let childNodes = getChildNodes(collection.body, collection);
|
|
259
|
+
for (let node of childNodes) {
|
|
255
260
|
// Skip rows before the valid rectangle unless they are already cached.
|
|
256
261
|
if (y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) {
|
|
257
262
|
y += rowHeight;
|
|
@@ -267,13 +272,32 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
|
|
|
267
272
|
children.push(layoutNode);
|
|
268
273
|
|
|
269
274
|
if (y > this.requestedRect.maxY) {
|
|
275
|
+
let rowsAfterRect = collection.size - (children.length + skipped);
|
|
276
|
+
let lastNode = getLastItem(childNodes);
|
|
277
|
+
if (lastNode?.type === 'loader') {
|
|
278
|
+
rowsAfterRect--;
|
|
279
|
+
}
|
|
280
|
+
|
|
270
281
|
// Estimate the remaining height for rows that we don't need to layout right now.
|
|
271
|
-
y +=
|
|
282
|
+
y += rowsAfterRect * rowHeight;
|
|
283
|
+
|
|
284
|
+
// Always add the loader sentinel if present. This assumes the loader is the last row in the body,
|
|
285
|
+
// will need to refactor when handling multi section loading
|
|
286
|
+
if (lastNode?.type === 'loader' && children.at(-1)?.layoutInfo.type !== 'loader') {
|
|
287
|
+
let loader = this.buildChild(lastNode, this.padding, y, layoutInfo.key);
|
|
288
|
+
loader.layoutInfo.parentKey = layoutInfo.key;
|
|
289
|
+
loader.index = collection.size;
|
|
290
|
+
width = Math.max(width, loader.layoutInfo.rect.width);
|
|
291
|
+
children.push(loader);
|
|
292
|
+
y = loader.layoutInfo.rect.maxY;
|
|
293
|
+
}
|
|
272
294
|
break;
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
|
|
276
|
-
if
|
|
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');
|
|
300
|
+
if (isEmptyOrLoading) {
|
|
277
301
|
y = this.virtualizer!.visibleRect.maxY;
|
|
278
302
|
} else {
|
|
279
303
|
y -= this.gap;
|
|
@@ -367,7 +391,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
|
|
|
367
391
|
};
|
|
368
392
|
}
|
|
369
393
|
|
|
370
|
-
getVisibleLayoutInfos(rect: Rect) {
|
|
394
|
+
getVisibleLayoutInfos(rect: Rect): LayoutInfo[] {
|
|
371
395
|
// Adjust rect to keep number of visible rows consistent.
|
|
372
396
|
// (only if height > 1 for getDropTargetFromPoint)
|
|
373
397
|
if (rect.height > 1) {
|
|
@@ -442,6 +466,12 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
|
|
|
442
466
|
this.addVisibleLayoutInfos(res, node.children[idx], rect);
|
|
443
467
|
}
|
|
444
468
|
}
|
|
469
|
+
|
|
470
|
+
// Always include loading sentinel even when virtualized, we assume it is always the last child for now
|
|
471
|
+
let lastRow = node.children.at(-1);
|
|
472
|
+
if (lastRow?.layoutInfo.type === 'loader') {
|
|
473
|
+
res.push(lastRow.layoutInfo);
|
|
474
|
+
}
|
|
445
475
|
break;
|
|
446
476
|
}
|
|
447
477
|
case 'headerrow':
|
|
@@ -543,7 +573,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
|
|
|
543
573
|
y += this.virtualizer!.visibleRect.y;
|
|
544
574
|
|
|
545
575
|
// 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);
|
|
576
|
+
let searchRect = new Rect(x, Math.max(0, y - this.gap), 1, Math.max(1, this.gap * 2));
|
|
547
577
|
let candidates = this.getVisibleLayoutInfos(searchRect);
|
|
548
578
|
let key: Key | null = null;
|
|
549
579
|
let minDistance = Infinity;
|
package/src/WaterfallLayout.ts
CHANGED
|
@@ -140,8 +140,9 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
|
|
|
140
140
|
columnHeights[column] += layoutInfo.rect.height + minSpace.height;
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
let collection = this.virtualizer!.collection;
|
|
143
144
|
let skeletonCount = 0;
|
|
144
|
-
for (let node of
|
|
145
|
+
for (let node of collection) {
|
|
145
146
|
if (node.type === 'skeleton') {
|
|
146
147
|
// Add skeleton cards until every column has at least one, and we fill the viewport.
|
|
147
148
|
let startingHeights = [...columnHeights];
|
|
@@ -154,13 +155,23 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
|
|
|
154
155
|
addNode(key, content);
|
|
155
156
|
}
|
|
156
157
|
break;
|
|
157
|
-
} else {
|
|
158
|
+
} else if (node.type !== 'loader') {
|
|
158
159
|
addNode(node.key, node);
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
//
|
|
163
|
-
|
|
163
|
+
// Always add the loader sentinel if present in the collection so we can make sure it is never virtualized out.
|
|
164
|
+
// Add it under the first column for simplicity
|
|
165
|
+
let lastNode = collection.getItem(collection.getLastKey()!);
|
|
166
|
+
if (lastNode?.type === 'loader') {
|
|
167
|
+
let rect = new Rect(horizontalSpacing, columnHeights[0], itemWidth, 0);
|
|
168
|
+
let layoutInfo = new LayoutInfo('loader', lastNode.key, rect);
|
|
169
|
+
newLayoutInfos.set(lastNode.key, layoutInfo);
|
|
170
|
+
}
|
|
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');
|
|
174
|
+
let maxHeight = isEmptyOrLoading ? 0 : Math.max(...columnHeights);
|
|
164
175
|
this.contentSize = new Size(this.virtualizer!.visibleRect.width, maxHeight);
|
|
165
176
|
this.layoutInfos = newLayoutInfos;
|
|
166
177
|
this.numColumns = numColumns;
|
|
@@ -177,14 +188,14 @@ export class WaterfallLayout<T extends object, O extends WaterfallLayoutOptions
|
|
|
177
188
|
getVisibleLayoutInfos(rect: Rect): LayoutInfo[] {
|
|
178
189
|
let layoutInfos: LayoutInfo[] = [];
|
|
179
190
|
for (let layoutInfo of this.layoutInfos.values()) {
|
|
180
|
-
if (layoutInfo.rect.intersects(rect) || this.virtualizer!.isPersistedKey(layoutInfo.key)) {
|
|
191
|
+
if (layoutInfo.rect.intersects(rect) || this.virtualizer!.isPersistedKey(layoutInfo.key) || layoutInfo.type === 'loader') {
|
|
181
192
|
layoutInfos.push(layoutInfo);
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
195
|
return layoutInfos;
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
updateItemSize(key: Key, size: Size) {
|
|
198
|
+
updateItemSize(key: Key, size: Size): boolean {
|
|
188
199
|
let layoutInfo = this.layoutInfos.get(key);
|
|
189
200
|
if (!size || !layoutInfo) {
|
|
190
201
|
return false;
|