1010 * governing permissions and limitations under the License.
1111 */
1212
13+ import { Collection , DropEvent , DropOperation , DroppableCollectionProps , DropPosition , DropTarget , KeyboardDelegate , Node } from '@react-types/shared' ;
1314import * as DragManager from './DragManager' ;
14- import { DropOperation , DroppableCollectionProps , DropPosition , DropTarget , KeyboardDelegate } from '@react-types/shared' ;
1515import { DroppableCollectionState } from '@react-stately/dnd' ;
1616import { getTypes } from './utils' ;
17- import { HTMLAttributes , RefObject , useEffect , useRef } from 'react' ;
17+ import { HTMLAttributes , Key , RefObject , useCallback , useEffect , useLayoutEffect , useRef } from 'react' ;
1818import { mergeProps } from '@react-aria/utils' ;
19+ import { setInteractionModality } from '@react-aria/interactions' ;
1920import { useAutoScroll } from './useAutoScroll' ;
2021import { useDrop } from './useDrop' ;
2122import { useDroppableCollectionId } from './utils' ;
@@ -29,6 +30,13 @@ interface DroppableCollectionResult {
2930 collectionProps : HTMLAttributes < HTMLElement >
3031}
3132
33+ interface DroppingState {
34+ collection : Collection < Node < unknown > > ,
35+ focusedKey : Key ,
36+ selectedKeys : Set < Key > ,
37+ timeout : NodeJS . Timeout
38+ }
39+
3240const DROP_POSITIONS : DropPosition [ ] = [ 'before' , 'on' , 'after' ] ;
3341
3442export function useDroppableCollection ( props : DroppableCollectionOptions , state : DroppableCollectionState , ref : RefObject < HTMLElement > ) : DroppableCollectionResult {
@@ -96,15 +104,100 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
96104 } ,
97105 onDrop ( e ) {
98106 if ( state . target && typeof props . onDrop === 'function' ) {
99- props . onDrop ( {
100- type : 'drop' ,
101- x : e . x , // todo
102- y : e . y ,
103- target : state . target ,
104- items : e . items ,
105- dropOperation : e . dropOperation
106- } ) ;
107+ onDrop ( e , state . target ) ;
108+ }
109+ }
110+ } ) ;
111+
112+ let droppingState = useRef < DroppingState > ( null ) ;
113+ let onDrop = useCallback ( ( e : DropEvent , target : DropTarget ) => {
114+ let { state} = localState ;
115+
116+ // Focus the collection.
117+ state . selectionManager . setFocused ( true ) ;
118+
119+ // Save some state of the collection/selection before the drop occurs so we can compare later.
120+ let focusedKey = state . selectionManager . focusedKey ;
121+ droppingState . current = {
122+ timeout : null ,
123+ focusedKey,
124+ collection : state . collection ,
125+ selectedKeys : state . selectionManager . selectedKeys
126+ } ;
127+
128+ localState . props . onDrop ( {
129+ type : 'drop' ,
130+ x : e . x , // todo
131+ y : e . y ,
132+ target,
133+ items : e . items ,
134+ dropOperation : e . dropOperation
135+ } ) ;
136+
137+ // Wait for a short time period after the onDrop is called to allow the data to be read asynchronously
138+ // and for React to re-render. If an insert occurs during this time, it will be selected/focused below.
139+ // If items are not "immediately" inserted by the onDrop handler, the application will need to handle
140+ // selecting and focusing those items themselves.
141+ droppingState . current . timeout = setTimeout ( ( ) => {
142+ // If focus didn't move already (e.g. due to an insert), and the user dropped on an item,
143+ // focus that item and show the focus ring to give the user feedback that the drop occurred.
144+ // Also show the focus ring if the focused key is not selected, e.g. in case of a reorder.
145+ let { state} = localState ;
146+ if ( state . selectionManager . focusedKey === focusedKey ) {
147+ if ( target . type === 'item' && target . dropPosition === 'on' && state . collection . getItem ( target . key ) != null ) {
148+ state . selectionManager . setFocusedKey ( target . key ) ;
149+ state . selectionManager . setFocused ( true ) ;
150+ setInteractionModality ( 'keyboard' ) ;
151+ } else if ( ! state . selectionManager . isSelected ( focusedKey ) ) {
152+ setInteractionModality ( 'keyboard' ) ;
153+ }
107154 }
155+
156+ droppingState . current = null ;
157+ } , 50 ) ;
158+ } , [ localState ] ) ;
159+
160+ // eslint-disable-next-line arrow-body-style
161+ useEffect ( ( ) => {
162+ return ( ) => {
163+ if ( droppingState . current ) {
164+ clearTimeout ( droppingState . current . timeout ) ;
165+ }
166+ } ;
167+ } , [ ] ) ;
168+
169+ useLayoutEffect ( ( ) => {
170+ // If an insert occurs during a drop, we want to immediately select these items to give
171+ // feedback to the user that a drop occurred. Only do this if the selection didn't change
172+ // since the drop started so we don't override if the user or application did something.
173+ if (
174+ droppingState . current &&
175+ state . selectionManager . isFocused &&
176+ state . collection . size > droppingState . current . collection . size &&
177+ state . selectionManager . isSelectionEqual ( droppingState . current . selectedKeys )
178+ ) {
179+ let newKeys = new Set < Key > ( ) ;
180+ for ( let key of state . collection . getKeys ( ) ) {
181+ if ( ! droppingState . current . collection . getItem ( key ) ) {
182+ newKeys . add ( key ) ;
183+ }
184+ }
185+
186+ state . selectionManager . setSelectedKeys ( newKeys ) ;
187+
188+ // If the focused item didn't change since the drop occurred, also focus the first
189+ // inserted item. If selection is disabled, then also show the focus ring so there
190+ // is some indication that items were added.
191+ if ( state . selectionManager . focusedKey === droppingState . current . focusedKey ) {
192+ let first = newKeys . keys ( ) . next ( ) . value ;
193+ state . selectionManager . setFocusedKey ( first ) ;
194+
195+ if ( state . selectionManager . selectionMode === 'none' ) {
196+ setInteractionModality ( 'keyboard' ) ;
197+ }
198+ }
199+
200+ droppingState . current = null ;
108201 }
109202 } ) ;
110203
@@ -315,14 +408,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
315408 } ,
316409 onDrop ( e , target ) {
317410 if ( localState . state . target && typeof localState . props . onDrop === 'function' ) {
318- localState . props . onDrop ( {
319- type : 'drop' ,
320- x : e . x , // todo
321- y : e . y ,
322- target : target || localState . state . target ,
323- items : e . items ,
324- dropOperation : e . dropOperation
325- } ) ;
411+ onDrop ( e , target || localState . state . target ) ;
326412 }
327413 } ,
328414 onKeyDown ( e , drag ) {
@@ -440,7 +526,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
440526 }
441527 }
442528 } ) ;
443- } , [ localState , ref ] ) ;
529+ } , [ localState , ref , onDrop ] ) ;
444530
445531 let id = useDroppableCollectionId ( state ) ;
446532 return {
0 commit comments