@react-spectrum/s2 3.0.0-nightly-73414999f-240916 → 3.0.0-nightly-c904e066c-240917

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/CardView.tsx CHANGED
@@ -18,13 +18,14 @@ import {
18
18
  UNSTABLE_Virtualizer
19
19
  } from 'react-aria-components';
20
20
  import {CardContext, CardViewContext} from './Card';
21
+ import {DOMRef, forwardRefType, Key, LayoutDelegate, LoadingState, Node} from '@react-types/shared';
21
22
  import {focusRing, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
23
+ import {forwardRef, useMemo, useState} from 'react';
22
24
  import {ImageCoordinator} from './ImageCoordinator';
23
25
  import {InvalidationContext, Layout, LayoutInfo, Rect, Size} from '@react-stately/virtualizer';
24
- import {Key, LoadingState, Node} from '@react-types/shared';
25
26
  import {style} from '../style/spectrum-theme' with {type: 'macro'};
26
- import {useLoadMore} from '@react-aria/utils';
27
- import {useMemo, useRef} from 'react';
27
+ import {useDOMRef} from '@react-spectrum/utils';
28
+ import {useEffectEvent, useLayoutEffect, useLoadMore, useResizeObserver} from '@react-aria/utils';
28
29
 
29
30
  export interface CardViewProps<T> extends Omit<GridListProps<T>, 'layout' | 'keyboardNavigationBehavior' | 'selectionBehavior' | 'className' | 'style'>, UnsafeStyles {
30
31
  /**
@@ -60,56 +61,48 @@ export interface CardViewProps<T> extends Omit<GridListProps<T>, 'layout' | 'key
60
61
  styles?: StylesPropWithHeight
61
62
  }
62
63
 
63
- class FlexibleGridLayout<T extends object, O> extends Layout<Node<T>, O> {
64
- protected minItemSize: Size;
65
- protected maxItemSize: Size;
66
- protected minSpace: Size;
67
- protected maxColumns: number;
68
- protected dropIndicatorThickness: number;
64
+ class FlexibleGridLayout<T extends object> extends Layout<Node<T>, GridLayoutOptions> {
69
65
  protected contentSize: Size = new Size();
70
66
  protected layoutInfos: Map<Key, LayoutInfo> = new Map();
71
67
 
72
- constructor(options: GridLayoutOptions) {
73
- super();
74
- this.minItemSize = options.minItemSize || new Size(200, 200);
75
- this.maxItemSize = options.maxItemSize || new Size(Infinity, Infinity);
76
- this.minSpace = options.minSpace || new Size(18, 18);
77
- this.maxColumns = options.maxColumns || Infinity;
78
- this.dropIndicatorThickness = options.dropIndicatorThickness || 2;
79
- }
80
-
81
- update(invalidationContext: InvalidationContext): void {
68
+ update(invalidationContext: InvalidationContext<GridLayoutOptions>): void {
69
+ let {
70
+ minItemSize = new Size(200, 200),
71
+ maxItemSize = new Size(Infinity, Infinity),
72
+ minSpace = new Size(18, 18),
73
+ maxColumns = Infinity
74
+ } = invalidationContext.layoutOptions || {};
82
75
  let visibleWidth = this.virtualizer.visibleRect.width;
83
76
 
84
77
  // The max item width is always the entire viewport.
85
78
  // If the max item height is infinity, scale in proportion to the max width.
86
- let maxItemWidth = Math.min(this.maxItemSize.width, visibleWidth);
87
- let maxItemHeight = Number.isFinite(this.maxItemSize.height)
88
- ? this.maxItemSize.height
89
- : Math.floor((this.minItemSize.height / this.minItemSize.width) * maxItemWidth);
79
+ let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
80
+ let maxItemHeight = Number.isFinite(maxItemSize.height)
81
+ ? maxItemSize.height
82
+ : Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);
90
83
 
91
84
  // Compute the number of rows and columns needed to display the content
92
- let columns = Math.floor(visibleWidth / (this.minItemSize.width + this.minSpace.width));
93
- let numColumns = Math.max(1, Math.min(this.maxColumns, columns));
85
+ let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
86
+ let numColumns = Math.max(1, Math.min(maxColumns, columns));
94
87
 
95
88
  // Compute the available width (minus the space between items)
96
- let width = visibleWidth - (this.minSpace.width * Math.max(0, numColumns));
89
+ let width = visibleWidth - (minSpace.width * Math.max(0, numColumns));
97
90
 
98
91
  // Compute the item width based on the space available
99
92
  let itemWidth = Math.floor(width / numColumns);
100
- itemWidth = Math.max(this.minItemSize.width, Math.min(maxItemWidth, itemWidth));
93
+ itemWidth = Math.max(minItemSize.width, Math.min(maxItemWidth, itemWidth));
101
94
 
102
95
  // Compute the item height, which is proportional to the item width
103
- let t = ((itemWidth - this.minItemSize.width) / Math.max(1, maxItemWidth - this.minItemSize.width));
104
- let itemHeight = this.minItemSize.height + Math.floor((maxItemHeight - this.minItemSize.height) * t);
105
- itemHeight = Math.max(this.minItemSize.height, Math.min(maxItemHeight, itemHeight));
96
+ let t = ((itemWidth - minItemSize.width) / Math.max(1, maxItemWidth - minItemSize.width));
97
+ let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t);
98
+ itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));
106
99
 
107
100
  // Compute the horizontal spacing and content height
108
101
  let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
109
102
 
110
103
  let rows = Math.ceil(this.virtualizer.collection.size / numColumns);
111
104
  let iterator = this.virtualizer.collection[Symbol.iterator]();
112
- let y = rows > 0 ? this.minSpace.height : 0;
105
+ let y = rows > 0 ? minSpace.height : 0;
113
106
  let newLayoutInfos = new Map();
114
107
  let skeleton: Node<T> | null = null;
115
108
  let skeletonCount = 0;
@@ -128,22 +121,21 @@ class FlexibleGridLayout<T extends object, O> extends Layout<Node<T>, O> {
128
121
  }
129
122
 
130
123
  let key = skeleton ? `${skeleton.key}-${skeletonCount++}` : node.key;
124
+ let content = skeleton ? {...skeleton} : node;
131
125
  let x = horizontalSpacing + col * (itemWidth + horizontalSpacing);
132
126
  let oldLayoutInfo = this.layoutInfos.get(key);
133
127
  let height = itemHeight;
134
128
  let estimatedSize = true;
135
129
  if (oldLayoutInfo) {
136
130
  height = oldLayoutInfo.rect.height;
137
- estimatedSize = invalidationContext.sizeChanged || oldLayoutInfo.estimatedSize;
131
+ estimatedSize = invalidationContext.sizeChanged || oldLayoutInfo.estimatedSize || (oldLayoutInfo.content !== content);
138
132
  }
139
133
 
140
134
  let rect = new Rect(x, y, itemWidth, height);
141
135
  let layoutInfo = new LayoutInfo(node.type, key, rect);
142
136
  layoutInfo.estimatedSize = estimatedSize;
143
137
  layoutInfo.allowOverflow = true;
144
- if (skeleton) {
145
- layoutInfo.content = {...skeleton};
146
- }
138
+ layoutInfo.content = content;
147
139
  newLayoutInfos.set(key, layoutInfo);
148
140
  rowLayoutInfos.push(layoutInfo);
149
141
 
@@ -154,7 +146,7 @@ class FlexibleGridLayout<T extends object, O> extends Layout<Node<T>, O> {
154
146
  layoutInfo.rect.height = maxHeight;
155
147
  }
156
148
 
157
- y += maxHeight + this.minSpace.height;
149
+ y += maxHeight + minSpace.height;
158
150
 
159
151
  // Keep adding skeleton rows until we fill the viewport
160
152
  if (skeleton && row === rows - 1 && y < this.virtualizer.visibleRect.height) {
@@ -202,63 +194,55 @@ class FlexibleGridLayout<T extends object, O> extends Layout<Node<T>, O> {
202
194
  }
203
195
  }
204
196
 
205
- class WaterfallLayout<T extends object, O> extends Layout<Node<T>, O> {
206
- protected minItemSize: Size;
207
- protected maxItemSize: Size;
208
- protected minSpace: Size;
209
- protected maxColumns: number;
210
- protected dropIndicatorThickness: number;
197
+ class WaterfallLayout<T extends object> extends Layout<Node<T>, GridLayoutOptions> implements LayoutDelegate {
211
198
  protected contentSize: Size = new Size();
212
199
  protected layoutInfos: Map<Key, LayoutInfo> = new Map();
213
200
 
214
- constructor(options: GridLayoutOptions) {
215
- super();
216
- this.minItemSize = options.minItemSize || new Size(200, 200);
217
- this.maxItemSize = options.maxItemSize || new Size(Infinity, Infinity);
218
- this.minSpace = options.minSpace || new Size(18, 18);
219
- this.maxColumns = options.maxColumns || Infinity;
220
- this.dropIndicatorThickness = options.dropIndicatorThickness || 2;
221
- }
222
-
223
- update(invalidationContext: InvalidationContext): void {
201
+ update(invalidationContext: InvalidationContext<GridLayoutOptions>): void {
202
+ let {
203
+ minItemSize = new Size(200, 200),
204
+ maxItemSize = new Size(Infinity, Infinity),
205
+ minSpace = new Size(18, 18),
206
+ maxColumns = Infinity
207
+ } = invalidationContext.layoutOptions || {};
224
208
  let visibleWidth = this.virtualizer.visibleRect.width;
225
209
 
226
210
  // The max item width is always the entire viewport.
227
211
  // If the max item height is infinity, scale in proportion to the max width.
228
- let maxItemWidth = Math.min(this.maxItemSize.width, visibleWidth);
229
- let maxItemHeight = Number.isFinite(this.maxItemSize.height)
230
- ? this.maxItemSize.height
231
- : Math.floor((this.minItemSize.height / this.minItemSize.width) * maxItemWidth);
212
+ let maxItemWidth = Math.min(maxItemSize.width, visibleWidth);
213
+ let maxItemHeight = Number.isFinite(maxItemSize.height)
214
+ ? maxItemSize.height
215
+ : Math.floor((minItemSize.height / minItemSize.width) * maxItemWidth);
232
216
 
233
217
  // Compute the number of rows and columns needed to display the content
234
- let columns = Math.floor(visibleWidth / (this.minItemSize.width + this.minSpace.width));
235
- let numColumns = Math.max(1, Math.min(this.maxColumns, columns));
218
+ let columns = Math.floor(visibleWidth / (minItemSize.width + minSpace.width));
219
+ let numColumns = Math.max(1, Math.min(maxColumns, columns));
236
220
 
237
221
  // Compute the available width (minus the space between items)
238
- let width = visibleWidth - (this.minSpace.width * Math.max(0, numColumns));
222
+ let width = visibleWidth - (minSpace.width * Math.max(0, numColumns));
239
223
 
240
224
  // Compute the item width based on the space available
241
225
  let itemWidth = Math.floor(width / numColumns);
242
- itemWidth = Math.max(this.minItemSize.width, Math.min(maxItemWidth, itemWidth));
226
+ itemWidth = Math.max(minItemSize.width, Math.min(maxItemWidth, itemWidth));
243
227
 
244
228
  // Compute the item height, which is proportional to the item width
245
- let t = ((itemWidth - this.minItemSize.width) / Math.max(1, maxItemWidth - this.minItemSize.width));
246
- let itemHeight = this.minItemSize.height + Math.floor((maxItemHeight - this.minItemSize.height) * t);
247
- itemHeight = Math.max(this.minItemSize.height, Math.min(maxItemHeight, itemHeight));
229
+ let t = ((itemWidth - minItemSize.width) / Math.max(1, maxItemWidth - minItemSize.width));
230
+ let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t);
231
+ itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));
248
232
 
249
233
  // Compute the horizontal spacing and content height
250
234
  let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
251
235
 
252
236
  // Setup an array of column heights
253
- let columnHeights = Array(numColumns).fill(this.minSpace.height);
237
+ let columnHeights = Array(numColumns).fill(minSpace.height);
254
238
  let newLayoutInfos = new Map();
255
- let addNode = (key, node) => {
239
+ let addNode = (key: Key, node: Node<T>) => {
256
240
  let oldLayoutInfo = this.layoutInfos.get(key);
257
241
  let height = itemHeight;
258
242
  let estimatedSize = true;
259
243
  if (oldLayoutInfo) {
260
244
  height = oldLayoutInfo.rect.height;
261
- estimatedSize = invalidationContext.sizeChanged || oldLayoutInfo.estimatedSize;
245
+ estimatedSize = invalidationContext.sizeChanged || oldLayoutInfo.estimatedSize || oldLayoutInfo.content !== node;
262
246
  }
263
247
 
264
248
  // Figure out which column to place the item in, and compute its position.
@@ -270,10 +254,10 @@ class WaterfallLayout<T extends object, O> extends Layout<Node<T>, O> {
270
254
  let layoutInfo = new LayoutInfo(node.type, key, rect);
271
255
  layoutInfo.estimatedSize = estimatedSize;
272
256
  layoutInfo.allowOverflow = true;
257
+ layoutInfo.content = node;
273
258
  newLayoutInfos.set(key, layoutInfo);
274
259
 
275
- columnHeights[column] += layoutInfo.rect.height + this.minSpace.height;
276
- return layoutInfo;
260
+ columnHeights[column] += layoutInfo.rect.height + minSpace.height;
277
261
  };
278
262
 
279
263
  let skeletonCount = 0;
@@ -285,8 +269,9 @@ class WaterfallLayout<T extends object, O> extends Layout<Node<T>, O> {
285
269
  !columnHeights.every((h, i) => h !== startingHeights[i]) ||
286
270
  Math.min(...columnHeights) < this.virtualizer.visibleRect.height
287
271
  ) {
288
- let layoutInfo = addNode(`${node.key}-${skeletonCount++}`, node);
289
- layoutInfo.content = this.layoutInfos.get(layoutInfo.key)?.content || {...node};
272
+ let key = `${node.key}-${skeletonCount++}`;
273
+ let content = this.layoutInfos.get(key)?.content || {...node};
274
+ addNode(key, content);
290
275
  }
291
276
  break;
292
277
  } else {
@@ -391,6 +376,27 @@ class WaterfallLayout<T extends object, O> extends Layout<Node<T>, O> {
391
376
 
392
377
  return bestKey;
393
378
  }
379
+
380
+ // This overrides the default behavior of shift selection to work spacially
381
+ // rather than following the order of the items in the collection (which may appear unpredictable).
382
+ getKeyRange(from: Key, to: Key): Key[] {
383
+ let fromLayoutInfo = this.getLayoutInfo(from);
384
+ let toLayoutInfo = this.getLayoutInfo(to);
385
+ if (!fromLayoutInfo || !toLayoutInfo) {
386
+ return [];
387
+ }
388
+
389
+ // Find items where half of the area intersects the rectangle
390
+ // formed from the first item to the last item in the range.
391
+ let rect = fromLayoutInfo.rect.union(toLayoutInfo.rect);
392
+ let keys: Key[] = [];
393
+ for (let layoutInfo of this.layoutInfos.values()) {
394
+ if (rect.intersection(layoutInfo.rect).area > layoutInfo.rect.area / 2) {
395
+ keys.push(layoutInfo.key);
396
+ }
397
+ }
398
+ return keys;
399
+ }
394
400
  }
395
401
 
396
402
  const layoutOptions = {
@@ -481,6 +487,8 @@ const layoutOptions = {
481
487
  }
482
488
  };
483
489
 
490
+ const SIZES = ['XS', 'S', 'M', 'L', 'XL'] as const;
491
+
484
492
  const cardViewStyles = style({
485
493
  overflowY: {
486
494
  default: 'auto',
@@ -502,28 +510,55 @@ const cardViewStyles = style({
502
510
  outlineOffset: -2
503
511
  }, getAllowedOverrides({height: true}));
504
512
 
505
- export function CardView<T extends object>(props: CardViewProps<T>) {
506
- let {children, layout: layoutName = 'grid', size = 'M', density = 'regular', variant = 'primary', selectionStyle = 'checkbox', UNSAFE_className = '', UNSAFE_style, styles, ...otherProps} = props;
507
- let options = layoutOptions[size][density];
513
+ function CardView<T extends object>(props: CardViewProps<T>, ref: DOMRef<HTMLDivElement>) {
514
+ let {children, layout: layoutName = 'grid', size: sizeProp = 'M', density = 'regular', variant = 'primary', selectionStyle = 'checkbox', UNSAFE_className = '', UNSAFE_style, styles, ...otherProps} = props;
515
+ let domRef = useDOMRef(ref);
508
516
  let layout = useMemo(() => {
509
- variant; // needed to invalidate useMemo
510
- return layoutName === 'waterfall' ? new WaterfallLayout(options) : new FlexibleGridLayout(options);
511
- }, [options, variant, layoutName]);
517
+ return layoutName === 'waterfall' ? new WaterfallLayout() : new FlexibleGridLayout();
518
+ }, [layoutName]);
519
+
520
+ // This calculates the maximum t-shirt size where at least two columns fit in the available width.
521
+ let [maxSizeIndex, setMaxSizeIndex] = useState(SIZES.length - 1);
522
+ let updateSize = useEffectEvent(() => {
523
+ let w = domRef.current?.clientWidth ?? 0;
524
+ let i = SIZES.length - 1;
525
+ while (i > 0) {
526
+ let opts = layoutOptions[SIZES[i]][density];
527
+ if (w >= opts.minItemSize.width * 2 + opts.minSpace.width * 3) {
528
+ break;
529
+ }
530
+ i--;
531
+ }
532
+ setMaxSizeIndex(i);
533
+ });
534
+
535
+ useResizeObserver({
536
+ ref: domRef,
537
+ box: 'border-box',
538
+ onResize: updateSize
539
+ });
540
+
541
+ useLayoutEffect(() => {
542
+ updateSize();
543
+ }, [updateSize]);
544
+
545
+ // The actual rendered t-shirt size is the minimum between the size prop and the maximum possible size.
546
+ let size = SIZES[Math.min(maxSizeIndex, SIZES.indexOf(sizeProp))];
547
+ let options = layoutOptions[size][density];
512
548
 
513
- let ref = useRef(null);
514
549
  useLoadMore({
515
550
  isLoading: props.loadingState !== 'idle' && props.loadingState !== 'error',
516
551
  items: props.items, // TODO: ideally this would be the collection. items won't exist for static collections, or those using <Collection>
517
552
  onLoadMore: props.onLoadMore
518
- }, ref);
519
-
553
+ }, domRef);
554
+
520
555
  return (
521
- <UNSTABLE_Virtualizer layout={layout}>
556
+ <UNSTABLE_Virtualizer layout={layout} layoutOptions={options}>
522
557
  <CardViewContext.Provider value={GridListItem}>
523
558
  <CardContext.Provider value={{size, variant}}>
524
559
  <ImageCoordinator>
525
560
  <AriaGridList
526
- ref={ref}
561
+ ref={domRef}
527
562
  {...otherProps}
528
563
  layout="grid"
529
564
  selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'}
@@ -540,3 +575,6 @@ export function CardView<T extends object>(props: CardViewProps<T>) {
540
575
  </UNSTABLE_Virtualizer>
541
576
  );
542
577
  }
578
+
579
+ const _CardView = /*#__PURE__*/ (forwardRef as forwardRefType)(CardView);
580
+ export {_CardView as CardView};