From f61d08e816bf596451ef5ea229f4f510eb0a1f9d Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 25 Mar 2026 11:01:43 -0700 Subject: [PATCH 01/10] prevent orientation from being exposed in S2 for gridlist --- packages/@react-spectrum/s2/src/ListView.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ListView.tsx b/packages/@react-spectrum/s2/src/ListView.tsx index 6537b3cc437..d9fd0c50fbe 100644 --- a/packages/@react-spectrum/s2/src/ListView.tsx +++ b/packages/@react-spectrum/s2/src/ListView.tsx @@ -53,7 +53,7 @@ import {useScale} from './utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; import {Virtualizer} from 'react-aria-components/Virtualizer'; -export interface ListViewProps extends Omit, 'className' | 'style' | 'children' | 'selectionBehavior' | 'dragAndDropHooks' | 'layout' | 'render' | 'keyboardNavigationBehavior' | keyof GlobalDOMAttributes>, DOMProps, UnsafeStyles, ListViewStylesProps, SlotProps { +export interface ListViewProps extends Omit, '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. */ @@ -866,4 +866,3 @@ export function ListViewItem(props: ListViewItemProps): ReactNode { ); } - From 01b5d84a02f0a5430d7ec22eb6bb00ded857c69e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 25 Mar 2026 13:07:51 -0700 Subject: [PATCH 02/10] add docs for gridlist orientation --- .../dev/s2-docs/pages/react-aria/GridList.mdx | 37 +++++++++++++++++++ starters/docs/src/GridList.css | 20 ++++++++++ 2 files changed, 57 insertions(+) diff --git a/packages/dev/s2-docs/pages/react-aria/GridList.mdx b/packages/dev/s2-docs/pages/react-aria/GridList.mdx index 0be7e40e264..28a89997092 100644 --- a/packages/dev/s2-docs/pages/react-aria/GridList.mdx +++ b/packages/dev/s2-docs/pages/react-aria/GridList.mdx @@ -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 -/// + + + {item => ( + + + {item.title} + {item.description} + + )} + +``` + ## Drag and drop GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the 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. diff --git a/starters/docs/src/GridList.css b/starters/docs/src/GridList.css index 4b74fb78712..74e558b148c 100644 --- a/starters/docs/src/GridList.css +++ b/starters/docs/src/GridList.css @@ -58,6 +58,26 @@ display: grid; grid-template-columns: auto; align-items: center; + + &[data-orientation=horizontal] { + 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] { From 4a1bba033219966fc34f7ef41dfe39b60d548cec Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 25 Mar 2026 15:05:40 -0700 Subject: [PATCH 03/10] update ListLayoutOptions to clearer names and document horizontal orientation --- .../s2-docs/pages/react-aria/Virtualizer.mdx | 45 ++++++++-- .../react-aria-components/exports/index.ts | 1 + .../react-stately/src/layout/ListLayout.ts | 89 ++++++++++++------- .../react-stately/src/layout/TableLayout.ts | 26 +++++- 4 files changed, 123 insertions(+), 38 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index c6663a908ad..37c1d46d018 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -12,7 +12,7 @@ export const description = 'Renders a scrollable collection of data using custom {docs.exports.Virtualizer.description} -```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'; @@ -49,9 +49,9 @@ Virtualizer uses 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. -```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'; @@ -80,6 +80,41 @@ for (let i = 0; i < 5000; i++) { ``` + +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}); +} + + + {/*- end highlight -*/} + + {(item) => {item.name}} + + +``` + ### 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. @@ -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'; @@ -501,4 +536,4 @@ for (let i = 0; i < 1000; i++) { ### TableLayout - + diff --git a/packages/react-aria-components/exports/index.ts b/packages/react-aria-components/exports/index.ts index 62757ad11c8..c05e33ed28c 100644 --- a/packages/react-aria-components/exports/index.ts +++ b/packages/react-aria-components/exports/index.ts @@ -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'; diff --git a/packages/react-stately/src/layout/ListLayout.ts b/packages/react-stately/src/layout/ListLayout.ts index 2fd0a574f5a..4fa61709049 100644 --- a/packages/react-stately/src/layout/ListLayout.ts +++ b/packages/react-stately/src/layout/ListLayout.ts @@ -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 @@ -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 @@ -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 extends Layout, O> implements DropTargetDelegate { protected rowHeight: number | null; @@ -103,12 +130,12 @@ export class ListLayout exte */ constructor(options: ListLayoutOptions = {}) { super(); - this.rowHeight = options.rowHeight ?? null; + this.rowHeight = 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.estimatedRowHeight = options?.estimatedRowSize ?? options?.estimatedRowHeight ?? null; + 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; @@ -208,21 +235,21 @@ export class ListLayout 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; @@ -240,12 +267,12 @@ export class ListLayout 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; diff --git a/packages/react-stately/src/layout/TableLayout.ts b/packages/react-stately/src/layout/TableLayout.ts index 1dab4f942ab..4794c70d046 100644 --- a/packages/react-stately/src/layout/TableLayout.ts +++ b/packages/react-stately/src/layout/TableLayout.ts @@ -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 { + /** + * 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 } @@ -39,7 +61,7 @@ export class TableLayout exten private lastPersistedKeys: Set | null = null; private persistedIndices: Map = new Map(); - constructor(options?: ListLayoutOptions) { + constructor(options?: TableLayoutProps) { super(options); this.stickyColumnIndices = []; } From 177c7fe86f937cf6e4cee8f9d4b166870019aaa1 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 25 Mar 2026 15:07:33 -0700 Subject: [PATCH 04/10] make it a bit smaller --- packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index 37c1d46d018..70715b105da 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -109,7 +109,7 @@ for (let i = 0; i < 5000; i++) { aria-label="Horizontal virtualized list" selectionMode="multiple" items={items} - style={{display: 'block', padding: 0, height: 150}}> + style={{display: 'block', padding: 0, height: 100}}> {(item) => {item.name}} From cbfdd679f3744e2172d0bdcc11488429afcbd477 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 26 Mar 2026 11:05:06 -0700 Subject: [PATCH 05/10] update virtualizer list example per review --- .../s2-docs/pages/react-aria/Virtualizer.mdx | 239 +++++++++++++++++- 1 file changed, 228 insertions(+), 11 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index 70715b105da..d92745e4a58 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -83,17 +83,228 @@ for (let i = 0; i < 5000; i++) { 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 +```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['gap', 'padding']} initialProps={{estimatedRowSize: 300, gap: 8, padding: 8, orientation: 'horizontal'}} propsObject="layoutOptions" wide "use client"; -import {Virtualizer, ListLayout} from 'react-aria-components'; +import {Virtualizer, ListLayout, Text} 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}); +///- begin collapse -/// +let imageOptions = [ + { + "id": "8SXaMMWCTGc", + "title": "A Ficus Lyrata Leaf in the sunlight (2/2) (IG: @clay.banks)", + "user": "Clay Banks", + "image": "https://images.unsplash.com/photo-1580133318324-f2f76d987dd8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666" + }, + { + "id": "pYjCqqDEOFo", + "title": "beach of Italy", + "user": "alan bajura", + "image": "https://images.unsplash.com/photo-1737100522891-e8946ac97fd1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "CF-2tl6MQj0", + "title": "A winding road in the middle of a forest", + "user": "Artem Stoliar", + "image": "https://images.unsplash.com/photo-1738249034651-1896f689be58?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.3333333333333333" + }, + { + "id": "OW97sLU0cOw", + "title": "A green and purple aurora over a snow covered forest", + "user": "Janosch Diggelmann", + "image": "https://images.unsplash.com/photo-1738189669835-61808a9d5981?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6669921875" + }, + { + "id": "WfeLZ02IhkM", + "title": "A blue and white firework is seen from above", + "user": "Janosch Diggelmann", + "image": "https://images.unsplash.com/photo-1738168601630-1c1f3ef5a95a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.3353596757852078" + }, + { + "id": "w1GpST72Bg8", + "title": "A snow covered mountain with a sky background", + "user": "Daniil Silantev", + "image": "https://images.unsplash.com/photo-1738165170747-ecc6e3a4d97c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.4978580171358629" + }, + { + "id": "0iN0KIt6lYI", + "title": "\"Pastel Sunset\"", + "user": "Marek Piwnicki", + "image": "https://images.unsplash.com/photo-1737917818689-f3b3708de5d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6249763481551561" + }, + { + "id": "-mFKPfXXUG0", + "title": "Leave the weight behind! You must make yourself light to strive upwards — to reach the light. (A serene winter landscape featuring a dense collection of bare, white trees.)", + "user": "Simon Berger", + "image": "https://images.unsplash.com/photo-1737972970322-cc2e255021bd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1" + }, + { + "id": "MOk6URQ28R4", + "title": "A snow covered tree with a sky background", + "user": "Daniil Silantev", + "image": "https://images.unsplash.com/photo-1738081359113-a7a33c509cf9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.666598611678236" + }, + { + "id": "y36Nj_edtRE", + "title": "A lake surrounded by trees covered in snow", + "user": "Daniel Seßler", + "image": "https://images.unsplash.com/photo-1736018545810-3de4c7ec25fa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.667" + }, + { + "id": "NvBV-YwlgBw", + "title": "The night sky with stars above a rock formation", + "user": "Dennis Haug", + "image": "https://images.unsplash.com/photo-1735528655501-cf671a3323c3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1" + }, + { + "id": "UthQdrPFxt0", + "title": "A pine tree covered in snow in a forest", + "user": "Anita Austvika", + "image": "https://images.unsplash.com/photo-1737312905026-5dfdff1097bc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "2k74xaf8dfc", + "title": "The sun shines through the trees in the forest", + "user": "Joyce G", + "image": "https://images.unsplash.com/photo-1736185597807-371cae1c7e4e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "Yje5kgfvCm0", + "title": "A blurry photo of a field of flowers", + "user": "Eugene Golovesov", + "image": "https://images.unsplash.com/photo-1736483065204-e55e62092780?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6661569826707442" + }, + { + "id": "G2bsj2LVttI", + "title": "A foggy road lined with trees and grass", + "user": "Ingmar H", + "image": "https://images.unsplash.com/photo-1737903071772-4d20348b4d81?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.7499509707785841" + }, + { + "id": "ppyNBOkfiuY", + "title": "A close up of a green palm tree", + "user": "Junel Mujar", + "image": "https://images.unsplash.com/photo-1736849544918-6ddb5cfc2c42?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.7507507507507507" + }, + { + "id": "UcWUMqIsld8", + "title": "A green leaf floating on top of a body of water", + "user": "Allec Gomes", + "image": "https://images.unsplash.com/photo-1737559217439-a5703e9b65cb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "xHqOVq9w8OI", + "title": "green-leafed plant", + "user": "Joshua Michaels", + "image": "https://images.unsplash.com/photo-1563364664-399838d1394c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.504" + }, + { + "id": "uWx3_XEc-Jw", + "title": "A view of a mountain covered in fog", + "user": "iuliu illes", + "image": "https://images.unsplash.com/photo-1737403428945-c584529b7b17?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.3430962343096233" + }, + { + "id": "2_3lhGt8i-Y", + "title": "A field with tall grass and fog in the background", + "user": "Ingmar H", + "image": "https://images.unsplash.com/photo-1737439987404-a3ee9fb95351?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "FV-__IOxb08", + "title": "A close up of a wave on a sandy beach", + "user": "Jonathan Borba", + "image": "https://images.unsplash.com/photo-1726502102472-2108ef2a5cae?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "_BS-vK3boOU", + "title": "Desert textures", + "user": "Braden Jarvis", + "image": "https://images.unsplash.com/photo-1722359546494-8e3a00f88e95?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.7135258358662614" + }, + { + "id": "LjAcS9lJdBg", + "title": "Tew Falls, waterfall, in Hamilton, Canada.", + "user": "Andre Portolesi", + "image": "https://images.unsplash.com/photo-1705021246536-aecfad654893?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.8" + }, + { + "id": "hlj6xJG30FE", + "title": "Find me on Instagram! @intricateexplorer", + "user": "Intricate Explorer", + "image": "https://images.unsplash.com/photo-1631641551473-fbe46919289d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.4992510164776376" + }, + { + "id": "vMoZvKeZOhw", + "title": "Salt Marshes, Isle of Harris, Scotland by Nils Leonhardt. Visit my website: https://nilsleonhardt.com/storytelling-harris/ Instagram: @am.basteir", + "user": "Nils Leonhardt", + "image": "https://images.unsplash.com/photo-1585951301678-8fd6f3b32c7e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "wCLCK9LDDjI", + "title": "An aerial view of a snow covered forest", + "user": "Lukas Hädrich", + "image": "https://images.unsplash.com/photo-1737405555489-78b3755eaa81?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.5" + }, + { + "id": "OdDx3_NB-Wk", + "title": "A close up of a tall grass with a sky in the background", + "user": "Ingmar H", + "image": "https://images.unsplash.com/photo-1737301519296-062cd324dbfa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "Gn-FOw1geFc", + "title": "Larches on Maple Pass, Washington", + "user": "noelle", + "image": "https://images.unsplash.com/photo-1737496538329-a59d10148a08?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + }, + { + "id": "VhKJHOz2tJ8", + "title": "IC 1805 La nébuleuse du coeur", + "user": "arnaud girault", + "image": "https://images.unsplash.com/photo-1737478598284-b9bc11cb1e9b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "1.504158004158004" + }, + { + "id": "w5QmH_uqB0U", + "title": "A pile of shells sitting on top of a sandy beach", + "user": "Toa Heftiba", + "image": "https://images.unsplash.com/photo-1725366351350-a64a1be919ef?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzNDA4NDh8MHwxfHRvcGljfHw2c01WalRMU2tlUXx8fHx8Mnx8MTczODM2NzE4M3w&ixlib=rb-4.0.3&q=80&w=400", + "aspectRatio": "0.6666666666666666" + } +]; +///- end collapse -/// + +for (let i = 0; imageOptions.length < 500; i++) { + imageOptions.push({...imageOptions[i % 30], id: String(i)}); } - {(item) => {item.name}} + items={imageOptions} + style={{display: 'block', padding: 0, height: 400}}> + {(item) => ( + + + {item.title} + {item.user} + + )} ``` From f1250cbd328e4c2e79698f071aaee15d0a5abc3e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 26 Mar 2026 11:20:04 -0700 Subject: [PATCH 06/10] update gridlist for horizontal --- starters/tailwind/src/GridList.tsx | 17 ++++++++++--- .../tailwind/stories/GridList.stories.tsx | 24 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/starters/tailwind/src/GridList.tsx b/starters/tailwind/src/GridList.tsx index e80c9d345ed..38abe7442d4 100644 --- a/starters/tailwind/src/GridList.tsx +++ b/starters/tailwind/src/GridList.tsx @@ -18,8 +18,11 @@ import { twMerge } from 'tailwind-merge'; export function GridList( { children, ...props }: GridListProps ) { + let isHorizontal = (props as {orientation?: 'horizontal' | 'vertical'}).orientation === 'horizontal'; return ( - + {children} ); @@ -27,11 +30,19 @@ export function GridList( const itemStyles = tv({ extend: focusRing, - base: 'relative flex gap-3 cursor-default select-none py-2 px-3 text-sm text-neutral-900 dark:text-neutral-200 border-t dark:border-t-neutral-700 border-transparent first:border-t-0 first:rounded-t-lg last:rounded-b-lg last:mb-0 -outline-offset-2', + base: [ + 'relative flex gap-3 cursor-default select-none py-2 px-3 text-sm text-neutral-900 dark:text-neutral-200 border-transparent rounded-none -outline-offset-2', + '[[data-orientation=vertical]_&]:border-t [[data-orientation=vertical]_&]:dark:border-t-neutral-700 [[data-orientation=vertical]_&]:first:border-t-0 [[data-orientation=vertical]_&]:first:rounded-t-lg [[data-orientation=vertical]_&]:last:rounded-b-lg', + '[[data-orientation=horizontal]_&]:border-l [[data-orientation=horizontal]_&]:dark:border-l-neutral-700 [[data-orientation=horizontal]_&]:first:border-l-0 [[data-orientation=horizontal]_&]:first:rounded-s-lg [[data-orientation=horizontal]_&]:last:rounded-e-lg [[data-orientation=horizontal]_&]:flex-shrink-0' + ].join(' '), variants: { isSelected: { false: 'hover:bg-neutral-100 pressed:bg-neutral-100 dark:hover:bg-neutral-700/60 dark:pressed:bg-neutral-700/60', - true: 'bg-blue-100 dark:bg-blue-700/30 hover:bg-blue-200 pressed:bg-blue-200 dark:hover:bg-blue-700/40 dark:pressed:bg-blue-700/40 border-y-blue-200 dark:border-y-blue-900 z-20' + true: [ + 'bg-blue-100 dark:bg-blue-700/30 hover:bg-blue-200 pressed:bg-blue-200 dark:hover:bg-blue-700/40 dark:pressed:bg-blue-700/40 z-20', + '[[data-orientation=vertical]_&]:border-y-blue-200 [[data-orientation=vertical]_&]:dark:border-y-blue-900', + '[[data-orientation=horizontal]_&]:border-x-blue-200 [[data-orientation=horizontal]_&]:dark:border-x-blue-900 ' + ].join(' ') }, isDisabled: { true: 'text-neutral-300 dark:text-neutral-600 forced-colors:text-[GrayText] z-10' diff --git a/starters/tailwind/stories/GridList.stories.tsx b/starters/tailwind/stories/GridList.stories.tsx index fbb287b1702..a67197b59ca 100644 --- a/starters/tailwind/stories/GridList.stories.tsx +++ b/starters/tailwind/stories/GridList.stories.tsx @@ -8,6 +8,14 @@ const meta: Meta = { parameters: { layout: 'centered' }, + argTypes: { + keyboardNavigationBehavior: { + control: { + type: 'radio' + }, + options: ['arrow', 'tab'] + } + }, tags: ['autodocs'] }; @@ -24,7 +32,21 @@ export const Example = (args: any) => ( Example.args = { onAction: null, - selectionMode: 'multiple' + selectionMode: 'multiple', + keyboardNavigationBehavior: 'arrow' +}; + +export const Horizontal = (args: any) => ( + + Chocolate + Mint + Strawberry + Vanilla + +); + +Horizontal.args = { + ...Example.args }; export const DisabledItems = (args: any) => ; From 164706ae18fafa269eee66fa4038b606d5f85470 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 26 Mar 2026 11:22:08 -0700 Subject: [PATCH 07/10] forgot to remove --- starters/tailwind/src/GridList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/tailwind/src/GridList.tsx b/starters/tailwind/src/GridList.tsx index 38abe7442d4..cc368f73eb9 100644 --- a/starters/tailwind/src/GridList.tsx +++ b/starters/tailwind/src/GridList.tsx @@ -31,7 +31,7 @@ export function GridList( const itemStyles = tv({ extend: focusRing, base: [ - 'relative flex gap-3 cursor-default select-none py-2 px-3 text-sm text-neutral-900 dark:text-neutral-200 border-transparent rounded-none -outline-offset-2', + 'relative flex gap-3 cursor-default select-none py-2 px-3 text-sm text-neutral-900 dark:text-neutral-200 border-transparent -outline-offset-2', '[[data-orientation=vertical]_&]:border-t [[data-orientation=vertical]_&]:dark:border-t-neutral-700 [[data-orientation=vertical]_&]:first:border-t-0 [[data-orientation=vertical]_&]:first:rounded-t-lg [[data-orientation=vertical]_&]:last:rounded-b-lg', '[[data-orientation=horizontal]_&]:border-l [[data-orientation=horizontal]_&]:dark:border-l-neutral-700 [[data-orientation=horizontal]_&]:first:border-l-0 [[data-orientation=horizontal]_&]:first:rounded-s-lg [[data-orientation=horizontal]_&]:last:rounded-e-lg [[data-orientation=horizontal]_&]:flex-shrink-0' ].join(' '), From c020c544a4dd34d683d4feac63fe9f1893ea6b9e Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 26 Mar 2026 16:07:52 -0700 Subject: [PATCH 08/10] rename rowHeight and other height related properties --- .../react-stately/src/layout/ListLayout.ts | 79 ++++++++++++------- .../react-stately/src/layout/TableLayout.ts | 21 +++++ 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/packages/react-stately/src/layout/ListLayout.ts b/packages/react-stately/src/layout/ListLayout.ts index 4fa61709049..09de7fb02c0 100644 --- a/packages/react-stately/src/layout/ListLayout.ts +++ b/packages/react-stately/src/layout/ListLayout.ts @@ -105,12 +105,12 @@ const DEFAULT_HEIGHT = 48; * It supports both fixed and variable size items. */ export class ListLayout extends Layout, 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; @@ -130,12 +130,12 @@ export class ListLayout exte */ constructor(options: ListLayoutOptions = {}) { super(); - this.rowHeight = options?.rowSize ?? options?.rowHeight ?? null; + this.rowSize = options?.rowSize ?? options?.rowHeight ?? null; this.orientation = options.orientation ?? 'vertical'; - this.estimatedRowHeight = options?.estimatedRowSize ?? options?.estimatedRowHeight ?? null; - this.headingHeight = options?.headingSize ?? options?.headingHeight ?? null; - this.estimatedHeadingHeight = options?.estimatedHeadingSize ?? options?.estimatedHeadingHeight ?? null; - this.loaderHeight = options?.loaderSize ?? 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; @@ -153,6 +153,31 @@ export class ListLayout 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; @@ -166,7 +191,7 @@ export class ListLayout 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; } @@ -235,10 +260,10 @@ export class ListLayout exte // Also invalidate if fixed sizes/gaps change. let options = invalidationContext.layoutOptions; return invalidationContext.sizeChanged - || this.rowHeight !== (options?.rowSize ?? options?.rowHeight ?? this.rowHeight) + || this.rowSize !== (options?.rowSize ?? options?.rowHeight ?? this.rowSize) || this.orientation !== (options?.orientation ?? this.orientation) - || this.headingHeight !== (options?.headingSize ?? options?.headingHeight ?? this.headingHeight) - || this.loaderHeight !== (options?.loaderSize ?? 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); } @@ -267,12 +292,12 @@ export class ListLayout exte } let options = invalidationContext.layoutOptions; - this.rowHeight = options?.rowSize ?? options?.rowHeight ?? this.rowHeight; + this.rowSize = options?.rowSize ?? options?.rowHeight ?? this.rowSize; this.orientation = options?.orientation ?? this.orientation; - 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.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; @@ -311,7 +336,7 @@ export class ListLayout 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; @@ -404,10 +429,10 @@ export class ListLayout 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 { @@ -436,7 +461,7 @@ export class ListLayout 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)) { @@ -471,7 +496,7 @@ export class ListLayout 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. @@ -487,7 +512,7 @@ export class ListLayout 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; } } @@ -512,7 +537,7 @@ export class ListLayout 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. @@ -525,7 +550,7 @@ export class ListLayout 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; } } diff --git a/packages/react-stately/src/layout/TableLayout.ts b/packages/react-stately/src/layout/TableLayout.ts index 4794c70d046..f6b72220116 100644 --- a/packages/react-stately/src/layout/TableLayout.ts +++ b/packages/react-stately/src/layout/TableLayout.ts @@ -71,6 +71,27 @@ export class TableLayout exten return this.virtualizer!.collection as TableCollection; } + // Preserve the old rowHeight/other "height" properties since Table doesn't support a "horizontal" orientation + protected get rowHeight(): number | null { + return super.rowHeight; + } + + protected get estimatedRowHeight(): number | null { + return super.estimatedRowHeight; + } + + protected get headingHeight(): number | null { + return super.headingHeight; + } + + protected get estimatedHeadingHeight(): number | null { + return super.estimatedHeadingHeight; + } + + protected get loaderHeight(): number | null { + return super.loaderHeight; + } + private columnsChanged(newCollection: TableCollection, oldCollection: TableCollection | null) { return !oldCollection || newCollection.columns !== oldCollection.columns && From 66e00c1186ae81c3af22b05577962d94fffe8bc6 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 27 Mar 2026 10:43:21 -0700 Subject: [PATCH 09/10] simplify and reduce image size --- .../dev/s2-docs/pages/react-aria/Virtualizer.mdx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index d92745e4a58..40b2f50619d 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -83,9 +83,9 @@ for (let i = 0; i < 5000; i++) { 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: 300, gap: 8, padding: 8, orientation: 'horizontal'}} propsObject="layoutOptions" wide +```tsx render docs={docs.exports.ListLayoutOptions} links={docs.links} props={['gap', 'padding']} initialProps={{estimatedRowSize: 100, gap: 8, padding: 8, orientation: 'horizontal'}} propsObject="layoutOptions" wide "use client"; -import {Virtualizer, ListLayout, Text} from 'react-aria-components'; +import {Virtualizer, ListLayout} from 'react-aria-components'; import {ListBox, ListBoxItem} from 'vanilla-starter/ListBox'; ///- begin collapse -/// @@ -320,12 +320,10 @@ for (let i = 0; imageOptions.length < 500; i++) { aria-label="Horizontal virtualized list" selectionMode="multiple" items={imageOptions} - style={{display: 'block', padding: 0, height: 400}}> + style={{display: 'block', padding: 0, height: 250}}> {(item) => ( - - - {item.title} - {item.user} + + )} From d39ee75195ed07892cfde5ce7ddd23fb0ed3f156 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Fri, 27 Mar 2026 10:44:35 -0700 Subject: [PATCH 10/10] Apply suggestion from @yihuiliao Co-authored-by: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> --- packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx index 40b2f50619d..a24c1187cec 100644 --- a/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx +++ b/packages/dev/s2-docs/pages/react-aria/Virtualizer.mdx @@ -49,7 +49,7 @@ Virtualizer uses obje ### List -`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={{estimatedRowSize: 75, gap: 4, padding: 4}} propsObject="layoutOptions" wide "use client";