Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WaterfallLayout to RAC, and update GridLayout #7729

Merged
merged 10 commits into from
Feb 20, 2025

Conversation

devongovett
Copy link
Member

Depends on #7700

This moves the WaterfallLayout from S2 into RAC, and adds it to the documentation. It also updates GridLayout to support variable row heights like in S2 instead of only equal sized items.

Note that the GridLayout changes are breaking, especially in cases where it is subclassed. This was exported as UNSTABLE from RAC, but not directly from the @react-stately/layout package so we need to decide how to release this.

Also updated the docs to use the layoutOptions prop instead of passing options to the Layout constructor. This allows options to be changed at runtime without invalidating the entire layout. Virtualizer will ask the layout if it needs to invalidate in response to layoutOptions changing, meaning it doesn't need to be manually memoized by the user.

* is true, all rows will have equal heights.
* @default false
*/
preserveAspectRatio?: boolean,
Copy link
Member Author

Choose a reason for hiding this comment

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

Do we think variable row heights or fixed row heights are a better default?

@rspbot
Copy link

rspbot commented Feb 6, 2025

@rspbot
Copy link

rspbot commented Feb 6, 2025

@rspbot
Copy link

rspbot commented Feb 14, 2025

@rspbot
Copy link

rspbot commented Feb 14, 2025

reidbarber
reidbarber previously approved these changes Feb 14, 2025
Copy link
Member

@reidbarber reidbarber left a comment

Choose a reason for hiding this comment

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

New API looks good

snowystinger
snowystinger previously approved these changes Feb 17, 2025
Copy link
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

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

Looks good in general.

The one comment I have is that the scrolling experience on https://reactspectrum.blob.core.windows.net/reactspectrum/2e42453df169d922e48d7c5cf18000f2a8dc2a0a/docs/react-aria/Virtualizer.html#listlayout GridList with variable height rows due to text wrapping. is really pretty bad. Even after scrolling the entire thing so that the layoutInfos have been calculated. Oddly enough, waterfall only has this issue initially, it's fine once all the layoutinfos are calculated.
I haven't looked into why that would be, but happy to help out. I don't know if it's bad enough to block release on.

Trying to click and drag the scrollbar is pretty much a no go. When scrolling fast with the scrollwheel, the contents sometimes entirely disappear, making it hard to scan as you scroll past. Maybe the second one could be solved with a larger overscroll area?

@devongovett
Copy link
Member Author

@snowystinger I'm not seeing the behavior you describe. It scrolls really smoothly for me. Did you have the dev tools open when testing this? That definitely causes a slowdown.

@snowystinger
Copy link
Member

No, no dev tools open. Here's a screen recording of what I'm seeing. Keep in mind, in this video I already scrolled the entire list once, so all the layoutInfos should exist already. It's way worse on first scroll.
https://github.com/user-attachments/assets/f5a27696-eaa9-41cb-803d-1810c5973c87

@devongovett
Copy link
Member Author

what browser? here's how it looks in chrome for me

Screen.Recording.2025-02-18.at.11.56.22.AM.mov

@snowystinger
Copy link
Member

Also chrome
but you have a similar issue, the disappearing items I'm assuming is because I'm on an older computer, but your mouse is drifting far away from the actual scroll bar. That shouldn't be happening if we know the heights of everything.

Base automatically changed from virtualizer-docs to main February 20, 2025 01:32
@devongovett devongovett dismissed stale reviews from snowystinger and reidbarber February 20, 2025 01:32

The base branch was changed.

@rspbot
Copy link

rspbot commented Feb 20, 2025

@devongovett
Copy link
Member Author

last commit should fix verdaccio build on main

snowystinger
snowystinger previously approved these changes Feb 20, 2025
@rspbot
Copy link

rspbot commented Feb 20, 2025

@rspbot
Copy link

rspbot commented Feb 20, 2025

@rspbot
Copy link

rspbot commented Feb 20, 2025

@rspbot
Copy link

rspbot commented Feb 20, 2025

## API Changes

react-aria-components

/react-aria-components:TableLayout

-TableLayout <T> {
+TableLayout <O extends TableLayoutProps = TableLayoutProps, T> {
   constructor: (ListLayoutOptions) => void
   getContentSize: () => void
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget | null
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect) => void
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (TableLayoutProps, TableLayoutProps) => boolean
   update: (InvalidationContext<TableLayoutProps>) => void
   updateItemSize: (Key, Size) => void
   useLayoutOptions: () => void
   virtualizer: Virtualizer<{}, any> | null

/react-aria-components:Virtualizer

 Virtualizer <O> {
   children: ReactNode
-  layout: ILayout<O>
+  layout: LayoutClass<O> | ILayout<O>
   layoutOptions?: O
 }

/react-aria-components:ListLayout

-ListLayout <O = any, T> {
+ListLayout <O extends ListLayoutOptions = ListLayoutOptions, T> {
   constructor: (ListLayoutOptions) => void
   getContentSize: () => void
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget | null
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect) => void
   shouldInvalidate: (Rect, Rect) => boolean
-  update: (InvalidationContext<O>) => void
+  shouldInvalidateLayoutOptions: (ListLayoutOptions, ListLayoutOptions) => boolean
+  update: (InvalidationContext<ListLayoutOptions>) => void
   updateItemSize: (Key, Size) => void
   virtualizer: Virtualizer<{}, any> | null
 }

/react-aria-components:GridLayout

-GridLayout <O = any, T> {
+GridLayout <O extends GridLayoutOptions = GridLayoutOptions, T> {
-  constructor: (GridLayoutOptions) => void
   getContentSize: () => Size
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
-  getLayoutInfo: (Key) => LayoutInfo | null
+  getLayoutInfo: (Key) => LayoutInfo
   getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
   shouldInvalidate: (Rect, Rect) => boolean
-  update: () => void
-  updateItemSize: (Key, Size) => boolean
+  shouldInvalidateLayoutOptions: (GridLayoutOptions, GridLayoutOptions) => boolean
+  update: (InvalidationContext<GridLayoutOptions>) => void
+  updateItemSize: (Key, Size) => void
   virtualizer: Virtualizer<{}, any> | null
 }

/react-aria-components:Layout

 Layout <O = any, T extends {} = Node<any>> {
   getContentSize: () => Size
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getLayoutInfo: (Key) => LayoutInfo | null
   getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (O, O) => boolean
   update: (InvalidationContext<O>) => void
   updateItemSize: (Key, Size) => boolean
   virtualizer: Virtualizer<{}, any> | null
 }

/react-aria-components:VirtualizerProps

 VirtualizerProps <O> {
   children: ReactNode
-  layout: ILayout<O>
+  layout: LayoutClass<O> | ILayout<O>
   layoutOptions?: O
 }

/react-aria-components:GridLayoutOptions

 GridLayoutOptions {
   dropIndicatorThickness?: number = 2
   maxColumns?: number = Infinity
   maxItemSize?: Size = Infinity
   minItemSize?: Size = 200 x 200
   minSpace?: Size = 18 x 18
+  preserveAspectRatio?: boolean = false
 }

/react-aria-components:WaterfallLayout

+WaterfallLayout <O extends WaterfallLayoutOptions = WaterfallLayoutOptions, T extends {}> {
+  getContentSize: () => Size
+  getDropTargetFromPoint: (number, number) => DropTarget
+  getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
+  getKeyLeftOf: (Key) => Key | null
+  getKeyRange: (Key, Key) => Array<Key>
+  getKeyRightOf: (Key) => Key | null
+  getLayoutInfo: (Key) => LayoutInfo
+  getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
+  shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (WaterfallLayoutOptions, WaterfallLayoutOptions) => boolean
+  update: (InvalidationContext<WaterfallLayoutOptions>) => void
+  updateItemSize: (Key, Size) => void
+  virtualizer: Virtualizer<{}, any> | null
+}

/react-aria-components:WaterfallLayoutOptions

+WaterfallLayoutOptions {
+  dropIndicatorThickness?: number = 2
+  maxColumns?: number = Infinity
+  maxItemSize?: Size = Infinity
+  minItemSize?: Size = 200 x 200
+  minSpace?: Size = 18 x 18
+}

@react-spectrum/card

/@react-spectrum/card:GalleryLayout

 GalleryLayout <T> {
   _distributeWidths: (any) => void
   _findClosest: (Rect, Rect) => void
   _findClosestLayoutInfo: (Rect, Rect) => void
   buildCollection: () => void
   collection: GridCollection<T>
   constructor: (GalleryLayoutOptions) => void
   direction: Direction
   disabledKeys: Set<Key>
   getContentSize: () => void
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getFirstKey: () => void
   getKeyAbove: (Key) => void
   getKeyBelow: (Key) => void
   getKeyForSearch: (string, Key) => void
   getKeyLeftOf: (Key) => void
   getKeyPageAbove: (Key) => void
   getKeyPageBelow: (Key) => void
   getKeyRightOf: (Key) => void
   getLastKey: () => void
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect, any) => void
   isLoading: boolean
   isVisible: (LayoutInfo, Rect, boolean) => void
   itemPadding: number
   layoutType: any
   margin: number
   scale: Scale
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (O, O) => boolean
   update: (InvalidationContext<CardViewLayoutOptions>) => void
   updateItemSize: (Key, Size) => boolean
   virtualizer: Virtualizer<{}, any> | null
 }

/@react-spectrum/card:GridLayout

 GridLayout <T> {
   _findClosest: (Rect, Rect) => void
   _findClosestLayoutInfo: (Rect, Rect) => void
   buildChild: (Node<T>, number, number) => LayoutInfo
   buildCollection: () => void
   cardOrientation: Orientation
   collection: GridCollection<T>
   constructor: (GridLayoutOptions) => void
   direction: Direction
   disabledKeys: Set<Key>
   getContentSize: () => void
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getFirstKey: () => void
   getIndexAtPoint: (any, any, any) => void
   getKeyAbove: (Key) => void
   getKeyBelow: (Key) => void
   getKeyForSearch: (string, Key) => void
   getKeyLeftOf: (Key) => void
   getKeyPageAbove: (Key) => void
   getKeyPageBelow: (Key) => void
   getKeyRightOf: (Key) => void
   getLastKey: () => void
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect, any) => void
   isLoading: boolean
   isVisible: (LayoutInfo, Rect, boolean) => void
   itemPadding: number
   layoutType: any
   margin: number
   scale: Scale
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (O, O) => boolean
   update: (InvalidationContext<CardViewLayoutOptions>) => void
   updateItemSize: (Key, Size) => boolean
   virtualizer: Virtualizer<{}, any> | null
 }

/@react-spectrum/card:WaterfallLayout

 WaterfallLayout <T> {
   _findClosest: (Rect, Rect) => void
   _findClosestLayoutInfo: (Rect, Rect) => void
   buildCollection: (InvalidationContext) => void
   collection: GridCollection<T>
   constructor: (WaterfallLayoutOptions) => void
   direction: Direction
   disabledKeys: Set<Key>
   getClosestLeft: (Key) => void
   getClosestRight: (Key) => void
   getContentSize: () => void
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getFirstKey: () => void
   getKeyAbove: (Key) => void
   getKeyBelow: (Key) => void
   getKeyForSearch: (string, Key) => void
   getKeyLeftOf: (Key) => void
   getKeyPageAbove: (Key) => void
   getKeyPageBelow: (Key) => void
   getKeyRightOf: (Key) => void
   getLastKey: () => void
   getLayoutInfo: (Key) => void
   getNextColumnIndex: (any) => void
   getVisibleLayoutInfos: (Rect, any) => void
   isLoading: boolean
   isVisible: (LayoutInfo, Rect, boolean) => void
   layoutType: any
   margin: number
   scale: Scale
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (O, O) => boolean
   update: (InvalidationContext<CardViewLayoutOptions>) => void
   updateItemSize: (Key, Size) => void
   virtualizer: Virtualizer<{}, any> | null
 }

@react-stately/layout

/@react-stately/layout:GridLayoutOptions

 GridLayoutOptions {
   dropIndicatorThickness?: number = 2
   maxColumns?: number = Infinity
   maxItemSize?: Size = Infinity
   minItemSize?: Size = 200 x 200
   minSpace?: Size = 18 x 18
+  preserveAspectRatio?: boolean = false
 }

/@react-stately/layout:TableLayoutProps

 TableLayoutProps {
   columnWidths?: Map<Key, number>
+  dropIndicatorThickness?: number = 2
+  estimatedHeadingHeight?: number
+  estimatedRowHeight?: number
+  gap?: number = 0
+  headingHeight?: number = 48
+  loaderHeight?: number = 48
+  padding?: number = 0
+  rowHeight?: number = 48
 }

/@react-stately/layout:GridLayout

-GridLayout <O = any, T> {
+GridLayout <O extends GridLayoutOptions = GridLayoutOptions, T> {
-  constructor: (GridLayoutOptions) => void
   getContentSize: () => Size
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
-  getLayoutInfo: (Key) => LayoutInfo | null
+  getLayoutInfo: (Key) => LayoutInfo
   getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
   shouldInvalidate: (Rect, Rect) => boolean
-  update: () => void
-  updateItemSize: (Key, Size) => boolean
+  shouldInvalidateLayoutOptions: (GridLayoutOptions, GridLayoutOptions) => boolean
+  update: (InvalidationContext<GridLayoutOptions>) => void
+  updateItemSize: (Key, Size) => void
   virtualizer: Virtualizer<{}, any> | null
 }

/@react-stately/layout:ListLayout

-ListLayout <O = any, T> {
+ListLayout <O extends ListLayoutOptions = ListLayoutOptions, T> {
   constructor: (ListLayoutOptions) => void
   getContentSize: () => void
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget | null
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect) => void
   shouldInvalidate: (Rect, Rect) => boolean
-  update: (InvalidationContext<O>) => void
+  shouldInvalidateLayoutOptions: (ListLayoutOptions, ListLayoutOptions) => boolean
+  update: (InvalidationContext<ListLayoutOptions>) => void
   updateItemSize: (Key, Size) => void
   virtualizer: Virtualizer<{}, any> | null
 }

/@react-stately/layout:TableLayout

 TableLayout <O extends TableLayoutProps = TableLayoutProps, T> {
   constructor: (ListLayoutOptions) => void
   getContentSize: () => void
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget | null
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect) => void
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (TableLayoutProps, TableLayoutProps) => boolean
   update: (InvalidationContext<TableLayoutProps>) => void
   updateItemSize: (Key, Size) => void
   virtualizer: Virtualizer<{}, any> | null
 }

/@react-stately/layout:WaterfallLayoutOptions

+WaterfallLayoutOptions {
+  dropIndicatorThickness?: number = 2
+  maxColumns?: number = Infinity
+  maxItemSize?: Size = Infinity
+  minItemSize?: Size = 200 x 200
+  minSpace?: Size = 18 x 18
+}

/@react-stately/layout:WaterfallLayout

+WaterfallLayout <O extends WaterfallLayoutOptions = WaterfallLayoutOptions, T extends {}> {
+  getContentSize: () => Size
+  getDropTargetFromPoint: (number, number) => DropTarget
+  getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
+  getKeyLeftOf: (Key) => Key | null
+  getKeyRange: (Key, Key) => Array<Key>
+  getKeyRightOf: (Key) => Key | null
+  getLayoutInfo: (Key) => LayoutInfo
+  getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
+  shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (WaterfallLayoutOptions, WaterfallLayoutOptions) => boolean
+  update: (InvalidationContext<WaterfallLayoutOptions>) => void
+  updateItemSize: (Key, Size) => void
+  virtualizer: Virtualizer<{}, any> | null
+}

@react-stately/virtualizer

/@react-stately/virtualizer:InvalidationContext

 InvalidationContext <O = any> {
   contentChanged?: boolean
   itemSizeChanged?: boolean
   layoutOptions?: O
+  layoutOptionsChanged?: boolean
   offsetChanged?: boolean
   sizeChanged?: boolean
 }

/@react-stately/virtualizer:Layout

 Layout <O = any, T extends {} = Node<any>> {
   getContentSize: () => Size
   getDropTargetLayoutInfo: (ItemDropTarget) => LayoutInfo
   getLayoutInfo: (Key) => LayoutInfo | null
   getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
   shouldInvalidate: (Rect, Rect) => boolean
+  shouldInvalidateLayoutOptions: (O, O) => boolean
   update: (InvalidationContext<O>) => void
   updateItemSize: (Key, Size) => boolean
   virtualizer: Virtualizer<{}, any> | null
 }

@devongovett devongovett added this pull request to the merge queue Feb 20, 2025
Merged via the queue into main with commit 060be36 Feb 20, 2025
30 checks passed
@devongovett devongovett deleted the virtualizer-layout branch February 20, 2025 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants