diff --git a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx index 71527967aa6..2cce6f9b307 100644 --- a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx +++ b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx @@ -29,7 +29,7 @@ import {baseColor, focusRing, size, style} from '../style' with { type: 'macro' import ChevronIcon from '../ui-icons/Chevron'; import {Collection, DOMRef, DOMRefValue, LinkDOMProps, Node} from '@react-types/shared'; import {controlFont, controlSize, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, Fragment, ReactNode, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import {createContext, forwardRef, ReactNode, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import FolderIcon from '../s2wf-icons/S2_Icon_FolderBreadcrumb_20_N.svg'; import {forwardRefType} from './types'; import {inertValue, useLayoutEffect} from '@react-aria/utils'; @@ -380,6 +380,7 @@ let CollapsingCollectionRenderer: CollectionRenderer = { }; let useCollectionRender = (collection: Collection>) => { + let {CollectionNode = DefaultCollectionRenderer.CollectionNode} = useContext(CollectionRendererContext); let {containerRef, onAction} = useContext(CollapseContext) ?? {}; let [visibleItems, setVisibleItems] = useState(collection.size); let {size = 'M'} = useContext(InternalBreadcrumbsContext); @@ -468,13 +469,13 @@ let useCollectionRender = (collection: Collection>) => { {visibleItems < collection.size && collection.size > 2 ? ( <> - {children[0].render?.(children[0])} + - {children.slice(sliceIndex).map(node => {node.render?.(node)})} + {children.slice(sliceIndex).map(node => )} ) : ( <> - {children.map(node => {node.render?.(node)})} + {children.map(node => )} )} diff --git a/packages/react-aria-components/src/Collection.tsx b/packages/react-aria-components/src/Collection.tsx index 8139c31249f..d3b1baf46c0 100644 --- a/packages/react-aria-components/src/Collection.tsx +++ b/packages/react-aria-components/src/Collection.tsx @@ -108,7 +108,20 @@ export const Section = /*#__PURE__*/ createBranchComponent('section', > extends HTMLAttributes { + /** The collection of items to render. */ + collection: ICollection>, + /** The node of the item to render. */ + node: T, + /** The parent node of the item to render. */ + parent: T | null, + /** The content that should be rendered before the item. */ + before?: ReactNode, + /** The content that should be rendered after the item. */ + after?: ReactNode +} + +export interface CollectionBranchProps extends HTMLAttributes { /** The collection of items to render. */ collection: ICollection>, /** The parent node of the items to render. */ @@ -128,7 +141,7 @@ export interface CollectionRootProps extends HTMLAttributes { renderDropIndicator?: (target: ItemDropTarget, keys?: Set, draggedKey?: Key) => ReactNode } -export interface CollectionRenderer { +export interface CollectionRenderer> { /** Whether this is a virtualized collection. */ isVirtualized?: boolean, /** A delegate object that provides layout information for items in the collection. */ @@ -138,15 +151,25 @@ export interface CollectionRenderer { /** A component that renders the root collection items. */ CollectionRoot: React.ComponentType, /** A component that renders the child collection items. */ - CollectionBranch: React.ComponentType + CollectionBranch: React.ComponentType, + /** A component that renders the collection item. */ + CollectionNode?: React.ComponentType> } -export const DefaultCollectionRenderer: CollectionRenderer = { +interface DefaultRenderer extends CollectionRenderer> { + /** A component that renders the collection item. */ + CollectionNode: React.ComponentType>> +} + +export const DefaultCollectionRenderer: DefaultRenderer = { CollectionRoot({collection, renderDropIndicator}) { return useCollectionRender(collection, null, renderDropIndicator); }, CollectionBranch({collection, parent, renderDropIndicator}) { return useCollectionRender(collection, parent, renderDropIndicator); + }, + CollectionNode({node, before, after}) { + return <>{before}{node.render!(node)}{after}; } }; @@ -155,22 +178,22 @@ function useCollectionRender( parent: Node | null, renderDropIndicator?: (target: ItemDropTarget, keys?: Set, draggedKey?: Key) => ReactNode ) { + let {CollectionNode = DefaultCollectionRenderer.CollectionNode} = useContext(CollectionRendererContext); + return useCachedChildren({ items: parent ? collection.getChildren!(parent.key) : collection, - dependencies: [renderDropIndicator], + dependencies: [CollectionNode, parent, renderDropIndicator], children(node) { - let rendered = node.render!(node); - if (!renderDropIndicator || node.type !== 'item') { - return rendered; + let pseudoProps = {}; + + if (renderDropIndicator && node.type === 'item') { + pseudoProps = { + before: renderDropIndicator({type: 'item', key: node.key, dropPosition: 'before'}), + after: renderAfterDropIndicators(collection, node, renderDropIndicator) + }; } - return ( - <> - {renderDropIndicator({type: 'item', key: node.key, dropPosition: 'before'})} - {rendered} - {renderAfterDropIndicators(collection, node, renderDropIndicator)} - - ); + return ; } }); } @@ -211,7 +234,7 @@ export function renderAfterDropIndicators(collection: ICollection> return afterIndicators; } -export const CollectionRendererContext = createContext(DefaultCollectionRenderer); +export const CollectionRendererContext = createContext>(DefaultCollectionRenderer); type PersistedKeysReturnValue = Set | null; export function usePersistedKeys(focusedKey: Key | null): PersistedKeysReturnValue { diff --git a/packages/react-aria-components/src/Virtualizer.tsx b/packages/react-aria-components/src/Virtualizer.tsx index 0f6d13c7667..3bc02fd2e8c 100644 --- a/packages/react-aria-components/src/Virtualizer.tsx +++ b/packages/react-aria-components/src/Virtualizer.tsx @@ -10,9 +10,10 @@ * governing permissions and limitations under the License. */ -import {CollectionBranchProps, CollectionRenderer, CollectionRendererContext, CollectionRootProps, renderAfterDropIndicators} from './Collection'; +import {CollectionBranchProps, CollectionNodeProps, CollectionRenderer, CollectionRendererContext, CollectionRootProps, renderAfterDropIndicators} from './Collection'; import {DropTargetDelegate, ItemDropTarget, Key, Node} from '@react-types/shared'; import {Layout, ReusableView, useVirtualizerState, VirtualizerState} from '@react-stately/virtualizer'; +import {mergeProps} from '@react-aria/utils'; import React, {createContext, JSX, ReactNode, useContext, useMemo} from 'react'; import {useScrollView, VirtualizerItem} from '@react-aria/virtualizer'; @@ -53,12 +54,13 @@ const LayoutContext = createContext(null); export function Virtualizer(props: VirtualizerProps): JSX.Element { let {children, layout: layoutProp, layoutOptions} = props; let layout = useMemo(() => typeof layoutProp === 'function' ? new layoutProp() : layoutProp, [layoutProp]); - let renderer: CollectionRenderer = useMemo(() => ({ + let renderer: CollectionRenderer, ReactNode>> = useMemo(() => ({ isVirtualized: true, layoutDelegate: layout, dropTargetDelegate: layout.getDropTargetFromPoint ? layout as DropTargetDelegate : undefined, CollectionRoot, - CollectionBranch + CollectionBranch, + CollectionNode }), [layout]); return ( @@ -70,7 +72,7 @@ export function Virtualizer(props: VirtualizerProps): JSX.Element { ); } -function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicator}: CollectionRootProps) { +function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicator, ...props}: CollectionRootProps) { let {layout, layoutOptions} = useContext(LayoutContext)!; let layoutOptions2 = layout.useLayoutOptions?.(); let state = useVirtualizerState({ @@ -103,9 +105,9 @@ function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicat }, scrollRef!); return ( -
+
- {renderChildren(null, state.visibleViews, renderDropIndicator)} + {useRenderChildren(null, state.visibleViews, renderDropIndicator)}
); @@ -114,41 +116,43 @@ function CollectionRoot({collection, persistedKeys, scrollRef, renderDropIndicat function CollectionBranch({parent, renderDropIndicator}: CollectionBranchProps) { let virtualizer = useContext(VirtualizerContext); let parentView = virtualizer!.virtualizer.getVisibleView(parent.key)!; - return renderChildren(parentView, Array.from(parentView.children), renderDropIndicator); -} -function renderChildren(parent: View | null, children: View[], renderDropIndicator?: (target: ItemDropTarget, keys?: Set, draggedKey?: Key) => ReactNode) { - return children.map(view => renderWrapper(parent, view, renderDropIndicator)); + return useRenderChildren(parentView, Array.from(parentView.children), renderDropIndicator); } -function renderWrapper( - parent: View | null, - reusableView: View, - renderDropIndicator?: (target: ItemDropTarget, keys?: Set, draggedKey?: Key) => ReactNode -): ReactNode { - let rendered = ( - - {reusableView.rendered} - +function CollectionNode({node, parent, before, after, ...props}: CollectionNodeProps, ReactNode>>) { + return ( + <> + {before} + + {node.rendered} + + {after} + ); +} - let {collection, layout} = reusableView.virtualizer; - let node = reusableView.content; - if (node?.type === 'item' && renderDropIndicator && layout.getDropTargetLayoutInfo) { - rendered = ( - - {renderDropIndicatorWrapper(parent, reusableView, {type: 'item', key: reusableView.content!.key, dropPosition: 'before'}, (target, keys, draggedKey) => renderDropIndicator(target, keys, draggedKey))} - {rendered} - {renderAfterDropIndicators(collection, node, (target, keys, draggedKey) => renderDropIndicatorWrapper(parent, reusableView, target, (innerTarget, innerKeys, innerDraggedKey) => renderDropIndicator(innerTarget, innerKeys, innerDraggedKey), keys, draggedKey))} - - ); - } +function useRenderChildren(parent: View | null, children: View[], renderDropIndicator?: (target: ItemDropTarget, keys?: Set, draggedKey?: Key) => ReactNode) { + let {CollectionNode: Item = CollectionNode} = useContext(CollectionRendererContext); - return rendered; + return children.map(node => { + let {collection, layout} = node.virtualizer; + let pseudoProps = {}; + + if (layout.getDropTargetLayoutInfo && renderDropIndicator && node.content?.type === 'item') { + pseudoProps = { + before: renderDropIndicatorWrapper(parent, node, {type: 'item', key: node.content!.key, dropPosition: 'before'}, renderDropIndicator), + after: renderAfterDropIndicators(collection, node.content, target => renderDropIndicatorWrapper(parent, node, target, renderDropIndicator)) + }; + } + + return ; + }); } function renderDropIndicatorWrapper(