Skip to content
Merged
3 changes: 1 addition & 2 deletions packages/@react-spectrum/s2/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import {useScale} from './utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';
import {Virtualizer} from 'react-aria-components/Virtualizer';

export interface ListViewProps<T> extends Omit<GridListProps<T>, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps {
export interface ListViewProps<T> extends Omit<GridListProps<T>, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | 'orientation' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps {
/** Spectrum-defined styles, returned by the `style()` macro. */
styles?: StylesPropWithHeight,
/** The current loading state of the ListView. */
Expand Down Expand Up @@ -864,4 +864,3 @@ export function ListViewItem(props: ListViewItemProps): ReactNode {
</GridListItem>
);
}

37 changes: 37 additions & 0 deletions packages/dev/s2-docs/pages/react-aria/GridList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,43 @@ function Example(props) {
}
```

## Layouts

Use the `layout` and `orientation` props to arrange items in horizontal and vertical stacks and grids. This affects keyboard navigation and drag and drop behavior.

```tsx render docs={docs.exports.GridList} links={docs.links} props={['layout', 'orientation', 'keyboardNavigationBehavior']} initialProps={{layout: 'grid', orientation: 'horizontal', keyboardNavigationBehavior: 'tab'}} wide
"use client";
import {Text} from 'react-aria-components';
import {GridList, GridListItem} from 'vanilla-starter/GridList';

///- begin collapse -///
let photos = [
{id: 1, title: 'Desert Sunset', description: 'PNG • 2/3/2024', src: 'https://images.unsplash.com/photo-1705034598432-1694e203cdf3?q=80&w=600&auto=format&fit=crop'},
{id: 2, title: 'Hiking Trail', description: 'JPEG • 1/10/2022', src: 'https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=600&auto=format&fit=crop'},
{id: 3, title: 'Lion', description: 'JPEG • 8/28/2021', src: 'https://images.unsplash.com/photo-1629812456605-4a044aa38fbc?q=80&w=600&auto=format&fit=crop'},
{id: 4, title: 'Mountain Sunrise', description: 'PNG • 3/15/2015', src: 'https://images.unsplash.com/photo-1722172118908-1a97c312ce8c?q=80&w=600&auto=format&fit=crop'},
{id: 5, title: 'Giraffe tongue', description: 'PNG • 11/27/2019', src: 'https://images.unsplash.com/photo-1574870111867-089730e5a72b?q=80&w=600&auto=format&fit=crop'},
{id: 6, title: 'Golden Hour', description: 'WEBP • 7/24/2024', src: 'https://images.unsplash.com/photo-1718378037953-ab21bf2cf771?q=80&w=600&auto=format&fit=crop'},
];
///- end collapse -///

<GridList
/*- begin highlight -*/
/* PROPS */
/*- end highlight -*/
aria-label="Photos"
items={photos}
selectionMode="multiple">
{item => (
<GridListItem textValue={item.title}>
<img src={item.src} alt="" />
<Text>{item.title}</Text>
<Text slot="description">{item.description}</Text>
</GridListItem>
)}
</GridList>
```

## Drag and drop

GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the <TypeLink links={docs.links} type={docs.exports.useDragAndDrop} /> hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the [drag and drop guide](dnd?component=GridList) to learn more.
Expand Down
260 changes: 255 additions & 5 deletions packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/react-aria-components/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,5 @@ export type {ListOptions as ListDataOptions, ListData} from 'react-stately/useLi
export type {TreeOptions as TreeDataOptions, TreeData} from 'react-stately/useTreeData';
export type {AsyncListOptions, AsyncListData, AsyncListLoadFunction, AsyncListLoadOptions, AsyncListStateUpdate} from 'react-stately/useAsyncList';
export type {AutocompleteState} from 'react-stately/private/autocomplete/useAutocompleteState';
export type {ListLayoutOptions, GridLayoutOptions, WaterfallLayoutOptions} from 'react-stately/useVirtualizerState';
export type {ListLayoutOptions, GridLayoutOptions, TableLayoutProps, WaterfallLayoutOptions} from 'react-stately/useVirtualizerState';
export type {RangeValue, ValidationResult, RouterConfig} from '@react-types/shared';
142 changes: 97 additions & 45 deletions packages/react-stately/src/layout/ListLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ export interface ListLayoutOptions {
*/
orientation?: Orientation,
/**
* The fixed height of a row in px.
* The fixed size of a row in px with respect to the applied orientation.
* @default 48
*/
rowHeight?: number,
/** The estimated height of a row, when row heights are variable. */
estimatedRowHeight?: number,
rowSize?: number,
/** The estimated size of a row in px with respect to the applied orientation, when row sizes are variable. */
estimatedRowSize?: number,
/**
* The fixed height of a section header in px.
* The fixed size of a section header in px with respect to the applied orientation.
* @default 48
*/
headingHeight?: number,
/** The estimated height of a section header, when the height is variable. */
estimatedHeadingHeight?: number,
headingSize?: number,
/** The estimated size of a section header in px with respect to the applied orientation, when heading sizes are variable. */
estimatedHeadingSize?: number,
/**
* The fixed height of a loader element in px. This loader is specifically for
* The fixed size of a loader element in px with respect to the applied orientation. This loader is specifically for
* "load more" elements rendered when loading more rows at the root level or inside nested row/sections.
* @default 48
*/
loaderHeight?: number,
loaderSize?: number,
/**
* The thickness of the drop indicator.
* @default 2
Expand All @@ -58,7 +58,34 @@ export interface ListLayoutOptions {
* The padding around the list.
* @default 0
*/
padding?: number
padding?: number,
/**
* The fixed height of a row in px.
* @default 48
* @deprecated Use `rowSize` instead.
*/
rowHeight?: number,
/** The estimated height of a row, when row heights are variable.
* @deprecated Use `estimatedRowSize` instead.
*/
estimatedRowHeight?: number,
/**
* The fixed height of a section header in px.
* @default 48
* @deprecated Use `headingSize` instead.
*/
headingHeight?: number,
/** The estimated height of a section header, when the height is variable.
* @deprecated Use `estimatedHeadingSize` instead.
*/
estimatedHeadingHeight?: number,
/**
* The fixed height of a loader element in px. This loader is specifically for
* "load more" elements rendered when loading more rows at the root level or inside nested row/sections.
* @default 48
* @deprecated Use `loaderSize` instead.
*/
loaderHeight?: number
}

// A wrapper around LayoutInfo that supports hierarchy
Expand All @@ -74,16 +101,16 @@ const DEFAULT_HEIGHT = 48;

/**
* ListLayout is a virtualizer Layout implementation
* that arranges its items in a vertical stack. It supports both fixed
* and variable height items.
* that arranges its items in a stack along its applied orientation.
* It supports both fixed and variable size items.
*/
export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> extends Layout<Node<T>, O> implements DropTargetDelegate {
protected rowHeight: number | null;
protected rowSize: number | null;
protected orientation: Orientation;
protected estimatedRowHeight: number | null;
protected headingHeight: number | null;
protected estimatedHeadingHeight: number | null;
protected loaderHeight: number | null;
protected estimatedRowSize: number | null;
protected headingSize: number | null;
protected estimatedHeadingSize: number | null;
protected loaderSize: number | null;
protected dropIndicatorThickness: number;
protected gap: number;
protected padding: number;
Expand All @@ -103,12 +130,12 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
*/
constructor(options: ListLayoutOptions = {}) {
super();
this.rowHeight = options.rowHeight ?? null;
this.rowSize = options?.rowSize ?? options?.rowHeight ?? null;
this.orientation = options.orientation ?? 'vertical';
this.estimatedRowHeight = options.estimatedRowHeight ?? null;
this.headingHeight = options.headingHeight ?? null;
this.estimatedHeadingHeight = options.estimatedHeadingHeight ?? null;
this.loaderHeight = options.loaderHeight ?? null;
this.estimatedRowSize = options?.estimatedRowSize ?? options?.estimatedRowHeight ?? null;
this.headingSize = options?.headingSize ?? options?.headingHeight ?? null;
this.estimatedHeadingSize = options?.estimatedHeadingSize ?? options?.estimatedHeadingHeight ?? null;
this.loaderSize = options?.loaderSize ?? options?.loaderHeight ?? null;
this.dropIndicatorThickness = options.dropIndicatorThickness || 2;
this.gap = options.gap || 0;
this.padding = options.padding || 0;
Expand All @@ -126,6 +153,31 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
return this.virtualizer!.collection;
}

/** @deprecated Use `rowSize` instead. */
protected get rowHeight(): number | null {
return this.rowSize;
}

/** @deprecated Use `estimatedRowSize` instead. */
protected get estimatedRowHeight(): number | null {
return this.estimatedRowSize;
}

/** @deprecated Use `headingSize` instead. */
protected get headingHeight(): number | null {
return this.headingSize;

}
/** @deprecated Use `estimatedHeadingSize` instead. */
protected get estimatedHeadingHeight(): number | null {
return this.estimatedHeadingSize;
}

/** @deprecated Use `loaderSize` instead. */
protected get loaderHeight(): number | null {
return this.loaderSize;
}

getLayoutInfo(key: Key): LayoutInfo | null {
this.ensureLayoutInfo(key);
return this.layoutNodes.get(key)?.layoutInfo || null;
Expand All @@ -139,7 +191,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
// Adjust rect to keep number of visible rows consistent.
// (only if height > 1 or width > 1 for getDropTargetFromPoint)
if (visibleRect[heightProperty] > 1) {
let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;
let rowHeight = (this.rowSize ?? this.estimatedRowSize ?? DEFAULT_HEIGHT) + this.gap;
visibleRect[offsetProperty] = Math.floor(visibleRect[offsetProperty] / rowHeight) * rowHeight;
visibleRect[heightProperty] = Math.ceil(visibleRect[heightProperty] / rowHeight) * rowHeight;
}
Expand Down Expand Up @@ -208,21 +260,21 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
// Also invalidate if fixed sizes/gaps change.
let options = invalidationContext.layoutOptions;
return invalidationContext.sizeChanged
|| this.rowHeight !== (options?.rowHeight ?? this.rowHeight)
|| this.rowSize !== (options?.rowSize ?? options?.rowHeight ?? this.rowSize)
|| this.orientation !== (options?.orientation ?? this.orientation)
|| this.headingHeight !== (options?.headingHeight ?? this.headingHeight)
|| this.loaderHeight !== (options?.loaderHeight ?? this.loaderHeight)
|| this.headingSize !== (options?.headingSize ?? options?.headingHeight ?? this.headingSize)
|| this.loaderSize !== (options?.loaderSize ?? options?.loaderHeight ?? this.loaderSize)
|| this.gap !== (options?.gap ?? this.gap)
|| this.padding !== (options?.padding ?? this.padding);
}

shouldInvalidateLayoutOptions(newOptions: O, oldOptions: O): boolean {
return newOptions.rowHeight !== oldOptions.rowHeight
return (newOptions?.rowSize ?? newOptions?.rowHeight) !== (oldOptions?.rowSize ?? oldOptions?.rowHeight)
|| newOptions.orientation !== oldOptions.orientation
|| newOptions.estimatedRowHeight !== oldOptions.estimatedRowHeight
|| newOptions.headingHeight !== oldOptions.headingHeight
|| newOptions.estimatedHeadingHeight !== oldOptions.estimatedHeadingHeight
|| newOptions.loaderHeight !== oldOptions.loaderHeight
|| (newOptions?.estimatedRowSize ?? newOptions?.estimatedRowHeight) !== (oldOptions?.estimatedRowSize ?? oldOptions?.estimatedRowHeight)
|| (newOptions?.headingSize ?? newOptions?.headingHeight) !== (oldOptions?.headingSize ?? oldOptions?.headingHeight)
|| (newOptions?.estimatedHeadingSize ?? newOptions?.estimatedHeadingHeight) !== (oldOptions?.estimatedHeadingSize ?? oldOptions?.estimatedHeadingHeight)
|| (newOptions?.loaderSize ?? newOptions?.loaderHeight) !== (oldOptions?.loaderSize ?? oldOptions?.loaderHeight)
|| newOptions.dropIndicatorThickness !== oldOptions.dropIndicatorThickness
|| newOptions.gap !== oldOptions.gap
|| newOptions.padding !== oldOptions.padding;
Expand All @@ -240,12 +292,12 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
}

let options = invalidationContext.layoutOptions;
this.rowHeight = options?.rowHeight ?? this.rowHeight;
this.rowSize = options?.rowSize ?? options?.rowHeight ?? this.rowSize;
this.orientation = options?.orientation ?? this.orientation;
this.estimatedRowHeight = options?.estimatedRowHeight ?? this.estimatedRowHeight;
this.headingHeight = options?.headingHeight ?? this.headingHeight;
this.estimatedHeadingHeight = options?.estimatedHeadingHeight ?? this.estimatedHeadingHeight;
this.loaderHeight = options?.loaderHeight ?? this.loaderHeight;
this.estimatedRowSize = options?.estimatedRowSize ?? options?.estimatedRowHeight ?? this.estimatedRowSize;
this.headingSize = options?.headingSize ?? options?.headingHeight ?? this.headingSize;
this.estimatedHeadingSize = options?.estimatedHeadingSize ?? options?.estimatedHeadingHeight ?? this.estimatedHeadingSize;
this.loaderSize = options?.loaderSize ?? options?.loaderHeight ?? this.loaderSize;
this.dropIndicatorThickness = options?.dropIndicatorThickness ?? this.dropIndicatorThickness;
this.gap = options?.gap ?? this.gap;
this.padding = options?.padding ?? this.padding;
Expand Down Expand Up @@ -284,7 +336,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
for (let node of collectionNodes) {
let offsetProperty = this.orientation === 'horizontal' ? 'x' : 'y';
let maxOffsetProperty = this.orientation === 'horizontal' ? 'maxX' : 'maxY';
let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;
let rowHeight = (this.rowSize ?? this.estimatedRowSize ?? DEFAULT_HEIGHT) + this.gap;
// Skip rows before the valid rectangle unless they are already cached.
if (node.type === 'item' && offset + rowHeight < this.requestedRect[offsetProperty] && !this.isValid(node, offset)) {
offset += rowHeight;
Expand Down Expand Up @@ -377,10 +429,10 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
// room for the loader alongside rendering the emptyState
if (this.orientation === 'horizontal') {
rect.height = this.virtualizer!.contentSize.height - this.padding - y;
rect.width = node.props.isLoading ? this.loaderHeight ?? this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT : 0;
rect.width = node.props.isLoading ? this.loaderSize ?? this.rowSize ?? this.estimatedRowSize ?? DEFAULT_HEIGHT : 0;
} else {
rect.width = this.virtualizer!.contentSize.width - this.padding - x;
rect.height = node.props.isLoading ? this.loaderHeight ?? this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT : 0;
rect.height = node.props.isLoading ? this.loaderSize ?? this.rowSize ?? this.estimatedRowSize ?? DEFAULT_HEIGHT : 0;
}

return {
Expand Down Expand Up @@ -409,7 +461,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
continue;
}

let rowHeight = (this.rowHeight ?? this.estimatedRowHeight ?? DEFAULT_HEIGHT) + this.gap;
let rowHeight = (this.rowSize ?? this.estimatedRowSize ?? DEFAULT_HEIGHT) + this.gap;

// Skip rows before the valid rectangle unless they are already cached.
if (offset + rowHeight < this.requestedRect[offsetProperty] && !this.isValid(node, offset)) {
Expand Down Expand Up @@ -444,7 +496,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
let widthProperty = this.orientation === 'horizontal' ? 'height' : 'width';
let heightProperty = this.orientation === 'horizontal' ? 'width' : 'height';
let width = this.virtualizer!.visibleRect[widthProperty] - this.padding - (this.orientation === 'horizontal' ? y : x);
let rectHeight = this.headingHeight;
let rectHeight = this.headingSize;
let isEstimated = false;

// If no explicit height is available, use an estimated height.
Expand All @@ -460,7 +512,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
rectHeight = previousLayoutNode!.layoutInfo.rect[heightProperty];
isEstimated = width !== previousLayoutInfo.rect[widthProperty] || curNode !== lastNode || previousLayoutInfo.estimatedSize;
} else {
rectHeight = (node.rendered ? this.estimatedHeadingHeight : 0);
rectHeight = (node.rendered ? this.estimatedHeadingSize : 0);
isEstimated = true;
}
}
Expand All @@ -485,7 +537,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
let heightProperty = this.orientation === 'horizontal' ? 'width' : 'height';

let width = this.virtualizer!.visibleRect[widthProperty] - this.padding - (this.orientation === 'horizontal' ? y : x);
let rectHeight = this.rowHeight;
let rectHeight = this.rowSize;
let isEstimated = false;

// If no explicit height is available, use an estimated height.
Expand All @@ -498,7 +550,7 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
rectHeight = previousLayoutNode.layoutInfo.rect[heightProperty];
isEstimated = width !== previousLayoutNode.layoutInfo.rect[widthProperty] || node !== previousLayoutNode.node || previousLayoutNode.layoutInfo.estimatedSize;
} else {
rectHeight = this.estimatedRowHeight;
rectHeight = this.estimatedRowSize;
isEstimated = true;
}
}
Expand Down
Loading
Loading