Skip to content
This repository was archived by the owner on Aug 18, 2025. It is now read-only.

Commit 24f01df

Browse files
authored
Moving between lists (#73)
Adding support for moving between lists
1 parent 4e128a3 commit 24f01df

File tree

102 files changed

+7795
-2499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+7795
-2499
lines changed

.eslintrc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@
8787
// All blocks must be wrapped in curly braces {}
8888
// Preventing if(condition) return;
8989
// https://eslint.org/docs/rules/curly
90-
"curly": ["error", "all"]
90+
"curly": ["error", "all"],
91+
92+
// Allowing Math.pow rather than forcing `**`
93+
// https://eslint.org/docs/rules/no-restricted-properties
94+
"no-restricted-properties": ["off", {
95+
"object": "Math",
96+
"property": "pow"
97+
}]
9198
}
9299
}

README.md

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class App extends Component {
143143
)}
144144
</Draggable>
145145
))}
146+
{provided.placeholder}
146147
</div>
147148
)}
148149
</Droppable>
@@ -390,7 +391,7 @@ type DraggableLocation = {|
390391
391392
### Best practices for `hooks`
392393
393-
**Block updates during a drag**
394+
#### Block updates during a drag
394395
395396
It is **highly** recommended that while a user is dragging that you block any state updates that might impact the amount of `Draggable`s and `Droppable`s, or their dimensions. Please listen to `onDragStart` and block updates to the `Draggable`s and `Droppable`s until you receive at `onDragEnd`.
396397
@@ -404,12 +405,7 @@ Here are a few poor user experiences that can occur if you change things *during
404405
- If you remove the node that the user is dragging the drag will instantly end
405406
- If you change the dimension of the dragging node then other things will not move out of the way at the correct time.
406407
407-
408-
**`onDragStart` and `onDragEnd` pairing**
409-
410-
We try very hard to ensure that each `onDragStart` event is paired with a single `onDragEnd` event. However, there maybe a rouge situation where this is not the case. If that occurs - it is a bug. Currently there is no mechanism to tell the library to cancel a current drag externally.
411-
412-
**Style**
408+
#### Add a cursor style and block selection
413409
414410
During a drag it is recommended that you add two styles to the body:
415411
@@ -420,9 +416,24 @@ During a drag it is recommended that you add two styles to the body:
420416
421417
`cursor: [your desired cursor];` is needed because we apply `pointer-events: none;` to the dragging item. This prevents you setting your own cursor style on the Draggable directly based on `snapshot.isDragging` (see `Draggable`).
422418
419+
#### Force focus after a transition between lists
420+
421+
When an item is moved from one list to a different list it looses browser focus if it had it. This is because `React` creates a new node in this situation. It will not loose focus if transitioned within the same list. The dragging item will always have had browser focus if it is dragging with a keyboard. It is highly recommended that you give the item (which is now in a different list) focus again. You can see an example of how to do this in our stories. Here is an example of how you could do it:
422+
423+
- `onDragEnd`: move the item into the new list and record the id fo the item that has moved
424+
- When rendering the reordered list pass down a prop which will tell the newly moved item to obtain focus
425+
- In the `componentDidMount` lifecycle call back check if the item needs to gain focus based on its props (such as an `autoFocus` prop)
426+
- If focus is required - call `.focus` on the node. You can obtain the node by using `ReactDOM.findDOMNode` or monkey patching the `provided.innerRef` callback.
427+
428+
### Other `hooks` information
429+
430+
**`onDragStart` and `onDragEnd` pairing**
431+
432+
We try very hard to ensure that each `onDragStart` event is paired with a single `onDragEnd` event. However, there maybe a rouge situation where this is not the case. If that occurs - it is a bug. Currently there is no mechanism to tell the library to cancel a current drag externally.
433+
423434
**Dynamic hooks**
424435
425-
Your *hook* functions will only be captured *once at start up*. Please do not change the function after that. If there is a valid use case for this then dynamic hooks could be supported. However, at this time it is not.
436+
Your *hook* functions will only be captured *once at start up*. Please do not change the function after that. This behaviour will be changed soon to allow dynamic hooks.
426437
427438
## `Droppable`
428439
@@ -437,7 +448,8 @@ import { Droppable } from 'react-beautiful-dnd';
437448
ref={provided.innerRef}
438449
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
439450
>
440-
I am a droppable!
451+
<h2>I am a droppable!</h2>
452+
{provided.placeholder}
441453
</div>
442454
)}
443455
</Droppable>;
@@ -468,14 +480,23 @@ The function is provided with two arguments:
468480
```js
469481
type DroppableProvided = {|
470482
innerRef: (?HTMLElement) => void,
483+
placeholder: ?ReactElement,
471484
|}
472485
```
473486
474-
In order for the droppable to function correctly, **you must** bind the `provided.innerRef` to the highest possible DOM node in the `ReactElement`. We do this in order to avoid needing to use `ReactDOM` to look up your DOM node.
487+
- `provided.innerRef`: In order for the droppable to function correctly, **you must** bind the `provided.innerRef` to the highest possible DOM node in the `ReactElement`. We do this in order to avoid needing to use `ReactDOM` to look up your DOM node.
488+
489+
- `provided.placeholder`: This is used to create space in the `Droppable` as needed during a drag. This space is needed when a user is dragging over a list that is not the home list. Please be sure to put the placeholder inside of the component that you have provided the ref for. We need to increase the side of the `Droppable` itself. This is different from `Draggable` where the `placeholder` needs to be a *silbing* to the draggable node.
475490
476491
```js
477492
<Droppable droppableId="droppable-1">
478-
{(provided, snapshot) => <div ref={provided.innerRef}>Good to go</div>}
493+
{(provided, snapshot) => (
494+
<div ref={provided.innerRef}>
495+
Good to go
496+
497+
{provided.placeholder}
498+
</div>
499+
)}
479500
</Droppable>;
480501
```
481502
@@ -497,6 +518,8 @@ The `children` function is also provided with a small amount of state relating t
497518
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
498519
>
499520
I am a droppable!
521+
522+
{provided.placeholder}
500523
</div>
501524
)}
502525
</Droppable>;
@@ -681,7 +704,7 @@ type NotDraggingStyle = {|
681704
|};
682705
```
683706
684-
- `provided.placeholder (?ReactElement)` The `Draggable` element has `position: fixed` applied to it while it is dragging. The role of the `placeholder` is to sit in the place that the `Draggable` was during a drag. It is needed to stop the `Droppable` list from collapsing when you drag. It is advised to render it as a sibling to the `Draggable` node. When the library moves to `React` 16 the `placeholder` will be removed from api.
707+
- `provided.placeholder (?ReactElement)` The `Draggable` element has `position: fixed` applied to it while it is dragging. The role of the `placeholder` is to sit in the place that the `Draggable` was during a drag. It is needed to stop the `Droppable` list from collapsing when you drag. It is advised to render it as a sibling to the `Draggable` node. This is unlike `Droppable` where the `placeholder` needs to be *within* the `Droppable` node. When the library moves to `React` 16 the `placeholder` will be removed from api.
685708
686709
```js
687710
<Draggable draggableId="draggable-1">
@@ -856,6 +879,11 @@ type DraggableLocation = {|
856879
// Droppable
857880
type DroppableProvided = {|
858881
innerRef: (?HTMLElement) => void,
882+
placeholder: ?ReactElement,
883+
|}
884+
885+
type DraggableStateSnapshot = {|
886+
isDraggingOver: boolean,
859887
|}
860888

861889
// Draggable
@@ -865,6 +893,11 @@ type DraggableProvided = {|
865893
dragHandleProps: ?DragHandleProvided,
866894
placeholder: ?ReactElement,
867895
|}
896+
897+
type DraggableStateSnapshot = {|
898+
isDragging: boolean,
899+
|}
900+
868901
type DraggableStyle = DraggingStyle | NotDraggingStyle
869902
type DraggingStyle = {|
870903
pointerEvents: 'none',

src/state/action-creators.js

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,33 @@ import type {
1616
InitialDrag,
1717
} from '../types';
1818
import noImpact from './no-impact';
19-
import getNewHomeClientOffset from './get-new-home-client-offset';
19+
import getNewHomeClientCenter from './get-new-home-client-center';
2020
import { add, subtract, isEqual } from './position';
2121

2222
const origin: Position = { x: 0, y: 0 };
2323

24-
type ScrollDiffResult = {|
25-
droppable: Position,
26-
window: Position,
27-
|}
28-
29-
const getScrollDiff = (
24+
type ScrollDiffArgs = {|
3025
initial: InitialDrag,
3126
current: CurrentDrag,
32-
droppable: DroppableDimension
33-
): ScrollDiffResult => {
27+
droppable: ?DroppableDimension
28+
|}
29+
30+
const getScrollDiff = ({
31+
initial,
32+
current,
33+
droppable,
34+
}: ScrollDiffArgs): Position => {
3435
const windowScrollDiff: Position = subtract(
3536
initial.windowScroll,
3637
current.windowScroll
3738
);
38-
const droppableScrollDiff: Position = subtract(
39+
40+
const droppableScrollDiff: Position = droppable ? subtract(
3941
droppable.scroll.initial,
4042
droppable.scroll.current
41-
);
43+
) : origin;
4244

43-
return {
44-
window: windowScrollDiff,
45-
droppable: droppableScrollDiff,
46-
};
45+
return add(windowScrollDiff, droppableScrollDiff);
4746
};
4847

4948
export type RequestDimensionsAction = {|
@@ -72,6 +71,7 @@ export type CompleteLiftAction = {|
7271
client: InitialDragLocation,
7372
page: InitialDragLocation,
7473
windowScroll: Position,
74+
isScrollAllowed: boolean,
7575
|}
7676
|}
7777

@@ -80,6 +80,7 @@ const completeLift = (id: DraggableId,
8080
client: InitialDragLocation,
8181
page: InitialDragLocation,
8282
windowScroll: Position,
83+
isScrollAllowed: boolean,
8384
): CompleteLiftAction => ({
8485
type: 'COMPLETE_LIFT',
8586
payload: {
@@ -88,6 +89,7 @@ const completeLift = (id: DraggableId,
8889
client,
8990
page,
9091
windowScroll,
92+
isScrollAllowed,
9193
},
9294
});
9395

@@ -130,6 +132,23 @@ export const updateDroppableDimensionScroll =
130132
},
131133
});
132134

135+
export type UpdateDroppableDimensionIsEnabledAction = {|
136+
type: 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED',
137+
payload: {
138+
id: DroppableId,
139+
isEnabled: boolean,
140+
}
141+
|}
142+
143+
export const updateDroppableDimensionIsEnabled =
144+
(id: DroppableId, isEnabled: boolean): UpdateDroppableDimensionIsEnabledAction => ({
145+
type: 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED',
146+
payload: {
147+
id,
148+
isEnabled,
149+
},
150+
});
151+
133152
export type MoveAction = {|
134153
type: 'MOVE',
135154
payload: {|
@@ -190,6 +209,26 @@ export const moveForward = (id: DraggableId): MoveForwardAction => ({
190209
payload: id,
191210
});
192211

212+
export type CrossAxisMoveForwardAction = {|
213+
type: 'CROSS_AXIS_MOVE_FORWARD',
214+
payload: DraggableId
215+
|}
216+
217+
export const crossAxisMoveForward = (id: DraggableId): CrossAxisMoveForwardAction => ({
218+
type: 'CROSS_AXIS_MOVE_FORWARD',
219+
payload: id,
220+
});
221+
222+
export type CrossAxisMoveBackwardAction = {|
223+
type: 'CROSS_AXIS_MOVE_BACKWARD',
224+
payload: DraggableId
225+
|}
226+
227+
export const crossAxisMoveBackward = (id: DraggableId): CrossAxisMoveBackwardAction => ({
228+
type: 'CROSS_AXIS_MOVE_BACKWARD',
229+
payload: id,
230+
});
231+
193232
type CleanAction = {
194233
type: 'CLEAN',
195234
payload: null,
@@ -269,11 +308,10 @@ export const drop = () =>
269308
}
270309

271310
const { impact, initial, current } = state.drag;
272-
const sourceDroppable: DroppableDimension =
273-
state.dimension.droppable[initial.source.droppableId];
274-
const destinationDroppable: ?DroppableDimension = impact.destination ?
311+
const droppable: ?DroppableDimension = impact.destination ?
275312
state.dimension.droppable[impact.destination.droppableId] :
276313
null;
314+
const draggable: DraggableDimension = state.dimension.draggable[current.id];
277315

278316
const result: DropResult = {
279317
draggableId: current.id,
@@ -282,22 +320,17 @@ export const drop = () =>
282320
destination: impact.destination,
283321
};
284322

285-
const scrollDiff = getScrollDiff(
286-
initial,
287-
current,
288-
sourceDroppable,
289-
);
290-
291-
const newHomeOffset: Position = getNewHomeClientOffset({
323+
const newCenter: Position = getNewHomeClientCenter({
292324
movement: impact.movement,
293-
clientOffset: current.client.offset,
294-
pageOffset: current.page.offset,
295-
droppableScrollDiff: scrollDiff.droppable,
296-
windowScrollDiff: scrollDiff.window,
325+
draggable,
297326
draggables: state.dimension.draggable,
298-
axis: destinationDroppable ? destinationDroppable.axis : null,
327+
destination: droppable,
299328
});
300329

330+
const clientOffset: Position = subtract(newCenter, draggable.client.withMargin.center);
331+
const scrollDiff: Position = getScrollDiff({ initial, current, droppable });
332+
const newHomeOffset: Position = add(clientOffset, scrollDiff);
333+
301334
// Do not animate if you do not need to.
302335
// This will be the case if either you are dragging with a
303336
// keyboard or if you manage to nail it just with a mouse.
@@ -353,11 +386,11 @@ export const cancel = () =>
353386
return;
354387
}
355388

356-
const scrollDiff = getScrollDiff(initial, current, droppable);
389+
const scrollDiff: Position = getScrollDiff({ initial, current, droppable });
357390

358391
dispatch(animateDrop({
359392
trigger: 'CANCEL',
360-
newHomeOffset: add(scrollDiff.droppable, scrollDiff.window),
393+
newHomeOffset: scrollDiff,
361394
impact: noImpact,
362395
result,
363396
}));
@@ -390,6 +423,7 @@ export type LiftAction = {|
390423
client: InitialDragLocation,
391424
page: InitialDragLocation,
392425
windowScroll: Position,
426+
isScrollAllowed: boolean,
393427
|}
394428
|}
395429

@@ -399,6 +433,7 @@ export const lift = (id: DraggableId,
399433
client: InitialDragLocation,
400434
page: InitialDragLocation,
401435
windowScroll: Position,
436+
isScrollAllowed: boolean,
402437
) => (dispatch: Dispatch, getState: Function) => {
403438
(() => {
404439
const state: State = getState();
@@ -436,7 +471,7 @@ export const lift = (id: DraggableId,
436471
if (newState.phase !== 'COLLECTING_DIMENSIONS') {
437472
return;
438473
}
439-
dispatch(completeLift(id, type, client, page, windowScroll));
474+
dispatch(completeLift(id, type, client, page, windowScroll, isScrollAllowed));
440475
});
441476
});
442477
};
@@ -449,6 +484,8 @@ export type Action = BeginLiftAction |
449484
MoveAction |
450485
MoveBackwardAction |
451486
MoveForwardAction |
487+
CrossAxisMoveForwardAction |
488+
CrossAxisMoveBackwardAction |
452489
DropAnimateAction |
453490
DropCompleteAction |
454491
CleanAction;

src/state/axis.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@ import type { HorizontalAxis, VerticalAxis } from '../types';
44
export const vertical: VerticalAxis = {
55
direction: 'vertical',
66
line: 'y',
7+
crossLine: 'x',
78
start: 'top',
89
end: 'bottom',
910
size: 'height',
11+
crossAxisStart: 'left',
12+
crossAxisEnd: 'right',
13+
crossAxisSize: 'width',
1014
};
1115

1216
export const horizontal: HorizontalAxis = {
1317
direction: 'horizontal',
1418
line: 'x',
19+
crossLine: 'y',
1520
start: 'left',
1621
end: 'right',
1722
size: 'width',
23+
crossAxisStart: 'top',
24+
crossAxisEnd: 'bottom',
25+
crossAxisSize: 'height',
1826
};

0 commit comments

Comments
 (0)