jbpenrath
2025-01-07 23:28:47 +01:00
committed by Jean-Baptiste PENRATH
parent 0f6a8dfa72
commit 56d9ed88f0
27 changed files with 1497 additions and 1546 deletions

View File

@@ -1,6 +1,6 @@
import React, {
forwardRef,
PropsWithChildren,
RefAttributes,
useEffect,
useImperativeHandle,
useRef,
@@ -15,147 +15,141 @@ import {
FileUploaderRefType,
} from ":/components/Forms/FileUploader/index";
interface DropZoneProps extends FileUploaderProps, PropsWithChildren {
files: File[];
}
type DropZoneProps = FileUploaderProps &
RefAttributes<FileUploaderRefType> &
PropsWithChildren<{
files: File[];
}>;
export const DropZone = forwardRef<FileUploaderRefType, DropZoneProps>(
(
{
multiple,
name,
state,
icon,
animateIcon,
successIcon,
uploadingIcon,
text,
bigText,
files,
onFilesChange,
children,
...props
}: DropZoneProps,
ref,
) => {
const [dragActive, setDragActive] = useState(false);
const container = useRef<HTMLLabelElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useCunningham();
export const DropZone = ({
multiple,
name,
state,
icon,
animateIcon,
successIcon,
uploadingIcon,
text,
bigText,
files,
onFilesChange,
children,
ref,
...props
}: DropZoneProps) => {
const [dragActive, setDragActive] = useState(false);
const container = useRef<HTMLLabelElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useCunningham();
useImperativeHandle(ref, () => ({
get input() {
return inputRef.current;
},
reset() {
onFilesChange?.({ target: { value: [] } });
},
}));
useImperativeHandle(ref, () => ({
get input() {
return inputRef.current;
},
reset() {
onFilesChange?.({ target: { value: [] } });
},
}));
useEffect(() => {
if (!inputRef.current) {
return;
}
replaceInputFilters(inputRef.current, files);
}, [files]);
useEffect(() => {
if (!inputRef.current) {
return;
}
replaceInputFilters(inputRef.current, files);
}, [files]);
useEffect(() => {
onFilesChange?.({ target: { value: files ?? [] } });
}, [files]);
useEffect(() => {
onFilesChange?.({ target: { value: files ?? [] } });
}, [files]);
const renderIcon = () => {
if (state === "success") {
return successIcon ?? <span className="material-icons">done</span>;
}
if (state === "uploading") {
return React.cloneElement(uploadingIcon ?? <Loader size="small" />, {
"aria-label": t("components.forms.file_uploader.uploading"),
});
}
return icon ?? <span className="material-icons">upload</span>;
};
const renderCaption = () => {
if (state === "uploading") {
return t("components.forms.file_uploader.uploading");
}
if (bigText) {
return bigText;
}
return (
<>
{t("components.forms.file_uploader.caption")}
<span>{t("components.forms.file_uploader.browse_files")}</span>
</>
);
};
const renderIcon = () => {
if (state === "success") {
return successIcon ?? <span className="material-icons">done</span>;
}
if (state === "uploading") {
return React.cloneElement(uploadingIcon ?? <Loader size="small" />, {
"aria-label": t("components.forms.file_uploader.uploading"),
});
}
return icon ?? <span className="material-icons">upload</span>;
};
const renderCaption = () => {
if (state === "uploading") {
return t("components.forms.file_uploader.uploading");
}
if (bigText) {
return bigText;
}
return (
<label
className={classNames(
"c__file-uploader",
"c__file-uploader--" + state,
{
"c__file-uploader--active": dragActive,
"c__file-uploader--animate-icon": animateIcon,
},
)}
onDragEnter={() => {
setDragActive(true);
}}
onDragLeave={(e) => {
/**
* This condition is important because onDragLeave is called when the cursor goes over
* a child of the current node, which is not intuitive. So here we need to make sure that
* the relatedTarget is not a child of the current node.
*/
if (!container.current!.contains(e.relatedTarget as Node)) {
setDragActive(false);
}
}}
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
// To prevent a new tab to open.
e.preventDefault();
const newFiles = Array.from(e.dataTransfer.files);
if (inputRef.current) {
inputRef.current.files = e.dataTransfer.files;
onFilesChange?.({ target: { value: [...newFiles] } });
}
setDragActive(false);
}}
ref={container}
>
<div className="c__file-uploader__inner">
<div className="c__file-uploader__inner__icon">{renderIcon()}</div>
{children ?? (
<>
<div className="c__file-uploader__inner__caption">
{renderCaption()}
</div>
{text && (
<div className="c__file-uploader__inner__text">{text}</div>
)}
</>
)}
<input
type="file"
name={name}
ref={inputRef}
onChange={(e) => {
if (e.target.files) {
onFilesChange?.({ target: { value: [...e.target.files] } });
} else {
onFilesChange?.({ target: { value: [] } });
}
}}
multiple={multiple}
{...props}
/>
</div>
</label>
<>
{t("components.forms.file_uploader.caption")}
<span>{t("components.forms.file_uploader.browse_files")}</span>
</>
);
},
);
};
return (
<label
className={classNames("c__file-uploader", "c__file-uploader--" + state, {
"c__file-uploader--active": dragActive,
"c__file-uploader--animate-icon": animateIcon,
})}
onDragEnter={() => {
setDragActive(true);
}}
onDragLeave={(e) => {
/**
* This condition is important because onDragLeave is called when the cursor goes over
* a child of the current node, which is not intuitive. So here we need to make sure that
* the relatedTarget is not a child of the current node.
*/
if (!container.current!.contains(e.relatedTarget as Node)) {
setDragActive(false);
}
}}
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
// To prevent a new tab to open.
e.preventDefault();
const newFiles = Array.from(e.dataTransfer.files);
if (inputRef.current) {
inputRef.current.files = e.dataTransfer.files;
onFilesChange?.({ target: { value: [...newFiles] } });
}
setDragActive(false);
}}
ref={container}
>
<div className="c__file-uploader__inner">
<div className="c__file-uploader__inner__icon">{renderIcon()}</div>
{children ?? (
<>
<div className="c__file-uploader__inner__caption">
{renderCaption()}
</div>
{text && (
<div className="c__file-uploader__inner__text">{text}</div>
)}
</>
)}
<input
type="file"
name={name}
ref={inputRef}
onChange={(e) => {
if (e.target.files) {
onFilesChange?.({ target: { value: [...e.target.files] } });
} else {
onFilesChange?.({ target: { value: [] } });
}
}}
multiple={multiple}
{...props}
/>
</div>
</label>
);
};

View File

@@ -1,16 +1,14 @@
import React, { forwardRef, useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useCunningham } from ":/components/Provider";
import { Button } from ":/components/Button";
import {
FileUploaderProps,
FileUploaderRefType,
} from ":/components/Forms/FileUploader/index";
import { FileUploaderProps } from ":/components/Forms/FileUploader/index";
import { DropZone } from ":/components/Forms/FileUploader/DropZone";
export const FileUploaderMono = forwardRef<
FileUploaderRefType,
FileUploaderProps
>(({ fakeDefaultFiles, ...props }, ref) => {
export const FileUploaderMono = ({
fakeDefaultFiles,
ref,
...props
}: FileUploaderProps) => {
const { t } = useCunningham();
const [file, setFile] = useState<File | undefined>(
fakeDefaultFiles && fakeDefaultFiles.length > 0
@@ -91,4 +89,4 @@ export const FileUploaderMono = forwardRef<
)}
</>
);
});
};

View File

@@ -1,17 +1,16 @@
import React, { forwardRef, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useCunningham } from ":/components/Provider";
import { formatBytes } from ":/components/Forms/FileUploader/utils";
import { Button } from ":/components/Button";
import {
FileUploaderProps,
FileUploaderRefType,
} from ":/components/Forms/FileUploader/index";
import { FileUploaderProps } from ":/components/Forms/FileUploader/index";
import { DropZone } from ":/components/Forms/FileUploader/DropZone";
export const FileUploaderMulti = forwardRef<
FileUploaderRefType,
FileUploaderProps
>(({ fullWidth, fakeDefaultFiles, ...props }, ref) => {
export const FileUploaderMulti = ({
fullWidth,
fakeDefaultFiles,
ref,
...props
}: FileUploaderProps) => {
const { t } = useCunningham();
const [files, setFiles] = useState<File[]>(fakeDefaultFiles || []);
@@ -59,4 +58,4 @@ export const FileUploaderMulti = forwardRef<
)}
</>
);
});
};

View File

@@ -1,11 +1,12 @@
import React, { forwardRef, InputHTMLAttributes, ReactElement } from "react";
import React, { InputHTMLAttributes, ReactElement, RefAttributes } from "react";
import { Field, FieldProps, FieldState } from ":/components/Forms/Field";
import { FileUploaderMulti } from ":/components/Forms/FileUploader/FileUploaderMulti";
import { FileUploaderMono } from ":/components/Forms/FileUploader/FileUploaderMono";
export interface FileUploaderProps
extends Omit<FieldProps, "state">,
InputHTMLAttributes<HTMLInputElement> {
InputHTMLAttributes<HTMLInputElement>,
RefAttributes<FileUploaderRefType> {
state?: FieldState | "uploading" | undefined;
multiple?: boolean;
icon?: ReactElement;
@@ -25,16 +26,18 @@ export interface FileUploaderRefType {
reset: () => void;
}
export const FileUploader = forwardRef<FileUploaderRefType, FileUploaderProps>(
({ fullWidth, ...props }, ref) => {
return (
<Field fullWidth={fullWidth} className={props.className}>
{props.multiple ? (
<FileUploaderMulti {...props} ref={ref} />
) : (
<FileUploaderMono {...props} ref={ref} />
)}
</Field>
);
},
);
export const FileUploader = ({
fullWidth,
ref,
...props
}: FileUploaderProps) => {
return (
<Field fullWidth={fullWidth} className={props.className}>
{props.multiple ? (
<FileUploaderMulti {...props} ref={ref} />
) : (
<FileUploaderMono {...props} ref={ref} />
)}
</Field>
);
};