@react-stately/layout 3.13.10-nightly.4674 → 3.13.10-nightly.4683

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/ListLayout.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Collection, DropTarget, DropTargetDelegate, Key, KeyboardDelegate, Node} from '@react-types/shared';
13
+ import {Collection, DropTarget, DropTargetDelegate, Key, Node} from '@react-types/shared';
14
14
  import {getChildNodes} from '@react-stately/collections';
15
15
  import {InvalidationContext, Layout, LayoutInfo, Point, Rect, Size} from '@react-stately/virtualizer';
16
16
 
@@ -22,11 +22,10 @@ export type ListLayoutOptions<T> = {
22
22
  estimatedHeadingHeight?: number,
23
23
  padding?: number,
24
24
  indentationForItem?: (collection: Collection<Node<T>>, key: Key) => number,
25
- collator?: Intl.Collator,
26
25
  loaderHeight?: number,
27
26
  placeholderHeight?: number,
28
- allowDisabledKeyFocus?: boolean,
29
- forceSectionHeaders?: boolean
27
+ forceSectionHeaders?: boolean,
28
+ enableEmptyState?: boolean
30
29
  };
31
30
 
32
31
  // A wrapper around LayoutInfo that supports hierarchy
@@ -39,23 +38,23 @@ export interface LayoutNode {
39
38
  index?: number
40
39
  }
41
40
 
42
- interface ListLayoutProps {
41
+ export interface ListLayoutProps {
43
42
  isLoading?: boolean
44
43
  }
45
44
 
46
45
  const DEFAULT_HEIGHT = 48;
47
46
 
48
47
  /**
49
- * The ListLayout class is an implementation of a collection view {@link Layout}
48
+ * The ListLayout class is an implementation of a virtualizer {@link Layout}
50
49
  * it is used for creating lists and lists with indented sub-lists.
51
50
  *
52
51
  * To configure a ListLayout, you can use the properties to define the
53
52
  * layouts and/or use the method for defining indentation.
54
- * The {@link ListKeyboardDelegate} extends the existing collection view
53
+ * The {@link ListKeyboardDelegate} extends the existing virtualizer
55
54
  * delegate with an additional method to do this (it uses the same delegate object as
56
- * the collection view itself).
55
+ * the virtualizer itself).
57
56
  */
58
- export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements KeyboardDelegate, DropTargetDelegate {
57
+ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements DropTargetDelegate {
59
58
  protected rowHeight: number;
60
59
  protected estimatedRowHeight: number;
61
60
  protected headingHeight: number;
@@ -66,19 +65,19 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
66
65
  protected layoutInfos: Map<Key, LayoutInfo>;
67
66
  protected layoutNodes: Map<Key, LayoutNode>;
68
67
  protected contentSize: Size;
69
- collection: Collection<Node<T>>;
70
- disabledKeys: Set<Key> = new Set();
71
- allowDisabledKeyFocus: boolean = false;
72
- isLoading: boolean;
68
+ protected collection: Collection<Node<T>>;
69
+ protected isLoading: boolean;
73
70
  protected lastWidth: number;
74
71
  protected lastCollection: Collection<Node<T>>;
75
72
  protected rootNodes: LayoutNode[];
76
- protected collator: Intl.Collator;
77
73
  protected invalidateEverything: boolean;
78
74
  protected loaderHeight: number;
79
75
  protected placeholderHeight: number;
80
- protected lastValidRect: Rect;
76
+ protected enableEmptyState: boolean;
77
+ /** The rectangle containing currently valid layout infos. */
81
78
  protected validRect: Rect;
79
+ /** The rectangle of requested layout infos so far. */
80
+ protected requestedRect: Rect;
82
81
 
83
82
  /**
84
83
  * Creates a new ListLayout with options. See the list of properties below for a description
@@ -93,17 +92,16 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
93
92
  this.forceSectionHeaders = options.forceSectionHeaders;
94
93
  this.padding = options.padding || 0;
95
94
  this.indentationForItem = options.indentationForItem;
96
- this.collator = options.collator;
97
95
  this.loaderHeight = options.loaderHeight;
98
96
  this.placeholderHeight = options.placeholderHeight;
97
+ this.enableEmptyState = options.enableEmptyState || false;
99
98
  this.layoutInfos = new Map();
100
99
  this.layoutNodes = new Map();
101
100
  this.rootNodes = [];
102
101
  this.lastWidth = 0;
103
102
  this.lastCollection = null;
104
- this.allowDisabledKeyFocus = options.allowDisabledKeyFocus;
105
- this.lastValidRect = new Rect();
106
103
  this.validRect = new Rect();
104
+ this.requestedRect = new Rect();
107
105
  this.contentSize = new Size();
108
106
  }
109
107
 
@@ -146,14 +144,13 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
146
144
  return res;
147
145
  }
148
146
 
149
- layoutIfNeeded(rect: Rect) {
147
+ protected layoutIfNeeded(rect: Rect) {
150
148
  if (!this.lastCollection) {
151
149
  return;
152
150
  }
153
151
 
154
- if (!this.validRect.containsRect(rect)) {
155
- this.lastValidRect = this.validRect;
156
- this.validRect = this.validRect.union(rect);
152
+ if (!this.requestedRect.containsRect(rect)) {
153
+ this.requestedRect = this.requestedRect.union(rect);
157
154
  this.rootNodes = this.buildCollection();
158
155
  } else {
159
156
  // Ensure all of the persisted keys are available.
@@ -165,22 +162,21 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
165
162
  }
166
163
  }
167
164
 
168
- ensureLayoutInfo(key: Key) {
165
+ private ensureLayoutInfo(key: Key) {
169
166
  // If the layout info wasn't found, it might be outside the bounds of the area that we've
170
167
  // computed layout for so far. This can happen when accessing a random key, e.g pressing Home/End.
171
168
  // Compute the full layout and try again.
172
- if (!this.layoutInfos.has(key) && this.validRect.area < this.contentSize.area && this.lastCollection) {
173
- this.lastValidRect = this.validRect;
174
- this.validRect = new Rect(0, 0, Infinity, Infinity);
169
+ if (!this.layoutInfos.has(key) && this.requestedRect.area < this.contentSize.area && this.lastCollection) {
170
+ this.requestedRect = new Rect(0, 0, Infinity, Infinity);
175
171
  this.rootNodes = this.buildCollection();
176
- this.validRect = new Rect(0, 0, this.contentSize.width, this.contentSize.height);
172
+ this.requestedRect = new Rect(0, 0, this.contentSize.width, this.contentSize.height);
177
173
  return true;
178
174
  }
179
175
 
180
176
  return false;
181
177
  }
182
178
 
183
- isVisible(node: LayoutNode, rect: Rect) {
179
+ private isVisible(node: LayoutNode, rect: Rect) {
184
180
  return node.layoutInfo.rect.intersects(rect) || node.layoutInfo.isSticky || this.virtualizer.isPersistedKey(node.layoutInfo.key);
185
181
  }
186
182
 
@@ -198,8 +194,7 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
198
194
  // Otherwise we can reuse cached layout infos outside the current visible rect.
199
195
  this.invalidateEverything = this.shouldInvalidateEverything(invalidationContext);
200
196
  if (this.invalidateEverything) {
201
- this.lastValidRect = this.validRect;
202
- this.validRect = this.virtualizer.visibleRect.copy();
197
+ this.requestedRect = this.virtualizer.visibleRect.copy();
203
198
  }
204
199
 
205
200
  this.rootNodes = this.buildCollection();
@@ -221,9 +216,10 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
221
216
  this.lastWidth = this.virtualizer.visibleRect.width;
222
217
  this.lastCollection = this.collection;
223
218
  this.invalidateEverything = false;
219
+ this.validRect = this.requestedRect.copy();
224
220
  }
225
221
 
226
- buildCollection(): LayoutNode[] {
222
+ protected buildCollection(): LayoutNode[] {
227
223
  let y = this.padding;
228
224
  let skipped = 0;
229
225
  let nodes = [];
@@ -231,17 +227,17 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
231
227
  let rowHeight = (this.rowHeight ?? this.estimatedRowHeight);
232
228
 
233
229
  // Skip rows before the valid rectangle unless they are already cached.
234
- if (node.type === 'item' && y + rowHeight < this.validRect.y && !this.isValid(node, y)) {
230
+ if (node.type === 'item' && y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) {
235
231
  y += rowHeight;
236
232
  skipped++;
237
233
  continue;
238
234
  }
239
235
 
240
- let layoutNode = this.buildChild(node, 0, y);
236
+ let layoutNode = this.buildChild(node, 0, y, null);
241
237
  y = layoutNode.layoutInfo.rect.maxY;
242
238
  nodes.push(layoutNode);
243
239
 
244
- if (node.type === 'item' && y > this.validRect.maxY) {
240
+ if (node.type === 'item' && y > this.requestedRect.maxY) {
245
241
  y += (this.collection.size - (nodes.length + skipped)) * rowHeight;
246
242
  break;
247
243
  }
@@ -256,7 +252,7 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
256
252
  y = loader.rect.maxY;
257
253
  }
258
254
 
259
- if (nodes.length === 0) {
255
+ if (nodes.length === 0 && this.enableEmptyState) {
260
256
  let rect = new Rect(0, y, this.virtualizer.visibleRect.width,
261
257
  this.placeholderHeight ?? this.virtualizer.visibleRect.height);
262
258
  let placeholder = new LayoutInfo('placeholder', 'placeholder', rect);
@@ -269,19 +265,19 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
269
265
  return nodes;
270
266
  }
271
267
 
272
- isValid(node: Node<T>, y: number) {
268
+ protected isValid(node: Node<T>, y: number) {
273
269
  let cached = this.layoutNodes.get(node.key);
274
270
  return (
275
271
  !this.invalidateEverything &&
276
272
  cached &&
277
273
  cached.node === node &&
278
274
  y === (cached.header || cached.layoutInfo).rect.y &&
279
- cached.layoutInfo.rect.intersects(this.lastValidRect) &&
280
- cached.validRect.containsRect(cached.layoutInfo.rect.intersection(this.validRect))
275
+ cached.layoutInfo.rect.intersects(this.validRect) &&
276
+ cached.validRect.containsRect(cached.layoutInfo.rect.intersection(this.requestedRect))
281
277
  );
282
278
  }
283
279
 
284
- buildChild(node: Node<T>, x: number, y: number): LayoutNode {
280
+ protected buildChild(node: Node<T>, x: number, y: number, parentKey: Key | null): LayoutNode {
285
281
  if (this.isValid(node, y)) {
286
282
  return this.layoutNodes.get(node.key);
287
283
  }
@@ -289,7 +285,7 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
289
285
  let layoutNode = this.buildNode(node, x, y);
290
286
  layoutNode.node = node;
291
287
 
292
- layoutNode.layoutInfo.parentKey = node.parentKey ?? null;
288
+ layoutNode.layoutInfo.parentKey = parentKey ?? null;
293
289
  this.layoutInfos.set(layoutNode.layoutInfo.key, layoutNode.layoutInfo);
294
290
  if (layoutNode.header) {
295
291
  this.layoutInfos.set(layoutNode.header.key, layoutNode.header);
@@ -299,22 +295,22 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
299
295
  return layoutNode;
300
296
  }
301
297
 
302
- buildNode(node: Node<T>, x: number, y: number): LayoutNode {
298
+ protected buildNode(node: Node<T>, x: number, y: number): LayoutNode {
303
299
  switch (node.type) {
304
300
  case 'section':
305
301
  return this.buildSection(node, x, y);
306
302
  case 'item':
307
303
  return this.buildItem(node, x, y);
308
304
  case 'header':
309
- return this.buildHeader(node, x, y);
305
+ return this.buildSectionHeader(node, x, y);
310
306
  }
311
307
  }
312
308
 
313
- buildSection(node: Node<T>, x: number, y: number): LayoutNode {
309
+ private buildSection(node: Node<T>, x: number, y: number): LayoutNode {
314
310
  let width = this.virtualizer.visibleRect.width;
315
311
  let header = null;
316
312
  if (node.rendered || this.forceSectionHeaders) {
317
- let headerNode = this.buildHeader(node, x, y);
313
+ let headerNode = this.buildSectionHeader(node, x, y);
318
314
  header = headerNode.layoutInfo;
319
315
  header.key += ':header';
320
316
  header.parentKey = node.key;
@@ -331,17 +327,17 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
331
327
  let rowHeight = (this.rowHeight ?? this.estimatedRowHeight);
332
328
 
333
329
  // Skip rows before the valid rectangle unless they are already cached.
334
- if (y + rowHeight < this.validRect.y && !this.isValid(node, y)) {
330
+ if (y + rowHeight < this.requestedRect.y && !this.isValid(node, y)) {
335
331
  y += rowHeight;
336
332
  skipped++;
337
333
  continue;
338
334
  }
339
335
 
340
- let layoutNode = this.buildChild(child, x, y);
336
+ let layoutNode = this.buildChild(child, x, y, layoutInfo.key);
341
337
  y = layoutNode.layoutInfo.rect.maxY;
342
338
  children.push(layoutNode);
343
339
 
344
- if (y > this.validRect.maxY) {
340
+ if (y > this.requestedRect.maxY) {
345
341
  // Estimate the remaining height for rows that we don't need to layout right now.
346
342
  y += ([...getChildNodes(node, this.collection)].length - (children.length + skipped)) * rowHeight;
347
343
  break;
@@ -354,11 +350,11 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
354
350
  header,
355
351
  layoutInfo,
356
352
  children,
357
- validRect: layoutInfo.rect.intersection(this.validRect)
353
+ validRect: layoutInfo.rect.intersection(this.requestedRect)
358
354
  };
359
355
  }
360
356
 
361
- buildHeader(node: Node<T>, x: number, y: number): LayoutNode {
357
+ private buildSectionHeader(node: Node<T>, x: number, y: number): LayoutNode {
362
358
  let width = this.virtualizer.visibleRect.width;
363
359
  let rectHeight = this.headingHeight;
364
360
  let isEstimated = false;
@@ -366,7 +362,7 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
366
362
  // If no explicit height is available, use an estimated height.
367
363
  if (rectHeight == null) {
368
364
  // If a previous version of this layout info exists, reuse its height.
369
- // Mark as estimated if the size of the overall collection view changed,
365
+ // Mark as estimated if the size of the overall virtualizer changed,
370
366
  // or the content of the item changed.
371
367
  let previousLayoutNode = this.layoutNodes.get(node.key);
372
368
  let previousLayoutInfo = previousLayoutNode?.header || previousLayoutNode?.layoutInfo;
@@ -391,11 +387,11 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
391
387
  return {
392
388
  layoutInfo: header,
393
389
  children: [],
394
- validRect: header.rect.intersection(this.validRect)
390
+ validRect: header.rect.intersection(this.requestedRect)
395
391
  };
396
392
  }
397
393
 
398
- buildItem(node: Node<T>, x: number, y: number): LayoutNode {
394
+ private buildItem(node: Node<T>, x: number, y: number): LayoutNode {
399
395
  let width = this.virtualizer.visibleRect.width;
400
396
  let rectHeight = this.rowHeight;
401
397
  let isEstimated = false;
@@ -403,7 +399,7 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
403
399
  // If no explicit height is available, use an estimated height.
404
400
  if (rectHeight == null) {
405
401
  // If a previous version of this layout info exists, reuse its height.
406
- // Mark as estimated if the size of the overall collection view changed,
402
+ // Mark as estimated if the size of the overall virtualizer changed,
407
403
  // or the content of the item changed.
408
404
  let previousLayoutNode = this.layoutNodes.get(node.key);
409
405
  if (previousLayoutNode) {
@@ -448,6 +444,13 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
448
444
  newLayoutInfo.rect.height = size.height;
449
445
  this.layoutInfos.set(key, newLayoutInfo);
450
446
 
447
+ // Items after this layoutInfo will need to be repositioned to account for the new height.
448
+ // Adjust the validRect so that only items above remain valid.
449
+ this.validRect.height = Math.min(this.validRect.height, layoutInfo.rect.y - this.validRect.y);
450
+
451
+ // The requestedRect also needs to be adjusted to account for the height difference.
452
+ this.requestedRect.height += newLayoutInfo.rect.height - layoutInfo.rect.height;
453
+
451
454
  // Invalidate layout for this layout node and all parents
452
455
  this.updateLayoutNode(key, layoutInfo, newLayoutInfo);
453
456
 
@@ -463,11 +466,11 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
463
466
  return false;
464
467
  }
465
468
 
466
- updateLayoutNode(key: Key, oldLayoutInfo: LayoutInfo, newLayoutInfo: LayoutInfo) {
469
+ private updateLayoutNode(key: Key, oldLayoutInfo: LayoutInfo, newLayoutInfo: LayoutInfo) {
467
470
  let n = this.layoutNodes.get(key);
468
471
  if (n) {
469
- // Invalidate by reseting validRect.
470
- n.validRect = new Rect();
472
+ // Invalidate by intersecting the validRect of this node with the overall validRect.
473
+ n.validRect = n.validRect.intersection(this.validRect);
471
474
 
472
475
  // Replace layout info in LayoutNode
473
476
  if (n.header === oldLayoutInfo) {
@@ -482,116 +485,6 @@ export class ListLayout<T> extends Layout<Node<T>, ListLayoutProps> implements K
482
485
  return this.contentSize;
483
486
  }
484
487
 
485
- getKeyAbove(key: Key): Key | null {
486
- let collection = this.collection;
487
-
488
- key = collection.getKeyBefore(key);
489
- while (key != null) {
490
- let item = collection.getItem(key);
491
- if (item.type === 'item' && (this.allowDisabledKeyFocus || !this.disabledKeys.has(item.key))) {
492
- return key;
493
- }
494
-
495
- key = collection.getKeyBefore(key);
496
- }
497
- }
498
-
499
- getKeyBelow(key: Key): Key | null {
500
- let collection = this.collection;
501
-
502
- key = collection.getKeyAfter(key);
503
- while (key != null) {
504
- let item = collection.getItem(key);
505
- if (item.type === 'item' && (this.allowDisabledKeyFocus || !this.disabledKeys.has(item.key))) {
506
- return key;
507
- }
508
-
509
- key = collection.getKeyAfter(key);
510
- }
511
- }
512
-
513
- getKeyPageAbove(key: Key): Key | null {
514
- let layoutInfo = this.getLayoutInfo(key);
515
-
516
- if (layoutInfo) {
517
- let pageY = Math.max(0, layoutInfo.rect.y + layoutInfo.rect.height - this.virtualizer.visibleRect.height);
518
- while (layoutInfo && layoutInfo.rect.y > pageY) {
519
- let keyAbove = this.getKeyAbove(layoutInfo.key);
520
- layoutInfo = this.getLayoutInfo(keyAbove);
521
- }
522
-
523
- if (layoutInfo) {
524
- return layoutInfo.key;
525
- }
526
- }
527
-
528
- return this.getFirstKey();
529
- }
530
-
531
- getKeyPageBelow(key: Key): Key | null {
532
- let layoutInfo = this.getLayoutInfo(key != null ? key : this.getFirstKey());
533
-
534
- if (layoutInfo) {
535
- let pageY = Math.min(this.virtualizer.contentSize.height, layoutInfo.rect.y - layoutInfo.rect.height + this.virtualizer.visibleRect.height);
536
- while (layoutInfo && layoutInfo.rect.y < pageY) {
537
- let keyBelow = this.getKeyBelow(layoutInfo.key);
538
- layoutInfo = this.getLayoutInfo(keyBelow);
539
- }
540
-
541
- if (layoutInfo) {
542
- return layoutInfo.key;
543
- }
544
- }
545
-
546
- return this.getLastKey();
547
- }
548
-
549
- getFirstKey(): Key | null {
550
- let collection = this.collection;
551
- let key = collection.getFirstKey();
552
- while (key != null) {
553
- let item = collection.getItem(key);
554
- if (item.type === 'item' && (this.allowDisabledKeyFocus || !this.disabledKeys.has(item.key))) {
555
- return key;
556
- }
557
-
558
- key = collection.getKeyAfter(key);
559
- }
560
- }
561
-
562
- getLastKey(): Key | null {
563
- let collection = this.collection;
564
- let key = collection.getLastKey();
565
- while (key != null) {
566
- let item = collection.getItem(key);
567
- if (item.type === 'item' && (this.allowDisabledKeyFocus || !this.disabledKeys.has(item.key))) {
568
- return key;
569
- }
570
-
571
- key = collection.getKeyBefore(key);
572
- }
573
- }
574
-
575
- getKeyForSearch(search: string, fromKey?: Key): Key | null {
576
- if (!this.collator) {
577
- return null;
578
- }
579
-
580
- let collection = this.collection;
581
- let key = fromKey || this.getFirstKey();
582
- while (key != null) {
583
- let item = collection.getItem(key);
584
- let substring = item.textValue.slice(0, search.length);
585
- if (item.textValue && this.collator.compare(substring, search) === 0) {
586
- return key;
587
- }
588
-
589
- key = this.getKeyBelow(key);
590
- }
591
-
592
- return null;
593
- }
594
-
595
488
  getDropTargetFromPoint(x: number, y: number, isValidDropTarget: (target: DropTarget) => boolean): DropTarget {
596
489
  x += this.virtualizer.visibleRect.x;
597
490
  y += this.virtualizer.visibleRect.y;