Skip to content

Commit bc07a1f

Browse files
committed
feat(utils): added useDropzone hook
1 parent 8594930 commit bc07a1f

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React, { ReactElement } from "react";
2+
import cn from "classnames";
3+
import { fireEvent, render } from "@testing-library/react";
4+
5+
import { DropzoneHanders, useDropzone } from "../useDropzone";
6+
7+
function Test(props: DropzoneHanders<HTMLElement>): ReactElement {
8+
const [isOver, handlers] = useDropzone(props);
9+
10+
return (
11+
<div
12+
data-testid="dropzone"
13+
{...handlers}
14+
className={cn(isOver && "over")}
15+
/>
16+
);
17+
}
18+
19+
describe("useDropzone", () => {
20+
it("should work correctly", () => {
21+
const onDragOver = jest.fn();
22+
const onDragEnter = jest.fn();
23+
const onDragLeave = jest.fn();
24+
const onDrop = jest.fn();
25+
26+
const { getByTestId } = render(
27+
<Test
28+
onDragLeave={onDragLeave}
29+
onDragEnter={onDragEnter}
30+
onDragOver={onDragOver}
31+
onDrop={onDrop}
32+
/>
33+
);
34+
const dropzone = getByTestId("dropzone");
35+
expect(dropzone).not.toHaveClass("over");
36+
expect(onDragOver).not.toBeCalled();
37+
expect(onDragEnter).not.toBeCalled();
38+
expect(onDragLeave).not.toBeCalled();
39+
expect(onDrop).not.toBeCalled();
40+
41+
fireEvent.dragEnter(dropzone);
42+
expect(dropzone).toHaveClass("over");
43+
expect(onDragOver).not.toBeCalled();
44+
expect(onDragEnter).toBeCalled();
45+
expect(onDragLeave).not.toBeCalled();
46+
expect(onDrop).not.toBeCalled();
47+
48+
fireEvent.dragLeave(dropzone);
49+
expect(dropzone).not.toHaveClass("over");
50+
expect(onDragOver).not.toBeCalled();
51+
expect(onDragEnter).toBeCalled();
52+
expect(onDragLeave).toBeCalled();
53+
expect(onDrop).not.toBeCalled();
54+
55+
fireEvent.dragOver(dropzone);
56+
expect(dropzone).toHaveClass("over");
57+
expect(onDragOver).toBeCalled();
58+
expect(onDragEnter).toBeCalled();
59+
expect(onDragLeave).toBeCalled();
60+
expect(onDrop).not.toBeCalled();
61+
62+
fireEvent.drop(dropzone);
63+
expect(dropzone).not.toHaveClass("over");
64+
expect(onDragOver).toBeCalled();
65+
expect(onDragEnter).toBeCalled();
66+
expect(onDragLeave).toBeCalled();
67+
expect(onDrop).toBeCalled();
68+
});
69+
70+
it("should prevent default and stop propagation", () => {
71+
const onDragOver = jest.fn();
72+
const onDragEnter = jest.fn();
73+
const onDragLeave = jest.fn();
74+
const onDrop = jest.fn();
75+
const { getByTestId } = render(
76+
<div
77+
data-testid="container"
78+
onDragLeave={onDragLeave}
79+
onDragEnter={onDragEnter}
80+
onDragOver={onDragOver}
81+
onDrop={onDrop}
82+
>
83+
<Test />
84+
</div>
85+
);
86+
87+
const dropzone = getByTestId("dropzone");
88+
fireEvent.dragEnter(dropzone);
89+
fireEvent.dragLeave(dropzone);
90+
fireEvent.dragOver(dropzone);
91+
fireEvent.drop(dropzone);
92+
93+
expect(onDragOver).not.toBeCalled();
94+
expect(onDragEnter).not.toBeCalled();
95+
expect(onDragLeave).not.toBeCalled();
96+
expect(onDrop).not.toBeCalled();
97+
});
98+
});

packages/utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from "./throttle";
2323
export * from "./types";
2424
export * from "./unitToNumber";
2525
export * from "./useCloseOnOutsideClick";
26+
export * from "./useDropzone";
2627
export * from "./useEnsuredRef";
2728
export * from "./useInterval";
2829
export * from "./useIsomorphicLayoutEffect";

packages/utils/src/useDropzone.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { DragEvent, HTMLAttributes, useCallback, useState } from "react";
2+
3+
/** @remarks \@since 2.9.0 */
4+
export type DropzoneHanders<E extends HTMLElement> = Pick<
5+
HTMLAttributes<E>,
6+
"onDragEnter" | "onDragOver" | "onDrop" | "onDragLeave"
7+
>;
8+
9+
/** @remarks \@since 2.9.0 */
10+
export type DropzoneHookReturnValue<E extends HTMLElement> = [
11+
boolean,
12+
DropzoneHanders<E>
13+
];
14+
15+
/**
16+
* This hook can be used to implement simple drag-and-drop behavior for file
17+
* uploads or special styles while dragging an element over a part of a page.
18+
*
19+
* @example
20+
* Simple File
21+
* ```ts
22+
* const style: CSSProperties = {
23+
* border: '1px solid blue',
24+
* };
25+
*
26+
* function Example(): ReactElement {
27+
* const { onDrop } = useFileUpload()
28+
* const [isOver, handlers] = useDropzone({
29+
* onDrop: (event) => {
30+
* // normally use the `onDrop` behavior from `useFileUpload` to upload
31+
* // files:
32+
* // onDrop(event);
33+
* }
34+
* });
35+
*
36+
* return (
37+
* <div {...handlers} style={isOver ? style : {}}>
38+
* Drag and drop some files!
39+
* {isOver && <UploadSVGIcon />}
40+
* </div>
41+
* );
42+
* }
43+
* ```
44+
*
45+
* @see {@link useFileUpload} for a more complex example
46+
* @param options - The {@link DropzoneHanders} that can be merged with the
47+
* default functionality.
48+
* @returns the {@link DropzoneHookReturnValue}
49+
* @remarks \@since 2.9.0
50+
*/
51+
export function useDropzone<E extends HTMLElement>(
52+
options: DropzoneHanders<E>
53+
): DropzoneHookReturnValue<E> {
54+
const {
55+
onDragEnter: propOnDragEnter,
56+
onDragOver: propOnDragOver,
57+
onDragLeave: propOnDragLeave,
58+
onDrop: propOnDrop,
59+
} = options;
60+
const [isOver, setOver] = useState(false);
61+
62+
const onDragOver = useCallback(
63+
(event: DragEvent<E>) => {
64+
propOnDragOver?.(event);
65+
event.preventDefault();
66+
event.stopPropagation();
67+
setOver(true);
68+
},
69+
[propOnDragOver]
70+
);
71+
const onDragEnter = useCallback(
72+
(event: DragEvent<E>) => {
73+
propOnDragEnter?.(event);
74+
event.preventDefault();
75+
event.stopPropagation();
76+
setOver(true);
77+
},
78+
[propOnDragEnter]
79+
);
80+
const onDrop = useCallback(
81+
(event: DragEvent<E>) => {
82+
propOnDrop?.(event);
83+
event.preventDefault();
84+
event.stopPropagation();
85+
setOver(false);
86+
},
87+
[propOnDrop]
88+
);
89+
const onDragLeave = useCallback(
90+
(event: DragEvent<E>) => {
91+
propOnDragLeave?.(event);
92+
event.preventDefault();
93+
event.stopPropagation();
94+
setOver(false);
95+
},
96+
[propOnDragLeave]
97+
);
98+
99+
return [
100+
isOver,
101+
{
102+
onDragOver,
103+
onDragEnter,
104+
onDrop,
105+
onDragLeave,
106+
},
107+
];
108+
}

0 commit comments

Comments
 (0)