Skip to content

Commit

Permalink
feat: add {noClick}, {noKeyboard}, {noDrag} and {noDragEventsBubbling}
Browse files Browse the repository at this point in the history
- use `{noClick}` to disable click to open file dialog
- use `{noKeyboard}` to disable SPACE/ENTER to open file dialog and focus/blur state
- use `{noDrag}` to disable drag events on the dropzone
- use `{noDragEventsBubbling}` to stop drag events from propagating to parents
- close #783
  • Loading branch information
rolandjitsu committed Mar 31, 2019
1 parent fff25ae commit 91ee25d
Show file tree
Hide file tree
Showing 6 changed files with 687 additions and 228 deletions.
320 changes: 132 additions & 188 deletions examples/events/README.md
@@ -1,160 +1,9 @@
Custom event handlers provided in `getRootProps()` (e.g. `onClick`), will be invoked before the dropzone handlers.

Therefore, if you'd like to prevent the default behaviour for: `onClick` and `onKeyDown` (open the file dialog), `onFocus` and `onBlur` (sets the `isFocused` state) and drag events; use the `stopPropagation()` fn on the event:

```jsx harmony
import React, {useCallback, useReducer} from 'react';
import {useDropzone} from 'react-dropzone';

const initialEvtsState = {
preventFocus: true,
preventClick: true,
preventKeyDown: true,
preventDrag: true,
files: []
};

function Events(props) {
const [state, dispatch] = useReducer(reducer, initialEvtsState);
const myRootProps = computeRootProps(state);
const createToggleHandler = type => () => dispatch({type});

const onDrop = useCallback(files => dispatch({
type: 'setFiles',
payload: files
}), []);

const {getRootProps, getInputProps, isFocused} = useDropzone({onDrop});
const files = state.files.map(file => <li key={file.path}>{file.path}</li>);

const options = ['preventFocus', 'preventClick', 'preventKeyDown', 'preventDrag'].map(key => (
<div key={key}>
<input
id={key}
type="checkbox"
onChange={createToggleHandler(key)}
checked={state[key]}
/>
<label htmlFor={key}>
{key}
</label>
</div>
));

return (
<section>
<aside>
{options}
</aside>
<div {...getRootProps(myRootProps)}>
<input {...getInputProps()} />
<p>{getDesc(state)} (<em>{isFocused ? 'focused' : 'not focused'}</em>)</p>
</div>
<aside>
<h4>Files</h4>
<ul>{files}</ul>
</aside>
</section>
);
}

function computeRootProps(state) {
const props = {};

if (state.preventFocus) {
Object.assign(props, {
onFocus: event => event.stopPropagation(),
onBlur: event => event.stopPropagation()
});
}

if (state.preventClick) {
Object.assign(props, {onClick: event => event.stopPropagation()});
}

if (state.preventKeyDown) {
Object.assign(props, {
onKeyDown: event => {
if (event.keyCode === 32 || event.keyCode === 13) {
event.stopPropagation();
}
}
});
}

if (state.preventDrag) {
['onDragEnter', 'onDragOver', 'onDragLeave', 'onDrop'].forEach(evtName => {
Object.assign(props, {
[evtName]: event => event.stopPropagation()
});
});
}

return props;
}

function getDesc(state) {
if (state.preventClick && state.preventKeyDown && state.preventDrag) {
return `Dropzone will not respond to any events`;
} else if (state.preventClick && state.preventKeyDown) {
return `Drag 'n' drop files here`;
} else if (state.preventClick && state.preventDrag) {
return `Press SPACE/ENTER to open the file dialog`;
} else if (state.preventKeyDown && state.preventDrag) {
return `Click to open the file dialog`;
} else if (state.preventClick) {
return `Drag 'n' drop files here or press SPACE/ENTER to open the file dialog`;
} else if (state.preventKeyDown) {
return `Drag 'n' drop files here or click to open the file dialog`;
} else if (state.preventDrag) {
return `Click/press SPACE/ENTER to open the file dialog`;
}
return `Drag 'n' drop files here or click/press SPACE/ENTER to open the file dialog`;
}

function reducer(state, action) {
switch (action.type) {
case 'preventFocus':
return {
...state,
preventFocus: !state.preventFocus
};
case 'preventClick':
return {
...state,
preventClick: !state.preventClick
};
case 'preventKeyDown':
return {
...state,
preventKeyDown: !state.preventKeyDown
};
case 'preventDrag':
return {
...state,
preventDrag: !state.preventDrag
};
case 'setFiles':
return {
...state,
files: action.payload
};
default:
return state;
}
}

<Events />
```

This sort of behavior can come in handy when you need to nest dropzone components and prevent any drag events from the child propagate to the parent:

If you'd like to prevent drag events propagation from the child to parent, you can use the `{noDragEventsBubbling}` property on the child:
```jsx harmony
import React, {useCallback, useMemo, useReducer} from 'react';
import {useDropzone} from 'react-dropzone';

const initialParentState = {
preventDrag: true,
parent: {},
child: {}
};
Expand All @@ -179,36 +28,12 @@ const childStyle = {

function Parent(props) {
const [state, dispatch] = useReducer(parentReducer, initialParentState);
const {preventDrag} = state;
const togglePreventDrag = useCallback(() => dispatch({type: 'preventDrag'}), []);
const dropzoneProps = useMemo(() => computeDropzoneProps({dispatch}, 'parent'), [dispatch]);

const {getRootProps} = useDropzone(dropzoneProps);

const childProps = useMemo(() => ({
preventDrag,
dispatch
}), [
preventDrag,
dispatch
]);
const childProps = useMemo(() => ({dispatch}), [dispatch]);

return (
<section>
<aside>
<div>
<input
id="toggleDrag"
type="checkbox"
onChange={togglePreventDrag}
checked={preventDrag}
/>
<label htmlFor="toggleDrag">
preventDrag
</label>
</div>
</aside>
<br />
<div {...getRootProps({style: parentStyle})}>
<Child {...childProps} />
</div>
Expand All @@ -221,8 +46,10 @@ function Parent(props) {
}

function Child(props) {
const dropzoneProps = useMemo(() => computeDropzoneProps(props, 'child'), [
props.preventDrag,
const dropzoneProps = useMemo(() => ({
...computeDropzoneProps(props, 'child'),
noDragEventsBubbling: true
}), [
props.dispatch
]);
const {getRootProps} = useDropzone(dropzoneProps);
Expand All @@ -238,11 +65,6 @@ function Child(props) {

function parentReducer(state, action) {
switch (action.type) {
case 'preventDrag':
return {
...state,
preventDrag: !state.preventDrag
};
case 'onDragEnter':
case 'onDragOver':
case 'onDragLeave':
Expand Down Expand Up @@ -274,9 +96,6 @@ function computeDropzoneProps(props, node) {
Object.assign(rootProps, {
[type]: (...args) => {
const event = type === 'onDrop' ? args.pop() : args.shift();
if (props.preventDrag) {
event.stopPropagation();
}
props.dispatch({
type,
payload: {node}
Expand All @@ -292,4 +111,129 @@ function computeDropzoneProps(props, node) {
<Parent />
```

Note that the sort of behavior illustrated above can lead to some confusion. For example, the `onDragLeave` callback of the parent will not be called if the callback for the same event is invoked on the child. This happens because we invoked `stopPropagation()` on the event from the child.
Note that internally we use `event.stopPropagation()` to achieve the behavior illustrated above, but this comes with its own [drawbacks](https://javascript.info/bubbling-and-capturing#stopping-bubbling).

If you'd like to selectively turn off the default dropzone behavior for `onClick`, `onKeyDown` (both open the file dialog), `onFocus` and `onBlur` (sets the `isFocused` state) and drag events; use the `{noClick}`, `{noKeyboard}` and `{noDrag}` properties:
```jsx harmony
import React, {useCallback, useReducer} from 'react';
import {useDropzone} from 'react-dropzone';

const initialEvtsState = {
noClick: true,
noKeyboard: true,
noDrag: true,
files: []
};

function Events(props) {
const [state, dispatch] = useReducer(reducer, initialEvtsState);
const createToggleHandler = type => () => dispatch({type});

const onDrop = useCallback(files => dispatch({
type: 'setFiles',
payload: files
}), []);

const {getRootProps, getInputProps, isFocused} = useDropzone({...state, onDrop});
const files = state.files.map(file => <li key={file.path}>{file.path}</li>);

const options = ['noClick', 'noKeyboard', 'noDrag'].map(key => (
<div key={key}>
<input
id={key}
type="checkbox"
onChange={createToggleHandler(key)}
checked={state[key]}
/>
<label htmlFor={key}>
{key}
</label>
</div>
));

return (
<section>
<aside>
{options}
</aside>
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>{getDesc(state)} (<em>{isFocused ? 'focused' : 'not focused'}</em>)</p>
</div>
<aside>
<h4>Files</h4>
<ul>{files}</ul>
</aside>
</section>
);
}


function getDesc(state) {
if (state.noClick && state.noKeyboard && state.noDrag) {
return `Dropzone will not respond to any events`;
} else if (state.noClick && state.noKeyboard) {
return `Drag 'n' drop files here`;
} else if (state.noClick && state.noDrag) {
return `Press SPACE/ENTER to open the file dialog`;
} else if (state.noKeyboard && state.noDrag) {
return `Click to open the file dialog`;
} else if (state.noClick) {
return `Drag 'n' drop files here or press SPACE/ENTER to open the file dialog`;
} else if (state.noKeyboard) {
return `Drag 'n' drop files here or click to open the file dialog`;
} else if (state.noDrag) {
return `Click/press SPACE/ENTER to open the file dialog`;
}
return `Drag 'n' drop files here or click/press SPACE/ENTER to open the file dialog`;
}

function reducer(state, action) {
switch (action.type) {
case 'noClick':
return {
...state,
noClick: !state.noClick
};
case 'noKeyboard':
return {
...state,
noKeyboard: !state.noKeyboard
};
case 'noDrag':
return {
...state,
noDrag: !state.noDrag
};
case 'setFiles':
return {
...state,
files: action.payload
};
default:
return state;
}
}

<Events />
```

Keep in mind that if you provide your own callback handlers as well and use `event.stopPropagation()`, it will prevent the default dropzone behavior:
```jsx harmony
import React from 'react';
import Dropzone from 'react-dropzone';

// Note that there will be nothing logged when files are dropped
<Dropzone onDrop={files => console.log(files)}>
{({getRootProps, getInputProps}) => (
<div
{...getRootProps({
onDrop: event => event.stopPropagation()
})}
>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
)}
</Dropzone>
```

0 comments on commit 91ee25d

Please sign in to comment.