@mainframework/dropzone 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -4
- package/dist/index.d.ts +10 -1
- package/dist/index.js +42 -8
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -26,10 +26,32 @@ The `useFileSelector` hook accepts optional configuration:
|
|
|
26
26
|
const { ... } = useFileSelector({
|
|
27
27
|
maximumUploadCount: 5, // Default: 30
|
|
28
28
|
maximumFileSize: 5e6, // Default: 5 MB
|
|
29
|
-
acceptedTypes: defaultTypeExtensions, // MIME type map, see below
|
|
29
|
+
acceptedTypes: defaultTypeExtensions, // MIME type -> extension map, see below
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Minimal Usage
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useFileSelector } from "@mainframework/dropzone";
|
|
37
|
+
|
|
38
|
+
export function Uploader() {
|
|
39
|
+
const { FileSelector, validFiles, invalidFiles, clearCache } = useFileSelector();
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<FileSelector messageParagraph="Drop files here or click to select" />
|
|
44
|
+
|
|
45
|
+
{invalidFiles.length > 0 && <p role="alert">Some files were rejected.</p>}
|
|
46
|
+
|
|
47
|
+
<button type="button" onClick={clearCache} disabled={validFiles.length === 0}>
|
|
48
|
+
Clear
|
|
49
|
+
</button>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
33
55
|
## Hook Exports
|
|
34
56
|
|
|
35
57
|
The hook returns the following:
|
|
@@ -70,12 +92,11 @@ The hook returns the following:
|
|
|
70
92
|
|
|
71
93
|
### FileSelector props (`FileSelectorViewProps`)
|
|
72
94
|
|
|
73
|
-
The `FileSelector` from the hook accepts **view-only** props (styling, copy, accessibility
|
|
95
|
+
The `FileSelector` from the hook accepts **view-only** props (styling, copy, accessibility). Do not pass `onChange` or drag handlers; the hook supplies those. When rendering it, you can pass:
|
|
74
96
|
|
|
75
97
|
| Prop | Type | Default | Description |
|
|
76
98
|
| --------------------------- | -------- | --------------------------------------------------------- | -------------------------------------------- |
|
|
77
99
|
| `inputId` | `string` | auto-generated | ID for the file input (for `aria-controls`). |
|
|
78
|
-
| `acceptTypes` | `string` | `.png, .jpg, .jpeg, .pdf, .svg, ...` | `accept` attribute for the file input. |
|
|
79
100
|
| `messageParagraph` | `string` | "Drag 'n' drop some files here, or click to select files" | Text shown in the dropzone. |
|
|
80
101
|
| `inputClassName` | `string` | `"hiddenInput"` | CSS classes for the hidden input. |
|
|
81
102
|
| `clickableAreaClassName` | `string` | Tailwind dropzone styles | CSS classes for the clickable area. |
|
|
@@ -130,7 +151,7 @@ export const App = () => {
|
|
|
130
151
|
|
|
131
152
|
return (
|
|
132
153
|
<>
|
|
133
|
-
<FileSelector messageParagraph="Drop files here or click to select"
|
|
154
|
+
<FileSelector messageParagraph="Drop files here or click to select" />
|
|
134
155
|
|
|
135
156
|
{maxUploadError.status && <p role="alert">{maxUploadError.message}</p>}
|
|
136
157
|
{maxFileSizeError.status && <p role="alert">{maxFileSizeError.message}</p>}
|
|
@@ -149,6 +170,25 @@ export const App = () => {
|
|
|
149
170
|
};
|
|
150
171
|
```
|
|
151
172
|
|
|
173
|
+
## Cleanup / memory
|
|
174
|
+
|
|
175
|
+
- Prefer clearing via the provided methods:
|
|
176
|
+
- `clearCache()` / `onCancel()` to clear files and revoke blob URLs
|
|
177
|
+
- `onRemoveFile(index)` to remove one file and revoke its blob URL
|
|
178
|
+
- `clearBlobs()` to revoke blob URLs without clearing arrays
|
|
179
|
+
- When the hook instance unmounts, it clears internal state and revokes any blob URLs it created.
|
|
180
|
+
- Mutating the `validFiles` array directly (for example `validFiles.length = 0`) is not a supported way to clear files/blobs; use `clearCache()` instead so blob URLs are properly revoked.
|
|
181
|
+
|
|
182
|
+
## Exported types
|
|
183
|
+
|
|
184
|
+
If you need types externally, the package exports:
|
|
185
|
+
|
|
186
|
+
- `FileData`
|
|
187
|
+
- `ErrorMessage`
|
|
188
|
+
- `IFileUploaderProps`
|
|
189
|
+
- `FileSelectorViewProps`
|
|
190
|
+
- `FileSelectorHandlerProps`
|
|
191
|
+
|
|
152
192
|
## Manipulating validFiles in a Preview Component
|
|
153
193
|
|
|
154
194
|
As an example, when building a preview component (e.g. `PreviewImages`), use the hook’s methods to update `validFiles` based on user actions:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ChangeEvent, DragEvent } from 'react';
|
|
2
3
|
|
|
3
4
|
/** Props for styling, copy, and accessibility on the dropzone. Used by the `FileSelector` returned from `useFileSelector`. */
|
|
4
5
|
interface FileSelectorViewProps {
|
|
5
6
|
inputId?: string;
|
|
6
|
-
acceptTypes?: string;
|
|
7
7
|
messageParagraph?: string;
|
|
8
8
|
inputClassName?: string;
|
|
9
9
|
clickableAreaClassName?: string;
|
|
@@ -17,6 +17,14 @@ interface FileSelectorViewProps {
|
|
|
17
17
|
ariaLabelButton?: string;
|
|
18
18
|
ariaLabelledBy?: string;
|
|
19
19
|
}
|
|
20
|
+
/** File input and drag-and-drop handlers; supplied internally by `useFileSelector` to the presentational component. */
|
|
21
|
+
interface FileSelectorHandlerProps {
|
|
22
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
23
|
+
onDragOver: (e: DragEvent<HTMLButtonElement>) => void;
|
|
24
|
+
onDrop: (e: DragEvent<HTMLButtonElement>) => void;
|
|
25
|
+
onDragEnter: (e: DragEvent<HTMLButtonElement>) => void;
|
|
26
|
+
onDragLeave: (e: DragEvent<HTMLButtonElement>) => void;
|
|
27
|
+
}
|
|
20
28
|
interface IFileUploaderProps {
|
|
21
29
|
acceptTypes?: string;
|
|
22
30
|
maximumUploadCount?: number;
|
|
@@ -55,3 +63,4 @@ declare const useFileSelector: ({ maximumUploadCount, maximumFileSize, acceptedT
|
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
export { useFileSelector };
|
|
66
|
+
export type { ErrorMessage, FileData, FileSelectorHandlerProps, FileSelectorViewProps, IFileUploaderProps };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
-
import { useRef, memo, useId, useCallback, useState, useMemo } from 'react';
|
|
3
|
+
import { useRef, memo, useId, useCallback, useState, useEffect, useMemo } from 'react';
|
|
4
4
|
|
|
5
5
|
//Use this to quickly get the file extensions
|
|
6
6
|
//Devs can pass their own custom type mappings to override these defaults
|
|
@@ -3391,7 +3391,7 @@ const withDragDefaults = (handler) => {
|
|
|
3391
3391
|
};
|
|
3392
3392
|
};
|
|
3393
3393
|
|
|
3394
|
-
const
|
|
3394
|
+
const defaultAccept = ".png, .jpg, .jpeg, .pdf, .svg, image/svg+xml, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
3395
3395
|
const SCOPE_CLASS = "file-selector-scope";
|
|
3396
3396
|
const defaultInputClassName = "hiddenInput";
|
|
3397
3397
|
const defaultClickableAreaClassName = "dropzone border-dashed border-2 border-gray-500 p-4 rounded-md text-center cursor-pointer bg-inherit hover:border-gray-400 text-inherit font-bold py-2 px-4";
|
|
@@ -3401,7 +3401,7 @@ const defaultMessageParagraph = "Drag 'n' drop some files here, or click to sele
|
|
|
3401
3401
|
const defaultAriaLabel = "File upload drop zone";
|
|
3402
3402
|
const defaultAriaDescribedBy = undefined;
|
|
3403
3403
|
const defaultAriaLabelButton = "Choose files to upload";
|
|
3404
|
-
const FileSelectorComponent = ({ inputId,
|
|
3404
|
+
const FileSelectorComponent = ({ inputId, accept = defaultAccept, messageParagraph = defaultMessageParagraph, inputClassName, clickableAreaClassName, dropZoneWrapperClassName, messageParagraphClassName, ariaLabel = defaultAriaLabel, ariaDescribedBy = defaultAriaDescribedBy, ariaLabelButton = defaultAriaLabelButton, ariaLabelledBy, // New prop for button
|
|
3405
3405
|
onChange, onDragOver, onDrop, onDragEnter, onDragLeave, }) => {
|
|
3406
3406
|
const fileInputRef = useRef(null);
|
|
3407
3407
|
const messageId = useId();
|
|
@@ -3431,11 +3431,37 @@ onChange, onDragOver, onDrop, onDragEnter, onDragLeave, }) => {
|
|
|
3431
3431
|
const resolvedDropZoneWrapperClassName = mergeStyles(dropZoneWrapperClassName ?? defaultDropZoneWrapperClassName);
|
|
3432
3432
|
const resolvedMessageParagraphClassName = mergeStyles(messageParagraphClassName ?? defaultMessageParagraphClassName);
|
|
3433
3433
|
const resolvedInputId = inputId ?? messageId;
|
|
3434
|
-
return (jsxs("div", { role: "group", "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className: mergeStyles(SCOPE_CLASS, resolvedDropZoneWrapperClassName), draggable: false, children: [jsx("button", { type: "button", className: resolvedClickableAreaClassName, onClick: internalOnClick, onDragOver: internalOnDragOver, onDrop: internalOnDrop, onDragEnter: internalOnDragEnter, onDragLeave: internalOnDragLeave, draggable: false, "aria-label": ariaLabelButton, "aria-labelledby": ariaLabelledBy ?? messageId, "aria-controls": resolvedInputId, "aria-haspopup": "dialog", children: jsx("p", { id: messageId, className: resolvedMessageParagraphClassName, children: messageParagraph }) }), jsx("input", { ref: fileInputRef, id: resolvedInputId, type: "file", className: inputClasses, onChange: onChange, accept:
|
|
3434
|
+
return (jsxs("div", { role: "group", "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className: mergeStyles(SCOPE_CLASS, resolvedDropZoneWrapperClassName), draggable: false, children: [jsx("button", { type: "button", className: resolvedClickableAreaClassName, onClick: internalOnClick, onDragOver: internalOnDragOver, onDrop: internalOnDrop, onDragEnter: internalOnDragEnter, onDragLeave: internalOnDragLeave, draggable: false, "aria-label": ariaLabelButton, "aria-labelledby": ariaLabelledBy ?? messageId, "aria-controls": resolvedInputId, "aria-haspopup": "dialog", children: jsx("p", { id: messageId, className: resolvedMessageParagraphClassName, children: messageParagraph }) }), jsx("input", { ref: fileInputRef, id: resolvedInputId, type: "file", className: inputClasses, onChange: onChange, accept: accept, multiple: true, "aria-hidden": "true", tabIndex: -1 })] }));
|
|
3435
3435
|
};
|
|
3436
3436
|
const FileSelector = memo(FileSelectorComponent);
|
|
3437
3437
|
FileSelector.displayName = "FileSelector";
|
|
3438
3438
|
|
|
3439
|
+
const buildAcceptString = (acceptedTypes) => {
|
|
3440
|
+
const seen = new Set();
|
|
3441
|
+
// Prefer extensions (input accept supports extensions and MIME types)
|
|
3442
|
+
for (const ext of Object.values(acceptedTypes)) {
|
|
3443
|
+
const trimmed = ext.trim();
|
|
3444
|
+
if (!trimmed)
|
|
3445
|
+
continue;
|
|
3446
|
+
if (!trimmed.startsWith(".")) {
|
|
3447
|
+
const withDot = `.${trimmed}`;
|
|
3448
|
+
if (!seen.has(withDot))
|
|
3449
|
+
seen.add(withDot);
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
if (!seen.has(trimmed))
|
|
3453
|
+
seen.add(trimmed);
|
|
3454
|
+
}
|
|
3455
|
+
// Add MIME keys too (helps some pickers)
|
|
3456
|
+
for (const mime of Object.keys(acceptedTypes)) {
|
|
3457
|
+
const trimmed = mime.trim();
|
|
3458
|
+
if (!trimmed)
|
|
3459
|
+
continue;
|
|
3460
|
+
if (!seen.has(trimmed))
|
|
3461
|
+
seen.add(trimmed);
|
|
3462
|
+
}
|
|
3463
|
+
return Array.from(seen).join(", ");
|
|
3464
|
+
};
|
|
3439
3465
|
const useFileSelector = ({ maximumUploadCount: maximumUploadCount$1 = maximumUploadCount, maximumFileSize: maximumFileSize$1 = maximumFileSize, acceptedTypes = defaultTypeExtensions, } = {}) => {
|
|
3440
3466
|
//State
|
|
3441
3467
|
//Trigger a re-render
|
|
@@ -3456,14 +3482,14 @@ const useFileSelector = ({ maximumUploadCount: maximumUploadCount$1 = maximumUpl
|
|
|
3456
3482
|
maxUploadErrorRef.current.message = status
|
|
3457
3483
|
? `You have attempted to upload ${fileCount} files. The maximum allowable uploads for this feature is ${maximumUploads}`
|
|
3458
3484
|
: "";
|
|
3459
|
-
setUpdateTrigger((state) =>
|
|
3485
|
+
setUpdateTrigger((state) => state + 1);
|
|
3460
3486
|
}, []);
|
|
3461
3487
|
const setMaximumFileSizeExceeded = useCallback((status = false) => {
|
|
3462
3488
|
maxFileSizeErrorRef.current.status = status;
|
|
3463
3489
|
maxFileSizeErrorRef.current.message = status
|
|
3464
3490
|
? `You have attempted upload a file(s) that exceeds the maximum size of ${printableMaximumFileSize}`
|
|
3465
3491
|
: "";
|
|
3466
|
-
setUpdateTrigger((state) =>
|
|
3492
|
+
setUpdateTrigger((state) => state + 1);
|
|
3467
3493
|
}, []);
|
|
3468
3494
|
const clearBlobs = useCustomCallback(() => {
|
|
3469
3495
|
//Remove any blobs created in memory
|
|
@@ -3482,6 +3508,11 @@ const useFileSelector = ({ maximumUploadCount: maximumUploadCount$1 = maximumUpl
|
|
|
3482
3508
|
SetInvalidFiles([]);
|
|
3483
3509
|
SetValidFiles([]);
|
|
3484
3510
|
}, [clearBlobs]);
|
|
3511
|
+
useEffect(() => {
|
|
3512
|
+
return () => {
|
|
3513
|
+
clearCache();
|
|
3514
|
+
};
|
|
3515
|
+
}, [clearCache]);
|
|
3485
3516
|
const onCancel = useCustomCallback(() => {
|
|
3486
3517
|
clearCache();
|
|
3487
3518
|
}, [clearCache]);
|
|
@@ -3611,18 +3642,21 @@ const useFileSelector = ({ maximumUploadCount: maximumUploadCount$1 = maximumUpl
|
|
|
3611
3642
|
onDragEnter: handlers.onDragEnter,
|
|
3612
3643
|
onDragLeave: handlers.onDragLeave,
|
|
3613
3644
|
};
|
|
3614
|
-
const
|
|
3645
|
+
const internalProps = { accept: handlers.accept };
|
|
3646
|
+
const Component = (props) => (jsx(FileSelector, { ...props, ...handlerProps, ...internalProps }));
|
|
3615
3647
|
Component.displayName = "FileSelectorWrapper";
|
|
3616
3648
|
return Component;
|
|
3617
3649
|
};
|
|
3618
3650
|
// Then in the hook's return:
|
|
3651
|
+
const accept = useMemo(() => buildAcceptString(acceptedTypes), [acceptedTypes]);
|
|
3619
3652
|
const BoundFileSelector = useMemo(() => createFileSelectorComponent({
|
|
3620
3653
|
onInputChange,
|
|
3621
3654
|
onDragOver,
|
|
3622
3655
|
onDrop,
|
|
3623
3656
|
onDragEnter,
|
|
3624
3657
|
onDragLeave,
|
|
3625
|
-
|
|
3658
|
+
accept,
|
|
3659
|
+
}), [onInputChange, onDragOver, onDrop, onDragEnter, onDragLeave, accept]);
|
|
3626
3660
|
return {
|
|
3627
3661
|
//Properties
|
|
3628
3662
|
validFiles,
|