Skip to content

Commit

Permalink
Adding a multi drag pattern (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexreardon authored Mar 13, 2018
1 parent 702f699 commit 828be4a
Show file tree
Hide file tree
Showing 22 changed files with 1,177 additions and 27 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ We have created upgrade instructions in our release notes to help you upgrade to
- Movement between lists (▤ ↔ ▤)
- Mouse 🐭, keyboard 🎹 and touch 👉📱 (mobile, tablet and so on) support
- Auto scrolling - automatically scroll containers and the window as required during a drag (even with keyboard 🔥)
- [Multi drag support](/docs/patterns/multi-drag.md)
- Incredible screen reader support - we provide an amazing experience for english screen readers out of the box 📦. We also provide complete customisation control and internationalisation support for those who need it 💖
- Conditional [dragging](https://github.com/atlassian/react-beautiful-dnd#props-1) and [dropping](https://github.com/atlassian/react-beautiful-dnd#conditionally-dropping)
- Multiple independent lists on the one page
Expand Down Expand Up @@ -308,6 +309,12 @@ class App extends React.Component {
}
```

## Multi drag

We have created a [multi drag pattern](/docs/patterns/multi-drag.md) that you can build on top of `react-beautiful-dnd` in order to support dragging multiple `Draggable` items at once.

![multi drag demo](https://user-images.githubusercontent.com/2182637/37322724-7843a218-26d3-11e8-9ebb-8d5853387bb3.gif)

## Preset styles

We apply a number of non-visible styles to facilitate the dragging experience. We do this using combination of styling targets and techniques. It is a goal of the library to provide unopinioned styling. However, we do apply some reasonable `cursor` styling on drag handles by default. This is designed to make the library work as simply as possible out of the box. If you want to use your own cursors you are more than welcome to. All you need to do is override our cursor style rules by using a rule with [higher specificity](https://css-tricks.com/specifics-on-css-specificity/).
Expand Down
6 changes: 4 additions & 2 deletions docs/guides/how-we-use-dom-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ Some user events have a direct impact on a drag: such as a `mousemove` when drag

### Initial `mousedown`

- `preventDefault()` not called on `mousedown`
- `preventDefault()` **is called on `mousedown`** 😞

When the user first performs a `mousedown` on a *drag handle* we are not sure if they are intending to click or drag. Because at this stage we are not sure, we do not call `preventDefault()` on the event.
> This is the only known exception to our rule set. It is unfortunate that it is the first one to appear in this guide!
When the user first performs a `mousedown` on a *drag handle* we are not sure if they are intending to click or drag. Ideally we would not call `preventDefault()` on this event as we are not sure if it is a part of a drag. However, we need to call `preventDefault()` in order to avoid the item obtaining focus as it has a `tabIndex`.

### We are not sure yet if a drag will start

Expand Down
287 changes: 287 additions & 0 deletions docs/patterns/multi-drag.md

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions src/debug/timings.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ type Records = {

const records: Records = {};

const flag: string = '__react-beautiful-dnd-debug-hook__';

// temp
// window[flag] = true;
const flag: string = '__react-beautiful-dnd-debug-timings-hook__';

const isTimingsEnabled = (): boolean => Boolean(window[flag]);

Expand Down
2 changes: 1 addition & 1 deletion src/view/drag-drop-context/drag-drop-context.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default class DragDropContext extends React.Component<Props> {
this.styleMarshal.onPhaseChange(current);
}

const isDragEnding: boolean = current.phase !== 'DRAGGING' && previousInThisExecution.phase === 'DRAGGING';
const isDragEnding: boolean = previousInThisExecution.phase === 'DRAGGING' && current.phase !== 'DRAGGING';

// in the case that a drag is ending we need to instruct the dimension marshal
// to stop listening to changes. Otherwise it will try to process
Expand Down
10 changes: 8 additions & 2 deletions src/view/drag-handle/sensor/create-mouse-sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type State = {|
|}

// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
const primaryButton = 0;
const primaryButton: number = 0;
const noop = () => { };

// shared management of mousedown without needing to call preventDefault()
Expand Down Expand Up @@ -256,10 +256,16 @@ export default ({
// Registering that this event has been handled.
// This is to prevent parent draggables using this event
// to start also.
// Not using preventDefault() as we are not sure
// Ideally we would not use preventDefault() as we are not sure
// if this mouse down is part of a drag interaction
// Unfortunately we do to prevent the element obtaining focus (see below).
mouseDownMarshal.handle();

// Unfortunately we do need to prevent the drag handle from getting focus on mousedown.
// This goes against our policy on not blocking events before a drag has started.
// See [How we use dom events](/docs/guides/how-we-use-dom-events.md).
event.preventDefault();

const point: Position = {
x: clientX,
y: clientY,
Expand Down
9 changes: 9 additions & 0 deletions stories/9-multi-drag-story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow
import React from 'react';
import { storiesOf } from '@storybook/react';
import TaskApp from './src/multi-drag/task-app';

storiesOf('Multi drag', module)
.add('pattern', () => (
<TaskApp />
));
2 changes: 1 addition & 1 deletion stories/src/accessible/data.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import type { Task } from './types';
import type { Task } from '../types';

const tasks: Task[] = [
{
Expand Down
2 changes: 1 addition & 1 deletion stories/src/accessible/task-app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
DraggableLocation,
HookProvided,
} from '../../../src/';
import type { Task } from './types';
import type { Task } from '../types';

type State = {|
tasks: Task[],
Expand Down
6 changes: 3 additions & 3 deletions stories/src/accessible/task-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import React, { Component } from 'react';
import styled from 'styled-components';
import { Droppable } from '../../../src/';
import Task from './task';
import type { DroppableProvided, DroppableStateSnapshot } from '../../../src/';
import type { Task as TaskType } from './types';
import type { DroppableProvided } from '../../../src/';
import type { Task as TaskType } from '../types';
import { colors, grid, borderRadius } from '../constants';

type Props = {|
Expand All @@ -14,7 +14,7 @@ type Props = {|

const Container = styled.div`
width: 300px;
background-color: ${colors.grey};
background-color: ${colors.grey.dark};
border-radius: ${borderRadius}px;
`;

Expand Down
2 changes: 1 addition & 1 deletion stories/src/accessible/task.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { Component } from 'react';
import styled from 'styled-components';
import { Draggable } from '../../../src/';
import type { DraggableProvided, DraggableStateSnapshot } from '../../../src/';
import type { Task as TaskType } from './types';
import type { Task as TaskType } from '../types';
import { colors, grid, borderRadius } from '../constants';

type Props = {|
Expand Down
6 changes: 0 additions & 6 deletions stories/src/accessible/types.js

This file was deleted.

9 changes: 8 additions & 1 deletion stories/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ export const colors = {
deep: 'rgb(0, 121, 191)',
light: 'lightblue',
lighter: '#d9fcff',
soft: '#E6FCFF',
},
black: '#4d4d4d',
shadow: 'rgba(0,0,0,0.2)',
grey: '#E2E4E6',
grey: {
darker: '#C1C7D0',
dark: '#E2E4E6',
medium: '#DFE1E5',
N30: '#EBECF0',
light: '#F4F5F7',
},
green: 'rgb(185, 244, 188)',
white: 'white',
purple: 'rebeccapurple',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const List = styled.div`
const Item = styled.div`
min-height: 80px;
background-color: ${colors.white};
border: 1px solid ${colors.grey};
border: 1px solid ${colors.grey.dark};
padding: ${grid}px;
margin-bottom: ${grid}px;
`;
Expand Down
99 changes: 99 additions & 0 deletions stories/src/multi-drag/column.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// @flow
import React, { Component } from 'react';
import styled from 'styled-components';
import memoizeOne from 'memoize-one';
import { Droppable } from '../../../src/';
import { grid, colors, borderRadius } from '../constants';
import Task from './task';
import type { DroppableProvided, DroppableStateSnapshot } from '../../../src/';
import type { Column as ColumnType } from './types';
import type { Task as TaskType, Id } from '../types';

type Props = {|
column: ColumnType,
tasks: TaskType[],
selectedTaskIds: Id[],
multiSelectTo: (taskId: Id) => void,
draggingTaskId: ?Id,
toggleSelection: (taskId: Id) => void,
toggleSelectionInGroup: (taskId: Id) => void,
multiSelectTo: (taskId: Id) => void,
|}

const Container = styled.div`
width: 300px;
margin: ${grid}px;
border-radius: ${borderRadius}px;
border: 1px solid ${colors.grey.dark};
background-color: ${colors.grey.medium};
/* we want the column to take up its full height */
display: flex;
flex-direction: column;
`;

const Title = styled.h3`
font-weight: bold;
padding: ${grid}px;
`;

const TaskList = styled.div`
padding: ${grid}px;
min-height: 200px;
flex-grow: 1;
transition: background-color 0.2s ease;
${props => (props.isDraggingOver ? `background-color: ${colors.grey.darker}` : '')};
`;

type TaskIdMap = {
[taskId: Id]: true,
}

const getSelectedMap = memoizeOne((selectedTaskIds: Id[]) =>
selectedTaskIds.reduce((previous: TaskIdMap, current: Id): TaskIdMap => {
previous[current] = true;
return previous;
}, {}));

export default class Column extends Component<Props> {
render() {
const column: ColumnType = this.props.column;
const tasks: TaskType[] = this.props.tasks;
const selectedTaskIds: Id[] = this.props.selectedTaskIds;
const draggingTaskId: ?Id = this.props.draggingTaskId;
return (
<Container>
<Title>{column.title}</Title>
<Droppable droppableId={column.id}>
{(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
<TaskList
innerRef={provided.innerRef}
isDraggingOver={snapshot.isDraggingOver}
{...provided.droppableProps}
>
{tasks.map((task: TaskType, index: number) => {
const isSelected: boolean = Boolean(getSelectedMap(selectedTaskIds)[task.id]);
const isGhosting: boolean =
isSelected && Boolean(draggingTaskId) && draggingTaskId !== task.id;
return (
<Task
task={task}
index={index}
key={task.id}
isSelected={isSelected}
isGhosting={isGhosting}
selectionCount={selectedTaskIds.length}
toggleSelection={this.props.toggleSelection}
toggleSelectionInGroup={this.props.toggleSelectionInGroup}
multiSelectTo={this.props.multiSelectTo}
/>
);
})}
{provided.placeholder}
</TaskList>
)}
</Droppable>
</Container>
);
}
}
36 changes: 36 additions & 0 deletions stories/src/multi-drag/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @flow
import type { Column, Entities, TaskMap } from './types';
import type { Task, Id } from '../types';

const tasks: Task[] = Array.from({ length: 20 }, (v, k) => k).map((val: number): Task => ({
id: `task-${val}`,
content: `Task ${val}`,
}));

const taskMap: TaskMap = tasks.reduce((previous: TaskMap, current: Task): TaskMap => {
previous[current.id] = current;
return previous;
}, {});

const todo: Column = {
id: 'todo',
title: 'To do',
taskIds: tasks.map((task: Task): Id => task.id),
};

const done: Column = {
id: 'done',
title: 'Done',
taskIds: [],
};

const entities: Entities = {
columnOrder: [todo.id, done.id],
columns: {
[todo.id]: todo,
[done.id]: done,
},
tasks: taskMap,
};

export default entities;
Loading

0 comments on commit 828be4a

Please sign in to comment.