10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
+ import { Collection , DropEvent , DropOperation , DroppableCollectionProps , DropPosition , DropTarget , KeyboardDelegate , Node } from '@react-types/shared' ;
13
14
import * as DragManager from './DragManager' ;
14
- import { DropOperation , DroppableCollectionProps , DropPosition , DropTarget , KeyboardDelegate } from '@react-types/shared' ;
15
15
import { DroppableCollectionState } from '@react-stately/dnd' ;
16
16
import { getTypes } from './utils' ;
17
- import { HTMLAttributes , RefObject , useEffect , useRef } from 'react' ;
17
+ import { HTMLAttributes , Key , RefObject , useCallback , useEffect , useLayoutEffect , useRef } from 'react' ;
18
18
import { mergeProps } from '@react-aria/utils' ;
19
+ import { setInteractionModality } from '@react-aria/interactions' ;
19
20
import { useAutoScroll } from './useAutoScroll' ;
20
21
import { useDrop } from './useDrop' ;
21
22
import { useDroppableCollectionId } from './utils' ;
@@ -29,6 +30,13 @@ interface DroppableCollectionResult {
29
30
collectionProps : HTMLAttributes < HTMLElement >
30
31
}
31
32
33
+ interface DroppingState {
34
+ collection : Collection < Node < unknown > > ,
35
+ focusedKey : Key ,
36
+ selectedKeys : Set < Key > ,
37
+ timeout : NodeJS . Timeout
38
+ }
39
+
32
40
const DROP_POSITIONS : DropPosition [ ] = [ 'before' , 'on' , 'after' ] ;
33
41
34
42
export function useDroppableCollection ( props : DroppableCollectionOptions , state : DroppableCollectionState , ref : RefObject < HTMLElement > ) : DroppableCollectionResult {
@@ -96,15 +104,100 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
96
104
} ,
97
105
onDrop ( e ) {
98
106
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
+ }
107
154
}
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 ;
108
201
}
109
202
} ) ;
110
203
@@ -315,14 +408,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
315
408
} ,
316
409
onDrop ( e , target ) {
317
410
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 ) ;
326
412
}
327
413
} ,
328
414
onKeyDown ( e , drag ) {
@@ -440,7 +526,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
440
526
}
441
527
}
442
528
} ) ;
443
- } , [ localState , ref ] ) ;
529
+ } , [ localState , ref , onDrop ] ) ;
444
530
445
531
let id = useDroppableCollectionId ( state ) ;
446
532
return {
0 commit comments