Skip to content

Commit 2d44556

Browse files
authored
feat: add maxSpace option to @react-stately/layout GridLayout (#8654)
* feat: add new maxSpace option * feat: add new private margin property * feat: take maxSpace into account when computing horizontal spacing & compute margin * example use of GridList maxSpace * chore: rename maxSpace into maxHorizontalSpace * feat: add controls to the VirtualizedGridListGrid story
1 parent 4de15b9 commit 2d44556

File tree

2 files changed

+65
-7
lines changed

2 files changed

+65
-7
lines changed

packages/@react-stately/layout/src/GridLayout.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export interface GridLayoutOptions {
3636
* @default 18 x 18
3737
*/
3838
minSpace?: Size,
39+
/**
40+
* The maximum allowed horizontal space between items.
41+
* @default Infinity
42+
*/
43+
maxHorizontalSpace?: number,
3944
/**
4045
* The maximum number of columns.
4146
* @default Infinity
@@ -53,6 +58,7 @@ const DEFAULT_OPTIONS = {
5358
maxItemSize: new Size(Infinity, Infinity),
5459
preserveAspectRatio: false,
5560
minSpace: new Size(18, 18),
61+
maxSpace: Infinity,
5662
maxColumns: Infinity,
5763
dropIndicatorThickness: 2
5864
};
@@ -69,14 +75,16 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
6975
protected numColumns: number = 0;
7076
private contentSize: Size = new Size();
7177
private layoutInfos: Map<Key, LayoutInfo> = new Map();
78+
private margin: number = 0;
7279

7380
shouldInvalidateLayoutOptions(newOptions: O, oldOptions: O): boolean {
7481
return newOptions.maxColumns !== oldOptions.maxColumns
7582
|| newOptions.dropIndicatorThickness !== oldOptions.dropIndicatorThickness
7683
|| newOptions.preserveAspectRatio !== oldOptions.preserveAspectRatio
7784
|| (!(newOptions.minItemSize || DEFAULT_OPTIONS.minItemSize).equals(oldOptions.minItemSize || DEFAULT_OPTIONS.minItemSize))
7885
|| (!(newOptions.maxItemSize || DEFAULT_OPTIONS.maxItemSize).equals(oldOptions.maxItemSize || DEFAULT_OPTIONS.maxItemSize))
79-
|| (!(newOptions.minSpace || DEFAULT_OPTIONS.minSpace).equals(oldOptions.minSpace || DEFAULT_OPTIONS.minSpace));
86+
|| (!(newOptions.minSpace || DEFAULT_OPTIONS.minSpace).equals(oldOptions.minSpace || DEFAULT_OPTIONS.minSpace))
87+
|| newOptions.maxHorizontalSpace !== oldOptions.maxHorizontalSpace;
8088
}
8189

8290
update(invalidationContext: InvalidationContext<O>): void {
@@ -85,6 +93,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
8593
maxItemSize = DEFAULT_OPTIONS.maxItemSize,
8694
preserveAspectRatio = DEFAULT_OPTIONS.preserveAspectRatio,
8795
minSpace = DEFAULT_OPTIONS.minSpace,
96+
maxHorizontalSpace = DEFAULT_OPTIONS.maxSpace,
8897
maxColumns = DEFAULT_OPTIONS.maxColumns,
8998
dropIndicatorThickness = DEFAULT_OPTIONS.dropIndicatorThickness
9099
} = invalidationContext.layoutOptions || {};
@@ -116,9 +125,10 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
116125
let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t);
117126
itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight));
118127

119-
// Compute the horizontal spacing and content height
120-
let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1));
128+
// Compute the horizontal spacing, content height and horizontal margin
129+
let horizontalSpacing = Math.min(maxHorizontalSpace, Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1)));
121130
this.gap = new Size(horizontalSpacing, minSpace.height);
131+
this.margin = Math.floor((visibleWidth - numColumns * itemWidth - horizontalSpacing * (numColumns + 1)) / 2);
122132

123133
// If there is a skeleton loader within the last 2 items in the collection, increment the collection size
124134
// so that an additional row is added for the skeletons.
@@ -133,7 +143,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
133143
}
134144
lastKey = collection.getKeyBefore(lastKey);
135145
}
136-
146+
137147
let rows = Math.ceil(collectionSize / numColumns);
138148
let iterator = collection[Symbol.iterator]();
139149
let y = rows > 0 ? minSpace.height : 0;
@@ -165,7 +175,7 @@ export class GridLayout<T, O extends GridLayoutOptions = GridLayoutOptions> exte
165175
if (skeleton) {
166176
content = oldLayoutInfo && oldLayoutInfo.content.key === key ? oldLayoutInfo.content : {...skeleton, key};
167177
}
168-
let x = horizontalSpacing + col * (itemWidth + horizontalSpacing);
178+
let x = horizontalSpacing + col * (itemWidth + horizontalSpacing) + this.margin;
169179
let height = itemHeight;
170180
let estimatedSize = !preserveAspectRatio;
171181
if (oldLayoutInfo && estimatedSize) {

packages/react-aria-components/stories/GridList.stories.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,20 @@ export const VirtualizedGridList: StoryObj<typeof VirtualizedGridListRender> = {
200200
}
201201
};
202202

203-
export let VirtualizedGridListGrid: GridListStory = () => {
203+
interface VirtualizedGridListGridProps {
204+
maxItemSizeWidth?: number,
205+
maxColumns?: number,
206+
minHorizontalSpace?: number,
207+
maxHorizontalSpace?: number
208+
}
209+
210+
export let VirtualizedGridListGrid: StoryFn<VirtualizedGridListGridProps> = (args) => {
211+
const {
212+
maxItemSizeWidth = 65,
213+
maxColumns = Infinity,
214+
minHorizontalSpace = 0,
215+
maxHorizontalSpace = Infinity
216+
} = args;
204217
let items: {id: number, name: string}[] = [];
205218
for (let i = 0; i < 10000; i++) {
206219
items.push({id: i, name: `Item ${i}`});
@@ -210,7 +223,11 @@ export let VirtualizedGridListGrid: GridListStory = () => {
210223
<Virtualizer
211224
layout={GridLayout}
212225
layoutOptions={{
213-
minItemSize: new Size(40, 40)
226+
minItemSize: new Size(40, 40),
227+
maxItemSize: new Size(maxItemSizeWidth, 40),
228+
minSpace: new Size(minHorizontalSpace, 18),
229+
maxColumns,
230+
maxHorizontalSpace
214231
}}>
215232
<GridList className={styles.menu} layout="grid" style={{height: 400, width: 400}} aria-label="virtualized listbox" items={items}>
216233
{item => <MyGridListItem>{item.name}</MyGridListItem>}
@@ -219,6 +236,37 @@ export let VirtualizedGridListGrid: GridListStory = () => {
219236
);
220237
};
221238

239+
VirtualizedGridListGrid.story = {
240+
args: {
241+
maxItemSizeWidth: 65,
242+
maxColumns: undefined,
243+
minHorizontalSpace: 0,
244+
maxHorizontalSpace: undefined
245+
},
246+
argTypes: {
247+
maxItemSizeWidth: {
248+
control: 'number',
249+
description: 'Maximum width of each item in the grid list.',
250+
defaultValue: 65
251+
},
252+
maxColumns: {
253+
control: 'number',
254+
description: 'Maximum number of columns in the grid list.',
255+
defaultValue: undefined
256+
},
257+
minHorizontalSpace: {
258+
control: 'number',
259+
description: 'Minimum horizontal space between grid items.',
260+
defaultValue: 0
261+
},
262+
maxHorizontalSpace: {
263+
control: 'number',
264+
description: 'Maximum horizontal space between grid items.',
265+
defaultValue: undefined
266+
}
267+
}
268+
};
269+
222270
let renderEmptyState = ({isLoading}) => {
223271
return (
224272
<div style={{height: 30, width: '100%'}}>

0 commit comments

Comments
 (0)