@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
372 lines (319 loc) • 9.73 kB
Markdown
# Dropzone
Import: `import { Dropzone } from '@neo4j-ndl/react'`
## Props
### Dropzone
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `acceptedFileExtensions` | `string[]` | | | The accepted file extensions for the Drag-and-Drop. |
| `dropZoneOptions` | `DropzoneOptions` | ✅ | | Props for the underlying `react-dropzone` component. Don't use `accept`. If using custom file extensions as this option is broken. Instead use the `acceptedFileExtensions` property. |
| `heading` | `ReactNode` | | | Use when wanting a custom heading. |
| `loadingElement` | `ReactNode` | | | Element to display when uploading the file. |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
| `rejectedText` | `ReactNode` | | | Message to be shown when file is rejected. |
| `supportedFilesDescription` | `ReactNode` | | | Informational text that highlights the supported file types, will be displayed in the footer. |
Where `DropzoneOptions` is the props for the `react-dropzone` library. For more information, see the [react-dropzone documentation](https://react-dropzone.js.org/#src).
### Dropzone.LoadingProgressBar
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `progressBarMinute` | `number` | | | The time in minute left to upload the file. |
| `progressBarPrecentage` | `number` | ✅ | | The percentage of the progress bar. |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
## Examples
### Default
```tsx
import { Dropzone } from '-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';
const Component = () => {
const onDrop: DropzoneOptions['onDrop'] = (files) => {
alert(
`attempted to upload ${
files.length
} file(s). Check console logs for more info`,
);
};
return (
<Dropzone
dropZoneOptions={{
onDrop,
}}
/>
);
};
export default Component;
```
### Csv Files
```tsx
import { Dropzone } from '-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';
const Component = () => {
const onDrop: DropzoneOptions['onDrop'] = (files) => {
alert(
`attempted to upload ${
files.length
} file(s). Check console logs for more info`,
);
};
return (
<Dropzone
supportedFilesDescription="Supports: CSV"
dropZoneOptions={{
accept: { 'text/csv': ['.csv'] },
onDrop,
}}
/>
);
};
export default Component;
```
### Custom Extensions
```tsx
import { Dropzone } from '-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';
const Component = () => {
const onDrop: DropzoneOptions['onDrop'] = (files) => {
alert(
`attempted to upload ${
files.length
} file(s). Check console logs for more info`,
);
};
return (
<Dropzone
supportedFilesDescription="Supports: .dump"
acceptedFileExtensions={['.dump']}
dropZoneOptions={{
onDrop,
}}
/>
);
};
export default Component;
```
### Disabled
```tsx
import { Dropzone } from '-ndl/react';
import { type DropzoneOptions } from 'react-dropzone';
const Component = () => {
const onDrop: DropzoneOptions['onDrop'] = (files) => {
alert(
`attempted to upload ${
files.length
} file(s). Check console logs for more info`,
);
};
return (
<Dropzone
supportedFilesDescription="Supports: CSV Files"
dropZoneOptions={{
accept: { 'text/csv': ['.csv'] },
disabled: true,
onDrop,
}}
/>
);
};
export default Component;
```
### Full
```tsx
import { XMarkIcon } from '/react/24/solid';
import { Dropzone, FilledButton, Typography } from '-ndl/react';
import { useCallback, useState } from 'react';
import {
type DropEvent,
type DropzoneOptions,
type FileRejection,
} from 'react-dropzone';
// Configuration constants
const CONFIG = {
MAX_FILES: 3,
MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
PROGRESS_INCREMENT: 5,
UPDATE_INTERVAL: 100,
ACCEPTED_EXTENSIONS: ['.txt', '.json'] as string[],
} as const;
enum UploadState {
Idle = 'idle',
Uploading = 'uploading',
Success = 'success',
Error = 'error',
}
interface UploadData {
state: UploadState;
progress: number;
files: File[];
errors: string[];
}
const Component = () => {
const [uploadData, setUploadData] = useState<UploadData>({
errors: [],
files: [],
progress: 0,
state: UploadState.Idle,
});
const resetUpload = useCallback(() => {
setUploadData({
errors: [],
files: [],
progress: 0,
state: UploadState.Idle,
});
}, []);
const simulateUpload = useCallback((files: File[]) => {
setUploadData((prev) => ({
...prev,
state: UploadState.Uploading,
files,
progress: 0,
}));
const interval = setInterval(() => {
setUploadData((prev) => {
const newProgress = prev.progress + CONFIG.PROGRESS_INCREMENT;
if (newProgress >= 100) {
clearInterval(interval);
setTimeout(
() =>
setUploadData((current) => ({
...current,
state: UploadState.Success,
})),
500,
);
return { ...prev, progress: 100 };
}
return { ...prev, progress: newProgress };
});
}, CONFIG.UPDATE_INTERVAL);
}, []);
const handleDrop: DropzoneOptions['onDrop'] = useCallback(
(
acceptedFiles: File[],
rejectedFiles: FileRejection[],
event: DropEvent,
) => {
console.info('Drop event:', { acceptedFiles, event, rejectedFiles });
if (acceptedFiles.length > 0) {
simulateUpload(acceptedFiles);
}
},
[simulateUpload],
);
const handleDropRejected: DropzoneOptions['onDropRejected'] = useCallback(
(rejectedFiles: FileRejection[]) => {
const errors = rejectedFiles.map(
(file) =>
`${file.file.name}: ${file.errors.map((e) => e.message).join(', ')}`,
);
setUploadData((prev) => ({ ...prev, state: UploadState.Error, errors }));
},
[],
);
const handleError = useCallback((error: Error) => {
console.error('Dropzone error:', error);
setUploadData((prev) => ({
...prev,
state: UploadState.Error,
errors: [error.message],
}));
}, []);
const renderStatusContent = () => {
const { state, progress, files, errors } = uploadData;
switch (state) {
case UploadState.Uploading:
return (
<Dropzone.LoadingProgressBar
progressBarPrecentage={Math.round(progress)}
/>
);
case UploadState.Success:
return (
<div className="n-flex n-flex-col n-justify-center n-items-center n-gap-y-token-32">
<Typography
variant="title-4"
className="n-text-success-text n-font-semibold"
>
Upload Complete!
</Typography>
<Typography
variant="body-small"
className="n-text-neutral-text-weak"
>
Successfully uploaded: {files.map((file) => file.name).join(', ')}
</Typography>
<FilledButton onClick={resetUpload}>Upload More Files</FilledButton>
</div>
);
case UploadState.Error:
return (
<div className="n-flex n-flex-col n-justify-center n-items-center n-gap-y-token-32">
<XMarkIcon className="n-w-token-32 n-h-token-32 n-text-danger-text" />
<div className="n-flex n-flex-col n-gap-y-token-2">
{errors.map((message, index) => (
<Typography
key={index}
variant="label"
className="n-text-danger-text n-font-semibold"
htmlAttributes={{ role: 'alert' }}
>
{message}
</Typography>
))}
</div>
<FilledButton onClick={resetUpload}>Try Again</FilledButton>
</div>
);
default:
return undefined;
}
};
return (
<Dropzone
dropZoneOptions={{
disabled: uploadData.state === UploadState.Uploading,
maxFiles: CONFIG.MAX_FILES,
maxSize: CONFIG.MAX_FILE_SIZE,
multiple: true,
onDrop: handleDrop,
onDropRejected: handleDropRejected,
onError: handleError,
}}
supportedFilesDescription={
<div className="n-text-center n-flex n-flex-col n-gap-y-token-2">
<Typography variant="body-small" className="n-text-neutral-text-weak">
Supports: Text files (.txt, .json) only
</Typography>
<Typography variant="body-small" className="n-text-neutral-text-weak">
Max {CONFIG.MAX_FILES} files •{' '}
{CONFIG.MAX_FILE_SIZE / (1024 * 1024)}MB per file
</Typography>
</div>
}
acceptedFileExtensions={CONFIG.ACCEPTED_EXTENSIONS}
loadingElement={renderStatusContent()}
rejectedText="Error: Please make sure to only upload .json or .txt files"
heading="Drag & Drop your files"
/>
);
};
export default Component;
```
### Loading Progress
```tsx
import { Dropzone } from '-ndl/react';
const Component = () => {
return (
<Dropzone
loadingElement={
<Dropzone.LoadingProgressBar
progressBarPrecentage={50}
progressBarMinute={5}
/>
}
dropZoneOptions={{
onDrop: () => void 0,
}}
/>
);
};
export default Component;
```