@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 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, `acceptTypes`). Do not pass `onChange` or drag handlers; the hook supplies those. When rendering it, you can pass:
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" acceptTypes=".png,.jpg,.jpeg,.pdf" />
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 defaultAcceptTypes = ".png, .jpg, .jpeg, .pdf, .svg, image/svg+xml, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document";
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, acceptTypes = defaultAcceptTypes, messageParagraph = defaultMessageParagraph, inputClassName, clickableAreaClassName, dropZoneWrapperClassName, messageParagraphClassName, ariaLabel = defaultAriaLabel, ariaDescribedBy = defaultAriaDescribedBy, ariaLabelButton = defaultAriaLabelButton, ariaLabelledBy, // New prop for button
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: acceptTypes, multiple: true, "aria-hidden": "true", tabIndex: -1 })] }));
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) => (state += 1));
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) => (state += 1));
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 Component = (props) => jsx(FileSelector, { ...props, ...handlerProps });
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
- }), [onInputChange, onDragOver, onDrop, onDragEnter, onDragLeave]);
3658
+ accept,
3659
+ }), [onInputChange, onDragOver, onDrop, onDragEnter, onDragLeave, accept]);
3626
3660
  return {
3627
3661
  //Properties
3628
3662
  validFiles,