Skip to content

Commit

Permalink
feat: 🎸 add useDropArea hook
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Mar 27, 2019
1 parent 0ccdf95 commit 676d0de
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
- [`useAudio`](./docs/useAudio.md) — plays audio and exposes its controls. [![][img-demo]](https://codesandbox.io/s/2o4lo6rqy)
- [`useClickAway`](./docs/useClickAway.md) — triggers callback when user clicks outside target area.
- [`useCss`](./docs/useCss.md) — dynamically adjusts CSS.
- [`useDrop`](./docs/useDrop.md) — tracks file, link and copy-paste drops.
- [`useDrop` and `useDropArea`](./docs/useDrop.md) — tracks file, link and copy-paste drops.
- [`useSpeech`](./docs/useSpeech.md) — synthesizes speech from a text string. [![][img-demo]](https://codesandbox.io/s/n090mqz69m)
- [`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)
- [`useWait`](./docs/useWait.md) — complex waiting management for UIs.
Expand Down
29 changes: 27 additions & 2 deletions docs/useDrop.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# `useDrop`
# `useDrop` and `useDropArea`

Triggers on file, link drop and copy-paste onto the page.
Triggers on file, link drop and copy-paste.

`useDrop` tracks events for the whole page, `useDropArea` tracks drop events
for a specific element.


## Usage

`useDrop`:

```jsx
import {useDrop} from 'react-use';

Expand All @@ -22,3 +27,23 @@ const Demo = () => {
);
};
```

`useDropArea`:

```jsx
import {useDropArea} from 'react-use';

const Demo = () => {
const [bond, state] = useDropArea({
onFiles: files => console.log('files', files),
onUri: uri => console.log('uri', uri),
onText: text => console.log('text', text),
});

return (
<div {...bond}>
Drop something here.
</div>
);
};
```
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
}
},
"release": {
"branches": ["master", {
"name": "next",
"prerelease": "rc"
}],
"verifyConditions": [
"@semantic-release/changelog",
"@semantic-release/npm",
Expand Down
50 changes: 50 additions & 0 deletions src/__stories__/useDropArea.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import {useDropArea} from '..';
import ShowDocs from '../util/ShowDocs';

const Demo = () => {
const [bond, state] = useDropArea({
onFiles: action('onFiles'),
onUri: action('onUri'),
onText: action('onText'),
});

const style: React.CSSProperties = {
width: 300,
height: 200,
margin: '50px auto',
border: '1px solid #000',
textAlign: 'center',
lineHeight: '200px',
...(state.over
? {
border: '1px solid green',
outline: '3px solid yellow',
background: '#f8f8f8',
}
: {}),
};

return (
<div>
<div {...bond} style={style}>Drop here</div>
<div style={{maxWidth: 300, margin: '0 auto'}}>
<ul style={{margin: 0, padding: '10px 18px'}}>
<li>See logs in <code>Actions</code> tab.</li>
<li>Drag in and drop files.</li>
<li><code>Cmd + V</code> paste text here.</li>
<li>Drag in images from other tabs.</li>
<li>Drag in link from navigation bar.</li>
<li>Below is state returned by the hook:</li>
</ul>
<pre>{JSON.stringify(state, null, 4)}</pre>
</div>
</div>
);
};

storiesOf('UI|useDropArea', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useDrop.md')} />)
.add('Default', () => <Demo />);
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useAudio from './useAudio';
import useBattery from './useBattery';
import useBoolean from './useBoolean';
import useDrop from './useDrop';
import useDropArea from './useDropArea';
import useCounter from './useCounter';
import useCss from './useCss';
import useDebounce from './useDebounce';
Expand Down Expand Up @@ -67,6 +68,7 @@ export {
useBattery,
useBoolean,
useDrop,
useDropArea,
useClickAway,
useCounter,
useCss,
Expand Down
91 changes: 81 additions & 10 deletions src/useDropArea.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,85 @@
import * as React from 'react';

const useDropArea = (el: React.ReactElement<any>) => {
if (process.env.NODE_ENV !== 'production') {
if (!React.isValidElement(el)) {
throw new TypeError(
'useDropArea first argument must be a valid ' +
'React element, such as <div/>.'
);
}
import {useMemo, useState} from 'react';
import useRefMounted from './useRefMounted';

export interface DropAreaState {
over: boolean;
}

export interface DropAreaBond {
onDragOver: React.DragEventHandler;
onDragEnter: React.DragEventHandler;
onDragLeave: React.DragEventHandler;
onDrop: React.DragEventHandler;
onPaste: React.ClipboardEventHandler;
}

export interface DropAreaOptions {
onFiles?: (files: File[], event?) => void;
onText?: (text: string, event?) => void;
onUri?: (url: string, event?) => void;
}

const noop = () => {};
const defaultState: DropAreaState = {
over: false,
};

const createProcess = (options: DropAreaOptions, mounted: React.RefObject<boolean>) => (
dataTransfer: DataTransfer,
event,
) => {
const uri = dataTransfer.getData('text/uri-list');

if (uri) {
(options.onUri || noop)(uri, event);
return;
}

if (dataTransfer.files && dataTransfer.files.length) {
(options.onFiles || noop)(Array.from(dataTransfer.files), event);
return;
}

if (dataTransfer.items && dataTransfer.items.length) {
dataTransfer.items[0].getAsString((text) => {
if (mounted.current) {
(options.onText || noop)(text, event);
}
});
}
};

const createBond = (process, setOver): DropAreaBond => ({
onDragOver: (event) => {
event.preventDefault();
},
onDragEnter: (event) => {
event.preventDefault();
setOver(true);
},
onDragLeave: () => {
setOver(false);
},
onDrop: (event) => {
event.preventDefault();
event.persist();
setOver(false);
process(event.dataTransfer, event);
},
onPaste: (event) => {
event.persist();
process(event.clipboardData, event);
},
});

const useDropArea = (options: DropAreaOptions = {}): [DropAreaBond, DropAreaState] => {
const {onFiles, onText, onUri} = options;
const mounted = useRefMounted();
const [over, setOver] = useState<boolean>(false);
const process = useMemo(() => createProcess(options, mounted), [onFiles, onText, onUri]);
const bond: DropAreaBond = useMemo(() => createBond(process, setOver), [process, setOver]);

return [bond, {over}];
};

export default useDropArea;

0 comments on commit 676d0de

Please sign in to comment.