Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -53,7 +53,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 @@ -866,4 +866,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
45 changes: 40 additions & 5 deletions packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const description = 'Renders a scrollable collection of data using custom

<PageDescription>{docs.exports.Virtualizer.description}</PageDescription>

```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['rowHeight', 'gap', 'padding']} initialProps={{rowHeight: 32, gap: 4, padding: 4}} propsObject="layoutOptions" wide
```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['rowSize', 'gap', 'padding']} initialProps={{rowSize: 32, gap: 4, padding: 4}} propsObject="layoutOptions" wide
"use client";
import {Virtualizer, ListLayout} from 'react-aria-components';
import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox';
Expand Down Expand Up @@ -49,9 +49,9 @@ Virtualizer uses <TypeLink links={docs.links} type={docs.exports.Layout} /> obje

### List

`ListLayout` supports layout of items in a vertical stack. Rows can be fixed or variable height. When using variable heights, set the `estimatedRowHeight` to a reasonable guess for how tall the rows will be on average. This allows the size of the scrollbar to be calculated.
`ListLayout` places items along its orientation. Rows can be fixed or variable size. When using variable size, set the `estimatedRowSize` to a reasonable guess for how tall or wide the rows will be on average. This allows the size of the scrollbar to be calculated.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`ListLayout` places items along its orientation. Rows can be fixed or variable size. When using variable size, set the `estimatedRowSize` to a reasonable guess for how tall or wide the rows will be on average. This allows the size of the scrollbar to be calculated.
`ListLayout` places items along its orientation. Rows can be fixed or variable in size. When using a variable size, set the `estimatedRowSize` to a reasonable guess for how tall or wide the rows will be on average. This allows the size of the scrollbar to be calculated.


```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['gap', 'padding']} initialProps={{estimatedRowHeight: 75, gap: 4, padding: 4}} propsObject="layoutOptions" wide
```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['gap', 'padding']} initialProps={{estimatedRowSize: 75, gap: 4, padding: 4}} propsObject="layoutOptions" wide
"use client";
import {Virtualizer, ListLayout} from 'react-aria-components';
import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox';
Expand Down Expand Up @@ -80,6 +80,41 @@ for (let i = 0; i < 5000; i++) {
</Virtualizer>
```


Use the `orientation` option to arrange items horizontally or vertically. Provide the same `orientation` on the collection component so keyboard navigation matches the layout.

```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['gap', 'padding']} initialProps={{estimatedRowSize: 100, gap: 4, padding: 4, orientation: 'horizontal'}} propsObject="layoutOptions" wide
"use client";
import {Virtualizer, ListLayout} from 'react-aria-components';
import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox';

let lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet tristique risus. In sit amet suscipit lorem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In condimentum imperdiet metus non condimentum. Duis eu velit et quam accumsan tempus at id velit. Duis elementum elementum purus, id tempus mauris posuere a. Nunc vestibulum sapien pellentesque lectus commodo ornare.'.split(' ');
let items: {id: number, name: string}[] = [];
for (let i = 0; i < 5000; i++) {
let words = Math.max(2, Math.floor(Math.random() * 10));
let name = lorem.slice(0, words).join(' ');
items.push({id: i, name});
}

<Virtualizer
/*- begin highlight -*/
layout={ListLayout}
/* PROPS */
>
{/*- end highlight -*/}
<ListBox
/*- begin highlight -*/
orientation="horizontal"
/*- end highlight -*/
aria-label="Horizontal virtualized list"
selectionMode="multiple"
items={items}
style={{display: 'block', padding: 0, height: 100}}>
{(item) => <ListBoxItem style={{height: '100%'}}>{item.name}</ListBoxItem>}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some styles to the items so that it's clear where one ends and the next begins. Right now it just looks like a long string of text with some random break in it.

It'd also be nice if this was an example I'd want to use, so if we can make them basically more like cards with a title and description, that'd be better

</ListBox>
</Virtualizer>
```

### Grid

`GridLayout` supports layout of items in an equal size grid. The items are sized between a minimum and maximum size depending on the width of the container. Make sure to set `layout="grid"` on the `ListBox` or `GridList` component as well so that keyboard navigation behavior is correct.
Expand Down Expand Up @@ -426,7 +461,7 @@ for (let i = 0; images.length < 500; i++) {

`TableLayout` provides layout of items in rows and columns, supporting virtualization of both horizontal and vertical scrolling. It should be used with the [Table](Table) component. Rows can be fixed or variable height. When using variable heights, set the `estimatedRowHeight` to a reasonable guess for how tall the rows will be on average. This allows the size of the scrollbar to be calculated.

```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['rowHeight', 'headingHeight', 'padding', 'gap']} initialProps={{rowHeight: 32, headingHeight: 32, padding: 4, gap: 4}} propsObject="layoutOptions" wide
```tsx render docs={docs.exports.TableLayoutProps} links={docs.links} props={['rowHeight', 'headingHeight', 'padding', 'gap']} initialProps={{rowHeight: 32, headingHeight: 32, padding: 4, gap: 4}} propsObject="layoutOptions" wide
"use client";
import {Virtualizer, TableLayout} from 'react-aria-components';
import {Cell, Column, Row, Table, TableBody, TableHeader} from 'vanilla-starter/Table';
Expand Down Expand Up @@ -501,4 +536,4 @@ for (let i = 0; i < 1000; i++) {

### TableLayout

<GroupedPropTable {...docs.exports.ListLayoutOptions} links={docs.links} />
<GroupedPropTable {...docs.exports.TableLayoutProps} links={docs.links} />
1 change: 1 addition & 0 deletions packages/react-aria-components/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,5 @@ export type {AutocompleteState} from 'react-stately/private/autocomplete/useAuto
export type {ListLayoutOptions} from 'react-stately/private/layout/ListLayout';
export type {GridLayoutOptions} from 'react-stately/private/layout/GridLayout';
export type {WaterfallLayoutOptions} from 'react-stately/private/layout/WaterfallLayout';
export type {TableLayoutProps} from 'react-stately/private/layout/TableLayout';
export type {RangeValue, ValidationResult, RouterConfig} from '@react-types/shared';
89 changes: 58 additions & 31 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,8 +101,8 @@ 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;
Expand Down Expand Up @@ -103,12 +130,12 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
*/
constructor(options: ListLayoutOptions = {}) {
super();
this.rowHeight = options.rowHeight ?? null;
this.rowHeight = options?.rowSize ?? options?.rowHeight ?? null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also go ahead and change these height properties to be size as well, would just need to make it back compat with additional getters. Not sure if worth it if those would stick around for quite some time, but might reduce confusion

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.estimatedRowHeight = options?.estimatedRowSize ?? options?.estimatedRowHeight ?? null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we want to change all the internal variables to size as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

o, i think that's what you were saying here #9843 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, thats right. I might try and do it to see how bad it is

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, when working on this I was kind of unsure whether to use "size" here, since that typically refers to a 2-dimensional value in the context of our virtualizer package. I think it's fine, but wanted to share it for consideration - naming is hard 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha yeah, I had a similar thought but couldn't think up of a better alternative either. I'll see about pushing up the full changes and then see if we can land on a better name

this.headingHeight = options?.headingSize ?? options?.headingHeight ?? null;
this.estimatedHeadingHeight = options?.estimatedHeadingSize ?? options?.estimatedHeadingHeight ?? null;
this.loaderHeight = options?.loaderSize ?? options?.loaderHeight ?? null;
this.dropIndicatorThickness = options.dropIndicatorThickness || 2;
this.gap = options.gap || 0;
this.padding = options.padding || 0;
Expand Down Expand Up @@ -208,21 +235,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.rowHeight !== (options?.rowSize ?? options?.rowHeight ?? this.rowHeight)
|| this.orientation !== (options?.orientation ?? this.orientation)
|| this.headingHeight !== (options?.headingHeight ?? this.headingHeight)
|| this.loaderHeight !== (options?.loaderHeight ?? this.loaderHeight)
|| this.headingHeight !== (options?.headingSize ?? options?.headingHeight ?? this.headingHeight)
|| this.loaderHeight !== (options?.loaderSize ?? options?.loaderHeight ?? this.loaderHeight)
|| 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 +267,12 @@ export class ListLayout<T, O extends ListLayoutOptions = ListLayoutOptions> exte
}

let options = invalidationContext.layoutOptions;
this.rowHeight = options?.rowHeight ?? this.rowHeight;
this.rowHeight = options?.rowSize ?? options?.rowHeight ?? this.rowHeight;
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.estimatedRowHeight = options?.estimatedRowSize ?? options?.estimatedRowHeight ?? this.estimatedRowHeight;
this.headingHeight = options?.headingSize ?? options?.headingHeight ?? this.headingHeight;
this.estimatedHeadingHeight = options?.estimatedHeadingSize ?? options?.estimatedHeadingHeight ?? this.estimatedHeadingHeight;
this.loaderHeight = options?.loaderSize ?? options?.loaderHeight ?? this.loaderHeight;
this.dropIndicatorThickness = options?.dropIndicatorThickness ?? this.dropIndicatorThickness;
this.gap = options?.gap ?? this.gap;
this.padding = options?.padding ?? this.padding;
Expand Down
26 changes: 24 additions & 2 deletions packages/react-stately/src/layout/TableLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,29 @@ import {Size} from '../virtualizer/Size';
import {ITableCollection as TableCollection} from '../table/TableCollection';
import {TableColumnLayout} from '../table/TableColumnLayout';

export interface TableLayoutProps extends ListLayoutOptions {
export interface TableLayoutProps extends Omit<ListLayoutOptions, 'orientation' | 'rowSize' | 'estimatedRowSize' | 'headingSize' | 'estimatedHeadingSize' | 'loaderSize'> {
/**
* The fixed height of a row in px.
* @default 48
*/
rowHeight?: number,
/** The estimated height of a row, when row heights are variable.
*/
estimatedRowHeight?: number,
/**
* The fixed height of a section header in px.
* @default 48
*/
headingHeight?: number,
/** The estimated height of a section header, when the height is variable.
*/
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
*/
loaderHeight?: number,
columnWidths?: Map<Key, number>
}

Expand All @@ -39,7 +61,7 @@ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> exten
private lastPersistedKeys: Set<Key> | null = null;
private persistedIndices: Map<Key, number[]> = new Map();

constructor(options?: ListLayoutOptions) {
constructor(options?: TableLayoutProps) {
super(options);
this.stickyColumnIndices = [];
}
Expand Down
20 changes: 20 additions & 0 deletions starters/docs/src/GridList.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@
display: grid;
grid-template-columns: auto;
align-items: center;

&[data-orientation=horizontal] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need this in the tailwind starter as well

display: flex;
flex-direction: row;
justify-content: normal;

.react-aria-GridListItem {
flex-shrink: 0;
width: var(--grid-item-size);
}
}
}

&[data-layout=grid][data-orientation=horizontal] {
grid-auto-flow: column;
grid-template-rows: auto auto;
grid-template-columns: none;
grid-auto-columns: var(--grid-item-size);
justify-content: normal;
max-height: none;
}

&[data-focus-visible] {
Expand Down
Loading