@@ -38,6 +38,7 @@ import {mergeProps, useId, useLayoutEffect} from '@react-aria/utils';
38
38
import { setInteractionModality } from '@react-aria/interactions' ;
39
39
import { useAutoScroll } from './useAutoScroll' ;
40
40
import { useDrop } from './useDrop' ;
41
+ import { useLocale } from '@react-aria/i18n' ;
41
42
42
43
export interface DroppableCollectionOptions extends DroppableCollectionProps {
43
44
/** A delegate object that implements behavior for keyboard focus movement. */
@@ -59,6 +60,7 @@ interface DroppingState {
59
60
}
60
61
61
62
const DROP_POSITIONS : DropPosition [ ] = [ 'before' , 'on' , 'after' ] ;
63
+ const DROP_POSITIONS_RTL : DropPosition [ ] = [ 'after' , 'on' , 'before' ] ;
62
64
63
65
/**
64
66
* Handles drop interactions for a collection component, with support for traditional mouse and touch
@@ -315,35 +317,48 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
315
317
}
316
318
} ) ;
317
319
320
+ let { direction} = useLocale ( ) ;
318
321
useEffect ( ( ) => {
319
- let getNextTarget = ( target : DropTarget , wrap = true ) : DropTarget => {
322
+ let getNextTarget = ( target : DropTarget , wrap = true , horizontal = false ) : DropTarget => {
320
323
if ( ! target ) {
321
324
return {
322
325
type : 'root'
323
326
} ;
324
327
}
325
328
326
329
let { keyboardDelegate} = localState . props ;
327
- let nextKey = target . type === 'item'
328
- ? keyboardDelegate . getKeyBelow ( target . key )
329
- : keyboardDelegate . getFirstKey ( ) ;
330
- let dropPosition : DropPosition = 'before' ;
330
+ let nextKey : Key ;
331
+ if ( target ?. type === 'item' ) {
332
+ nextKey = horizontal ? keyboardDelegate . getKeyRightOf ( target . key ) : keyboardDelegate . getKeyBelow ( target . key ) ;
333
+ } else {
334
+ nextKey = horizontal && direction === 'rtl' ? keyboardDelegate . getLastKey ( ) : keyboardDelegate . getFirstKey ( ) ;
335
+ }
336
+ let dropPositions = horizontal && direction === 'rtl' ? DROP_POSITIONS_RTL : DROP_POSITIONS ;
337
+ let dropPosition : DropPosition = dropPositions [ 0 ] ;
331
338
332
339
if ( target . type === 'item' ) {
333
- let positionIndex = DROP_POSITIONS . indexOf ( target . dropPosition ) ;
334
- let nextDropPosition = DROP_POSITIONS [ positionIndex + 1 ] ;
335
- if ( positionIndex < DROP_POSITIONS . length - 1 && ! ( nextDropPosition === 'after' && nextKey != null ) ) {
336
- return {
337
- type : 'item' ,
338
- key : target . key ,
339
- dropPosition : nextDropPosition
340
- } ;
341
- }
340
+ // If the the keyboard delegate returned the next key in the collection,
341
+ // first try the other positions in the current key. Otherwise (e.g. in a grid layout),
342
+ // jump to the same drop position in the new key.
343
+ let nextCollectionKey = horizontal && direction === 'rtl' ? localState . state . collection . getKeyBefore ( target . key ) : localState . state . collection . getKeyAfter ( target . key ) ;
344
+ if ( nextKey == null || nextKey === nextCollectionKey ) {
345
+ let positionIndex = dropPositions . indexOf ( target . dropPosition ) ;
346
+ let nextDropPosition = dropPositions [ positionIndex + 1 ] ;
347
+ if ( positionIndex < dropPositions . length - 1 && ! ( nextDropPosition === dropPositions [ 2 ] && nextKey != null ) ) {
348
+ return {
349
+ type : 'item' ,
350
+ key : target . key ,
351
+ dropPosition : nextDropPosition
352
+ } ;
353
+ }
342
354
343
- // If the last drop position was 'after', then 'before' on the next key is equivalent.
344
- // Switch to 'on' instead.
345
- if ( target . dropPosition === 'after' ) {
346
- dropPosition = 'on' ;
355
+ // If the last drop position was 'after', then 'before' on the next key is equivalent.
356
+ // Switch to 'on' instead.
357
+ if ( target . dropPosition === dropPositions [ 2 ] ) {
358
+ dropPosition = 'on' ;
359
+ }
360
+ } else {
361
+ dropPosition = target . dropPosition ;
347
362
}
348
363
}
349
364
@@ -364,28 +379,40 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
364
379
} ;
365
380
} ;
366
381
367
- let getPreviousTarget = ( target : DropTarget , wrap = true ) : DropTarget => {
382
+ let getPreviousTarget = ( target : DropTarget , wrap = true , horizontal = false ) : DropTarget => {
368
383
let { keyboardDelegate} = localState . props ;
369
- let nextKey = target ?. type === 'item'
370
- ? keyboardDelegate . getKeyAbove ( target . key )
371
- : keyboardDelegate . getLastKey ( ) ;
372
- let dropPosition : DropPosition = ! target || target . type === 'root' ? 'after' : 'on' ;
384
+ let nextKey : Key ;
385
+ if ( target ?. type === 'item' ) {
386
+ nextKey = horizontal ? keyboardDelegate . getKeyLeftOf ( target . key ) : keyboardDelegate . getKeyAbove ( target . key ) ;
387
+ } else {
388
+ nextKey = horizontal && direction === 'rtl' ? keyboardDelegate . getFirstKey ( ) : keyboardDelegate . getLastKey ( ) ;
389
+ }
390
+ let dropPositions = horizontal && direction === 'rtl' ? DROP_POSITIONS_RTL : DROP_POSITIONS ;
391
+ let dropPosition : DropPosition = ! target || target . type === 'root' ? dropPositions [ 2 ] : 'on' ;
373
392
374
393
if ( target ?. type === 'item' ) {
375
- let positionIndex = DROP_POSITIONS . indexOf ( target . dropPosition ) ;
376
- let nextDropPosition = DROP_POSITIONS [ positionIndex - 1 ] ;
377
- if ( positionIndex > 0 && nextDropPosition !== 'after' ) {
378
- return {
379
- type : 'item' ,
380
- key : target . key ,
381
- dropPosition : nextDropPosition
382
- } ;
383
- }
394
+ // If the the keyboard delegate returned the previous key in the collection,
395
+ // first try the other positions in the current key. Otherwise (e.g. in a grid layout),
396
+ // jump to the same drop position in the new key.
397
+ let prevCollectionKey = horizontal && direction === 'rtl' ? localState . state . collection . getKeyAfter ( target . key ) : localState . state . collection . getKeyBefore ( target . key ) ;
398
+ if ( nextKey == null || nextKey === prevCollectionKey ) {
399
+ let positionIndex = dropPositions . indexOf ( target . dropPosition ) ;
400
+ let nextDropPosition = dropPositions [ positionIndex - 1 ] ;
401
+ if ( positionIndex > 0 && nextDropPosition !== dropPositions [ 2 ] ) {
402
+ return {
403
+ type : 'item' ,
404
+ key : target . key ,
405
+ dropPosition : nextDropPosition
406
+ } ;
407
+ }
384
408
385
- // If the last drop position was 'before', then 'after' on the previous key is equivalent.
386
- // Switch to 'on' instead.
387
- if ( target . dropPosition === 'before' ) {
388
- dropPosition = 'on' ;
409
+ // If the last drop position was 'before', then 'after' on the previous key is equivalent.
410
+ // Switch to 'on' instead.
411
+ if ( target . dropPosition === dropPositions [ 0 ] ) {
412
+ dropPosition = 'on' ;
413
+ }
414
+ } else {
415
+ dropPosition = target . dropPosition ;
389
416
}
390
417
}
391
418
@@ -553,6 +580,20 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
553
580
}
554
581
break ;
555
582
}
583
+ case 'ArrowLeft' : {
584
+ if ( keyboardDelegate . getKeyLeftOf ) {
585
+ let target = nextValidTarget ( localState . state . target , types , drag . allowedDropOperations , ( target , wrap ) => getPreviousTarget ( target , wrap , true ) ) ;
586
+ localState . state . setTarget ( target ) ;
587
+ }
588
+ break ;
589
+ }
590
+ case 'ArrowRight' : {
591
+ if ( keyboardDelegate . getKeyRightOf ) {
592
+ let target = nextValidTarget ( localState . state . target , types , drag . allowedDropOperations , ( target , wrap ) => getNextTarget ( target , wrap , true ) ) ;
593
+ localState . state . setTarget ( target ) ;
594
+ }
595
+ break ;
596
+ }
556
597
case 'Home' : {
557
598
if ( keyboardDelegate . getFirstKey ) {
558
599
let target = nextValidTarget ( null , types , drag . allowedDropOperations , getNextTarget ) ;
@@ -654,7 +695,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state:
654
695
}
655
696
}
656
697
} ) ;
657
- } , [ localState , ref , onDrop ] ) ;
698
+ } , [ localState , ref , onDrop , direction ] ) ;
658
699
659
700
let id = useId ( ) ;
660
701
droppableCollectionMap . set ( state , { id, ref} ) ;
0 commit comments