12
12
13
13
import { DragEvent , HTMLAttributes , RefObject , useRef , useState } from 'react' ;
14
14
import * as DragManager from './DragManager' ;
15
- import { DragTypes , readFromDataTransfer } from './utils' ;
15
+ import { DragTypes , globalAllowedDropOperations , readFromDataTransfer } from './utils' ;
16
16
import { DROP_EFFECT_TO_DROP_OPERATION , DROP_OPERATION , DROP_OPERATION_ALLOWED , DROP_OPERATION_TO_DROP_EFFECT } from './constants' ;
17
17
import { DropActivateEvent , DropEnterEvent , DropEvent , DropExitEvent , DropMoveEvent , DropOperation , DragTypes as IDragTypes } from '@react-types/shared' ;
18
- import { useLayoutEffect } from '@react-aria/utils' ;
18
+ import { isIPad , isMac , useLayoutEffect } from '@react-aria/utils' ;
19
19
import { useVirtualDrop } from './useVirtualDrop' ;
20
20
21
21
export interface DropOptions {
@@ -55,7 +55,7 @@ export function useDrop(options: DropOptions): DropResult {
55
55
y : 0 ,
56
56
dragOverElements : new Set < Element > ( ) ,
57
57
dropEffect : 'none' as DataTransfer [ 'dropEffect' ] ,
58
- effectAllowed : 'none' as DataTransfer [ 'effectAllowed' ] ,
58
+ allowedOperations : DROP_OPERATION . all ,
59
59
dropActivateTimer : null
60
60
} ) . current ;
61
61
@@ -89,7 +89,8 @@ export function useDrop(options: DropOptions): DropResult {
89
89
e . preventDefault ( ) ;
90
90
e . stopPropagation ( ) ;
91
91
92
- if ( e . clientX === state . x && e . clientY === state . y && e . dataTransfer . effectAllowed === state . effectAllowed ) {
92
+ let allowedOperations = getAllowedOperations ( e ) ;
93
+ if ( e . clientX === state . x && e . clientY === state . y && allowedOperations === state . allowedOperations ) {
93
94
e . dataTransfer . dropEffect = state . dropEffect ;
94
95
return ;
95
96
}
@@ -100,29 +101,27 @@ export function useDrop(options: DropOptions): DropResult {
100
101
let prevDropEffect = state . dropEffect ;
101
102
102
103
// Update drop effect if allowed drop operations changed (e.g. user pressed modifier key).
103
- if ( e . dataTransfer . effectAllowed !== state . effectAllowed ) {
104
- let allowedOperations = effectAllowedToOperations ( e . dataTransfer . effectAllowed ) ;
105
- let dropOperation = allowedOperations [ 0 ] ;
104
+ if ( allowedOperations !== state . allowedOperations ) {
105
+ let allowedOps = allowedOperationsToArray ( allowedOperations ) ;
106
+ let dropOperation = allowedOps [ 0 ] ;
106
107
if ( typeof options . getDropOperation === 'function' ) {
107
108
let types = new DragTypes ( e . dataTransfer ) ;
108
- dropOperation = getDropOperation ( e . dataTransfer . effectAllowed , options . getDropOperation ( types , allowedOperations ) ) ;
109
+ dropOperation = getDropOperation ( allowedOperations , options . getDropOperation ( types , allowedOps ) ) ;
109
110
}
110
-
111
111
state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
112
112
}
113
113
114
114
if ( typeof options . getDropOperationForPoint === 'function' ) {
115
- let allowedOperations = effectAllowedToOperations ( e . dataTransfer . effectAllowed ) ;
116
115
let types = new DragTypes ( e . dataTransfer ) ;
117
116
let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
118
117
let dropOperation = getDropOperation (
119
- e . dataTransfer . effectAllowed ,
120
- options . getDropOperationForPoint ( types , allowedOperations , state . x - rect . x , state . y - rect . y )
118
+ allowedOperations ,
119
+ options . getDropOperationForPoint ( types , allowedOperationsToArray ( allowedOperations ) , state . x - rect . x , state . y - rect . y )
121
120
) ;
122
121
state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
123
122
}
124
123
125
- state . effectAllowed = e . dataTransfer . effectAllowed ;
124
+ state . allowedOperations = allowedOperations ;
126
125
e . dataTransfer . dropEffect = state . dropEffect ;
127
126
128
127
// If the drop operation changes, update state and fire events appropriately.
@@ -162,26 +161,27 @@ export function useDrop(options: DropOptions): DropResult {
162
161
return ;
163
162
}
164
163
165
- let allowedOperations = effectAllowedToOperations ( e . dataTransfer . effectAllowed ) ;
164
+ let allowedOperationsBits = getAllowedOperations ( e ) ;
165
+ let allowedOperations = allowedOperationsToArray ( allowedOperationsBits ) ;
166
166
let dropOperation = allowedOperations [ 0 ] ;
167
167
168
168
if ( typeof options . getDropOperation === 'function' ) {
169
169
let types = new DragTypes ( e . dataTransfer ) ;
170
- dropOperation = getDropOperation ( e . dataTransfer . effectAllowed , options . getDropOperation ( types , allowedOperations ) ) ;
170
+ dropOperation = getDropOperation ( allowedOperationsBits , options . getDropOperation ( types , allowedOperations ) ) ;
171
171
}
172
172
173
173
if ( typeof options . getDropOperationForPoint === 'function' ) {
174
174
let types = new DragTypes ( e . dataTransfer ) ;
175
175
let rect = ( e . currentTarget as HTMLElement ) . getBoundingClientRect ( ) ;
176
176
dropOperation = getDropOperation (
177
- e . dataTransfer . effectAllowed ,
177
+ allowedOperationsBits ,
178
178
options . getDropOperationForPoint ( types , allowedOperations , e . clientX - rect . x , e . clientY - rect . y )
179
179
) ;
180
180
}
181
181
182
182
state . x = e . clientX ;
183
183
state . y = e . clientY ;
184
- state . effectAllowed = e . dataTransfer . effectAllowed ;
184
+ state . allowedOperations = allowedOperationsBits ;
185
185
state . dropEffect = DROP_OPERATION_TO_DROP_EFFECT [ dropOperation ] || 'none' ;
186
186
e . dataTransfer . dropEffect = state . dropEffect ;
187
187
@@ -294,8 +294,66 @@ export function useDrop(options: DropOptions): DropResult {
294
294
} ;
295
295
}
296
296
297
- function effectAllowedToOperations ( effectAllowed : string ) {
298
- let allowedOperationsBits = DROP_OPERATION_ALLOWED [ effectAllowed ] ;
297
+ function getAllowedOperations ( e : DragEvent ) {
298
+ let allowedOperations = DROP_OPERATION_ALLOWED [ e . dataTransfer . effectAllowed ] ;
299
+
300
+ // WebKit always sets effectAllowed to "copyMove" on macOS, and "all" on iOS, regardless of what was
301
+ // set during the dragstart event: https://bugs.webkit.org/show_bug.cgi?id=178058
302
+ //
303
+ // Android Chrome also sets effectAllowed to "copyMove" in all cases: https://bugs.chromium.org/p/chromium/issues/detail?id=1359182
304
+ //
305
+ // If the drag started within the page, we can use a global variable to get the real allowed operations.
306
+ // This needs to be intersected with the actual effectAllowed, which may have been filtered based on modifier keys.
307
+ // Unfortunately, this means that link operations do not work at all in Safari.
308
+ if ( globalAllowedDropOperations ) {
309
+ allowedOperations &= globalAllowedDropOperations ;
310
+ }
311
+
312
+ // Chrome and Safari on macOS will automatically filter effectAllowed when pressing modifier keys,
313
+ // allowing the user to switch between move, link, and copy operations. Firefox on macOS and all
314
+ // Windows browsers do not do this, so do it ourselves instead. The exact keys are platform dependent.
315
+ // https://ux.stackexchange.com/questions/83748/what-are-the-most-common-modifier-keys-for-dragging-objects-with-a-mouse
316
+ //
317
+ // Note that none of these modifiers are ever set in WebKit due to a bug: https://bugs.webkit.org/show_bug.cgi?id=77465
318
+ // However, Safari does update effectAllowed correctly, so we can just rely on that.
319
+ let allowedModifiers = DROP_OPERATION . none ;
320
+ if ( isMac ( ) ) {
321
+ if ( e . altKey ) {
322
+ allowedModifiers |= DROP_OPERATION . copy ;
323
+ }
324
+
325
+ // Chrome and Safari both use the Control key for link, even though Finder uses Command + Option.
326
+ // iPadOS doesn't support link operations and will not fire the drop event at all if dropEffect is set to link.
327
+ // https://bugs.webkit.org/show_bug.cgi?id=244701
328
+ if ( e . ctrlKey && ! isIPad ( ) ) {
329
+ allowedModifiers |= DROP_OPERATION . link ;
330
+ }
331
+
332
+ if ( e . metaKey ) {
333
+ allowedModifiers |= DROP_OPERATION . move ;
334
+ }
335
+ } else {
336
+ if ( e . altKey ) {
337
+ allowedModifiers |= DROP_OPERATION . link ;
338
+ }
339
+
340
+ if ( e . shiftKey ) {
341
+ allowedModifiers |= DROP_OPERATION . move ;
342
+ }
343
+
344
+ if ( e . ctrlKey ) {
345
+ allowedModifiers |= DROP_OPERATION . copy ;
346
+ }
347
+ }
348
+
349
+ if ( allowedModifiers ) {
350
+ return allowedOperations & allowedModifiers ;
351
+ }
352
+
353
+ return allowedOperations ;
354
+ }
355
+
356
+ function allowedOperationsToArray ( allowedOperationsBits : DROP_OPERATION ) {
299
357
let allowedOperations = [ ] ;
300
358
if ( allowedOperationsBits & DROP_OPERATION . move ) {
301
359
allowedOperations . push ( 'move' ) ;
@@ -312,8 +370,7 @@ function effectAllowedToOperations(effectAllowed: string) {
312
370
return allowedOperations ;
313
371
}
314
372
315
- function getDropOperation ( effectAllowed : string , operation : DropOperation ) {
316
- let allowedOperationsBits = DROP_OPERATION_ALLOWED [ effectAllowed ] ;
373
+ function getDropOperation ( allowedOperations : DROP_OPERATION , operation : DropOperation ) {
317
374
let op = DROP_OPERATION [ operation ] ;
318
- return allowedOperationsBits & op ? operation : 'cancel' ;
375
+ return allowedOperations & op ? operation : 'cancel' ;
319
376
}
0 commit comments