diff --git a/packages/utils/src/__tests__/useDropzone.tsx b/packages/utils/src/__tests__/useDropzone.tsx new file mode 100644 index 0000000000..f5adb8879c --- /dev/null +++ b/packages/utils/src/__tests__/useDropzone.tsx @@ -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): ReactElement { + const [isOver, handlers] = useDropzone(props); + + return ( +
+ ); +} + +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( + + ); + 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( +
+ +
+ ); + + 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(); + }); +}); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 817fbe4170..9062560b19 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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"; diff --git a/packages/utils/src/useDropzone.ts b/packages/utils/src/useDropzone.ts new file mode 100644 index 0000000000..bd76f5ddb0 --- /dev/null +++ b/packages/utils/src/useDropzone.ts @@ -0,0 +1,108 @@ +import { DragEvent, HTMLAttributes, useCallback, useState } from "react"; + +/** @remarks \@since 2.9.0 */ +export type DropzoneHanders = Pick< + HTMLAttributes, + "onDragEnter" | "onDragOver" | "onDrop" | "onDragLeave" +>; + +/** @remarks \@since 2.9.0 */ +export type DropzoneHookReturnValue = [ + boolean, + DropzoneHanders +]; + +/** + * 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 ( + *
+ * Drag and drop some files! + * {isOver && } + *
+ * ); + * } + * ``` + * + * @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( + options: DropzoneHanders +): DropzoneHookReturnValue { + const { + onDragEnter: propOnDragEnter, + onDragOver: propOnDragOver, + onDragLeave: propOnDragLeave, + onDrop: propOnDrop, + } = options; + const [isOver, setOver] = useState(false); + + const onDragOver = useCallback( + (event: DragEvent) => { + propOnDragOver?.(event); + event.preventDefault(); + event.stopPropagation(); + setOver(true); + }, + [propOnDragOver] + ); + const onDragEnter = useCallback( + (event: DragEvent) => { + propOnDragEnter?.(event); + event.preventDefault(); + event.stopPropagation(); + setOver(true); + }, + [propOnDragEnter] + ); + const onDrop = useCallback( + (event: DragEvent) => { + propOnDrop?.(event); + event.preventDefault(); + event.stopPropagation(); + setOver(false); + }, + [propOnDrop] + ); + const onDragLeave = useCallback( + (event: DragEvent) => { + propOnDragLeave?.(event); + event.preventDefault(); + event.stopPropagation(); + setOver(false); + }, + [propOnDragLeave] + ); + + return [ + isOver, + { + onDragOver, + onDragEnter, + onDrop, + onDragLeave, + }, + ]; +}