Skip to content

Commit

Permalink
CustomDragLayer component for enabling Drag & Drop ghost row
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Sturgess committed Jul 28, 2017
1 parent 7742259 commit f2a9669
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 28 deletions.
@@ -1,9 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';
import ConfigurableItemRow from './configurable-item-row';
import Checkbox from './../../checkbox';
import Icon from './../../icon';
import { WithDrag, WithDrop } from './../../drag-and-drop';
import DraggableContext from './../../drag-and-drop/draggable-context';
import { rootTagTest } from './../../../utils/helpers/tags/tags-specs';

describe('ConfigurableItemRow', () => {
Expand Down Expand Up @@ -140,13 +141,21 @@ describe('ConfigurableItemRow', () => {

describe('icon', () => {
beforeEach(() => {
wrapper = shallow(
<ConfigurableItemRow name='Foo' />
wrapper = mount(
<DraggableContext onDrag={() => {}}>
<ConfigurableItemRow name='Foo' />
</DraggableContext>
);
});

it('renders a drag vertical icon', () => {
expect(wrapper.find(Icon).props().type).toEqual('drag_vertical')
it('renders a drag vertical icon wrapped in WithDrag', () => {
wrapper.update(); // this is required because the _draggableNode ref is initially undefined.
const withDrag = wrapper.find(WithDrag);
const row = wrapper.find(ConfigurableItemRow);
expect(withDrag.length).toEqual(1);
expect(withDrag.find(Icon).props().type).toEqual('drag_vertical');
expect(withDrag.props().draggableProps).toEqual({ name: 'Foo', customDragLayer: false });
expect(withDrag.props().draggableNode).toEqual(row.node._draggableNode)
});
});

Expand All @@ -157,8 +166,28 @@ describe('ConfigurableItemRow', () => {
);
});

it('renders an <li>', () => {
expect(wrapper.find('li').length).toEqual(1)
it('renders an <li> wrapped in WithDrop', () => {
const withDrop = wrapper.find(WithDrop);
expect(withDrop.length).toEqual(1);
expect(withDrop.find('li').length).toEqual(1);
});
})
});

describe('when the customDragLayer prop is true', () => {
beforeEach(() => {
wrapper = shallow(
<ConfigurableItemRow name='Foo' customDragLayer={true} />
);
});

it('renders the Icon not wrapped in WithDrag', () => {
expect(wrapper.find(WithDrag).length).toEqual(0);
expect(wrapper.find(Icon).props().type).toEqual('drag_vertical');
});

it('renders the list item not wrapped in WithDrop', () => {
expect(wrapper.find(WithDrop).length).toEqual(0);
expect(wrapper.find('li').length).toEqual(1);
});
});
});
Expand Up @@ -16,6 +16,14 @@ class ConfigurableItemRow extends React.Component {
*/
className: PropTypes.string,

/**
* Determine if the component is being used as the ghost version for drag and drop.
*
* @property customDragLayer
* @type {Boolean}
*/
customDragLayer: PropTypes.bool.isRequired,

/**
* The checked value for the checkbox.
*
Expand Down Expand Up @@ -62,6 +70,10 @@ class ConfigurableItemRow extends React.Component {
dragAndDropActiveIndex: PropTypes.number // tracks the currently active index
}

static defaultProps = {
customDragLayer: false
}

checkbox(enabled, locked, name, onChange) {
return (
<Checkbox
Expand All @@ -73,15 +85,24 @@ class ConfigurableItemRow extends React.Component {
);
}

iconHTML() {
return (
<div>
<Icon
className='configurable-item-row__icon'
type='drag_vertical'
/>
</div>
);
}

icon() {
if (this.props.customDragLayer) {
return this.iconHTML();
}
return (
<WithDrag>
<div>
<Icon
className='configurable-item-row__icon'
type='drag_vertical'
/>
</div>
<WithDrag draggableProps={ this.props } draggableNode={ this._draggableNode } >
{this.iconHTML()}
</WithDrag>
);
}
Expand Down Expand Up @@ -119,16 +140,28 @@ class ConfigurableItemRow extends React.Component {
return typeof (dragAndDropActiveIndex) === 'number';
}

render() {
listItemHTML = () => {
const { rowIndex, enabled, locked, name, onChange } = this.props;
return (
<WithDrop index={ rowIndex } { ...tagComponent('configurable-item-row', this.props) }>
<li className={ this.classes(this.context.dragAndDropActiveIndex, rowIndex) }>
<div className='configurable-item-row__content-wrapper'>
{ this.icon() }
{ this.checkbox(enabled, locked, name, onChange) }
</div>
</li>
<li
className={ this.classes(this.context.dragAndDropActiveIndex, rowIndex) }
ref={ (node) => { this._draggableNode = node; } }
>
<div className='configurable-item-row__content-wrapper'>
{ this.icon() }
{ this.checkbox(enabled, locked, name, onChange) }
</div>
</li>
);
}

render() {
if (this.props.customDragLayer) {
return this.listItemHTML();
}
return (
<WithDrop index={ this.props.rowIndex } { ...tagComponent('configurable-item-row', this.props) }>
{ this.listItemHTML() }
</WithDrop>
);
}
Expand Down
Expand Up @@ -13,6 +13,12 @@
cursor: -webkit-grabbing;
}

.configurable-item-row--dragged {
.configurable-item-row__content-wrapper {
visibility: hidden;
}
}

.configurable-item-row__content-wrapper {
align-items: center;
display: flex;
Expand All @@ -22,3 +28,15 @@
.configurable-item-row__icon {
cursor: move;
}

.custom-drag-layer {
.configurable-item-row {
background-color: $grey-light;
border: none;

.configurable-item-row__content-wrapper {
visibility: visible;
}
}
}

5 changes: 4 additions & 1 deletion src/components/configurable-items/configurable-items.js
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import I18n from 'i18n-js';
import tagComponent from '../../utils/helpers/tags';
import { DraggableContext } from './../drag-and-drop';
import { DraggableContext, CustomDragLayer } from './../drag-and-drop';
import Button from './../button';
import ConfigurableItemRow from './configurable-item-row';
import Form from './../form';
Expand Down Expand Up @@ -99,6 +99,9 @@ class ConfigurableItems extends React.Component {
onSubmit={ this.props.onSave }
onCancel={ this.props.onCancel }
>
<CustomDragLayer>
<ConfigurableItemRow />
</CustomDragLayer>
{ this.rows() }
</Form>
</DraggableContext>
Expand Down
184 changes: 184 additions & 0 deletions src/components/drag-and-drop/custom-drag-layer/__spec__.js
@@ -0,0 +1,184 @@
import React from 'react';
import { UndecoratedCustomDragLayer, collect } from './custom-drag-layer';
import { mount } from 'enzyme';
import DraggableContext from './../draggable-context';

describe('CustomDragLayer', () => {
let wrapper, instance, child;

let draggableNode = {
getBoundingClientRect: () => ( {width: 10} )
}

const DummyComponent = () => {
return (
<div>Test</div>
)
}

describe('when the component is being dragged and there is a currentOffset and draggable node', () => {
beforeEach(() => {
wrapper = mount(
<DraggableContext onDrag={() => {}}>
<UndecoratedCustomDragLayer
className='custom-class-name'
currentOffset={{x: 1, y: 2}}
draggableProps={{foo: 'bar'}}
draggableNode={draggableNode}
isDragging={true}
>
<DummyComponent />
</UndecoratedCustomDragLayer>
</DraggableContext>
);
instance = wrapper.find(UndecoratedCustomDragLayer);
child = instance.find(DummyComponent)
});

it('renders a clone of the child with the props passed through', () => {
const childProps = child.props();
expect(childProps.customDragLayer).toBeTruthy();
expect(childProps.foo).toEqual('bar');
expect(childProps.draggableNode).toEqual(draggableNode);
});

it('renders the custom classname', () => {
expect(instance.find('.custom-class-name').length).toEqual(1);
expect(instance.find('.custom-drag-layer').length).toEqual(1);
});

it('transforms the component position by the currentOffset', () => {
const container = instance.find('.custom-drag-layer__container');
expect(container.length).toEqual(1);
const style = container.get(0).style
expect(style.webkitTransform).toEqual('translate(1px, 2px)');
expect(style.width).toEqual('10px');
})
});

describe('when the component is being dragged but there is no currentOffset', () => {
beforeEach(() => {
wrapper = mount(
<DraggableContext onDrag={() => {}}>
<UndecoratedCustomDragLayer
className='custom-class-name'
draggableProps={{foo: 'bar'}}
draggableNode={draggableNode}
isDragging={true}
>
<DummyComponent />
</UndecoratedCustomDragLayer>
</DraggableContext>
);
instance = wrapper.find(UndecoratedCustomDragLayer);
});

it('sets container to display: none', () => {
const container = instance.find('.custom-drag-layer__container');
expect(container.length).toEqual(1);
const style = container.get(0).style;
expect(style.display).toEqual('none');
});
});

describe('when the component is being dragged but there is no draggableNode', () => {
beforeEach(() => {
wrapper = mount(
<DraggableContext onDrag={() => {}}>
<UndecoratedCustomDragLayer
className='custom-class-name'
currentOffset={{x: 1, y: 2}}
draggableProps={{foo: 'bar'}}
isDragging={true}
>
<DummyComponent />
</UndecoratedCustomDragLayer>
</DraggableContext>
);
instance = wrapper.find(UndecoratedCustomDragLayer);
});

it('does not set the width style', () => {
const container = instance.find('.custom-drag-layer__container');
expect(container.length).toEqual(1);
const style = container.get(0).style;
expect(style.webkitTransform).toEqual('translate(1px, 2px)');
expect(style.width).toEqual('');
});
});

describe('when the component is not being dragged', () => {
beforeEach(() => {
wrapper = mount(
<DraggableContext onDrag={() => {}}>
<UndecoratedCustomDragLayer
className='custom-class-name'
draggableProps={{foo: 'bar'}}
draggableNode={draggableNode}
isDragging={false}
>
<DummyComponent />
</UndecoratedCustomDragLayer>
</DraggableContext>
);
instance = wrapper.find(UndecoratedCustomDragLayer);
child = instance.find(DummyComponent);
});

it('does not render the child component', () => {
expect(child.length).toEqual(0);
});
});

describe('collect', () => {
let monitor, item;

describe('when there is an item', () => {
beforeEach(() => {
item = {
draggableProps: 'draggableProps',
draggableNode: 'draggableNode'
}

monitor = {
getItem: () => item,
getSourceClientOffset: () => 'currentOffset',
isDragging: () => true
}
});

it('returns the draggableProps and draggableNode', () => {
expect(collect(monitor)).toEqual(
{
currentOffset: 'currentOffset',
item,
draggableProps: 'draggableProps',
draggableNode: 'draggableNode',
isDragging: true
}
)
});
});

describe('when there is not an item', () => {
beforeEach(() => {
monitor = {
getItem: () => null,
getSourceClientOffset: () => 'currentOffset',
isDragging: () => true
}
})
it('returns an object without the draggableProps and draggableNode defined', () => {
expect(collect(monitor)).toEqual(
{
currentOffset: 'currentOffset',
item: null,
draggableProps: null,
draggableNode: null,
isDragging: true
}
)
})
})
});
});

0 comments on commit f2a9669

Please sign in to comment.