react-spectrum
react-spectrum copied to clipboard
Add support for custom collection renderers (e.g. virtualization)
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 touseCollectionChildren
).
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.
Build successful! 🎉
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.
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!
Build successful! 🎉
Build successful! 🎉
Initial testing looks good!
Build successful! 🎉
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).
Build successful! 🎉
Build successful! 🎉
Build successful! 🎉
Build successful! 🎉
## 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
}