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

feat: Add onChange + various feature parity additions/fixes #886

Merged
merged 111 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
d7c6034
add onSelect to <UploadButton/>
markflorkowski Jul 12, 2024
5e5466d
exhaustive deps
markflorkowski Jul 12, 2024
f011e33
solid
markflorkowski Jul 12, 2024
52b616c
svelte
markflorkowski Jul 12, 2024
3f050ec
vue
markflorkowski Jul 12, 2024
27bec4d
optional on svelte
markflorkowski Jul 12, 2024
ad23bdb
rm unused import
markflorkowski Jul 12, 2024
89e53bb
Create giant-candles-wash.md
markflorkowski Jul 12, 2024
6e38c3b
fix pkg-pr-new action?
markflorkowski Jul 12, 2024
4e3e915
try again
markflorkowski Jul 12, 2024
4c05d8f
fetch depth
markflorkowski Jul 12, 2024
5f857b2
docs callout about files []
markflorkowski Jul 12, 2024
d49ea8f
onDrop -> onChange
markflorkowski Jul 13, 2024
ea44ee7
Update dropzone.tsx
markflorkowski Jul 13, 2024
7eef827
Update button.tsx
markflorkowski Jul 13, 2024
683b0f0
Update giant-candles-wash.md
markflorkowski Jul 13, 2024
ba82b4b
Update react.mdx
markflorkowski Jul 13, 2024
2ed096c
update docs
markflorkowski Jul 13, 2024
843b35f
svelte
markflorkowski Jul 13, 2024
2dba137
svelte dropzone
markflorkowski Jul 13, 2024
324b034
add paste to solid
markflorkowski Jul 13, 2024
b52d2d0
optional chain instead of if check
markflorkowski Jul 23, 2024
1c9db32
optional chain for svelte
markflorkowski Jul 23, 2024
1e893b9
missed a file
markflorkowski Jul 23, 2024
85d3cfa
fix pkg-pr-new templates?
markflorkowski Jul 23, 2024
f71ee95
fix paste on solid?
markflorkowski Jul 25, 2024
5634265
useEventListener
markflorkowski Jul 25, 2024
294752d
fix focusing
markflorkowski Jul 25, 2024
5de1491
fix ref
markflorkowski Jul 25, 2024
31e608e
clear button
markflorkowski Jul 26, 2024
afe79cb
button text
markflorkowski Jul 26, 2024
a4ff731
closer to react impl
markflorkowski Jul 26, 2024
34e4879
signals...
markflorkowski Jul 26, 2024
eaed3ed
abort + manual mode upload
markflorkowski Jul 26, 2024
ea0e01e
more abort error handling stuff. onUploadAborted()
markflorkowski Jul 26, 2024
d80dd87
move onclick
markflorkowski Jul 26, 2024
c8069fd
dedupe
markflorkowski Jul 26, 2024
88eb972
that was not deduping
markflorkowski Jul 26, 2024
59543da
rm unused import
markflorkowski Jul 26, 2024
33b25b1
abort for solid dropzone
markflorkowski Jul 26, 2024
3ff1b7a
svelte abort + ref fix
markflorkowski Jul 26, 2024
20dbc5f
bind paste eventListener to document not window
markflorkowski Jul 26, 2024
d2e8737
eslint
markflorkowski Jul 26, 2024
a3eefc7
manual trigger display logic
markflorkowski Jul 26, 2024
5297047
button content
markflorkowski Jul 26, 2024
7b7c5fe
rm weird import
markflorkowski Jul 26, 2024
aeeb44f
fix logic
markflorkowski Jul 26, 2024
0185890
oops
markflorkowski Jul 26, 2024
129b363
apply button content fix to dropzone
markflorkowski Jul 26, 2024
a2ae284
rm unused
markflorkowski Jul 26, 2024
1169371
misc fixed - vue
markflorkowski Jul 26, 2024
2ce3c6b
pkg-pr-new nuxt
markflorkowski Jul 26, 2024
214db4d
default loglevel nuxt
markflorkowski Jul 27, 2024
a059154
don't run onChange if no acceptedFiles
markflorkowski Jul 27, 2024
6b81b2e
add nuxt IFF not already in list
markflorkowski Jul 27, 2024
a3f6def
void -> await
markflorkowski Jul 28, 2024
85b1320
Update docs/src/pages/api-reference/react.mdx
markflorkowski Jul 28, 2024
ac4d76d
solid routeConfig + misc async fixes
markflorkowski Jul 28, 2024
8340619
button too
markflorkowski Jul 28, 2024
16c70a1
format
markflorkowski Jul 28, 2024
f836be4
svelte wasn't linting on my machine
markflorkowski Jul 28, 2024
6d98167
remove solidjs-use dep
markflorkowski Jul 28, 2024
36ae009
why is document undefined
markflorkowski Jul 28, 2024
f45f068
trycatch?
markflorkowski Jul 28, 2024
7dfef14
fix button text logic, unify button/dropzone
markflorkowski Jul 28, 2024
e49b947
disabled state on dropzone
markflorkowski Jul 28, 2024
efbdcca
add missing styles
markflorkowski Jul 28, 2024
1fd6fec
move styles?
markflorkowski Jul 28, 2024
efdae47
vue
markflorkowski Jul 29, 2024
e24865e
add disabled prop in vue
markflorkowski Jul 29, 2024
60ca0b1
types
markflorkowski Jul 29, 2024
61f3321
oops
markflorkowski Jul 29, 2024
a8ef3e6
dropzone button state fix vue
markflorkowski Jul 29, 2024
94ee085
dropzone type
markflorkowski Jul 29, 2024
051e603
dropzone state fix vue
markflorkowski Jul 29, 2024
0029045
vue button styles
markflorkowski Jul 29, 2024
249a27d
svelte
markflorkowski Jul 29, 2024
bed6b2e
lint
markflorkowski Jul 29, 2024
70bc541
svelte dropzone disable logic
markflorkowski Jul 29, 2024
6028a41
svelte dropzone disabled state
markflorkowski Jul 29, 2024
3fb5772
pointer events
markflorkowski Jul 29, 2024
fe594c8
|| -> ??
markflorkowski Jul 29, 2024
2d49b1a
dropzone button
markflorkowski Jul 29, 2024
3cb562b
try again?
markflorkowski Jul 29, 2024
f8d5a0a
explicitly set dropzone disabled state?
markflorkowski Jul 29, 2024
003204b
why is the button still working
markflorkowski Jul 29, 2024
4283500
svelte dropzone abort
markflorkowski Jul 29, 2024
531ed21
passthrough button events in dropzone if no files selected
markflorkowski Jul 29, 2024
fb83080
?
markflorkowski Jul 29, 2024
66c8d80
rm unused import
markflorkowski Jul 29, 2024
ebfc5ae
styling
markflorkowski Jul 29, 2024
927e5fa
disabled styling
markflorkowski Jul 29, 2024
39411ed
move cursor style to dropzone root
markflorkowski Jul 29, 2024
8a16458
dropzone button cursor pointer
markflorkowski Jul 29, 2024
97ef7a4
move disabled out of creator - svelte
markflorkowski Jul 29, 2024
41d440f
trivial type infer
markflorkowski Jul 29, 2024
d09c2e2
no cursor-pointer on label
markflorkowski Jul 29, 2024
7832cf4
correct cursor state on label
markflorkowski Jul 29, 2024
d0f5a9d
run onChange when button is cleared
markflorkowski Jul 29, 2024
afe8b8d
adjust dropzoneInput action
markflorkowski Aug 2, 2024
c11c8a5
logs
markflorkowski Aug 2, 2024
761cc89
more logs
markflorkowski Aug 2, 2024
1f1397a
log inputref
markflorkowski Aug 2, 2024
7d716b2
use .value on upload progress
markflorkowski Aug 7, 2024
5546dfc
also on dropzone
markflorkowski Aug 7, 2024
0da1aeb
Merge branch 'main' into mark/on-button-select
markflorkowski Sep 3, 2024
02a78dd
review feedback
markflorkowski Sep 3, 2024
885f3e0
update action to use
markflorkowski Sep 3, 2024
ee8550c
remove await
markflorkowski Sep 3, 2024
7478a92
refactor renderButton
markflorkowski Sep 4, 2024
cf6337f
chore: init some component tests (#898)
juliusmarminge Sep 5, 2024
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
8 changes: 8 additions & 0 deletions .changeset/giant-candles-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@uploadthing/react": minor
"@uploadthing/solid": minor
"@uploadthing/svelte": minor
"@uploadthing/vue": minor
---

feat: Add `onChange` to `<UploadButton/>` and `<UploadDropzone />`. Deprecate dropzone's `onDrop`
16 changes: 14 additions & 2 deletions .github/workflows/pkg-pr-new.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get changed packages
id: changed-packages
run: |
echo "::set-output name=changed_packages::$(git diff --name-only main ./packages | awk -F'/' '{print "./packages/" $2}' | sort -u | tr '\n' ' ')"
changed_packages=$(git diff --name-only origin/main origin/${GITHUB_HEAD_REF} ./packages | awk -F'/' '{print "./packages/" $2}' | sort -u | tr '\n' ' ')

# add nuxt IFF it is not already in the list but vue is
if echo "$changed_packages" | grep -q "./packages/vue"; then
if ! echo "$changed_packages" | grep -q "./packages/nuxt"; then
changed_packages="$changed_packages ./packages/nuxt"
fi
fi

echo "changed_packages=$changed_packages" >> $GITHUB_OUTPUT


- name: Setup
uses: ./tooling/gh-actions/setup
Expand All @@ -23,4 +35,4 @@ jobs:

- name: Release
if: steps.changed-packages.outputs.changed_packages != ''
run: pnpx pkg-pr-new --compact publish ${{ steps.changed-packages.outputs.changed_packages }} --template ./examples/minimal*
run: pnpx pkg-pr-new --compact publish ${{ steps.changed-packages.outputs.changed_packages }} --template './examples/minimal-*'
73 changes: 39 additions & 34 deletions docs/src/pages/api-reference/react.mdx
Copy link
Collaborator

@juliusmarminge juliusmarminge Sep 3, 2024

Choose a reason for hiding this comment

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

reminder for myself: copy these changes to new docs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
"@manypkg/cli": "^0.21.3",
"@playwright/test": "1.45.0",
"@prettier/sync": "^0.5.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@types/bun": "^1.1.5",
"@types/node": "^20.14.0",
"@uploadthing/eslint-config": "workspace:*",
Expand Down
16 changes: 11 additions & 5 deletions packages/dropzone/src/svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ export function createDropzone(_props: DropzoneOptions) {
dispatch({ type: "openDialog" });
input.value = "";
input.click();
} else {
console.warn(
"No input element found for file picker. Please make sure to use the `dropzoneInput` action.",
);
}
};

Expand Down Expand Up @@ -304,21 +308,23 @@ export function createDropzone(_props: DropzoneOptions) {
options,
) => {
inputRef.set(node);
node.setAttribute("type", "file");
node.style.display = "none";
node.setAttribute("type", "file");
node.setAttribute("multiple", String(options.multiple));
node.setAttribute("disabled", String(options.disabled));
node.setAttribute("tabIndex", "-1");
const acceptAttrUnsub = acceptAttr.subscribe((accept) => {
node.setAttribute("accept", accept!);
});
if (!options.disabled) {
node.addEventListener("change", onDropCb);
node.addEventListener("click", onInputElementClick);
}

node.addEventListener("change", onDropCb);
node.addEventListener("click", onInputElementClick);

return {
update(options: DropzoneOptions) {
props.update(($props) => ({ ...$props, ...options }));
node.setAttribute("multiple", String(options.multiple));
node.setAttribute("disabled", String(options.disabled));
},
destroy() {
inputRef.set(null);
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default defineNuxtModule<ModuleOptions>({
{
secret: options.secret,
appId: options.appId,
logLevel: options.logLevel,
logLevel: options.logLevel ?? "info",
},
);

Expand Down
56 changes: 35 additions & 21 deletions packages/react/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export type UploadButtonProps<
* @see https://docs.uploadthing.com/theming#content-customisation
*/
content?: ButtonContent;
disabled?: boolean;
};

/** These are some internal stuff we use to test the component and for forcing a state in docs */
Expand Down Expand Up @@ -142,7 +141,7 @@ export function UploadButton<

const uploadFiles = useCallback(
(files: File[]) => {
void startUpload(files, fileRouteInput).catch((e) => {
startUpload(files, fileRouteInput).catch((e) => {
if (e instanceof UploadAbortedError) {
void $props.onUploadAborted?.();
} else {
Expand All @@ -165,25 +164,27 @@ export function UploadButton<
if (!e.target.files) return;
const selectedFiles = Array.from(e.target.files);

$props.onChange?.(selectedFiles);

if (mode === "manual") {
setFiles(selectedFiles);
return;
}

uploadFiles(selectedFiles);
void uploadFiles(selectedFiles);
},
disabled: fileTypes.length === 0,
tabIndex: fileTypes.length === 0 ? -1 : 0,
}),
[fileTypes, mode, multiple, uploadFiles],
[$props, fileTypes, mode, multiple, uploadFiles],
);

if ($props.__internal_button_disabled) inputProps.disabled = true;
if ($props.disabled) inputProps.disabled = true;

const state = (() => {
if ($props.__internal_state) return $props.__internal_state;
if (inputProps.disabled) return "readying";
if (inputProps.disabled) return "disabled";
if (!inputProps.disabled && !isUploading) return "ready";
return "uploading";
})();
Expand All @@ -198,10 +199,13 @@ export function UploadButton<
let filesToUpload = pastedFiles;
setFiles((prev) => {
filesToUpload = [...prev, ...pastedFiles];

$props.onChange?.(filesToUpload);

return filesToUpload;
});

if (mode === "auto") uploadFiles(files);
if (mode === "auto") void uploadFiles(files);
});

const styleFieldArg = {
Expand All @@ -218,23 +222,28 @@ export function UploadButton<
);
if (customContent) return customContent;

if (state === "readying") return "Loading...";

if (state !== "uploading") {
if (mode === "manual" && files.length > 0) {
return `Upload ${files.length} file${files.length === 1 ? "" : "s"}`;
switch (state) {
case "readying": {
return "Loading...";
}
case "uploading": {
if (uploadProgress === 100) return <Spinner />;
return (
<span className="z-50">
<span className="block group-hover:hidden">{uploadProgress}%</span>
<Cancel className="hidden size-4 group-hover:block" />
</span>
);
}
case "disabled":
case "ready":
default: {
if (mode === "manual" && files.length > 0) {
return `Upload ${files.length} file${files.length === 1 ? "" : "s"}`;
}
return `Choose File${inputProps.multiple ? `(s)` : ``}`;
}
return `Choose File${inputProps.multiple ? `(s)` : ``}`;
}

if (uploadProgress === 100) return <Spinner />;

return (
<span className="z-50">
<span className="block group-hover:hidden">{uploadProgress}%</span>
<Cancel className="hidden size-4 group-hover:block" />
</span>
);
};

const renderClearButton = () => (
Expand All @@ -245,6 +254,8 @@ export function UploadButton<
if (fileInputRef.current) {
fileInputRef.current.value = "";
}

$props.onChange?.([]);
}}
className={twMerge(
"h-[1.25rem] cursor-pointer rounded border-none bg-transparent text-gray-500 transition-colors hover:bg-slate-200 hover:text-gray-600",
Expand Down Expand Up @@ -290,6 +301,7 @@ export function UploadButton<
<label
className={twMerge(
"group relative flex h-10 w-36 cursor-pointer items-center justify-center overflow-hidden rounded-md text-white after:transition-[width] after:duration-500 focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
state === "disabled" && "cursor-not-allowed bg-blue-400",
state === "readying" && "cursor-not-allowed bg-blue-400",
state === "uploading" &&
`bg-blue-400 after:absolute after:left-0 after:h-full after:bg-blue-600 after:content-[''] ${progressWidths[uploadProgress]}`,
Expand All @@ -304,13 +316,15 @@ export function UploadButton<
if (state === "uploading") {
e.preventDefault();
e.stopPropagation();

acRef.current.abort();
acRef.current = new AbortController();
return;
}
if (mode === "manual" && files.length > 0) {
e.preventDefault();
e.stopPropagation();

uploadFiles(files);
}
}}
Expand Down
72 changes: 43 additions & 29 deletions packages/react/src/components/dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ export type UploadDropzoneProps<
* Callback called when files are dropped or pasted.
*
* @param acceptedFiles - The files that were accepted.
* @deprecated Use `onChange` instead
*/
onDrop?: (acceptedFiles: File[]) => void;
disabled?: boolean;
};

/** These are some internal stuff we use to test the component and for forcing a state in docs */
Expand Down Expand Up @@ -146,8 +146,8 @@ export function UploadDropzone<
);

const uploadFiles = useCallback(
(files: File[]) => {
void startUpload(files, fileRouteInput).catch((e) => {
async (files: File[]) => {
await startUpload(files, fileRouteInput).catch((e) => {
if (e instanceof UploadAbortedError) {
void $props.onUploadAborted?.();
} else {
Expand All @@ -165,11 +165,12 @@ export function UploadDropzone<
const onDrop = useCallback(
(acceptedFiles: File[]) => {
$props.onDrop?.(acceptedFiles);
$props.onChange?.(acceptedFiles);

setFiles(acceptedFiles);

// If mode is auto, start upload immediately
if (mode === "auto") uploadFiles(acceptedFiles);
if (mode === "auto") void uploadFiles(acceptedFiles);
},
[$props, mode, uploadFiles],
);
Expand All @@ -192,7 +193,7 @@ export function UploadDropzone<
$props.__internal_ready ??
($props.__internal_state === "ready" || fileTypes.length > 0);

const onUploadClick = (
const onUploadClick = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => {
if (state === "uploading") {
Expand All @@ -207,7 +208,7 @@ export function UploadDropzone<
e.preventDefault();
e.stopPropagation();

uploadFiles(files);
await uploadFiles(files);
}
};

Expand All @@ -222,10 +223,15 @@ export function UploadDropzone<
let filesToUpload = pastedFiles;
setFiles((prev) => {
filesToUpload = [...prev, ...pastedFiles];

$props.onChange?.(filesToUpload);

return filesToUpload;
});

if (mode === "auto") uploadFiles(filesToUpload);
$props.onChange?.(filesToUpload);

if (mode === "auto") void uploadFiles(filesToUpload);
};

window.addEventListener("paste", handlePaste);
Expand All @@ -234,27 +240,34 @@ export function UploadDropzone<
};
}, [uploadFiles, $props, appendOnPaste, mode, fileTypes, rootRef, files]);

const getUploadButtonText = (fileTypes: string[]) => {
if (files.length > 0)
return `Upload ${files.length} file${files.length === 1 ? "" : "s"}`;
if (fileTypes.length === 0) return "Loading...";
return `Choose File${multiple ? `(s)` : ``}`;
};

const getUploadButtonContents = (fileTypes: string[]) => {
if (state !== "uploading") {
return getUploadButtonText(fileTypes);
}
if (uploadProgress === 100) {
return <Spinner />;
}

return (
<span className="z-50">
<span className="block group-hover:hidden">{uploadProgress}%</span>
<Cancel className="hidden size-4 group-hover:block" />
</span>
const getUploadButtonContents = () => {
const customContent = contentFieldToContent(
$props.content?.button,
styleFieldArg,
);
if (customContent) return customContent;

if (state === "readying") {
return "Loading...";
} else if (state === "uploading") {
if (uploadProgress === 100) {
return <Spinner />;
} else {
return (
<span className="z-50">
<span className="block group-hover:hidden">{uploadProgress}%</span>
<Cancel className="hidden size-4 group-hover:block" />
</span>
);
}
} else {
// Default case: "ready" or "disabled" state
if (mode === "manual" && files.length > 0) {
return `Upload ${files.length} file${files.length === 1 ? "" : "s"}`;
} else {
return `Choose File${multiple ? `(s)` : ``}`;
}
}
};

const styleFieldArg = {
Expand All @@ -267,6 +280,7 @@ export function UploadDropzone<

const state = (() => {
if ($props.__internal_state) return $props.__internal_state;
if (isDisabled) return "disabled";
if (!ready) return "readying";
if (ready && !isUploading) return "ready";

Expand Down Expand Up @@ -344,6 +358,7 @@ export function UploadDropzone<
<button
className={twMerge(
"group relative mt-4 flex h-10 w-36 cursor-pointer items-center justify-center overflow-hidden rounded-md border-none text-base text-white after:transition-[width] after:duration-500 focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
state === "disabled" && "cursor-not-allowed bg-blue-400",
state === "readying" && "cursor-not-allowed bg-blue-400",
state === "uploading" &&
`bg-blue-400 after:absolute after:left-0 after:h-full after:bg-blue-600 after:content-[''] ${progressWidths[uploadProgress]}`,
Expand All @@ -358,8 +373,7 @@ export function UploadDropzone<
type="button"
disabled={$props.__internal_button_disabled ?? !files.length}
>
{contentFieldToContent($props.content?.button, styleFieldArg) ??
getUploadButtonContents(fileTypes)}
{getUploadButtonContents()}
</button>
</div>
);
Expand Down
Loading
Loading