Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dnd zone enhancement #692

Open
wants to merge 2 commits into
base: beta-master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 124 additions & 82 deletions src/components/Shared/StringFileUploadField/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import React, { useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import Box from "@mui/material/Box";
import { Button, ButtonGroup, Grid2, TextField } from "@mui/material";
import {
Button,
ButtonGroup,
Grid2,
Stack,
TextField,
Typography,
} from "@mui/material";
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import CloseIcon from "@mui/icons-material/Close";
import LoadingButton from "@mui/lab/LoadingButton";
import accept from "attr-accept";
import { green } from "@mui/material/colors";
import { i18nLoadNamespace } from "../Languages/i18nLoadNamespace";

/**
* A reusable form component with a textfield and a local file with optional processing
Expand Down Expand Up @@ -42,17 +51,22 @@ const StringFileUploadField = ({
}) => {
const fileRef = useRef(null);

const [isOver, setIsOver] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [validDrop, setValidDrop] = useState(false);

const dropColor = green[50];

const keyword = i18nLoadNamespace("components/Shared/StringFileUploadField");

/**
*
* @param e {DragEvent}
*/
const onDragEnter = (e) => {
const onDragOver = (e) => {
e.preventDefault();
const file = e.dataTransfer?.files?.[0];
setIsOver(true);
setIsDragging(true);

if (file && accept(file, fileInputTypesAccepted)) {
setValidDrop(true);
} else {
Expand All @@ -62,7 +76,7 @@ const StringFileUploadField = ({

const onDragLeave = (e) => {
e.preventDefault();
setIsOver(false);
setIsDragging(false);
};

const handleFile = async (file) => {
Expand All @@ -74,95 +88,123 @@ const StringFileUploadField = ({

const onDrop = (e) => {
e.preventDefault();
setIsOver(false);
setIsDragging(false);
setValidDrop(false);
const file = e.dataTransfer?.files?.[0];
if (file && accept(file, fileInputTypesAccepted)) {
handleFile(file);
}
};

const fieldsRef = useRef(null);
const [fieldsDivHeight, setFieldsDivHeight] = useState(0);

// Keep track of the computed height
useEffect(() => {
if (fieldsRef.current) {
setFieldsDivHeight(fieldsRef.current.offsetHeight);
}
}, []);

return (
<Box>
<Grid2 container direction="row" spacing={3} alignItems="center">
<Grid2 size="grow">
<TextField
type="url"
id="standard-full-width"
label={labelKeyword}
placeholder={placeholderKeyword}
fullWidth
value={urlInput}
variant="outlined"
disabled={isParentLoading || fileInput instanceof Blob}
onChange={(e) => setUrlInput(e.target.value)}
/>
</Grid2>
<Grid2>
<LoadingButton
type="submit"
variant="contained"
color="primary"
onClick={async (e) => {
e.preventDefault();
urlInput ? await handleSubmit(urlInput) : await handleSubmit(e);
}}
loading={isParentLoading}
disabled={(urlInput === "" && !fileInput) || isParentLoading}
>
{submitButtonKeyword}
</LoadingButton>
</Grid2>
</Grid2>
<Grid2 mt={2}>
<ButtonGroup
variant="outlined"
disabled={isParentLoading || urlInput !== ""}
>
<Button
startIcon={<FolderOpenIcon />}
sx={{ textTransform: "none" }}
style={
isOver ? { cursor: validDrop ? "copy" : "no-drop" } : undefined
}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={(e) => e.preventDefault()}
onDrop={onDrop}
>
<label htmlFor="file">
{fileInput ? fileInput.name : localFileKeyword}
</label>
<input
id="file"
name="file"
type="file"
accept={fileInputTypesAccepted}
hidden={true}
ref={fileRef}
onChange={(e) => {
e.preventDefault();
handleFile(e.target.files[0]);
e.target.value = null;
}}
<Box
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
sx={{
border: isDragging ? "4px dashed #00926c" : "0",
height: isDragging ? fieldsDivHeight : "auto",
backgroundColor: isDragging ? dropColor : "initial",
}}
>
{isDragging && (
<Stack justifyContent="center" alignItems="center" height="100%">
<Typography>{keyword("droppable_zone")}</Typography>
</Stack>
)}

<Box visibility={isDragging ? "hidden" : "visible"} ref={fieldsRef}>
Comment on lines +110 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably get away without the useRef and the effect if you do something like

    <Box
      onDragOver={onDragOver}
      onDragLeave={onDragLeave}
      onDrop={onDrop}
      sx={{
        position: "relative",
      }}
    >
      <Box>
        {/* form content goes here */}
      </Box>

      {isDragging && (
        <Stack
          sx={{
            border: "4px dashed #00926c",
            backgroundColor: dropColor,
            justifyContent: "center",
            alignItems: "center",
            position: "absolute",
            top: 0,
            left: 0,
            bottom: 0,
            right: 0,
          }}
        >
          <Typography>{keyword("droppable_zone")}</Typography>
        </Stack>
      )}
    </Box>

This uses absolute positioning to put the drop zone box on top of the form fields area, and it doesn't need the effect to track the height as it gets that automatically with t/l/b/r all set to zero. To get this to work I've had to move the Stack after the Box, if you leave them the other way round you might need a zIndex on the Stack to make it sit on top.

<Grid2 container direction="row" spacing={3} alignItems="center">
<Grid2 size="grow">
<TextField
type="url"
id="standard-full-width"
label={labelKeyword}
placeholder={placeholderKeyword}
fullWidth
value={urlInput}
variant="outlined"
disabled={isParentLoading || fileInput instanceof Blob}
onChange={(e) => setUrlInput(e.target.value)}
/>
</Button>
{fileInput instanceof Blob && (
<Button
size="small"
aria-label="remove selected file"
onClick={(e) => {
</Grid2>
<Grid2>
<LoadingButton
type="submit"
variant="contained"
color="primary"
onClick={async (e) => {
e.preventDefault();
handleCloseSelectedFile();
fileRef.current.value = null;
setFileInput(null);
urlInput ? await handleSubmit(urlInput) : await handleSubmit(e);
}}
loading={isParentLoading}
disabled={(urlInput === "" && !fileInput) || isParentLoading}
>
{submitButtonKeyword}
</LoadingButton>
</Grid2>
</Grid2>
<Grid2 mt={2}>
<ButtonGroup
variant="outlined"
disabled={isParentLoading || urlInput !== ""}
>
<Button
startIcon={<FolderOpenIcon />}
sx={{ textTransform: "none" }}
style={
isDragging
? { cursor: validDrop ? "copy" : "no-drop" }
: undefined
}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
>
<CloseIcon fontSize="small" />
<label htmlFor="file">
{fileInput ? fileInput.name : localFileKeyword}
</label>
<input
id="file"
name="file"
type="file"
accept={fileInputTypesAccepted}
hidden={true}
ref={fileRef}
onChange={(e) => {
e.preventDefault();
handleFile(e.target.files[0]);
e.target.value = null;
}}
/>
</Button>
)}
</ButtonGroup>
</Grid2>
{fileInput instanceof Blob && (
<Button
size="small"
aria-label="remove selected file"
onClick={(e) => {
e.preventDefault();
handleCloseSelectedFile();
fileRef.current.value = null;
setFileInput(null);
}}
>
<CloseIcon fontSize="small" />
</Button>
)}
</ButtonGroup>
</Grid2>
</Box>
</Box>
);
};
Expand Down
Loading