Skip to content

Commit 676d0de

Browse files
committed
feat: 🎸 add useDropArea hook
1 parent 0ccdf95 commit 676d0de

File tree

6 files changed

+165
-13
lines changed

6 files changed

+165
-13
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
- [`useAudio`](./docs/useAudio.md) — plays audio and exposes its controls. [![][img-demo]](https://codesandbox.io/s/2o4lo6rqy)
5757
- [`useClickAway`](./docs/useClickAway.md) — triggers callback when user clicks outside target area.
5858
- [`useCss`](./docs/useCss.md) — dynamically adjusts CSS.
59-
- [`useDrop`](./docs/useDrop.md) — tracks file, link and copy-paste drops.
59+
- [`useDrop` and `useDropArea`](./docs/useDrop.md) — tracks file, link and copy-paste drops.
6060
- [`useSpeech`](./docs/useSpeech.md) — synthesizes speech from a text string. [![][img-demo]](https://codesandbox.io/s/n090mqz69m)
6161
- [`useVideo`](./docs/useVideo.md) — plays video, tracks its state, and exposes playback controls. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/ui-usevideo--demo)
6262
- [`useWait`](./docs/useWait.md) — complex waiting management for UIs.

docs/useDrop.md

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
# `useDrop`
1+
# `useDrop` and `useDropArea`
22

3-
Triggers on file, link drop and copy-paste onto the page.
3+
Triggers on file, link drop and copy-paste.
4+
5+
`useDrop` tracks events for the whole page, `useDropArea` tracks drop events
6+
for a specific element.
47

58

69
## Usage
710

11+
`useDrop`:
12+
813
```jsx
914
import {useDrop} from 'react-use';
1015

@@ -22,3 +27,23 @@ const Demo = () => {
2227
);
2328
};
2429
```
30+
31+
`useDropArea`:
32+
33+
```jsx
34+
import {useDropArea} from 'react-use';
35+
36+
const Demo = () => {
37+
const [bond, state] = useDropArea({
38+
onFiles: files => console.log('files', files),
39+
onUri: uri => console.log('uri', uri),
40+
onText: text => console.log('text', text),
41+
});
42+
43+
return (
44+
<div {...bond}>
45+
Drop something here.
46+
</div>
47+
);
48+
};
49+
```

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
}
7676
},
7777
"release": {
78+
"branches": ["master", {
79+
"name": "next",
80+
"prerelease": "rc"
81+
}],
7882
"verifyConditions": [
7983
"@semantic-release/changelog",
8084
"@semantic-release/npm",

src/__stories__/useDropArea.story.tsx

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as React from 'react';
2+
import {storiesOf} from '@storybook/react';
3+
import {action} from '@storybook/addon-actions';
4+
import {useDropArea} from '..';
5+
import ShowDocs from '../util/ShowDocs';
6+
7+
const Demo = () => {
8+
const [bond, state] = useDropArea({
9+
onFiles: action('onFiles'),
10+
onUri: action('onUri'),
11+
onText: action('onText'),
12+
});
13+
14+
const style: React.CSSProperties = {
15+
width: 300,
16+
height: 200,
17+
margin: '50px auto',
18+
border: '1px solid #000',
19+
textAlign: 'center',
20+
lineHeight: '200px',
21+
...(state.over
22+
? {
23+
border: '1px solid green',
24+
outline: '3px solid yellow',
25+
background: '#f8f8f8',
26+
}
27+
: {}),
28+
};
29+
30+
return (
31+
<div>
32+
<div {...bond} style={style}>Drop here</div>
33+
<div style={{maxWidth: 300, margin: '0 auto'}}>
34+
<ul style={{margin: 0, padding: '10px 18px'}}>
35+
<li>See logs in <code>Actions</code> tab.</li>
36+
<li>Drag in and drop files.</li>
37+
<li><code>Cmd + V</code> paste text here.</li>
38+
<li>Drag in images from other tabs.</li>
39+
<li>Drag in link from navigation bar.</li>
40+
<li>Below is state returned by the hook:</li>
41+
</ul>
42+
<pre>{JSON.stringify(state, null, 4)}</pre>
43+
</div>
44+
</div>
45+
);
46+
};
47+
48+
storiesOf('UI|useDropArea', module)
49+
.add('Docs', () => <ShowDocs md={require('../../docs/useDrop.md')} />)
50+
.add('Default', () => <Demo />);

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import useAudio from './useAudio';
55
import useBattery from './useBattery';
66
import useBoolean from './useBoolean';
77
import useDrop from './useDrop';
8+
import useDropArea from './useDropArea';
89
import useCounter from './useCounter';
910
import useCss from './useCss';
1011
import useDebounce from './useDebounce';
@@ -67,6 +68,7 @@ export {
6768
useBattery,
6869
useBoolean,
6970
useDrop,
71+
useDropArea,
7072
useClickAway,
7173
useCounter,
7274
useCss,

src/useDropArea.ts

+81-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,85 @@
1-
import * as React from 'react';
2-
3-
const useDropArea = (el: React.ReactElement<any>) => {
4-
if (process.env.NODE_ENV !== 'production') {
5-
if (!React.isValidElement(el)) {
6-
throw new TypeError(
7-
'useDropArea first argument must be a valid ' +
8-
'React element, such as <div/>.'
9-
);
10-
}
1+
import {useMemo, useState} from 'react';
2+
import useRefMounted from './useRefMounted';
3+
4+
export interface DropAreaState {
5+
over: boolean;
6+
}
7+
8+
export interface DropAreaBond {
9+
onDragOver: React.DragEventHandler;
10+
onDragEnter: React.DragEventHandler;
11+
onDragLeave: React.DragEventHandler;
12+
onDrop: React.DragEventHandler;
13+
onPaste: React.ClipboardEventHandler;
14+
}
15+
16+
export interface DropAreaOptions {
17+
onFiles?: (files: File[], event?) => void;
18+
onText?: (text: string, event?) => void;
19+
onUri?: (url: string, event?) => void;
20+
}
21+
22+
const noop = () => {};
23+
const defaultState: DropAreaState = {
24+
over: false,
25+
};
26+
27+
const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolean>) => (
28+
dataTransfer: DataTransfer,
29+
event,
30+
) => {
31+
const uri = dataTransfer.getData('text/uri-list');
32+
33+
if (uri) {
34+
(options.onUri || noop)(uri, event);
35+
return;
1136
}
37+
38+
if (dataTransfer.files && dataTransfer.files.length) {
39+
(options.onFiles || noop)(Array.from(dataTransfer.files), event);
40+
return;
41+
}
42+
43+
if (dataTransfer.items && dataTransfer.items.length) {
44+
dataTransfer.items[0].getAsString((text) => {
45+
if (mounted.current) {
46+
(options.onText || noop)(text, event);
47+
}
48+
});
49+
}
50+
};
51+
52+
const createBond = (process, setOver): DropAreaBond => ({
53+
onDragOver: (event) => {
54+
event.preventDefault();
55+
},
56+
onDragEnter: (event) => {
57+
event.preventDefault();
58+
setOver(true);
59+
},
60+
onDragLeave: () => {
61+
setOver(false);
62+
},
63+
onDrop: (event) => {
64+
event.preventDefault();
65+
event.persist();
66+
setOver(false);
67+
process(event.dataTransfer, event);
68+
},
69+
onPaste: (event) => {
70+
event.persist();
71+
process(event.clipboardData, event);
72+
},
73+
});
74+
75+
const useDropArea = (options: DropAreaOptions = {}): [DropAreaBond, DropAreaState] => {
76+
const {onFiles, onText, onUri} = options;
77+
const mounted = useRefMounted();
78+
const [over, setOver] = useState<boolean>(false);
79+
const process = useMemo(() => createProcess(options, mounted), [onFiles, onText, onUri]);
80+
const bond: DropAreaBond = useMemo(() => createBond(process, setOver), [process, setOver]);
81+
82+
return [bond, {over}];
1283
};
1384

1485
export default useDropArea;

0 commit comments

Comments
 (0)