feat: 🎸 add useDropArea hook

This commit is contained in:
streamich 2019-03-27 23:05:46 +01:00
parent 0ccdf9531e
commit 676d0ded3e
6 changed files with 164 additions and 12 deletions

View File

@ -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.

View File

@ -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';
@ -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>
);
};
```

View File

@ -75,6 +75,10 @@
}
},
"release": {
"branches": ["master", {
"name": "next",
"prerelease": "rc"
}],
"verifyConditions": [
"@semantic-release/changelog",
"@semantic-release/npm",

View File

@ -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 />);

View File

@ -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';
@ -67,6 +68,7 @@ export {
useBattery,
useBoolean,
useDrop,
useDropArea,
useClickAway,
useCounter,
useCss,

View File

@ -1,14 +1,85 @@
import * as React from 'react';
import {useMemo, useState} from 'react';
import useRefMounted from './useRefMounted';
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/>.'
);
}
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;