Skip to content

Commit

Permalink
fix: fix event propagation and close #686
Browse files Browse the repository at this point in the history
- persist event in callbacks where the event is passed down the custom callbacks
- do not call onDrop, onDragEnter callbacks if event propagation was stopped from a child
  • Loading branch information
Roland Groza authored and rolandjitsu committed Oct 16, 2018
1 parent 14e6b67 commit fd65863
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 2 deletions.
93 changes: 93 additions & 0 deletions examples/Nesting/Readme.md
@@ -0,0 +1,93 @@
In case you need to nest dropzone components and prevent any drag events from the child propagate to the parent, it can easily be achieved by using `.stopPropagation()` in the child dropzone.

```jsx harmony
const parentStyle = {
width: 200,
height: 200,
border: '2px dashed #888'
}

const childStyle = {
width: 160,
height: 160,
margin: 20,
border: '2px dashed #ccc'
}

class NestedDropzone extends React.Component {
constructor() {
super()
this.state = {
parent: {},
child: {}
}
}

createDragHandler(eventType, node) {
const updater = this.createStateUpdater(eventType, node)
return (evt) => {
evt.preventDefault();
if (node === 'child') {
evt.stopPropagation()
}
this.setState(updater)
}
}

createDropHandler(node) {
const updater = this.createStateUpdater('drop', node)
return (accepted, rejected, evt) => {
evt.preventDefault();
if (node === 'child') {
evt.stopPropagation()
}
this.setState(updater)
}
}

createStateUpdater(eventType, node) {
return state => {
const events = {...state[node]};
if (eventType !== events.current) {
events.previous = events.current;
}
events.current = eventType;
return {
[node]: events
}
}
}

render() {
return (
<section>
<div className="dropzone">
<Dropzone
onDragStart={this.createDragHandler('dragstart', 'parent')}
onDragEnter={this.createDragHandler('dragenter', 'parent')}
onDragOver={this.createDragHandler('dragover', 'parent')}
onDragLeave={this.createDragHandler('dragleave', 'parent')}
onDrop={this.createDropHandler('parent')}
style={parentStyle}
>
<Dropzone
onDragStart={this.createDragHandler('dragstart', 'child')}
onDragEnter={this.createDragHandler('dragenter', 'child')}
onDragOver={this.createDragHandler('dragover', 'child')}
onDragLeave={this.createDragHandler('dragleave', 'child')}
onDrop={this.createDropHandler('child')}
style={childStyle}
/>
</Dropzone>
</div>
<aside>
<p>Parent: {JSON.stringify(this.state.parent)}</p>
<p>Child: {JSON.stringify(this.state.child)}</p>
</aside>
</section>
);
}
}

<NestedDropzone />
```
13 changes: 12 additions & 1 deletion src/index.js
Expand Up @@ -85,6 +85,7 @@ class Dropzone extends React.Component {
}

onDragStart(evt) {
evt.persist()
if (this.props.onDragStart && isDragDataWithFiles(evt)) {
this.props.onDragStart.call(this, evt)
}
Expand All @@ -102,6 +103,10 @@ class Dropzone extends React.Component {

if (isDragDataWithFiles(evt)) {
Promise.resolve(this.props.getDataTransferItems(evt)).then(draggedFiles => {
if (evt.isPropagationStopped()) {
return
}

this.setState({
draggedFiles,
// Do not rely on files for the drag state. It doesn't work in Safari.
Expand All @@ -118,7 +123,8 @@ class Dropzone extends React.Component {
onDragOver(evt) {
// eslint-disable-line class-methods-use-this
evt.preventDefault()
evt.stopPropagation()
evt.persist()

try {
// The file dialog on Chrome allows users to drag files from the dialog onto
// the dropzone, causing the browser the crash when the file dialog is closed.
Expand All @@ -137,6 +143,7 @@ class Dropzone extends React.Component {

onDragLeave(evt) {
evt.preventDefault()
evt.persist()

// Only deactivate once the dropzone and all children have been left.
this.dragTargets = this.dragTargets.filter(el => el !== evt.target && this.node.contains(el))
Expand Down Expand Up @@ -190,6 +197,10 @@ class Dropzone extends React.Component {
const acceptedFiles = []
const rejectedFiles = []

if (evt.isPropagationStopped()) {
return
}

fileList.forEach(file => {
if (!disablePreview) {
file.preview = window.URL.createObjectURL(file) // eslint-disable-line no-param-reassign
Expand Down
45 changes: 44 additions & 1 deletion src/index.spec.js
Expand Up @@ -27,7 +27,7 @@ const createFile = (name, size, type) => {
return file
}

const createDtWithFiles = files => {
const createDtWithFiles = (files = []) => {
return {
dataTransfer: {
files,
Expand Down Expand Up @@ -384,6 +384,7 @@ describe('Dropzone', () => {

// Using Proxy we'll emulate IE throwing when setting dataTransfer.dropEffect
const eventProxy = {
persist() {},
preventDefault() {},
stopPropagation() {},
dataTransfer: new Proxy(
Expand Down Expand Up @@ -790,6 +791,7 @@ describe('Dropzone', () => {
const evt = {
target: { files },
preventDefault() {},
isPropagationStopped: () => false,
persist() {}
}
input.props().onChange(evt)
Expand Down Expand Up @@ -1238,6 +1240,47 @@ describe('Dropzone', () => {
expect(innerDropzone).toHaveProp('isDragActive', false)
expect(innerDropzone).toHaveProp('isDragReject', false)
})

it('does not invoke any drag event cbs on parent if child stopped event propagation', async () => {
const parentProps = {
onDragEnter: jest.fn(),
onDragOver: jest.fn(),
onDragLeave: jest.fn(),
onDrop: jest.fn()
}

const InnerDropzone = () => (
<Dropzone
onDragEnter={evt => evt.stopPropagation()}
onDragOver={evt => evt.stopPropagation()}
onDragLeave={evt => evt.stopPropagation()}
onDrop={(accepted, rejected, evt) => evt.stopPropagation()}
/>
)

const outerDropzone = mount(
<Dropzone {...parentProps}>
<InnerDropzone />
</Dropzone>
)

outerDropzone.find(InnerDropzone).simulate('dragEnter', createDtWithFiles())
await flushPromises(outerDropzone)

outerDropzone.find(InnerDropzone).simulate('dragOver', createDtWithFiles())
await flushPromises(outerDropzone)

outerDropzone.find(InnerDropzone).simulate('dragLeave', createDtWithFiles())
await flushPromises(outerDropzone)

outerDropzone.find(InnerDropzone).simulate('drop', createDtWithFiles(images))
await flushPromises(outerDropzone)

expect(parentProps.onDragEnter).not.toHaveBeenCalled()
expect(parentProps.onDragOver).not.toHaveBeenCalled()
expect(parentProps.onDragLeave).not.toHaveBeenCalled()
expect(parentProps.onDrop).not.toHaveBeenCalled()
})
})
})

Expand Down
4 changes: 4 additions & 0 deletions styleguide.config.js
Expand Up @@ -37,6 +37,10 @@ module.exports = {
name: 'Accepting specific file types',
content: 'examples/Accept/Readme.md'
},
{
name: 'Nested Dropzone',
content: 'examples/Nesting/Readme.md'
},
{
name: 'Opening File Dialog Programmatically',
content: 'examples/FileDialog/Readme.md'
Expand Down

0 comments on commit fd65863

Please sign in to comment.