@@ -20,7 +20,7 @@ import {DisabledBehavior, DragPreviewRenderer, Expandable, forwardRefType, Hover
20
20
import { DragAndDropContext , DropIndicatorContext , useDndPersistedKeys , useRenderDropIndicator } from './DragAndDrop' ;
21
21
import { DragAndDropHooks } from './useDragAndDrop' ;
22
22
import { DraggableCollectionState , DroppableCollectionState , Collection as ICollection , Node , SelectionBehavior , TreeState , useTreeState } from 'react-stately' ;
23
- import { filterDOMProps , useObjectRef } from '@react-aria/utils' ;
23
+ import { filterDOMProps , inertValue , LoadMoreSentinelProps , UNSTABLE_useLoadMoreSentinel , useObjectRef } from '@react-aria/utils' ;
24
24
import React , { createContext , ForwardedRef , forwardRef , JSX , ReactNode , useContext , useEffect , useMemo , useRef } from 'react' ;
25
25
import { useControlledState } from '@react-stately/utils' ;
26
26
@@ -699,51 +699,87 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent('item', <T extends o
699
699
) ;
700
700
} ) ;
701
701
702
- export interface UNSTABLE_TreeLoadingIndicatorRenderProps {
702
+ export interface UNSTABLE_TreeLoadingSentinelRenderProps extends Pick < TreeItemRenderProps , 'isFocused' | 'isFocusVisible' > {
703
703
/**
704
704
* What level the tree item has within the tree.
705
705
* @selector [data-level]
706
706
*/
707
707
level : number
708
708
}
709
709
710
- export interface TreeLoaderProps extends RenderProps < UNSTABLE_TreeLoadingIndicatorRenderProps > , StyleRenderProps < UNSTABLE_TreeLoadingIndicatorRenderProps > { }
710
+ export interface TreeLoadingSentinelProps extends Omit < LoadMoreSentinelProps , 'collection' > , RenderProps < UNSTABLE_TreeLoadingSentinelRenderProps > {
711
+ /**
712
+ * The load more spinner to render when loading additional items.
713
+ */
714
+ children ?: ReactNode | ( ( values : UNSTABLE_TreeLoadingSentinelRenderProps & { defaultChildren : ReactNode | undefined } ) => ReactNode ) ,
715
+ /**
716
+ * Whether or not the loading spinner should be rendered or not.
717
+ */
718
+ isLoading ?: boolean
719
+ }
711
720
712
- export const UNSTABLE_TreeLoadingIndicator = createLeafComponent ( 'loader' , function TreeLoader < T extends object > ( props : TreeLoaderProps , ref : ForwardedRef < HTMLDivElement > , item : Node < T > ) {
721
+ export const UNSTABLE_TreeLoadingSentinel = createLeafComponent ( 'loader' , function TreeLoadingSentinel < T extends object > ( props : TreeLoadingSentinelProps , ref : ForwardedRef < HTMLDivElement > , item : Node < T > ) {
713
722
let state = useContext ( TreeStateContext ) ! ;
714
- // This loader row is is non-interactable, but we want the same aria props calculated as a typical row
715
- // @ts -ignore
716
- let { rowProps} = useTreeItem ( { node : item } , state , ref ) ;
723
+ let { isLoading, onLoadMore, scrollOffset, ...otherProps } = props ;
724
+ let sentinelRef = useRef ( null ) ;
725
+ let memoedLoadMoreProps = useMemo ( ( ) => ( {
726
+ onLoadMore,
727
+ // TODO: this collection will update anytime a row is expanded/collapsed becaused the flattenedRows will change.
728
+ // This means onLoadMore will trigger but that might be ok cause the user should have logic to handle multiple loadMore calls
729
+ collection : state ?. collection ,
730
+ sentinelRef,
731
+ scrollOffset
732
+ } ) , [ onLoadMore , scrollOffset , state ?. collection ] ) ;
733
+ UNSTABLE_useLoadMoreSentinel ( memoedLoadMoreProps , sentinelRef ) ;
734
+
735
+ ref = useObjectRef < HTMLDivElement > ( ref ) ;
736
+ let { rowProps, gridCellProps, ...states } = useTreeItem ( { node : item } , state , ref ) ;
717
737
let level = rowProps [ 'aria-level' ] || 1 ;
718
738
719
739
let ariaProps = {
740
+ role : 'row' ,
720
741
'aria-level' : rowProps [ 'aria-level' ] ,
721
742
'aria-posinset' : rowProps [ 'aria-posinset' ] ,
722
- 'aria-setsize' : rowProps [ 'aria-setsize' ]
743
+ 'aria-setsize' : rowProps [ 'aria-setsize' ] ,
744
+ tabIndex : rowProps . tabIndex
723
745
} ;
724
746
747
+ let { isFocusVisible, focusProps} = useFocusRing ( ) ;
748
+
725
749
let renderProps = useRenderProps ( {
726
- ...props ,
750
+ ...otherProps ,
727
751
id : undefined ,
728
752
children : item . rendered ,
729
753
defaultClassName : 'react-aria-TreeLoader' ,
730
754
values : {
731
- level
755
+ level,
756
+ isFocused : states . isFocused ,
757
+ isFocusVisible
732
758
}
733
759
} ) ;
734
760
735
761
return (
736
762
< >
737
- < div
738
- role = "row"
739
- ref = { ref }
740
- { ...mergeProps ( filterDOMProps ( props as any ) , ariaProps ) }
741
- { ...renderProps }
742
- data-level = { level } >
743
- < div role = "gridcell" aria-colindex = { 1 } >
744
- { renderProps . children }
745
- </ div >
763
+ { /* Alway render the sentinel. For now onus is on the user for styling when using flex + gap (this would introduce a gap even though it doesn't take room) */ }
764
+ { /* @ts -ignore - compatibility with React < 19 */ }
765
+ < div style = { { position : 'relative' , width : 0 , height : 0 } } inert = { inertValue ( true ) } >
766
+ < div data-testid = "loadMoreSentinel" ref = { sentinelRef } style = { { position : 'absolute' , height : 1 , width : 1 } } />
746
767
</ div >
768
+ { isLoading && renderProps . children && (
769
+ < div
770
+ ref = { ref }
771
+ { ...mergeProps ( filterDOMProps ( props as any ) , ariaProps , focusProps ) }
772
+ { ...renderProps }
773
+ data-key = { rowProps [ 'data-key' ] }
774
+ data-collection = { rowProps [ 'data-collection' ] }
775
+ data-focused = { states . isFocused || undefined }
776
+ data-focus-visible = { isFocusVisible || undefined }
777
+ data-level = { level } >
778
+ < div { ...gridCellProps } >
779
+ { renderProps . children }
780
+ </ div >
781
+ </ div >
782
+ ) }
747
783
</ >
748
784
) ;
749
785
} ) ;
@@ -796,9 +832,10 @@ function flattenTree<T>(collection: TreeCollection<T>, opts: TreeGridCollectionO
796
832
keyMap . set ( node . key , node as CollectionNode < T > ) ;
797
833
}
798
834
799
- if ( node . level === 0 || ( parentKey != null && expandedKeys . has ( parentKey ) && flattenedRows . find ( row => row . key === parentKey ) ) ) {
800
- // Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
801
- flattenedRows . push ( keyMap . get ( node . key ) || node ) ;
835
+ // Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
836
+ let modifiedNode = keyMap . get ( node . key ) || node ;
837
+ if ( modifiedNode . level === 0 || ( modifiedNode . parentKey != null && expandedKeys . has ( modifiedNode . parentKey ) && flattenedRows . find ( row => row . key === modifiedNode . parentKey ) ) ) {
838
+ flattenedRows . push ( modifiedNode ) ;
802
839
}
803
840
} else if ( node . type !== null ) {
804
841
keyMap . set ( node . key , node as CollectionNode < T > ) ;
@@ -836,7 +873,7 @@ function TreeDropIndicatorWrapper(props: DropIndicatorProps, ref: ForwardedRef<H
836
873
let level = dropState && props . target . type === 'item' ? ( dropState . collection . getItem ( props . target . key ) ?. level || 0 ) + 1 : 1 ;
837
874
838
875
return (
839
- < TreeDropIndicatorForwardRef
876
+ < TreeDropIndicatorForwardRef
840
877
{ ...props }
841
878
dropIndicatorProps = { dropIndicatorProps }
842
879
isDropTarget = { isDropTarget }
0 commit comments