Skip to content

Commit

Permalink
Moving between lists (#73)
Browse files Browse the repository at this point in the history
Adding support for moving between lists
  • Loading branch information
alexreardon authored Sep 14, 2017
1 parent 4e128a3 commit 24f01df
Show file tree
Hide file tree
Showing 102 changed files with 7,795 additions and 2,499 deletions.
9 changes: 8 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@
// All blocks must be wrapped in curly braces {}
// Preventing if(condition) return;
// https://eslint.org/docs/rules/curly
"curly": ["error", "all"]
"curly": ["error", "all"],

// Allowing Math.pow rather than forcing `**`
// https://eslint.org/docs/rules/no-restricted-properties
"no-restricted-properties": ["off", {
"object": "Math",
"property": "pow"
}]
}
}
57 changes: 45 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class App extends Component {
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
Expand Down Expand Up @@ -390,7 +391,7 @@ type DraggableLocation = {|
### Best practices for `hooks`
**Block updates during a drag**
#### Block updates during a drag
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`.
Expand All @@ -404,12 +405,7 @@ Here are a few poor user experiences that can occur if you change things *during
- If you remove the node that the user is dragging the drag will instantly end
- If you change the dimension of the dragging node then other things will not move out of the way at the correct time.
**`onDragStart` and `onDragEnd` pairing**
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.
**Style**
#### Add a cursor style and block selection
During a drag it is recommended that you add two styles to the body:
Expand All @@ -420,9 +416,24 @@ During a drag it is recommended that you add two styles to the body:
`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`).
#### Force focus after a transition between lists
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:
- `onDragEnd`: move the item into the new list and record the id fo the item that has moved
- When rendering the reordered list pass down a prop which will tell the newly moved item to obtain focus
- In the `componentDidMount` lifecycle call back check if the item needs to gain focus based on its props (such as an `autoFocus` prop)
- 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.
### Other `hooks` information
**`onDragStart` and `onDragEnd` pairing**
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.
**Dynamic hooks**
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.
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.
## `Droppable`
Expand All @@ -437,7 +448,8 @@ import { Droppable } from 'react-beautiful-dnd';
ref={provided.innerRef}
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
>
I am a droppable!
<h2>I am a droppable!</h2>
{provided.placeholder}
</div>
)}
</Droppable>;
Expand Down Expand Up @@ -468,14 +480,23 @@ The function is provided with two arguments:
```js
type DroppableProvided = {|
innerRef: (?HTMLElement) => void,
placeholder: ?ReactElement,
|}
```
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.
- `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.
- `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.
```js
<Droppable droppableId="droppable-1">
{(provided, snapshot) => <div ref={provided.innerRef}>Good to go</div>}
{(provided, snapshot) => (
<div ref={provided.innerRef}>
Good to go

{provided.placeholder}
</div>
)}
</Droppable>;
```
Expand All @@ -497,6 +518,8 @@ The `children` function is also provided with a small amount of state relating t
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
>
I am a droppable!

{provided.placeholder}
</div>
)}
</Droppable>;
Expand Down Expand Up @@ -681,7 +704,7 @@ type NotDraggingStyle = {|
|};
```
- `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.
- `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.
```js
<Draggable draggableId="draggable-1">
Expand Down Expand Up @@ -856,6 +879,11 @@ type DraggableLocation = {|
// Droppable
type DroppableProvided = {|
innerRef: (?HTMLElement) => void,
placeholder: ?ReactElement,
|}

type DraggableStateSnapshot = {|
isDraggingOver: boolean,
|}

// Draggable
Expand All @@ -865,6 +893,11 @@ type DraggableProvided = {|
dragHandleProps: ?DragHandleProvided,
placeholder: ?ReactElement,
|}

type DraggableStateSnapshot = {|
isDragging: boolean,
|}

type DraggableStyle = DraggingStyle | NotDraggingStyle
type DraggingStyle = {|
pointerEvents: 'none',
Expand Down
103 changes: 70 additions & 33 deletions src/state/action-creators.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,33 @@ import type {
InitialDrag,
} from '../types';
import noImpact from './no-impact';
import getNewHomeClientOffset from './get-new-home-client-offset';
import getNewHomeClientCenter from './get-new-home-client-center';
import { add, subtract, isEqual } from './position';

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

type ScrollDiffResult = {|
droppable: Position,
window: Position,
|}

const getScrollDiff = (
type ScrollDiffArgs = {|
initial: InitialDrag,
current: CurrentDrag,
droppable: DroppableDimension
): ScrollDiffResult => {
droppable: ?DroppableDimension
|}

const getScrollDiff = ({
initial,
current,
droppable,
}: ScrollDiffArgs): Position => {
const windowScrollDiff: Position = subtract(
initial.windowScroll,
current.windowScroll
);
const droppableScrollDiff: Position = subtract(

const droppableScrollDiff: Position = droppable ? subtract(
droppable.scroll.initial,
droppable.scroll.current
);
) : origin;

return {
window: windowScrollDiff,
droppable: droppableScrollDiff,
};
return add(windowScrollDiff, droppableScrollDiff);
};

export type RequestDimensionsAction = {|
Expand Down Expand Up @@ -72,6 +71,7 @@ export type CompleteLiftAction = {|
client: InitialDragLocation,
page: InitialDragLocation,
windowScroll: Position,
isScrollAllowed: boolean,
|}
|}

Expand All @@ -80,6 +80,7 @@ const completeLift = (id: DraggableId,
client: InitialDragLocation,
page: InitialDragLocation,
windowScroll: Position,
isScrollAllowed: boolean,
): CompleteLiftAction => ({
type: 'COMPLETE_LIFT',
payload: {
Expand All @@ -88,6 +89,7 @@ const completeLift = (id: DraggableId,
client,
page,
windowScroll,
isScrollAllowed,
},
});

Expand Down Expand Up @@ -130,6 +132,23 @@ export const updateDroppableDimensionScroll =
},
});

export type UpdateDroppableDimensionIsEnabledAction = {|
type: 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED',
payload: {
id: DroppableId,
isEnabled: boolean,
}
|}

export const updateDroppableDimensionIsEnabled =
(id: DroppableId, isEnabled: boolean): UpdateDroppableDimensionIsEnabledAction => ({
type: 'UPDATE_DROPPABLE_DIMENSION_IS_ENABLED',
payload: {
id,
isEnabled,
},
});

export type MoveAction = {|
type: 'MOVE',
payload: {|
Expand Down Expand Up @@ -190,6 +209,26 @@ export const moveForward = (id: DraggableId): MoveForwardAction => ({
payload: id,
});

export type CrossAxisMoveForwardAction = {|
type: 'CROSS_AXIS_MOVE_FORWARD',
payload: DraggableId
|}

export const crossAxisMoveForward = (id: DraggableId): CrossAxisMoveForwardAction => ({
type: 'CROSS_AXIS_MOVE_FORWARD',
payload: id,
});

export type CrossAxisMoveBackwardAction = {|
type: 'CROSS_AXIS_MOVE_BACKWARD',
payload: DraggableId
|}

export const crossAxisMoveBackward = (id: DraggableId): CrossAxisMoveBackwardAction => ({
type: 'CROSS_AXIS_MOVE_BACKWARD',
payload: id,
});

type CleanAction = {
type: 'CLEAN',
payload: null,
Expand Down Expand Up @@ -269,11 +308,10 @@ export const drop = () =>
}

const { impact, initial, current } = state.drag;
const sourceDroppable: DroppableDimension =
state.dimension.droppable[initial.source.droppableId];
const destinationDroppable: ?DroppableDimension = impact.destination ?
const droppable: ?DroppableDimension = impact.destination ?
state.dimension.droppable[impact.destination.droppableId] :
null;
const draggable: DraggableDimension = state.dimension.draggable[current.id];

const result: DropResult = {
draggableId: current.id,
Expand All @@ -282,22 +320,17 @@ export const drop = () =>
destination: impact.destination,
};

const scrollDiff = getScrollDiff(
initial,
current,
sourceDroppable,
);

const newHomeOffset: Position = getNewHomeClientOffset({
const newCenter: Position = getNewHomeClientCenter({
movement: impact.movement,
clientOffset: current.client.offset,
pageOffset: current.page.offset,
droppableScrollDiff: scrollDiff.droppable,
windowScrollDiff: scrollDiff.window,
draggable,
draggables: state.dimension.draggable,
axis: destinationDroppable ? destinationDroppable.axis : null,
destination: droppable,
});

const clientOffset: Position = subtract(newCenter, draggable.client.withMargin.center);
const scrollDiff: Position = getScrollDiff({ initial, current, droppable });
const newHomeOffset: Position = add(clientOffset, scrollDiff);

// Do not animate if you do not need to.
// This will be the case if either you are dragging with a
// keyboard or if you manage to nail it just with a mouse.
Expand Down Expand Up @@ -353,11 +386,11 @@ export const cancel = () =>
return;
}

const scrollDiff = getScrollDiff(initial, current, droppable);
const scrollDiff: Position = getScrollDiff({ initial, current, droppable });

dispatch(animateDrop({
trigger: 'CANCEL',
newHomeOffset: add(scrollDiff.droppable, scrollDiff.window),
newHomeOffset: scrollDiff,
impact: noImpact,
result,
}));
Expand Down Expand Up @@ -390,6 +423,7 @@ export type LiftAction = {|
client: InitialDragLocation,
page: InitialDragLocation,
windowScroll: Position,
isScrollAllowed: boolean,
|}
|}

Expand All @@ -399,6 +433,7 @@ export const lift = (id: DraggableId,
client: InitialDragLocation,
page: InitialDragLocation,
windowScroll: Position,
isScrollAllowed: boolean,
) => (dispatch: Dispatch, getState: Function) => {
(() => {
const state: State = getState();
Expand Down Expand Up @@ -436,7 +471,7 @@ export const lift = (id: DraggableId,
if (newState.phase !== 'COLLECTING_DIMENSIONS') {
return;
}
dispatch(completeLift(id, type, client, page, windowScroll));
dispatch(completeLift(id, type, client, page, windowScroll, isScrollAllowed));
});
});
};
Expand All @@ -449,6 +484,8 @@ export type Action = BeginLiftAction |
MoveAction |
MoveBackwardAction |
MoveForwardAction |
CrossAxisMoveForwardAction |
CrossAxisMoveBackwardAction |
DropAnimateAction |
DropCompleteAction |
CleanAction;
8 changes: 8 additions & 0 deletions src/state/axis.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@ import type { HorizontalAxis, VerticalAxis } from '../types';
export const vertical: VerticalAxis = {
direction: 'vertical',
line: 'y',
crossLine: 'x',
start: 'top',
end: 'bottom',
size: 'height',
crossAxisStart: 'left',
crossAxisEnd: 'right',
crossAxisSize: 'width',
};

export const horizontal: HorizontalAxis = {
direction: 'horizontal',
line: 'x',
crossLine: 'y',
start: 'left',
end: 'right',
size: 'width',
crossAxisStart: 'top',
crossAxisEnd: 'bottom',
crossAxisSize: 'height',
};
Loading

0 comments on commit 24f01df

Please sign in to comment.