Use window-level drag detection for audit list dropzone

The previous approach attached drag handlers to a content div that
didn't fill the viewport, making the dropzone hard to target. Now
drag detection uses window-level events with a counter for correct
nested element handling, and the full-screen overlay itself becomes
the react-dropzone drop target.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-18 20:17:35 +01:00
parent d360725d24
commit d746f32903

View File

@@ -21,7 +21,7 @@ import {
Tr,
useDialogRef,
} from "@probo/ui";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useDropzone } from "react-dropzone";
import {
graphql,
@@ -110,10 +110,14 @@ export default function AuditsPage(props: Props) {
const canCreateAudit = data.node.canCreateAudit;
const [droppedFile, setDroppedFile] = useState<File | null>(null);
const [isDragging, setIsDragging] = useState(false);
const dropDialogRef = useDialogRef();
const dragCounterRef = useRef(0);
const onDrop = useCallback(
(acceptedFiles: File[]) => {
setIsDragging(false);
dragCounterRef.current = 0;
if (!canCreateAudit || acceptedFiles.length === 0) return;
setDroppedFile(acceptedFiles[0]);
dropDialogRef.current?.open();
@@ -121,7 +125,48 @@ export default function AuditsPage(props: Props) {
[canCreateAudit, dropDialogRef],
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
useEffect(() => {
if (!canCreateAudit) return;
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
dragCounterRef.current++;
if (e.dataTransfer?.types.includes("Files")) {
setIsDragging(true);
}
};
const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
dragCounterRef.current--;
if (dragCounterRef.current === 0) {
setIsDragging(false);
}
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
};
const handleDrop = () => {
setIsDragging(false);
dragCounterRef.current = 0;
};
window.addEventListener("dragenter", handleDragEnter);
window.addEventListener("dragleave", handleDragLeave);
window.addEventListener("dragover", handleDragOver);
window.addEventListener("drop", handleDrop);
return () => {
window.removeEventListener("dragenter", handleDragEnter);
window.removeEventListener("dragleave", handleDragLeave);
window.removeEventListener("dragover", handleDragOver);
window.removeEventListener("drop", handleDrop);
};
}, [canCreateAudit]);
const { getRootProps, getInputProps } = useDropzone({
noClick: true,
noKeyboard: true,
accept: { "application/pdf": [".pdf"] },
@@ -135,10 +180,13 @@ export default function AuditsPage(props: Props) {
};
return (
<div {...getRootProps()} className="relative space-y-6">
<input {...getInputProps()} />
{isDragActive && canCreateAudit && (
<div className="border-primary bg-primary/5 pointer-events-none absolute inset-0 z-40 flex flex-col items-center justify-center rounded-xl border-2 border-dashed">
<div className="space-y-6">
{isDragging && canCreateAudit && (
<div
{...getRootProps()}
className="border-primary bg-primary/5 pointer-events-auto fixed inset-0 z-40 flex flex-col items-center justify-center border-2 border-dashed"
>
<input {...getInputProps()} />
<IconUpload className="text-primary mb-2 size-8" />
<p className="text-primary text-sm font-medium">
{__("Drop a PDF to create an audit with a report")}