Drag and drop an image in the editor (#10145)

* Refactored drag n drop functionality to a hook and component.

* Add the snackbar to the article editor.

* Now you can drag and drop images in the post body content.

* Cleaning up a few things.

* Now post cover images can be dragged and dropped.

* Removed unused empty function.

* Added @testing-library/preact-hooks so that we can test hooks.

* Some renaming, small refactor.

* Added tests

* Added more tests.

* Added more tests.

* Added message about dropping only one image in post body.

* Added message about dropping only one image for cover image.

* put onDrop into it's own meethod.

* More tests.

* Added some API documentation.

* Updated editor help with drag and drop image help.

* Changed wording.

* Now the alt text for markdown removes the file extension.

* Updated help wording. Thanks @rhymes

* Now the image markdown adds a new line at the end as we currently do not support inline images.
This commit is contained in:
Nick Taylor 2020-09-04 14:12:59 -04:00 committed by GitHub
parent 2f36630ff0
commit f0963c1bfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 660 additions and 74 deletions

View file

@ -1,3 +1,5 @@
import { validateFileInputs } from '../packs/validateFileInputs';
export function previewArticle(payload, successCb, failureCb) {
fetch('/articles/preview', {
method: 'POST',
@ -97,3 +99,23 @@ export function generateMainImage(payload, successCb, failureCb) {
})
.catch(failureCb);
}
/**
* Processes images for upload.
*
* @param {FileList} images Images to be uploaded.
* @param {Function} handleImageSuccess The handler that runs when the image is uploaded successfully.
* @param {Function} handleImageFailure The handler that runs when the image upload fails.
*/
export function processImageUpload(
images,
handleImageSuccess,
handleImageFailure,
) {
// Currently only one image is supported for upload.
if (images.length > 0 && validateFileInputs()) {
const payload = { image: images };
generateMainImage(payload, handleImageSuccess, handleImageFailure);
}
}

View file

@ -1,9 +1,12 @@
import { h, Component, Fragment } from 'preact';
import PropTypes from 'prop-types';
import { addSnackbarItem } from '../../Snackbar';
import { generateMainImage } from '../actions';
import { validateFileInputs } from '../../packs/validateFileInputs';
import { onDragOver, onDragExit } from './dragAndDropHelpers';
import { Button } from '@crayons';
import { Spinner } from '@crayons/Spinner/Spinner';
import { DragAndDropZone } from '@utilities/dragAndDrop';
export class ArticleCoverImage extends Component {
state = {
@ -17,14 +20,16 @@ export class ArticleCoverImage extends Component {
this.setState({ uploadingImage: false });
};
handleMainImageUpload = (e) => {
e.preventDefault();
handleMainImageUpload = (event) => {
event.preventDefault();
this.setState({ uploadingImage: true });
this.clearUploadError();
if (validateFileInputs()) {
const payload = { image: e.target.files, wrap_cloudinary: true };
const { files: image } =
event instanceof DragEvent ? event.dataTransfer : event.target;
const payload = { image, wrap_cloudinary: true };
generateMainImage(payload, this.onImageUploadSuccess, this.onUploadError);
}
@ -53,55 +58,75 @@ export class ArticleCoverImage extends Component {
});
};
onDropImage = (event) => {
onDragExit(event);
if (event.dataTransfer.files.length > 1) {
addSnackbarItem({
message: 'Only one image can be dropped at a time.',
addCloseButton: true,
});
return;
}
this.handleMainImageUpload(event);
};
render() {
const { mainImage } = this.props;
const { uploadError, uploadErrorMessage, uploadingImage } = this.state;
const uploadLabel = mainImage ? 'Change' : 'Add a cover image';
return (
<div className="crayons-article-form__cover" role="presentation">
{!uploadingImage && mainImage && (
<img
src={mainImage}
className="crayons-article-form__cover__image"
width="250"
height="105"
alt="Post cover"
/>
)}
<div className="flex items-center">
{uploadingImage ? (
<span class="lh-base pl-1 border-0 py-2 inline-block">
<Spinner /> Uploading...
</span>
) : (
<Fragment>
<Button variant="outlined" className="mr-2 whitespace-nowrap">
<label htmlFor="cover-image-input">{uploadLabel}</label>
<input
id="cover-image-input"
type="file"
onChange={this.handleMainImageUpload}
accept="image/*"
className="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
data-max-file-size-mb="25"
/>
</Button>
{mainImage && (
<Button
variant="ghost-danger"
onClick={this.triggerMainImageRemoval}
>
Remove
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={this.onDropImage}
>
<div className="crayons-article-form__cover" role="presentation">
{!uploadingImage && mainImage && (
<img
src={mainImage}
className="crayons-article-form__cover__image"
width="250"
height="105"
alt="Post cover"
/>
)}
<div className="flex items-center">
{uploadingImage ? (
<span class="lh-base pl-1 border-0 py-2 inline-block">
<Spinner /> Uploading...
</span>
) : (
<Fragment>
<Button variant="outlined" className="mr-2 whitespace-nowrap">
<label htmlFor="cover-image-input">{uploadLabel}</label>
<input
id="cover-image-input"
type="file"
onChange={this.handleMainImageUpload}
accept="image/*"
className="w-100 h-100 absolute left-0 right-0 top-0 bottom-0 overflow-hidden opacity-0 cursor-pointer"
data-max-file-size-mb="25"
/>
</Button>
)}
</Fragment>
{mainImage && (
<Button
variant="ghost-danger"
onClick={this.triggerMainImageRemoval}
>
Remove
</Button>
)}
</Fragment>
)}
</div>
{uploadError && (
<p className="articleform__uploaderror">{uploadErrorMessage}</p>
)}
</div>
{uploadError && (
<p className="articleform__uploaderror">{uploadErrorMessage}</p>
)}
</div>
</DragAndDropZone>
);
}
}

View file

@ -1,7 +1,38 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import Textarea from 'preact-textarea-autosize';
import { useEffect, useRef } from 'preact/hooks';
import { Toolbar } from './Toolbar';
import {
handleImageDrop,
handleImageFailure,
onDragOver,
onDragExit,
} from './dragAndDropHelpers';
import { useDragAndDrop } from '@utilities/dragAndDrop';
function handleImageSuccess(textAreaRef) {
return function (response) {
// Function is within the component to be able to access
// textarea ref.
const editableBodyElement = textAreaRef.current.base;
const { links, image } = response;
const altText = image[0].name.replace(/\.[^.]+$/, '');
const markdownImageLink = `![${altText}](${links[0]})\n`;
const { selectionStart, selectionEnd, value } = editableBodyElement;
const before = value.substring(0, selectionStart);
const after = value.substring(selectionEnd, value.length);
editableBodyElement.value = `${before + markdownImageLink} ${after}`;
editableBodyElement.selectionStart =
selectionStart + markdownImageLink.length;
editableBodyElement.selectionEnd = editableBodyElement.selectionStart;
// Dispatching a new event so that linkstate, https://github.com/developit/linkstate,
// the function used to create the onChange prop gets called correctly.
editableBodyElement.dispatchEvent(new Event('input'));
};
}
export const EditorBody = ({
onChange,
@ -9,6 +40,22 @@ export const EditorBody = ({
switchHelpContext,
version,
}) => {
const textAreaRef = useRef(null);
const { setElement } = useDragAndDrop({
onDrop: handleImageDrop(
handleImageSuccess(textAreaRef),
handleImageFailure,
),
onDragOver,
onDragExit,
});
useEffect(() => {
if (textAreaRef.current) {
setElement(textAreaRef.current.base);
}
});
return (
<div
data-testid="article-form__body"
@ -27,6 +74,7 @@ export const EditorBody = ({
switchHelpContext(_event);
}}
name="body_markdown"
ref={textAreaRef}
/>
</div>
);

View file

@ -235,6 +235,10 @@ export class Help extends Component {
</a>{' '}
to add rich content such as Tweets, YouTube videos, etc.
</li>
<li>
In addition to images for the post's content, you can also drag and
drop a cover image
</li>
</ul>
</div>
);

View file

@ -79,6 +79,8 @@ describe('<ArticleCoverImage />', () => {
});
it('allows a user to change the image', async () => {
global.DragEvent = jest.fn();
fetch.mockResponse(
JSON.stringify({
image: ['/i/changed-fake-link.jpg'],

View file

@ -0,0 +1,114 @@
import {
handleImageDrop,
handleImageFailure,
matchesDataTransferType,
} from '../dragAndDropHelpers';
import { addSnackbarItem } from '../../../Snackbar';
import { processImageUpload } from '../../actions';
jest.mock('../../../Snackbar');
jest.mock('../../actions');
describe('Article drag and drop helpers', () => {
beforeEach(() => {
addSnackbarItem.mockReset();
});
describe('matchesDataTransferType', () => {
it('should match data transfer type Files when no data transfer type to match is set', () => {
expect(matchesDataTransferType(['Files'])).toEqual(true);
});
it('should match data transfer type when the type to match is in the list of types', () => {
expect(
matchesDataTransferType(
['SomeType', 'OtherType', 'BestType'],
'SomeType',
),
).toEqual(true);
});
it('should not match data transfer type when the type to match is not in the list of types', () => {
expect(
matchesDataTransferType(
['SomeType', 'OtherType', 'BestType'],
'NotInTheListType',
),
).toEqual(false);
});
});
describe('handleImageFailure', () => {
it('should handle image failure', () => {
handleImageFailure(new Error('oh no'));
expect(addSnackbarItem).toBeCalledTimes(1);
});
});
describe('handleImageDrop', () => {
it('should abort if data dropped is not files', () => {
const successHandler = jest.fn();
const failureHandler = jest.fn();
const imageHandler = handleImageDrop(successHandler, failureHandler);
const dropEvent = {
preventDefault: jest.fn(),
currentTarget: document.createElement('textarea'),
dataTransfer: { types: ['NotFiles'] },
};
imageHandler(dropEvent);
expect(processImageUpload).not.toHaveBeenCalled();
});
it('should abort if data dropped is multiple files', () => {
const successHandler = jest.fn();
const failureHandler = jest.fn();
const imageHandler = handleImageDrop(successHandler, failureHandler);
const dropEvent = {
preventDefault: jest.fn(),
currentTarget: document.createElement('textarea'),
dataTransfer: {
types: ['Files'],
files: [
new File(['(⌐□_□)'], 'chucknorris.png', {
type: 'image/png',
}),
new File(['ʕʘ̅͜ʘ̅ʔ'], 'vandamme.png', {
type: 'image/png',
}),
],
},
};
imageHandler(dropEvent);
expect(addSnackbarItem).toHaveBeenCalledTimes(1);
expect(processImageUpload).not.toHaveBeenCalled();
});
it('should process image upload if data dropped is one image file', () => {
const successHandler = jest.fn();
const failureHandler = jest.fn();
const imageHandler = handleImageDrop(successHandler, failureHandler);
const dropEvent = {
preventDefault: jest.fn(),
currentTarget: document.createElement('textarea'),
dataTransfer: {
types: ['Files'],
files: [
new File(['(⌐□_□)'], 'chucknorris.png', {
type: 'image/png',
}),
],
},
};
imageHandler(dropEvent);
expect(addSnackbarItem).not.toHaveBeenCalled();
expect(processImageUpload).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -0,0 +1,73 @@
import { addSnackbarItem } from '../../Snackbar';
import { processImageUpload } from '../actions';
/**
* Determines if at least one type of drag and drop datum type matches the data transfer type to match.
*
* @param {string[]} types An array of data transfer types.
* @param {string} dataTransferType The data transfer type to match.
*/
export function matchesDataTransferType(
types = [],
dataTransferType = 'Files',
) {
return types.some((type) => type === dataTransferType);
}
// TODO: Document functions
export function handleImageDrop(handleImageSuccess, handleImageFailure) {
return function (event) {
event.preventDefault();
if (!matchesDataTransferType(event.dataTransfer.types)) {
return;
}
event.currentTarget.classList.remove('opacity-25');
const { files } = event.dataTransfer;
if (files.length > 1) {
addSnackbarItem({
message: 'Only one image can be dropped at a time.',
addCloseButton: true,
});
return;
}
processImageUpload(files, handleImageSuccess, handleImageFailure);
};
}
/**
* Dragover handler for the editor
*
* @param {DragEvent} event the drag event.
*/
export function onDragOver(event) {
event.preventDefault();
event.currentTarget.classList.add('opacity-25');
}
/**
* DragExit handler for the editor
*
* @param {DragEvent} event the drag event.
*/
export function onDragExit(event) {
event.preventDefault();
event.currentTarget.classList.remove('opacity-25');
}
/**
* Handler for when image upload fails.
*
* @param {Error} error an error
* @param {string} error.message an error message
*/
export function handleImageFailure({ message }) {
addSnackbarItem({
message,
addCloseButton: true,
});
}

View file

@ -8,8 +8,8 @@ import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import { setupPusher } from '../utilities/connect';
import debounceAction from '../utilities/debounceAction';
import { dragDrop } from '../utilities/dragAndUpload';
import { addSnackbarItem } from '../Snackbar';
import { processImageUpload } from '../article-form/actions';
import {
conductModeration,
getAllMessages,
@ -41,6 +41,7 @@ import Message from './message';
import ActionMessage from './actionMessage';
import Content from './content';
import { VideoContent } from './videoContent';
import { DragAndDropZone } from '@utilities/dragAndDrop';
const NARROW_WIDTH_LIMIT = 767;
const WIDE_WIDTH_LIMIT = 1600;
@ -130,7 +131,9 @@ export default class Chat extends Component {
setupObserver(this.observerCallback);
this.subscribePusher(`private-message-notifications--${appName}-${currentUserId}`);
this.subscribePusher(
`private-message-notifications--${appName}-${currentUserId}`,
);
if (activeChannelId) {
sendOpen(activeChannelId, this.handleChannelOpenSuccess, null);
@ -1412,14 +1415,23 @@ export default class Chat extends Component {
.getElementById('jumpback_button')
.classList.remove('chatchanneljumpback__hide');
};
handleImageDrop = (e) => {
e.preventDefault();
const { files } = e.dataTransfer;
const messageArea = document.getElementById('messagelist');
messageArea.classList.remove('opacity-25');
messageArea.classList.add('opacity-100');
dragDrop(files, this.handleImageSuccess, this.handleImageFailure);
handleDragOver = (event) => {
event.preventDefault();
event.currentTarget.classList.add('opacity-25');
};
handleDragExit = (event) => {
event.preventDefault();
event.currentTarget.classList.remove('opacity-25');
};
handleImageDrop = (event) => {
event.preventDefault();
const { files } = event.dataTransfer;
event.currentTarget.classList.remove('opacity-25');
processImageUpload(files, this.handleImageSuccess, this.handleImageFailure);
};
handleImageSuccess = (res) => {
const { links, image } = res;
@ -1433,7 +1445,7 @@ export default class Chat extends Component {
const after = text.substring(end, text.length);
el.value = `${before + mLink} ${after}`;
el.selectionStart = start + mLink.length + 1;
el.selectionEnd = start + mLink.length + 1;
el.selectionEnd = el.selectionStart;
el.focus();
};
handleImageFailure = (e) => {
@ -1448,7 +1460,6 @@ export default class Chat extends Component {
e.preventDefault();
const messageArea = document.getElementById('messagelist');
messageArea.classList.remove('opacity-25');
messageArea.classList.add('opacity-100');
}
renderActiveChatChannel = (channelHeader) => {
const { state, props } = this;
@ -1457,20 +1468,26 @@ export default class Chat extends Component {
<div className="activechatchannel">
<div className="activechatchannel__conversation">
{channelHeader}
<div
className="activechatchannel__messages"
onScroll={this.handleMessageScroll}
ref={(scroller) => {
this.scroller = scroller;
}}
id="messagelist"
onDragOver={this.handleDragHover}
<DragAndDropZone
onDragOver={this.handleDragOver}
onDragExit={this.handleDragExit}
onDrop={this.handleImageDrop}
>
{this.renderMessages()}
<div className="messagelist__sentinel" id="messagelist__sentinel" />
</div>
<div
className="activechatchannel__messages"
onScroll={this.handleMessageScroll}
ref={(scroller) => {
this.scroller = scroller;
}}
id="messagelist"
>
{this.renderMessages()}
<div
className="messagelist__sentinel"
id="messagelist__sentinel"
/>
</div>
</DragAndDropZone>
<div
className="chatchanneljumpback chatchanneljumpback__hide"
id="jumpback_button"

View file

@ -1,6 +1,7 @@
import { h, render } from 'preact';
import { getUserDataAndCsrfToken } from '../chat/util';
import ArticleForm from '../article-form/articleForm';
import { Snackbar } from '../Snackbar';
HTMLDocument.prototype.ready = new Promise((resolve) => {
if (document.readyState !== 'loading') {
@ -11,6 +12,13 @@ HTMLDocument.prototype.ready = new Promise((resolve) => {
});
function loadForm() {
// The Snackbar for the article page
const snackZone = document.getElementById('snack-zone');
if (snackZone) {
render(<Snackbar lifespan="3" />, snackZone);
}
getUserDataAndCsrfToken().then(({ currentUser, csrfToken }) => {
window.currentUser = currentUser;
window.csrfToken = csrfToken;

View file

@ -0,0 +1,179 @@
import { h } from 'preact';
import { render } from '@testing-library/preact';
import { renderHook, act } from '@testing-library/preact-hooks';
import { DragAndDropZone, useDragAndDrop } from '@utilities/dragAndDrop';
describe('drag and drop for components', () => {
describe('useDragAndDrop', () => {
it('should not add drag and drop event listeners when DOM element is null', () => {
const onDrop = jest.fn();
const onDragOver = jest.fn();
const onDragExit = jest.fn();
HTMLElement.prototype.addEventListener = jest.fn();
HTMLDocument.prototype.addEventListener = jest.fn();
const { result } = renderHook(() =>
useDragAndDrop({
onDrop,
onDragOver,
onDragExit,
}),
);
act(() => {
result.current.setElement(null);
});
expect(HTMLElement.prototype.addEventListener).not.toHaveBeenCalled();
expect(HTMLDocument.prototype.addEventListener).not.toHaveBeenCalled();
});
it('should attach drag and drop events when setElement receives a DOM node', () => {
const onDrop = jest.fn();
const onDragOver = jest.fn();
const onDragExit = jest.fn();
HTMLElement.prototype.addEventListener = jest.fn();
HTMLDocument.prototype.addEventListener = jest.fn();
const { result } = renderHook(() =>
useDragAndDrop({
onDrop,
onDragOver,
onDragExit,
}),
);
act(() => {
result.current.setElement(document.createElement('textarea'));
});
expect(HTMLElement.prototype.addEventListener).toHaveBeenCalledTimes(4);
expect(HTMLDocument.prototype.addEventListener).toHaveBeenCalledTimes(2);
});
it('should remove drag and drop event listeners when the hook is unmounted', () => {
const onDrop = jest.fn();
const onDragOver = jest.fn();
const onDragExit = jest.fn();
HTMLElement.prototype.removeEventListener = jest.fn();
HTMLDocument.prototype.removeEventListener = jest.fn();
const { unmount, result } = renderHook(() =>
useDragAndDrop({
onDrop,
onDragOver,
onDragExit,
}),
);
act(() => {
result.current.setElement(document.createElement('textarea'));
});
unmount();
expect(HTMLDocument.prototype.removeEventListener).toHaveBeenCalledTimes(
2,
);
expect(HTMLElement.prototype.removeEventListener).toHaveBeenCalledTimes(
4,
);
});
});
describe('<DragAndDropZone />', () => {
it('should attach drag and drop events', async () => {
const onDrop = jest.fn();
const onDragOver = jest.fn();
const onDragExit = jest.fn();
HTMLElement.prototype.addEventListener = jest.fn();
HTMLDocument.prototype.addEventListener = jest.fn();
const { rerender } = render(
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={onDrop}
>
<textarea>I'm a text area</textarea>
</DragAndDropZone>,
);
// Rerendering so that the ref gets set to the DOM node.
// represented by the Preact element <textarea />
rerender(
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={onDrop}
>
<textarea>I'm a text area</textarea>
</DragAndDropZone>,
);
expect(HTMLDocument.prototype.addEventListener).toBeCalledTimes(2);
expect(HTMLElement.prototype.addEventListener).toHaveBeenCalledTimes(4);
});
it('should not attach drag and drop events', async () => {
const onDrop = jest.fn();
const onDragOver = jest.fn();
const onDragExit = jest.fn();
HTMLElement.prototype.removeEventListener = jest.fn();
HTMLDocument.prototype.removeEventListener = jest.fn();
const { unmount, rerender } = render(
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={onDrop}
>
<textarea>I'm a text area</textarea>
</DragAndDropZone>,
);
// Rerendering so that the ref gets set to the DOM node.
// represented by the Preact element <textarea />
rerender(
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={onDrop}
>
<textarea>I'm a text area</textarea>
</DragAndDropZone>,
);
unmount();
expect(HTMLDocument.prototype.removeEventListener).toBeCalledTimes(2);
expect(HTMLElement.prototype.removeEventListener).toHaveBeenCalledTimes(
4,
);
});
it('should not attach drag and drop events when no children.', async () => {
const onDrop = jest.fn();
const onDragOver = jest.fn();
const onDragExit = jest.fn();
HTMLElement.prototype.addEventListener = jest.fn();
HTMLDocument.prototype.addEventListener = jest.fn();
expect(() => {
render(
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={onDrop}
/>,
);
}).toThrowError(
'The <DragAndDropZone /> component children prop is null or was not specified.',
);
});
});
});

View file

@ -0,0 +1,97 @@
import { cloneElement } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
/**
* A custom Preact hook used to attach drag and drop functionality to a DOM element.
* @example
* function SomeComponent(props) {
* const { setElement } = useDragAndDrop({
* onDrop: someDropHandler,
* onDragOver: someDragOverHandler,
* onDragExit: someDragExitHandler
* });
*
* const someDomRef = useRef(null);
*
* useEffect(() => {
* if (someDomRef.current) {
* setElement(someDomRef.current);
* }
* });
*
* return <textarea ref={someDomRef}>I'm a text area</textarea>;
* };
*
* @param {object} props
* @param {Function} props.onDragOver The handler that runs when the dragover event is fired.
* @param {Function} props.onDragExit The handler that runs when the dragexit/dragleave events are fired.
* @param {Function} props.onDrop The handler that runs when the drop event is fired.
*/
export function useDragAndDrop({ onDragOver, onDragExit, onDrop }) {
const [element, setElement] = useState(null);
useEffect(() => {
if (!element) {
return;
}
const noDragAndDropHandler = (event) => event.preventDefault();
document.addEventListener('dragover', noDragAndDropHandler);
document.addEventListener('drop', noDragAndDropHandler);
element.addEventListener('dragover', onDragOver);
element.addEventListener('dragexit', onDragExit);
element.addEventListener('dragleave', onDragExit);
element.addEventListener('drop', onDrop);
return () => {
document.removeEventListener('dragover', noDragAndDropHandler);
document.removeEventListener('drop', noDragAndDropHandler);
element.removeEventListener('dragover', onDragOver);
element.removeEventListener('dragexit', onDragExit);
element.removeEventListener('dragleave', onDragExit);
element.removeEventListener('drop', onDrop);
};
}, [element, onDragOver, onDragExit, onDrop]);
return { setElement };
}
/**
* Registers drag and drop events for the child element that is wrapped by this component.
*
* @example
* <DragAndDropZone
* onDragOver={someDragOverHandler}
* onDragExit={someDragExitHandler}
* onDrop={someDropHandler}
* >
* <textarea>I'm a text area</textarea>
* <DragAndDropZone>
*
* @param {object} props
* @param {JSX.Element} props.children The React element that will register it's DOM counterpart with drag and drop events.
* @param {Function} props.onDragOver The handler that runs when the dragover event is fired.
* @param {Function} props.onDragExit The handler that runs when the dragexit/dragleave events are fired.
* @param {Function} props.onDrop The handler that runs when the drop event is fired.
*/
export function DragAndDropZone({ children, onDragOver, onDragExit, onDrop }) {
if (!children) {
throw new Error(
'The <DragAndDropZone /> component children prop is null or was not specified.',
);
}
const { setElement } = useDragAndDrop({ onDragOver, onDragExit, onDrop });
const dropZoneRef = useRef(null);
if (dropZoneRef.current) {
setElement(dropZoneRef.current);
}
return cloneElement(children, {
ref: dropZoneRef,
});
}

View file

@ -1,9 +0,0 @@
import { generateMainImage } from '../article-form/actions';
import { validateFileInputs } from '../packs/validateFileInputs';
export function dragDrop(files, handleImageSuccess, handleImageFailure) {
if (files.length > 0 && validateFileInputs()) {
const payload = { image: files };
generateMainImage(payload, handleImageSuccess, handleImageFailure);
}
}

View file

@ -100,6 +100,7 @@
"@storybook/preact": "^6.0.20",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/preact": "^2.0.0",
"@testing-library/preact-hooks": "^1.0.6",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.3.0",

View file

@ -2324,6 +2324,11 @@
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/preact-hooks@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@testing-library/preact-hooks/-/preact-hooks-1.0.6.tgz#f66d1f029548ca6200c3a35ba534531671747284"
integrity sha512-zNMnuFWqNYp/Ac5jG7o+GPsRAGnaNdHvHORFXZT7k0bseOdKJijLza1Xx+ty19+GsE/AUTYC8BCbAC67Z+sgMg==
"@testing-library/preact@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@testing-library/preact/-/preact-2.0.0.tgz#bf40faa24136a2388e74655d154fe3538541c33b"