@@ -16,7 +16,7 @@ import {DragEndEvent, DragItem, DropActivateEvent, DropEnterEvent, DropEvent, Dr
16
16
import { getDragModality , getTypes } from './utils' ;
17
17
import { isVirtualClick , isVirtualPointerEvent } from '@react-aria/utils' ;
18
18
import type { LocalizedStringFormatter } from '@internationalized/string' ;
19
- import { useEffect , useState } from 'react' ;
19
+ import { RefObject , useEffect , useState } from 'react' ;
20
20
21
21
let dropTargets = new Map < Element , DropTarget > ( ) ;
22
22
let dropItems = new Map < Element , DroppableItem > ( ) ;
@@ -32,7 +32,8 @@ interface DropTarget {
32
32
onDropTargetEnter ?: ( target : DroppableCollectionTarget | null ) => void ,
33
33
onDropActivate ?: ( e : DropActivateEvent , target : DroppableCollectionTarget | null ) => void ,
34
34
onDrop ?: ( e : DropEvent , target : DroppableCollectionTarget | null ) => void ,
35
- onKeyDown ?: ( e : KeyboardEvent , dragTarget : DragTarget ) => void
35
+ onKeyDown ?: ( e : KeyboardEvent , dragTarget : DragTarget ) => void ,
36
+ activateButtonRef ?: RefObject < FocusableElement | null >
36
37
}
37
38
38
39
export function registerDropTarget ( target : DropTarget ) {
@@ -47,7 +48,8 @@ export function registerDropTarget(target: DropTarget) {
47
48
interface DroppableItem {
48
49
element : FocusableElement ,
49
50
target : DroppableCollectionTarget ,
50
- getDropOperation ?: ( types : Set < string > , allowedOperations : DropOperation [ ] ) => DropOperation
51
+ getDropOperation ?: ( types : Set < string > , allowedOperations : DropOperation [ ] ) => DropOperation ,
52
+ activateButtonRef ?: RefObject < FocusableElement | null >
51
53
}
52
54
53
55
export function registerDropItem ( item : DroppableItem ) {
@@ -241,15 +243,26 @@ class DragSession {
241
243
this . cancelEvent ( e ) ;
242
244
243
245
if ( e . key === 'Enter' ) {
244
- if ( e . altKey ) {
245
- this . activate ( ) ;
246
+ if ( e . altKey || this . getCurrentActivateButton ( ) ?. contains ( e . target as Node ) ) {
247
+ this . activate ( this . currentDropTarget , this . currentDropItem ) ;
246
248
} else {
247
249
this . drop ( ) ;
248
250
}
249
251
}
250
252
}
251
253
254
+ getCurrentActivateButton ( ) : FocusableElement | null {
255
+ return this . currentDropItem ?. activateButtonRef ?. current ?? this . currentDropTarget ?. activateButtonRef ?. current ?? null ;
256
+ }
257
+
252
258
onFocus ( e : FocusEvent ) {
259
+ let activateButton = this . getCurrentActivateButton ( ) ;
260
+ if ( e . target === activateButton ) {
261
+ // TODO: canceling this breaks the focus ring. Revisit when we support tabbing.
262
+ this . cancelEvent ( e ) ;
263
+ return ;
264
+ }
265
+
253
266
// Prevent focus events, except to the original drag target.
254
267
if ( e . target !== this . dragTarget . element ) {
255
268
this . cancelEvent ( e ) ;
@@ -265,6 +278,9 @@ class DragSession {
265
278
this . validDropTargets . find ( target => target . element . contains ( e . target as HTMLElement ) ) ;
266
279
267
280
if ( ! dropTarget ) {
281
+ // if (e.target === activateButton) {
282
+ // activateButton.focus();
283
+ // }
268
284
if ( this . currentDropTarget ) {
269
285
this . currentDropTarget . element . focus ( ) ;
270
286
} else {
@@ -274,10 +290,18 @@ class DragSession {
274
290
}
275
291
276
292
let item = dropItems . get ( e . target as HTMLElement ) ;
277
- this . setCurrentDropTarget ( dropTarget , item ) ;
293
+ if ( dropTarget ) {
294
+ this . setCurrentDropTarget ( dropTarget , item ) ;
295
+ }
278
296
}
279
297
280
298
onBlur ( e : FocusEvent ) {
299
+ let activateButton = this . getCurrentActivateButton ( ) ;
300
+ if ( e . relatedTarget === activateButton ) {
301
+ this . cancelEvent ( e ) ;
302
+ return ;
303
+ }
304
+
281
305
if ( e . target !== this . dragTarget . element ) {
282
306
this . cancelEvent ( e ) ;
283
307
}
@@ -296,14 +320,21 @@ class DragSession {
296
320
onClick ( e : MouseEvent ) {
297
321
this . cancelEvent ( e ) ;
298
322
if ( isVirtualClick ( e ) || this . isVirtualClick ) {
323
+ let dropElements = dropItems . values ( ) ;
324
+ let item = [ ...dropElements ] . find ( item => item . element === e . target as HTMLElement || item . activateButtonRef ?. current ?. contains ( e . target as HTMLElement ) ) ;
325
+ let dropTarget = this . validDropTargets . find ( target => target . element . contains ( e . target as HTMLElement ) ) ;
326
+ let activateButton = item ?. activateButtonRef ?. current ?? dropTarget ?. activateButtonRef ?. current ;
327
+ if ( activateButton ?. contains ( e . target as HTMLElement ) && dropTarget ) {
328
+ this . activate ( dropTarget , item ) ;
329
+ return ;
330
+ }
331
+
299
332
if ( e . target === this . dragTarget . element ) {
300
333
this . cancel ( ) ;
301
334
return ;
302
335
}
303
336
304
- let dropTarget = this . validDropTargets . find ( target => target . element . contains ( e . target as HTMLElement ) ) ;
305
337
if ( dropTarget ) {
306
- let item = dropItems . get ( e . target as HTMLElement ) ;
307
338
this . setCurrentDropTarget ( dropTarget , item ) ;
308
339
this . drop ( item ) ;
309
340
}
@@ -319,7 +350,7 @@ class DragSession {
319
350
320
351
cancelEvent ( e : Event ) {
321
352
// Allow focusin and focusout on the drag target so focus ring works properly.
322
- if ( ( e . type === 'focusin' || e . type === 'focusout' ) && e . target === this . dragTarget ?. element ) {
353
+ if ( ( e . type === 'focusin' || e . type === 'focusout' ) && ( e . target === this . dragTarget ?. element || e . target === this . getCurrentActivateButton ( ) ) ) {
323
354
return ;
324
355
}
325
356
@@ -375,14 +406,23 @@ class DragSession {
375
406
376
407
this . restoreAriaHidden = ariaHideOutside ( [
377
408
this . dragTarget . element ,
378
- ...validDropItems . map ( item => item . element ) ,
379
- ...visibleDropTargets . map ( target => target . element )
409
+ ...validDropItems . flatMap ( item => item . activateButtonRef ?. current ? [ item . element , item . activateButtonRef ?. current ] : [ item . element ] ) ,
410
+ ...visibleDropTargets . flatMap ( target => target . activateButtonRef ?. current ? [ target . element , target . activateButtonRef ?. current ] : [ target . element ] )
380
411
] ) ;
381
412
382
413
this . mutationObserver . observe ( document . body , { subtree : true , attributes : true , attributeFilter : [ 'aria-hidden' ] } ) ;
383
414
}
384
415
385
416
next ( ) {
417
+ // TODO: Allow tabbing to the activate button. Revisit once we fix the focus ring.
418
+ // For now, the activate button is reachable by screen readers and ArrowLeft/ArrowRight
419
+ // is usable specifically by Tree. Will need tabbing for other components.
420
+ // let activateButton = this.getCurrentActivateButton();
421
+ // if (activateButton && document.activeElement !== activateButton) {
422
+ // activateButton.focus();
423
+ // return;
424
+ // }
425
+
386
426
if ( ! this . currentDropTarget ) {
387
427
this . setCurrentDropTarget ( this . validDropTargets [ 0 ] ) ;
388
428
return ;
@@ -409,6 +449,15 @@ class DragSession {
409
449
}
410
450
411
451
previous ( ) {
452
+ // let activateButton = this.getCurrentActivateButton();
453
+ // if (activateButton && document.activeElement === activateButton) {
454
+ // let target = this.currentDropItem ?? this.currentDropTarget;
455
+ // if (target) {
456
+ // target.element.focus();
457
+ // return;
458
+ // }
459
+ // }
460
+
412
461
if ( ! this . currentDropTarget ) {
413
462
this . setCurrentDropTarget ( this . validDropTargets [ this . validDropTargets . length - 1 ] ) ;
414
463
return ;
@@ -487,7 +536,6 @@ class DragSession {
487
536
if ( this . currentDropTarget && typeof this . currentDropTarget . onDropTargetEnter === 'function' ) {
488
537
this . currentDropTarget . onDropTargetEnter ( item . target ) ;
489
538
}
490
-
491
539
item . element . focus ( ) ;
492
540
this . currentDropItem = item ;
493
541
@@ -576,14 +624,15 @@ class DragSession {
576
624
announce ( this . stringFormatter . format ( 'dropComplete' ) ) ;
577
625
}
578
626
579
- activate ( ) {
580
- if ( this . currentDropTarget && typeof this . currentDropTarget . onDropActivate === 'function' ) {
581
- let rect = this . currentDropTarget . element . getBoundingClientRect ( ) ;
582
- this . currentDropTarget . onDropActivate ( {
627
+ activate ( dropTarget : DropTarget | null , dropItem : DroppableItem | null | undefined ) {
628
+ if ( dropTarget && typeof dropTarget . onDropActivate === 'function' ) {
629
+ let target = dropItem ?. target ?? null ;
630
+ let rect = dropTarget . element . getBoundingClientRect ( ) ;
631
+ dropTarget . onDropActivate ( {
583
632
type : 'dropactivate' ,
584
633
x : rect . left + ( rect . width / 2 ) ,
585
634
y : rect . top + ( rect . height / 2 )
586
- } , null ) ;
635
+ } , target ) ;
587
636
}
588
637
}
589
638
}
0 commit comments