react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

Add support for custom collection renderers (e.g. virtualization)

Open devongovett opened this issue 1 year ago • 8 comments

This adds support for custom collection renderers to React Aria Components, which enables features like virtualized scrolling where a subset of the collection is rendered to the DOM. Other features like breadcrumb and action group collapsing can also be built as collection renderers.

A collection renderer is a function that is provided to a component via context, which means it wraps around a collection component. For example, to make a ListBox virtualized, you would wrap it in a <Virtualizer>:

<Virtualizer>
  <ListBox>
    <ListBoxItem>Foo</ListBoxItem>
  </ListBox>
</Virtualizer>

The Virtualizer component provides the CollectionRendererContext to the ListBox, which uses it to render the collection to DOM nodes. By default, CollectionRendererContext is set to a function that renders all items to the DOM (the same way as currently). This function accepts two arguments: the collection itself, and the parent node whose items should be rendered (e.g. in the case of sections). A virtualizer would then filter the list of items to include only visible items.

To enable this in a generic way, collection nodes now know how to render themselves to the DOM. This means that a CollectionRenderer does not need to know how to render specific nodes, it delegates back to the nodes themselves. This also means that custom implementations of collection components such as ListBoxItem can be created and work within an existing ListBox – the item components are no longer coupled to the parent components.

This works by creating two new APIs for building collection components:

  • createLeafComponent – creates a collection component that doesn't accept child nodes. You must specify a node type (e.g. item), and a function to render that node.
  • createBranchComponent – creates a collection component that expects children. It also accepts a node type and render function, but also accepts a function that returns the collection children (defaulting to useCollectionChildren).

The plan would be to eventually expose these APIs publicly so custom collection components can be built, but I'm not totally happy with these APIs yet. Feels like there could be something that might be able to combine them a bit more. Suggestions welcome.

devongovett avatar Feb 20 '24 19:02 devongovett

With virtualization, rendered option items should include aria-posinset and aria-setsize.

Also, when the heading for a group is scrolled out of view, the group will no longer have an accessibility name, which differs from the behavior in React-Spectrum ListBox.

majornista avatar Feb 20 '24 20:02 majornista

For reviewers, keep in mind that the demo virtualizer is not a full implementation yet, it's just a simple demo in the story. The PR is mainly about setting up the infrastructure to support custom collection renderers. More work will be needed for a full virtualizer implementation. But feel free to post the issues you run into!

devongovett avatar Feb 20 '24 23:02 devongovett

Initial testing looks good!

snowystinger avatar May 09 '24 02:05 snowystinger

I realized I will need to do some significant refactoring to Virtualizer to support RAC, which is somewhat separate from this work to support custom collection renderers. I would like to land this change initially and follow up with that additional work next. Though there might be some additional changes to the collection renderer API, this is already useful on its own for other non-virtualizer cases (e.g. collapsing breadcrumbs).

devongovett avatar May 22 '24 20:05 devongovett

## API Changes

unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any', access: 'private' } unknown top level export { type: 'any', access: 'private' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'identifier', name: 'Column' } unknown top level export { type: 'identifier', name: 'Column' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown type { type: 'link' } unknown type { type: 'link' } unknown type { type: 'link' } unknown type { type: 'link' } unknown type { type: 'link' } unknown type { type: 'link' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } unknown top level export { type: 'any' } undefined already in set

@react-aria/menu

AriaSubmenuTriggerProps

 AriaSubmenuTriggerProps {
   delay?: number = 200
   isDisabled?: boolean
-  node: RSNode<unknown>
   parentMenuRef: RefObject<HTMLElement>
   submenuRef: RefObject<HTMLElement>
   type?: 'dialog' | 'menu'
 }

it changed:

  • useSubmenuTrigger

@react-spectrum/tree

TreeViewItem

-TreeViewItem {
-  TreeViewItem?: boolean
-}
+

SpectrumTreeViewItemProps

-SpectrumTreeViewItemProps {
-  children: ReactNode
-  hasChildItems?: boolean
-}
+

undefined

-
+TreeViewItem<T extends {}> {
+  TreeViewItem?: boolean
+}

undefined

-
+SpectrumTreeViewItemProps<T extends {} = {}> {
+  childItems?: Iterable<{}>
+  children: ReactNode
+  hasChildItems?: boolean
+}

@react-stately/layout

ListLayout

 ListLayout<T> {
   allowDisabledKeyFocus: boolean
   buildChild: (Node<T>, number, number) => LayoutNode
   buildCollection: () => Array<LayoutNode>
+  buildHeader: (Node<T>, number, number) => LayoutNode
   buildItem: (Node<T>, number, number) => LayoutNode
   buildNode: (Node<T>, number, number) => LayoutNode
   buildSection: (Node<T>, number, number) => LayoutNode
   collection: Collection<Node<T>>
   disabledKeys: Set<Key>
   getContentSize: () => void
   getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
   getFinalLayoutInfo: (LayoutInfo) => void
   getFirstKey: () => Key | null
   getInitialLayoutInfo: (LayoutInfo) => void
   getKeyAbove: (Key) => Key | null
   getKeyBelow: (Key) => Key | null
   getKeyForSearch: (string, Key) => Key | null
   getKeyPageAbove: (Key) => Key | null
   getKeyPageBelow: (Key) => Key | null
   getLastKey: () => Key | null
   getLayoutInfo: (Key) => void
   getVisibleLayoutInfos: (Rect) => void
   isLoading: boolean
   isValid: (Node<T>, number) => void
   isVisible: (LayoutNode, Rect) => void
   updateItemSize: (Key, Size) => void
   updateLayoutNode: (Key, LayoutInfo, LayoutInfo) => void
   validate: (InvalidationContext<Node<T>, unknown>) => void
 }
 

rspbot avatar May 25 '24 05:05 rspbot