Skip to content

Commit

Permalink
feat(utils): added useDropzone hook
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Jul 18, 2021
1 parent 8594930 commit bc07a1f
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
98 changes: 98 additions & 0 deletions packages/utils/src/__tests__/useDropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { ReactElement } from "react";
import cn from "classnames";
import { fireEvent, render } from "@testing-library/react";

import { DropzoneHanders, useDropzone } from "../useDropzone";

function Test(props: DropzoneHanders<HTMLElement>): ReactElement {
const [isOver, handlers] = useDropzone(props);

return (
<div
data-testid="dropzone"
{...handlers}
className={cn(isOver && "over")}
/>
);
}

describe("useDropzone", () => {
it("should work correctly", () => {
const onDragOver = jest.fn();
const onDragEnter = jest.fn();
const onDragLeave = jest.fn();
const onDrop = jest.fn();

const { getByTestId } = render(
<Test
onDragLeave={onDragLeave}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
onDrop={onDrop}
/>
);
const dropzone = getByTestId("dropzone");
expect(dropzone).not.toHaveClass("over");
expect(onDragOver).not.toBeCalled();
expect(onDragEnter).not.toBeCalled();
expect(onDragLeave).not.toBeCalled();
expect(onDrop).not.toBeCalled();

fireEvent.dragEnter(dropzone);
expect(dropzone).toHaveClass("over");
expect(onDragOver).not.toBeCalled();
expect(onDragEnter).toBeCalled();
expect(onDragLeave).not.toBeCalled();
expect(onDrop).not.toBeCalled();

fireEvent.dragLeave(dropzone);
expect(dropzone).not.toHaveClass("over");
expect(onDragOver).not.toBeCalled();
expect(onDragEnter).toBeCalled();
expect(onDragLeave).toBeCalled();
expect(onDrop).not.toBeCalled();

fireEvent.dragOver(dropzone);
expect(dropzone).toHaveClass("over");
expect(onDragOver).toBeCalled();
expect(onDragEnter).toBeCalled();
expect(onDragLeave).toBeCalled();
expect(onDrop).not.toBeCalled();

fireEvent.drop(dropzone);
expect(dropzone).not.toHaveClass("over");
expect(onDragOver).toBeCalled();
expect(onDragEnter).toBeCalled();
expect(onDragLeave).toBeCalled();
expect(onDrop).toBeCalled();
});

it("should prevent default and stop propagation", () => {
const onDragOver = jest.fn();
const onDragEnter = jest.fn();
const onDragLeave = jest.fn();
const onDrop = jest.fn();
const { getByTestId } = render(
<div
data-testid="container"
onDragLeave={onDragLeave}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
onDrop={onDrop}
>
<Test />
</div>
);

const dropzone = getByTestId("dropzone");
fireEvent.dragEnter(dropzone);
fireEvent.dragLeave(dropzone);
fireEvent.dragOver(dropzone);
fireEvent.drop(dropzone);

expect(onDragOver).not.toBeCalled();
expect(onDragEnter).not.toBeCalled();
expect(onDragLeave).not.toBeCalled();
expect(onDrop).not.toBeCalled();
});
});
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from "./throttle";
export * from "./types";
export * from "./unitToNumber";
export * from "./useCloseOnOutsideClick";
export * from "./useDropzone";
export * from "./useEnsuredRef";
export * from "./useInterval";
export * from "./useIsomorphicLayoutEffect";
Expand Down
108 changes: 108 additions & 0 deletions packages/utils/src/useDropzone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { DragEvent, HTMLAttributes, useCallback, useState } from "react";

/** @remarks \@since 2.9.0 */
export type DropzoneHanders<E extends HTMLElement> = Pick<
HTMLAttributes<E>,
"onDragEnter" | "onDragOver" | "onDrop" | "onDragLeave"
>;

/** @remarks \@since 2.9.0 */
export type DropzoneHookReturnValue<E extends HTMLElement> = [
boolean,
DropzoneHanders<E>
];

/**
* This hook can be used to implement simple drag-and-drop behavior for file
* uploads or special styles while dragging an element over a part of a page.
*
* @example
* Simple File
* ```ts
* const style: CSSProperties = {
* border: '1px solid blue',
* };
*
* function Example(): ReactElement {
* const { onDrop } = useFileUpload()
* const [isOver, handlers] = useDropzone({
* onDrop: (event) => {
* // normally use the `onDrop` behavior from `useFileUpload` to upload
* // files:
* // onDrop(event);
* }
* });
*
* return (
* <div {...handlers} style={isOver ? style : {}}>
* Drag and drop some files!
* {isOver && <UploadSVGIcon />}
* </div>
* );
* }
* ```
*
* @see {@link useFileUpload} for a more complex example
* @param options - The {@link DropzoneHanders} that can be merged with the
* default functionality.
* @returns the {@link DropzoneHookReturnValue}
* @remarks \@since 2.9.0
*/
export function useDropzone<E extends HTMLElement>(
options: DropzoneHanders<E>
): DropzoneHookReturnValue<E> {
const {
onDragEnter: propOnDragEnter,
onDragOver: propOnDragOver,
onDragLeave: propOnDragLeave,
onDrop: propOnDrop,
} = options;
const [isOver, setOver] = useState(false);

const onDragOver = useCallback(
(event: DragEvent<E>) => {
propOnDragOver?.(event);
event.preventDefault();
event.stopPropagation();
setOver(true);
},
[propOnDragOver]
);
const onDragEnter = useCallback(
(event: DragEvent<E>) => {
propOnDragEnter?.(event);
event.preventDefault();
event.stopPropagation();
setOver(true);
},
[propOnDragEnter]
);
const onDrop = useCallback(
(event: DragEvent<E>) => {
propOnDrop?.(event);
event.preventDefault();
event.stopPropagation();
setOver(false);
},
[propOnDrop]
);
const onDragLeave = useCallback(
(event: DragEvent<E>) => {
propOnDragLeave?.(event);
event.preventDefault();
event.stopPropagation();
setOver(false);
},
[propOnDragLeave]
);

return [
isOver,
{
onDragOver,
onDragEnter,
onDrop,
onDragLeave,
},
];
}

0 comments on commit bc07a1f

Please sign in to comment.