@react-spectrum/s2 3.0.0-nightly-73414999f-240915 → 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/dist/Card.cjs +145 -143
- package/dist/Card.cjs.map +1 -1
- package/dist/Card.css.map +1 -1
- package/dist/Card.mjs +146 -144
- package/dist/Card.mjs.map +1 -1
- package/dist/CardView.cjs +99 -60
- package/dist/CardView.cjs.map +1 -1
- package/dist/CardView.css.map +1 -1
- package/dist/CardView.mjs +100 -61
- package/dist/CardView.mjs.map +1 -1
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +14 -14
- package/src/Card.tsx +143 -132
- package/src/CardView.tsx +119 -81
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 {
|
|
27
|
-
import {
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
87
|
-
let maxItemHeight = Number.isFinite(
|
|
88
|
-
?
|
|
89
|
-
: Math.floor((
|
|
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 / (
|
|
93
|
-
let numColumns = Math.max(1, Math.min(
|
|
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 - (
|
|
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(
|
|
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 -
|
|
104
|
-
let itemHeight =
|
|
105
|
-
itemHeight = Math.max(
|
|
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 ?
|
|
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
|
-
|
|
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 +
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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(
|
|
229
|
-
let maxItemHeight = Number.isFinite(
|
|
230
|
-
?
|
|
231
|
-
: Math.floor((
|
|
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 / (
|
|
235
|
-
let numColumns = Math.max(1, Math.min(
|
|
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 - (
|
|
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(
|
|
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 -
|
|
246
|
-
let itemHeight =
|
|
247
|
-
itemHeight = Math.max(
|
|
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(
|
|
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 +
|
|
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
|
|
289
|
-
|
|
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
|
-
|
|
506
|
-
let {children, layout: layoutName = 'grid', size = 'M', density = 'regular', variant = 'primary', selectionStyle = 'checkbox', UNSAFE_className = '', UNSAFE_style, styles, ...otherProps} = props;
|
|
507
|
-
let
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
},
|
|
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={
|
|
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};
|