diff --git a/README.md b/README.md index 40b6a3611e..04781ba94c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/useDrop.md b/docs/useDrop.md index 9f0e438aa0..5fee420849 100644 --- a/docs/useDrop.md +++ b/docs/useDrop.md @@ -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 ( +
+ Drop something here. +
+ ); +}; +``` diff --git a/package.json b/package.json index cc3c389b25..712668c1a6 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,10 @@ } }, "release": { + "branches": ["master", { + "name": "next", + "prerelease": "rc" + }], "verifyConditions": [ "@semantic-release/changelog", "@semantic-release/npm", diff --git a/src/__stories__/useDropArea.story.tsx b/src/__stories__/useDropArea.story.tsx new file mode 100644 index 0000000000..fa231038eb --- /dev/null +++ b/src/__stories__/useDropArea.story.tsx @@ -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 ( +
+
Drop here
+
+ +
{JSON.stringify(state, null, 4)}
+
+
+ ); +}; + +storiesOf('UI|useDropArea', module) + .add('Docs', () => ) + .add('Default', () => ); diff --git a/src/index.ts b/src/index.ts index 9046830812..bef549a943 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/useDropArea.ts b/src/useDropArea.ts index 9710b7094e..2a6c8356a2 100644 --- a/src/useDropArea.ts +++ b/src/useDropArea.ts @@ -1,14 +1,85 @@ -import * as React from 'react'; - -const useDropArea = (el: React.ReactElement) => { - if (process.env.NODE_ENV !== 'production') { - if (!React.isValidElement(el)) { - throw new TypeError( - 'useDropArea first argument must be a valid ' + - 'React element, such as
.' - ); - } +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) => ( + 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(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;