Skip to content
This repository has been archived by the owner on Aug 21, 2023. It is now read-only.

Commit

Permalink
Merge pull request #45 from helpscout/dragon-drop
Browse files Browse the repository at this point in the history
Add Sortable Component for drag/drop
  • Loading branch information
ItsJonQ committed Sep 15, 2017
2 parents ac630d6 + 1f7d8c0 commit c462775
Show file tree
Hide file tree
Showing 28 changed files with 661 additions and 57 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"react-error-overlay": "^1.0.9",
"react-router": "4.2.0",
"react-router-dom": "4.2.2",
"react-sortable-hoc": "^0.6.7",
"react-test-renderer": "15.6.1",
"rgb-hex": "2.1.0",
"sass-loader": "6.0.6",
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import clockSmall from '../../icons/clock-small.svg'
import crossLarge from '../../icons/cross-large.svg'
import crossMedium from '../../icons/cross-medium.svg'
import crossSmall from '../../icons/cross-small.svg'
import drag from '../../icons/drag.svg'
import document from '../../icons/document.svg'
import emoji from '../../icons/emoji.svg'
import fullscreen from '../../icons/fullscreen.svg'
Expand Down Expand Up @@ -45,6 +46,7 @@ const ICONS = {
'cross-large': crossLarge,
'cross-medium': crossMedium,
'cross-small': crossSmall,
drag,
document,
emoji,
fullscreen,
Expand Down
32 changes: 32 additions & 0 deletions src/components/SidebarCollapsibleCard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,37 @@ const customHeader = (
Note: `header` will override `title` if both are used.



### Sortable

This component can be drag sortable using the [Sortable](../Sortable) component. Due to the design of this component, you must pass `useDragHandle` and `hideDragHandles` into `Sortable`. This is because this component has it's own drag handles, which is activated when either `sortable` is true or when it is used within `Sortable`.

```html
<Sortable
useDragHandle
hideDragHandles
>
<SidebarCollapsibleCard title='Zoolander 2'>
<dl>
<dt>Character</dt>
<dd>Jacobim Mugatu</dd>
<dt>Year</dt>
<dd>2016</dd>
</dl>
</SidebarCollapsibleCard>
<SidebarCollapsibleCard title='The Lego Movie'>
<dl>
<dt>Character</dt>
<dd>Lord Business</dd>
<dt>Year</dt>
<dd>2014</dd>
</dl>
</SidebarCollapsibleCard>
</Sortable>
```



## Props

| Prop | Type | Description |
Expand All @@ -41,5 +72,6 @@ Note: `header` will override `title` if both are used.
| isOpen | boolean | Opens/collapses the component. |
| onClose | function | Callback function when the component closes. |
| onOpen | function | Callback function when the component opens. |
| sortable | boolean | Renders the drag handler for sorting. See [Sortable](../Sortable) |
| style | string | Custom styles to be added to the component. |
| title | string | Title for the header in this component. |
18 changes: 14 additions & 4 deletions src/components/SidebarCollapsibleCard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ import PropTypes from 'prop-types'
import { default as Collapsible, propTypes as collapsibleTypes } from '../Collapsible'
import Flexy from '../Flexy'
import Heading from '../Heading'
import SortableDragHandle from '../Sortable/DragHandle'
import Icon from '../Icon'
import { createUniqueIDFactory } from '../../utilities/id'
import classNames from '../../utilities/classNames'

export const propTypes = Object.assign({}, collapsibleTypes, {
header: PropTypes.element,
title: PropTypes.string,
isOpen: PropTypes.bool
isOpen: PropTypes.bool,
sortable: PropTypes.bool
})

export const defaultProps = {
duration: 200,
isOpen: false
isOpen: false,
sortable: false
}

const uniqueID = createUniqueIDFactory('SidebarCollapsibleCard')
Expand Down Expand Up @@ -49,6 +52,7 @@ class SidebarCollapsibleCard extends Component {
onClose,
onOpen,
isOpen,
sortable,
title,
...rest
} = this.props
Expand Down Expand Up @@ -78,16 +82,22 @@ class SidebarCollapsibleCard extends Component {
}

const iconName = open ? 'caret-up' : 'caret-down'
const headerMarkup = displayHeader()
const regionId = `${cardId}-region`
const headerMarkup = displayHeader()
const dragHandleMarkup = sortable ? (
<Flexy.Item>
<SortableDragHandle className='c-SidebarCollapsibleCard__drag-handle' />
</Flexy.Item>
) : null

return (
<div className={componentClassName} {...rest} role='presentation' id={cardId}>
<a href='#' className='c-SidebarCollapsibleCard__header' onClick={handleToggleOpen} role='heading' aria-expanded={open} aria-controls={regionId}>
<Flexy>
<Flexy gap='sm'>
<Flexy.Block>
{headerMarkup}
</Flexy.Block>
{dragHandleMarkup}
<Flexy.Item>
<Icon name={iconName} size='14' muted />
</Flexy.Item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,23 @@ describe('Collapsible', () => {
expect(p.onClose).toBe(fn)
})
})

describe('Sortable', () => {
test('Not sortable by default', () => {
const wrapper = shallow(
<SidebarCollapsibleCard />
)
const o = wrapper.find('.c-SidebarCollapsibleCard__drag-handle')

expect(o.length).toBe(0)
})

test('Adds Sortable.DragHandle if sortable', () => {
const wrapper = shallow(
<SidebarCollapsibleCard sortable />
)
const o = wrapper.find('.c-SidebarCollapsibleCard__drag-handle')

expect(o.length).toBe(1)
})
})
23 changes: 23 additions & 0 deletions src/components/Sortable/DragHandle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import {SortableHandle} from 'react-sortable-hoc'
import Icon from '../Icon'
import classNames from '../../utilities/classNames'

const DragHandle = SortableHandle((props) => {
const {
className,
...rest
} = props

const componentClassName = classNames(
'c-SortableDragHandle',
className
)
return (
<div className={componentClassName} {...rest}>
<Icon name='drag' size='14' ignoreClick={false} />
</div>
)
})

export default DragHandle
42 changes: 42 additions & 0 deletions src/components/Sortable/Item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import {SortableElement} from 'react-sortable-hoc'
import DragHandle from './DragHandle'
import classNames from '../../utilities/classNames'

export const propTypes = {
classNames: PropTypes.string,
hideDragHandles: PropTypes.bool,
useDragHandle: PropTypes.bool
}

const Item = SortableElement((props) => {
const {
className,
children,
hideDragHandles,
sortable,
useDragHandle,
...rest
} = props

const componentClassName = classNames(
'c-SortableItem',
className
)

const dragHandleMarkup = (useDragHandle && !hideDragHandles) ? (
<DragHandle />
) : null

return (
<div className={componentClassName} {...rest}>
{dragHandleMarkup}
{children}
</div>
)
})

Item.propTypes = propTypes

export default Item
51 changes: 51 additions & 0 deletions src/components/Sortable/List.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import PropTypes from 'prop-types'
import {SortableContainer} from 'react-sortable-hoc'
import classNames from '../../utilities/classNames'
import { listTypes } from './propTypes'

export const propTypes = Object.assign({}, listTypes, {
items: PropTypes.array
})

const List = SortableContainer((props) => {
const {
className,
dragHandle: useDragHandle,
hideDragHandles,
items,
sortable,
...rest
} = props

const componentClassName = classNames(
'c-SortableList',
className
)

const itemsMarkup = items ? items.map((item, index) => {
const key = `item-${index}`
const {
index: itemIndex,
...itemRest
} = item.props

return React.cloneElement(item, {
index,
key,
useDragHandle,
hideDragHandles,
...itemRest
})
}) : null

return (
<div className={componentClassName} {...rest}>
{itemsMarkup}
</div>
)
})

List.propTypes = propTypes

export default List
50 changes: 50 additions & 0 deletions src/components/Sortable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Sortable

This component provides the ability to drag sort child components. This component is built on top of [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc) and enhanced to make it easier to use.


## Example

Any child components placed inside this component automatically become sortable.

```html
<Sortable>
<Card>Jacobim Mugatu</Card>
<Card>Lord Business</Card>
<Card>Brennan Huff</Card>
</Sortable>
```


## Props

| Prop | Type | Description |
| --- | --- | --- |
| className | string | Custom class names to be added to the component. |
| style | string | Custom styles to be added to the component. |


### react-sortable-hoc Props

This component accepts all props used by [react-sortable-hoc](https://github.com/clauderic/react-sortable-hoc).

| Prop | Type | Description |
| --- | --- | --- |
| axis | string | Items can be sorted horizontally, vertically or in a grid. Possible values: `x`, `y` or `xy` |
| distance | number | If you'd like elements to only become sortable after being dragged a certain number of pixels. Cannot be used in conjunction with the `pressDelay` prop. |
| getContainer | function | Optional function to return the scrollable container element. This property defaults to the `SortableContainer` element itself or (if `useWindowAsScrollContainer` is true) the window. Use this function to specify a custom container object (eg this is useful for integrating with certain 3rd party components such as `FlexTable`). This function is passed a single parameter (the `wrappedInstance` React element) and it is expected to return a DOM element. |
| getHelperDimensions | function | Optional `function({node, index, collection})` that should return the computed dimensions of the SortableHelper. See [default implementation](https://github.com/clauderic/react-sortable-hoc/blob/master/src/SortableContainer/index.js#L58) for more details |
| helperClass | string | You can provide a class you'd like to add to the sortable helper to add some styles to it |
| hideSortableGhost | boolean | Whether to auto-hide the ghost element. By default, as a convenience, React Sortable List will automatically hide the element that is currently being sorted. Set this to false if you would like to apply your own styling. |
| lockAxis | string | If you'd like, you can lock movement to an axis while sorting. This is not something that is possible with HTML5 Drag & Drop |
| lockToContainerEdges | boolean | You can lock movement of the sortable element to it's parent `SortableContainer` |
| lockOffset | boolean | When `lockToContainerEdges` is set to `true`, this controls the offset distance between the sortable helper and the top/bottom edges of it's parent `SortableContainer`. Percentage values are relative to the height of the item currently being sorted. If you wish to specify different behaviours for locking to the *top* of the container vs the *bottom*, you may also pass in an `array` (For example: `["0%", "100%"]`). |
| pressDelay | number | If you'd like elements to only become sortable after being pressed for a certain time, change this property. A good sensible default value for mobile is `200`. Cannot be used in conjunction with the distance prop. |
| pressThreshold | number | Number of pixels of movement to tolerate before ignoring a press event. |
| onSortStart | function | Callback that get's invoked when sorting begins. `function({node, index, collection}, event)` |
| onSortMove | function | Callback that get's invoked during sorting as the cursor moves. `function(event)` |
| onSortEnd | function | Callback that get's invoked when sorting ends. `function({oldIndex, newIndex, collection}, e)` |
| shouldCancelStart | function | This function get's invoked before sorting begins, and can be used to programatically cancel sorting before it begins. By default, it will cancel sorting if the event target is either an `input`, `textarea`, `select` or `option`. |
| useDragHandle | boolean | If you're using the `SortableHandle` HOC, set this to `true` |
| useWindowAsScrollContainer | boolean | If you want, you can set the `window` as the scrolling container |
| transitionDuration | number | The duration of the transition when elements shift positions. Set this to `0` if you'd like to disable transitions |
Loading

0 comments on commit c462775

Please sign in to comment.