Переглянути джерело

Merge branch 'main' into 4.x

* main: (22 commits)
  @uppy/xhr-upload: refactor to use `fetcher` (#5074)
  docs: use StackBlitz for all examples/issue template (#5125)
  Update yarn.lock
  Add svelte 5 as peer dep (#5122)
  Bump docker/setup-buildx-action from 2 to 3 (#5124)
  Bump actions/checkout from 3 to 4 (#5123)
  Remove JSX global type everywhere (#5117)
  Revert "@uppy/core: reference updated i18n in Restricter"
  @uppy/core: reference updated i18n in Restricter
  @uppy/utils: improve return type of `dataURItoFile` (#5112)
  @uppy/drop-target: change drop event type to DragEvent (#5107)
  @uppy/image-editor: fix label definitions (#5111)
  meta: bump Prettier version (#5114)
  @uppy/provider-views: bring back "loaded X files..." (#5097)
  @uppy/dashboard: fix type of trigger option (#5106)
  meta: fix linter
  @uppy/form: fix `submitOnSuccess` and `triggerUploadOnSubmit` combination (#5058)
  Bump docker/build-push-action from 3 to 5 (#5105)
  Bump akhileshns/heroku-deploy from 3.12.12 to 3.13.15 (#5102)
  Bump docker/login-action from 2 to 3 (#5101)
  ...
Murderlon 11 місяців тому
батько
коміт
82a5999260
97 змінених файлів з 353 додано та 478 видалено
  1. 5 1
      .eslintrc.js
  2. 3 1
      .github/ISSUE_TEMPLATE/1-bug.yml
  3. 7 7
      .github/workflows/bundlers.yml
  4. 2 2
      .github/workflows/ci.yml
  5. 8 8
      .github/workflows/companion-deploy.yml
  6. 1 1
      .github/workflows/companion.yml
  7. 3 3
      .github/workflows/e2e.yml
  8. 4 4
      .github/workflows/linters.yml
  9. 1 1
      .github/workflows/lockfile_check.yml
  10. 1 1
      .github/workflows/manual-cdn.yml
  11. 1 1
      .github/workflows/release-candidate.yml
  12. 5 5
      .github/workflows/release.yml
  13. 9 3
      .prettierrc.js
  14. 3 3
      docs/quick-start.mdx
  15. 1 1
      docs/sources/audio.mdx
  16. 1 1
      docs/sources/companion-plugins/box.mdx
  17. 1 1
      docs/sources/companion-plugins/dropbox.mdx
  18. 1 1
      docs/sources/companion-plugins/facebook.mdx
  19. 1 1
      docs/sources/companion-plugins/google-drive.mdx
  20. 1 1
      docs/sources/companion-plugins/instagram.mdx
  21. 1 1
      docs/sources/companion-plugins/onedrive.mdx
  22. 1 1
      docs/sources/companion-plugins/unsplash.mdx
  23. 1 1
      docs/sources/companion-plugins/url.mdx
  24. 1 1
      docs/sources/companion-plugins/zoom.mdx
  25. 1 1
      docs/sources/screen-capture.mdx
  26. 1 1
      docs/sources/webcam.mdx
  27. 1 1
      docs/user-interfaces/dashboard.mdx
  28. 1 1
      docs/user-interfaces/drag-drop.mdx
  29. 1 1
      docs/user-interfaces/elements/drop-target.mdx
  30. 1 1
      docs/user-interfaces/elements/status-bar.mdx
  31. 1 1
      packages/@uppy/audio/src/Audio.tsx
  32. 1 1
      packages/@uppy/audio/src/AudioSourceSelect.tsx
  33. 1 1
      packages/@uppy/audio/src/DiscardButton.tsx
  34. 2 2
      packages/@uppy/audio/src/PermissionsScreen.tsx
  35. 1 1
      packages/@uppy/audio/src/RecordButton.tsx
  36. 1 1
      packages/@uppy/audio/src/RecordingLength.tsx
  37. 1 3
      packages/@uppy/audio/src/RecordingScreen.tsx
  38. 1 1
      packages/@uppy/audio/src/SubmitButton.tsx
  39. 1 1
      packages/@uppy/box/src/Box.tsx
  40. 3 2
      packages/@uppy/core/src/Uppy.ts
  41. 2 2
      packages/@uppy/dashboard/src/Dashboard.tsx
  42. 1 1
      packages/@uppy/dashboard/src/components/Dashboard.tsx
  43. 1 1
      packages/@uppy/dashboard/src/components/EditorPanel.tsx
  44. 1 1
      packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.tsx
  45. 1 1
      packages/@uppy/dashboard/src/components/FileCard/index.tsx
  46. 1 1
      packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.tsx
  47. 1 1
      packages/@uppy/dashboard/src/components/FileList.tsx
  48. 1 1
      packages/@uppy/dashboard/src/components/FilePreview.tsx
  49. 1 1
      packages/@uppy/dashboard/src/components/PickerPanelContent.tsx
  50. 1 1
      packages/@uppy/dashboard/src/components/PickerPanelTopBar.tsx
  51. 1 5
      packages/@uppy/dashboard/src/components/Slide.tsx
  52. 1 1
      packages/@uppy/dropbox/src/Dropbox.tsx
  53. 1 1
      packages/@uppy/facebook/src/Facebook.tsx
  54. 1 1
      packages/@uppy/file-input/src/FileInput.tsx
  55. 9 1
      packages/@uppy/form/src/index.ts
  56. 1 1
      packages/@uppy/google-drive/src/GoogleDrive.tsx
  57. 26 50
      packages/@uppy/image-editor/src/Editor.tsx
  58. 1 1
      packages/@uppy/image-editor/src/ImageEditor.tsx
  59. 1 1
      packages/@uppy/instagram/src/Instagram.tsx
  60. 1 1
      packages/@uppy/onedrive/src/OneDrive.tsx
  61. 2 2
      packages/@uppy/provider-views/src/Breadcrumbs.tsx
  62. 6 8
      packages/@uppy/provider-views/src/Browser.tsx
  63. 1 1
      packages/@uppy/provider-views/src/FooterActions.tsx
  64. 2 2
      packages/@uppy/provider-views/src/Item/components/GridLi.tsx
  65. 1 1
      packages/@uppy/provider-views/src/Loader.tsx
  66. 2 2
      packages/@uppy/provider-views/src/ProviderView/AuthView.tsx
  67. 2 2
      packages/@uppy/provider-views/src/ProviderView/Header.tsx
  68. 3 3
      packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx
  69. 1 5
      packages/@uppy/provider-views/src/ProviderView/User.tsx
  70. 1 1
      packages/@uppy/provider-views/src/SearchFilterInput.tsx
  71. 1 1
      packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx
  72. 2 1
      packages/@uppy/react/src/Dashboard.ts
  73. 2 1
      packages/@uppy/react/src/DashboardModal.ts
  74. 2 1
      packages/@uppy/react/src/DragDrop.ts
  75. 2 1
      packages/@uppy/react/src/FileInput.ts
  76. 2 1
      packages/@uppy/react/src/ProgressBar.ts
  77. 2 1
      packages/@uppy/react/src/StatusBar.ts
  78. 14 18
      packages/@uppy/status-bar/src/Components.tsx
  79. 2 2
      packages/@uppy/status-bar/src/StatusBarUI.tsx
  80. 1 1
      packages/@uppy/svelte/package.json
  81. 1 1
      packages/@uppy/unsplash/src/Unsplash.tsx
  82. 1 1
      packages/@uppy/url/src/Url.tsx
  83. 3 2
      packages/@uppy/utils/src/ProgressTimeout.ts
  84. 13 2
      packages/@uppy/utils/src/dataURItoBlob.ts
  85. 1 1
      packages/@uppy/utils/src/dataURItoFile.ts
  86. 1 1
      packages/@uppy/utils/src/fetcher.ts
  87. 1 1
      packages/@uppy/webcam/src/DiscardButton.tsx
  88. 1 1
      packages/@uppy/webcam/src/PermissionsScreen.tsx
  89. 1 1
      packages/@uppy/webcam/src/RecordButton.tsx
  90. 1 1
      packages/@uppy/webcam/src/RecordingLength.tsx
  91. 1 1
      packages/@uppy/webcam/src/SnapshotButton.tsx
  92. 1 1
      packages/@uppy/webcam/src/SubmitButton.tsx
  93. 1 1
      packages/@uppy/webcam/src/Webcam.tsx
  94. 1 2
      packages/@uppy/xhr-upload/package.json
  95. 133 255
      packages/@uppy/xhr-upload/src/index.ts
  96. 1 1
      packages/@uppy/zoom/src/Zoom.tsx
  97. 4 5
      yarn.lock

+ 5 - 1
.eslintrc.js

@@ -175,6 +175,10 @@ module.exports = {
             name: 'require',
             message: 'Use import instead',
           },
+          {
+            name: 'JSX',
+            message: 'Use h.JSX.Element, ComponentChild, or ComponentChildren from Preact',
+          },
         ],
         'import/extensions': ['error', 'ignorePackages'],
       },
@@ -479,7 +483,7 @@ module.exports = {
       },
     },
     {
-      files: ['packages/@uppy/*/src/**/*.ts', 'packages/@uppy/*/src/**/*.tsx'],
+      files: ['packages/@uppy/*/src/**/*.ts'],
       excludedFiles: ['packages/@uppy/**/*.test.ts', 'packages/@uppy/core/src/mocks/*.ts'],
       rules: {
         '@typescript-eslint/explicit-module-boundary-types': 'error',

+ 3 - 1
.github/ISSUE_TEMPLATE/1-bug.yml

@@ -26,7 +26,9 @@ body:
 
         Starters:
 
-        - [Uppy Dashboard](https://codesandbox.io/s/uppy-dashboard-xpxuhd?file=/src/index.js)
+        - [Uppy Dashboard](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js)
+        - [Uppy React](https://stackblitz.com/edit/vitejs-vite-vehvbq?file=src%2FApp.tsx)
+        - [Uppy Vue](https://stackblitz.com/edit/vitejs-vite-ubjwys?file=src%2FApp.vue)
     validations:
       required: false
   - type: textarea

+ 7 - 7
.github/workflows/bundlers.yml

@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:
@@ -73,7 +73,7 @@ jobs:
           pack --install-if-needed -o /tmp/artifacts/%s-${{ github.sha }}.tgz
       - name: Upload artifact
         if: success()
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: packages
           path: /tmp/artifacts/
@@ -87,7 +87,7 @@ jobs:
         bundler-version: [latest]
     steps:
       - name: Download uppy tarball
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           path: /tmp/
       - name: Extract tarball
@@ -125,7 +125,7 @@ jobs:
         bundler-version: [latest]
     steps:
       - name: Download uppy tarball
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           path: /tmp/
       - name: Extract tarball
@@ -151,7 +151,7 @@ jobs:
         bundler-version: [latest]
     steps:
       - name: Download uppy tarball
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           path: /tmp/
       - name: Extract tarball
@@ -181,7 +181,7 @@ jobs:
         bundler-version: [latest]
     steps:
       - name: Download uppy tarball
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           path: /tmp/
       - name: Extract tarball
@@ -206,7 +206,7 @@ jobs:
         bundler-version: [latest]
     steps:
       - name: Download uppy tarball
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           path: /tmp/
       - name: Extract tarball

+ 2 - 2
.github/workflows/ci.yml

@@ -37,7 +37,7 @@ jobs:
         node-version: [18.x, 20.x]
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:
@@ -70,7 +70,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:

+ 8 - 8
.github/workflows/companion-deploy.yml

@@ -20,7 +20,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Set SHA commit in version
         run:
           (cd packages/@uppy/companion && node -e 'const
@@ -33,7 +33,7 @@ jobs:
           /tmp/companion-${{ github.sha }}.tar.gz
       - name: Upload artifact
         if: success()
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: companion-${{ github.sha }}.tar.gz
           path: /tmp/companion-${{ github.sha }}.tar.gz
@@ -46,7 +46,7 @@ jobs:
       COMPOSE_DOCKER_CLI_BUILD: 0
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Docker meta
         id: docker_meta
         uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
@@ -56,14 +56,14 @@ jobs:
             type=edge
             type=raw,value=latest,enable=false
       - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
-      - uses: docker/setup-buildx-action@v2
+      - uses: docker/setup-buildx-action@v3
       - name: Log in to DockerHub
-        uses: docker/login-action@v2
+        uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
         with:
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
       - name: Build and push
-        uses: docker/build-push-action@v3
+        uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
         with:
           push: true
           context: .
@@ -77,12 +77,12 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Alter dockerfile
         run: |
           sed -i 's/^EXPOSE 3020$/EXPOSE $PORT/g' Dockerfile
       - name: Deploy to heroku
-        uses: akhileshns/heroku-deploy@v3.12.12
+        uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 # v3.13.15
         with:
           heroku_api_key: ${{secrets.HEROKU_API_KEY}}
           heroku_app_name: companion-demo

+ 1 - 1
.github/workflows/companion.yml

@@ -26,7 +26,7 @@ jobs:
         node-version: [18.x, 20.x, latest]
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:

+ 3 - 3
.github/workflows/e2e.yml

@@ -48,7 +48,7 @@ jobs:
       is_accurate_diff: ${{ steps.diff.outputs.IS_ACCURATE_DIFF }}
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 2
           ref:
@@ -222,7 +222,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           ref: ${{ github.event.pull_request.head.sha || github.sha }}
       - name: Get yarn cache directory path
@@ -283,7 +283,7 @@ jobs:
           # https://docs.cypress.io/guides/references/advanced-installation#Binary-cache
           CYPRESS_CACHE_FOLDER: ${{ steps.cypress-cache-dir-path.outputs.dir }}
       - name: Upload videos in case of failure
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         if: failure()
         with:
           name: videos-and-screenshots

+ 4 - 4
.github/workflows/linters.yml

@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:
@@ -56,7 +56,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:
@@ -83,12 +83,12 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout Uppy.io sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           repository: transloadit/uppy.io
       - run: rm -rf docs # the other PR has not landed
       - name: Checkout docs
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           path: uppy
       - run: mv uppy /tmp/uppy && ln -s /tmp/uppy/docs docs

+ 1 - 1
.github/workflows/lockfile_check.yml

@@ -20,7 +20,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:

+ 1 - 1
.github/workflows/manual-cdn.yml

@@ -15,7 +15,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run:

+ 1 - 1
.github/workflows/release-candidate.yml

@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           branch: release-beta
           fetch-depth: 3 # the prepare commit, the merge commit, and the base ones.

+ 5 - 5
.github/workflows/release.yml

@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 2
       - name: Get yarn cache directory path
@@ -142,7 +142,7 @@ jobs:
       COMPOSE_DOCKER_CLI_BUILD: 0
     steps:
       - name: Checkout sources
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
       - name: Docker meta
         id: docker_meta
         uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
@@ -154,14 +154,14 @@ jobs:
             # set latest tag for default branch
             type=raw,value=latest,enable=true
       - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
-      - uses: docker/setup-buildx-action@v2
+      - uses: docker/setup-buildx-action@v3
       - name: Log in to DockerHub
-        uses: docker/login-action@v2
+        uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
         with:
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
       - name: Build and push
-        uses: docker/build-push-action@v3
+        uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0
         with:
           push: true
           context: .

+ 9 - 3
.prettierrc.js

@@ -13,11 +13,17 @@ module.exports = {
       },
     },
     {
-      files: "docs/**",
+      files: 'docs/**',
       options: {
         semi: true,
         useTabs: true,
-      }
-    }
+      },
+    },
+    {
+      files: ['tsconfig.json'],
+      options: {
+        parser: 'jsonc',
+      },
+    },
   ],
 }

+ 3 - 3
docs/quick-start.mdx

@@ -15,10 +15,10 @@ about more important problems than building a file uploader.
 
 :::tip
 
-You can take Uppy for a walk inside CodeSandbox with a
-[minimal drag & drop](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js)
+You can take Uppy for a walk inside StackBlitz with a
+[minimal drag & drop](https://stackblitz.com/edit/vitejs-vite-yzbujq?file=main.js/g)
 experience or a
-[full featured dashboard](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[full featured dashboard](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/audio.mdx

@@ -17,7 +17,7 @@ time sound wavelength when recording.
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/box.mdx

@@ -15,7 +15,7 @@ The `@uppy/box` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/dropbox.mdx

@@ -15,7 +15,7 @@ The `@uppy/dropbox` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/facebook.mdx

@@ -15,7 +15,7 @@ The `@uppy/facebook` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/google-drive.mdx

@@ -15,7 +15,7 @@ The `@uppy/google-drive` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/instagram.mdx

@@ -15,7 +15,7 @@ The `@uppy/instagram` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/onedrive.mdx

@@ -15,7 +15,7 @@ The `@uppy/onedrive` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/unsplash.mdx

@@ -15,7 +15,7 @@ The `@uppy/unsplash` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/url.mdx

@@ -15,7 +15,7 @@ URL and it will be added!
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/companion-plugins/zoom.mdx

@@ -15,7 +15,7 @@ The `@uppy/zoom` plugin lets users import files from their
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/screen-capture.mdx

@@ -16,7 +16,7 @@ save it as a video.
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/sources/webcam.mdx

@@ -16,7 +16,7 @@ camera on desktop and mobile devices.
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/user-interfaces/dashboard.mdx

@@ -21,7 +21,7 @@ integrate.
 :::tip
 
 [Try out the live example with all plugins](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-dashboard-xpxuhd).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
 
 :::
 

+ 1 - 1
docs/user-interfaces/drag-drop.mdx

@@ -15,7 +15,7 @@ The `@uppy/drag-drop` plugin renders a drag and drop area for file selection.
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-yzbujq?file=main.js/g).
 
 :::
 

+ 1 - 1
docs/user-interfaces/elements/drop-target.mdx

@@ -19,7 +19,7 @@ solution targeting any DOM element.
 :::tip
 
 [Try out the live example](/examples) or take it for a spin in
-[CodeSandbox](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js).
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-yzbujq?file=main.js/g).
 
 :::
 

+ 1 - 1
docs/user-interfaces/elements/status-bar.mdx

@@ -17,7 +17,7 @@ pre- and post-processing information, and allows users to control
 :::tip
 
 Try it out together with [`@uppy/drag-drop`](/docs/drag-drop) in
-[CodeSandbox](https://codesandbox.io/s/uppy-drag-drop-gyewzx?file=/src/index.js)
+[StackBlitz](https://stackblitz.com/edit/vitejs-vite-yzbujq?file=main.js/g)
 
 :::
 

+ 1 - 1
packages/@uppy/audio/src/Audio.tsx

@@ -348,7 +348,7 @@ export default class Audio<M extends Meta, B extends Body> extends UIPlugin<
     })
   }
 
-  render(): JSX.Element {
+  render() {
     if (!this.#audioActive) {
       this.#start()
     }

+ 1 - 1
packages/@uppy/audio/src/AudioSourceSelect.tsx

@@ -10,7 +10,7 @@ export default ({
   currentDeviceId,
   audioSources,
   onChangeSource,
-}: AudioSourceSelectProps): JSX.Element => {
+}: AudioSourceSelectProps) => {
   return (
     <div className="uppy-Audio-videoSource">
       <select

+ 1 - 1
packages/@uppy/audio/src/DiscardButton.tsx

@@ -6,7 +6,7 @@ interface DiscardButtonProps {
   i18n: I18n
 }
 
-function DiscardButton({ onDiscard, i18n }: DiscardButtonProps): JSX.Element {
+function DiscardButton({ onDiscard, i18n }: DiscardButtonProps) {
   return (
     <button
       className="uppy-u-reset uppy-c-btn uppy-Audio-button"

+ 2 - 2
packages/@uppy/audio/src/PermissionsScreen.tsx

@@ -2,12 +2,12 @@ import type { I18n } from '@uppy/utils/lib/Translator'
 import { h } from 'preact'
 
 interface PermissionsScreenProps {
-  icon: () => JSX.Element | null
+  icon: () => h.JSX.Element | null
   hasAudio: boolean
   i18n: I18n
 }
 
-export default (props: PermissionsScreenProps): JSX.Element => {
+export default (props: PermissionsScreenProps) => {
   const { icon, hasAudio, i18n } = props
   return (
     <div className="uppy-Audio-permissons">

+ 1 - 1
packages/@uppy/audio/src/RecordButton.tsx

@@ -13,7 +13,7 @@ export default function RecordButton({
   onStartRecording,
   onStopRecording,
   i18n,
-}: RecordButtonProps): JSX.Element {
+}: RecordButtonProps) {
   if (recording) {
     return (
       <button

+ 1 - 1
packages/@uppy/audio/src/RecordingLength.tsx

@@ -10,7 +10,7 @@ interface RecordingLengthProps {
 export default function RecordingLength({
   recordingLengthSeconds,
   i18n,
-}: RecordingLengthProps): JSX.Element {
+}: RecordingLengthProps) {
   const formattedRecordingLengthSeconds = formatSeconds(recordingLengthSeconds)
 
   return (

+ 1 - 3
packages/@uppy/audio/src/RecordingScreen.tsx

@@ -26,9 +26,7 @@ interface RecordingScreenProps extends AudioSourceSelectProps {
   recordingLengthSeconds: number
 }
 
-export default function RecordingScreen(
-  props: RecordingScreenProps,
-): JSX.Element {
+export default function RecordingScreen(props: RecordingScreenProps) {
   const {
     stream,
     recordedAudio,

+ 1 - 1
packages/@uppy/audio/src/SubmitButton.tsx

@@ -6,7 +6,7 @@ interface SubmitButtonProps {
   i18n: I18n
 }
 
-function SubmitButton({ onSubmit, i18n }: SubmitButtonProps): JSX.Element {
+function SubmitButton({ onSubmit, i18n }: SubmitButtonProps) {
   return (
     <button
       className="uppy-u-reset uppy-c-btn uppy-Audio-button uppy-Audio-button--submit"

+ 1 - 1
packages/@uppy/box/src/Box.tsx

@@ -25,7 +25,7 @@ export default class Box<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 3 - 2
packages/@uppy/core/src/Uppy.ts

@@ -1,6 +1,7 @@
 /* eslint-disable max-classes-per-file */
 /* global AggregateError */
 
+import type { h } from 'preact'
 import Translator from '@uppy/utils/lib/Translator'
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore untyped
@@ -96,7 +97,7 @@ export type UnknownProviderPlugin<
   onFirstRender: () => void
   title: string
   files: UppyFile<M, B>[]
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
   provider: CompanionClientProvider
   storage: {
     getItem: (key: string) => Promise<string | null>
@@ -132,7 +133,7 @@ export type UnknownSearchProviderPlugin<
 > = UnknownPlugin<M, B, UnknownSearchProviderPluginState> & {
   onFirstRender: () => void
   title: string
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
   provider: CompanionClientSearchProvider
 }
 

+ 2 - 2
packages/@uppy/dashboard/src/Dashboard.tsx

@@ -166,7 +166,7 @@ interface DashboardMiscOptions<M extends Meta, B extends Body>
   thumbnailHeight?: number
   thumbnailType?: string
   thumbnailWidth?: number
-  trigger?: string
+  trigger?: string | Element
   waitForThumbnailsBeforeUpload?: boolean
 }
 
@@ -1148,7 +1148,7 @@ export default class Dashboard<M extends Meta, B extends Body> extends UIPlugin<
       .map(this.#attachRenderFunctionToTarget)
   })
 
-  render = (state: State<M, B>): JSX.Element => {
+  render = (state: State<M, B>) => {
     const pluginState = this.getPluginState()
     const { files, capabilities, allowNewUpload } = state
     const {

+ 1 - 1
packages/@uppy/dashboard/src/components/Dashboard.tsx

@@ -25,7 +25,7 @@ const HEIGHT_MD = 330
 
 type $TSFixMe = any
 
-export default function Dashboard(props: $TSFixMe): JSX.Element {
+export default function Dashboard(props: $TSFixMe) {
   const isNoFiles = props.totalFileCount === 0
   const isSingleFile = props.totalFileCount === 1
   const isSizeMD = props.containerWidth > WIDTH_MD

+ 1 - 1
packages/@uppy/dashboard/src/components/EditorPanel.tsx

@@ -4,7 +4,7 @@ import classNames from 'classnames'
 
 type $TSFixMe = any
 
-function EditorPanel(props: $TSFixMe): JSX.Element {
+function EditorPanel(props: $TSFixMe) {
   const file = props.files[props.fileCardFor]
 
   const handleCancel = () => {

+ 1 - 1
packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.tsx

@@ -2,7 +2,7 @@ import { h } from 'preact'
 
 type $TSFixMe = any
 
-export default function RenderMetaFields(props: $TSFixMe): JSX.Element {
+export default function RenderMetaFields(props: $TSFixMe) {
   const {
     computedMetaFields,
     requiredMetaFields,

+ 1 - 1
packages/@uppy/dashboard/src/components/FileCard/index.tsx

@@ -9,7 +9,7 @@ import RenderMetaFields from './RenderMetaFields.tsx'
 
 type $TSFixMe = any
 
-export default function FileCard(props: $TSFixMe): JSX.Element {
+export default function FileCard(props: $TSFixMe) {
   const {
     files,
     fileCardFor,

+ 1 - 1
packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.tsx

@@ -8,7 +8,7 @@ const metaFieldIdToName = (metaFieldId: $TSFixMe, metaFields: $TSFixMe) => {
   return field[0].name
 }
 
-export default function MetaErrorMessage(props: $TSFixMe): JSX.Element {
+export default function MetaErrorMessage(props: $TSFixMe) {
   const { file, toggleFileCard, i18n, metaFields } = props
   const { missingRequiredMetaFields } = file
   if (!missingRequiredMetaFields?.length) {

+ 1 - 1
packages/@uppy/dashboard/src/components/FileList.tsx

@@ -49,7 +49,7 @@ export default function FileList({
   toggleAddFilesPanel,
   containerWidth,
   containerHeight,
-}: $TSFixMe): JSX.Element {
+}: $TSFixMe) {
   // It's not great that this is hardcoded!
   // It's ESPECIALLY not great that this is checking against `itemsPerRow`!
   const rowHeight =

+ 1 - 1
packages/@uppy/dashboard/src/components/FilePreview.tsx

@@ -3,7 +3,7 @@ import getFileTypeIcon from '../utils/getFileTypeIcon.tsx'
 
 type $TSFixMe = any
 
-export default function FilePreview(props: $TSFixMe): JSX.Element {
+export default function FilePreview(props: $TSFixMe) {
   const { file } = props
 
   if (file.preview) {

+ 1 - 1
packages/@uppy/dashboard/src/components/PickerPanelContent.tsx

@@ -11,7 +11,7 @@ function PickerPanelContent({
   i18n,
   state,
   uppy,
-}: $TSFixMe): JSX.Element {
+}: $TSFixMe) {
   return (
     <div
       className={classNames('uppy-DashboardContent-panel', className)}

+ 1 - 1
packages/@uppy/dashboard/src/components/PickerPanelTopBar.tsx

@@ -94,7 +94,7 @@ function UploadStatus({
   }
 }
 
-function PanelTopBar(props: $TSFixMe): JSX.Element {
+function PanelTopBar(props: $TSFixMe) {
   const {
     i18n,
     isAllComplete,

+ 1 - 5
packages/@uppy/dashboard/src/components/Slide.tsx

@@ -19,11 +19,7 @@ const duration = 250
  * but it should be simple to extend this for any type of single-element
  * transition by setting the CSS name and duration as props.
  */
-function Slide({
-  children,
-}: {
-  children: ComponentChildren
-}): JSX.Element | null {
+function Slide({ children }: { children: ComponentChildren }) {
   const [cachedChildren, setCachedChildren] = useState<VNode<{
     className?: string
   }> | null>(null)

+ 1 - 1
packages/@uppy/dropbox/src/Dropbox.tsx

@@ -25,7 +25,7 @@ export default class Dropbox<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 1 - 1
packages/@uppy/facebook/src/Facebook.tsx

@@ -25,7 +25,7 @@ export default class Facebook<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 1 - 1
packages/@uppy/file-input/src/FileInput.tsx

@@ -93,7 +93,7 @@ export default class FileInput<M extends Meta, B extends Body> extends UIPlugin<
       overflow: 'hidden',
       position: 'absolute',
       zIndex: -1,
-    } satisfies JSX.IntrinsicElements['input']['style']
+    } satisfies h.JSX.IntrinsicElements['input']['style']
 
     const { restrictions } = this.uppy.opts
     const accept =

+ 9 - 1
packages/@uppy/form/src/index.ts

@@ -52,6 +52,12 @@ export default class Form<M extends Meta, B extends Body> extends BasePlugin<
 
   form: HTMLFormElement // TODO: make this private (or at least, mark it as readonly)
 
+  /**
+   * Unfortunately Uppy isn't a state machine in which we can guarantee it's
+   * currently in one state and one state only so we use this completed property which is set on `upload-success'.
+   */
+  #completed = false
+
   constructor(uppy: Uppy<M, B>, opts?: FormOptions) {
     super(uppy, { ...defaultOptions, ...opts })
     this.type = 'acquirer'
@@ -65,12 +71,14 @@ export default class Form<M extends Meta, B extends Body> extends BasePlugin<
   }
 
   handleUploadStart(): void {
+    this.#completed = false
     if (this.opts.getMetaFromForm) {
       this.getMetaFromForm()
     }
   }
 
   handleSuccess(result: Result<M, B>): void {
+    this.#completed = true
     if (this.opts.addResultToForm) {
       this.addResultToForm(result)
     }
@@ -81,7 +89,7 @@ export default class Form<M extends Meta, B extends Body> extends BasePlugin<
   }
 
   handleFormSubmit(ev: Event): void {
-    if (this.opts.triggerUploadOnSubmit) {
+    if (this.opts.triggerUploadOnSubmit && !this.#completed) {
       ev.preventDefault()
       const elements = toArray((ev.target as HTMLFormElement).elements)
       const disabledByUppy: HTMLButtonElement[] = []

+ 1 - 1
packages/@uppy/google-drive/src/GoogleDrive.tsx

@@ -24,7 +24,7 @@ export default class GoogleDrive<
 > extends UIPlugin<GoogleDriveOptions, M, B, UnknownProviderPluginState> {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 26 - 50
packages/@uppy/image-editor/src/Editor.tsx

@@ -141,7 +141,7 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     this.cropper.scale(scaleFactorX, scaleFactor)
   }
 
-  renderGranularRotate(): JSX.Element {
+  renderGranularRotate() {
     const { i18n } = this.props
     const { angleGranular } = this.state
 
@@ -166,18 +166,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderRevert(): JSX.Element {
+  renderRevert() {
     const { i18n, opts } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('revert')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('revert')}
           onClick={() => {
             this.cropper.reset()
             this.cropper.setAspectRatio(
@@ -201,18 +198,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderRotate(): JSX.Element {
+  renderRotate() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('rotate')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('rotate')}
           onClick={this.onRotate90Deg}
         >
           <svg
@@ -230,18 +224,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderFlip(): JSX.Element {
+  renderFlip() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('flipHorizontal')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('flipHorizontal')}
           onClick={() =>
             this.cropper.scaleX(-this.cropper.getData().scaleX || -1)
           }
@@ -261,18 +252,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderZoomIn(): JSX.Element {
+  renderZoomIn() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('zoomIn')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('zoomIn')}
           onClick={() => this.cropper.zoom(0.1)}
         >
           <svg
@@ -291,18 +279,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderZoomOut(): JSX.Element {
+  renderZoomOut() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('zoomOut')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('zoomOut')}
           onClick={() => this.cropper.zoom(-0.1)}
         >
           <svg
@@ -320,18 +305,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderCropSquare(): JSX.Element {
+  renderCropSquare() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('aspectRatioSquare')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('aspectRatioSquare')}
           onClick={() => this.cropper.setAspectRatio(1)}
         >
           <svg
@@ -349,18 +331,15 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderCropWidescreen(): JSX.Element {
+  renderCropWidescreen() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('aspectRatioLandscape')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
           className="uppy-u-reset uppy-c-btn"
+          aria-label={i18n('aspectRatioLandscape')}
           onClick={() => this.cropper.setAspectRatio(16 / 9)}
         >
           <svg
@@ -378,17 +357,14 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  renderCropWidescreenVertical(): JSX.Element {
+  renderCropWidescreenVertical() {
     const { i18n } = this.props
 
     return (
-      <label
-        role="tooltip"
-        aria-label={i18n('aspectRatioPortrait')}
-        data-microtip-position="top"
-      >
+      <label role="tooltip" data-microtip-position="top">
         <button
           type="button"
+          aria-label={i18n('aspectRatioPortrait')}
           className="uppy-u-reset uppy-c-btn"
           onClick={() => this.cropper.setAspectRatio(9 / 16)}
         >
@@ -407,7 +383,7 @@ export default class Editor<M extends Meta, B extends Body> extends Component<
     )
   }
 
-  render(): JSX.Element {
+  render() {
     const { currentImage, opts } = this.props
     const { actions } = opts
     const imageURL = URL.createObjectURL(currentImage.data)

+ 1 - 1
packages/@uppy/image-editor/src/ImageEditor.tsx

@@ -218,7 +218,7 @@ export default class ImageEditor<
     this.unmount()
   }
 
-  render(): JSX.Element | null {
+  render() {
     const { currentImage } = this.getPluginState()
 
     if (currentImage === null || currentImage.isRemote) {

+ 1 - 1
packages/@uppy/instagram/src/Instagram.tsx

@@ -25,7 +25,7 @@ export default class Instagram<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 1 - 1
packages/@uppy/onedrive/src/OneDrive.tsx

@@ -25,7 +25,7 @@ export default class OneDrive<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 2 - 2
packages/@uppy/provider-views/src/Breadcrumbs.tsx

@@ -29,13 +29,13 @@ const Breadcrumb = (props: BreadcrumbProps) => {
 type BreadcrumbsProps<M extends Meta, B extends Body> = {
   getFolder: ProviderView<M, B>['getFolder']
   title: string
-  breadcrumbsIcon: JSX.Element
+  breadcrumbsIcon: h.JSX.Element
   breadcrumbs: UnknownProviderPluginState['breadcrumbs']
 }
 
 export default function Breadcrumbs<M extends Meta, B extends Body>(
   props: BreadcrumbsProps<M, B>,
-): JSX.Element {
+) {
   const { getFolder, title, breadcrumbsIcon, breadcrumbs } = props
 
   return (

+ 6 - 8
packages/@uppy/provider-views/src/Browser.tsx

@@ -31,9 +31,7 @@ type ListItemProps<M extends Meta, B extends Body> = {
   f: CompanionFile
 }
 
-function ListItem<M extends Meta, B extends Body>(
-  props: ListItemProps<M, B>,
-): JSX.Element {
+function ListItem<M extends Meta, B extends Body>(props: ListItemProps<M, B>) {
   const {
     currentSelection,
     uppyFiles,
@@ -96,7 +94,7 @@ type BrowserProps<M extends Meta, B extends Body> = {
   files: CompanionFile[]
   uppyFiles: UppyFile<M, B>[]
   viewType: string
-  headerComponent?: JSX.Element
+  headerComponent?: h.JSX.Element
   showBreadcrumbs: boolean
   isChecked: (file: any) => boolean
   toggleCheckbox: (event: Event, file: CompanionFile) => void
@@ -120,9 +118,7 @@ type BrowserProps<M extends Meta, B extends Body> = {
   loadAllFiles?: boolean
 }
 
-function Browser<M extends Meta, B extends Body>(
-  props: BrowserProps<M, B>,
-): JSX.Element {
+function Browser<M extends Meta, B extends Body>(props: BrowserProps<M, B>) {
   const {
     currentSelection,
     folders,
@@ -195,7 +191,9 @@ function Browser<M extends Meta, B extends Body>(
         if (isLoading) {
           return (
             <div className="uppy-Provider-loading">
-              <span>{i18n('loading')}</span>
+              <span>
+                {typeof isLoading === 'string' ? isLoading : i18n('loading')}
+              </span>
             </div>
           )
         }

+ 1 - 1
packages/@uppy/provider-views/src/FooterActions.tsx

@@ -11,7 +11,7 @@ export default function FooterActions({
   done: () => void
   i18n: I18n
   selected: number
-}): JSX.Element {
+}) {
   return (
     <div className="uppy-ProviderBrowser-footer">
       <button

+ 2 - 2
packages/@uppy/provider-views/src/Item/components/GridLi.tsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/require-default-props */
-import { h } from 'preact'
+import { h, type ComponentChildren } from 'preact'
 import classNames from 'classnames'
 import type { RestrictionError } from '@uppy/core/lib/Restricter'
 import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
@@ -15,7 +15,7 @@ type GridListItemProps<M extends Meta, B extends Body> = {
   toggleCheckbox: (event: Event) => void
   recordShiftKeyPress: (event: KeyboardEvent) => void
   id: string
-  children?: JSX.Element
+  children?: ComponentChildren
 }
 
 function GridListItem<M extends Meta, B extends Body>(

+ 1 - 1
packages/@uppy/provider-views/src/Loader.tsx

@@ -7,7 +7,7 @@ export default function Loader({
 }: {
   i18n: I18n
   loading: string | boolean
-}): JSX.Element {
+}) {
   return (
     <div className="uppy-Provider-loading">
       <span>{i18n('loading')}</span>

+ 2 - 2
packages/@uppy/provider-views/src/ProviderView/AuthView.tsx

@@ -9,7 +9,7 @@ import type ProviderViews from './ProviderView'
 type AuthViewProps<M extends Meta, B extends Body> = {
   loading: boolean | string
   pluginName: string
-  pluginIcon: () => JSX.Element
+  pluginIcon: () => h.JSX.Element
   i18n: Translator['translateArray']
   handleAuth: ProviderViews<M, B>['handleAuth']
   renderForm?: ProviderViewOptions<M, B>['renderAuthForm']
@@ -107,7 +107,7 @@ const defaultRenderForm = ({
 
 export default function AuthView<M extends Meta, B extends Body>(
   props: AuthViewProps<M, B>,
-): JSX.Element {
+) {
   const {
     loading,
     pluginName,

+ 2 - 2
packages/@uppy/provider-views/src/ProviderView/Header.tsx

@@ -11,7 +11,7 @@ type HeaderProps<M extends Meta, B extends Body> = {
   showBreadcrumbs: boolean
   getFolder: ProviderView<M, B>['getFolder']
   breadcrumbs: UnknownProviderPluginState['breadcrumbs']
-  pluginIcon: () => JSX.Element
+  pluginIcon: () => h.JSX.Element
   title: string
   logout: () => void
   username: string | undefined
@@ -20,7 +20,7 @@ type HeaderProps<M extends Meta, B extends Body> = {
 
 export default function Header<M extends Meta, B extends Body>(
   props: HeaderProps<M, B>,
-): JSX.Element {
+) {
   return (
     <Fragment>
       {props.showBreadcrumbs && (

+ 3 - 3
packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx

@@ -36,7 +36,7 @@ function prependPath(path: string | undefined, component: string): string {
   return `${path}/${component}`
 }
 
-export function defaultPickerIcon(): JSX.Element {
+export function defaultPickerIcon() {
   return (
     <svg
       aria-hidden="true"
@@ -67,7 +67,7 @@ export interface ProviderViewOptions<M extends Meta, B extends Body>
     i18n: Translator['translateArray']
     loading: boolean | string
     onAuth: (authFormData: unknown) => Promise<void>
-  }) => JSX.Element
+  }) => h.JSX.Element
 }
 
 type Opts<M extends Meta, B extends Body> = DefinePluginOpts<
@@ -547,7 +547,7 @@ export default class ProviderView<M extends Meta, B extends Body> extends View<
   render(
     state: unknown,
     viewOptions: Omit<ViewOptions<M, B, PluginType>, 'provider'> = {},
-  ): JSX.Element {
+  ) {
     const { authenticated, didFirstRender } = this.plugin.getPluginState()
     const { i18n } = this.plugin.uppy
 

+ 1 - 5
packages/@uppy/provider-views/src/ProviderView/User.tsx

@@ -6,11 +6,7 @@ type UserProps = {
   username: string | undefined
 }
 
-export default function User({
-  i18n,
-  logout,
-  username,
-}: UserProps): JSX.Element {
+export default function User({ i18n, logout, username }: UserProps) {
   return (
     <Fragment>
       <span className="uppy-ProviderBrowser-user" key="username">

+ 1 - 1
packages/@uppy/provider-views/src/SearchFilterInput.tsx

@@ -17,7 +17,7 @@ type Props = {
   buttonCSSClassName?: string
 }
 
-export default function SearchFilterInput(props: Props): JSX.Element {
+export default function SearchFilterInput(props: Props) {
   const {
     search,
     searchOnInput,

+ 1 - 1
packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx

@@ -155,7 +155,7 @@ export default class SearchProviderView<
   render(
     state: unknown,
     viewOptions: Omit<ViewOptions<M, B, PluginType>, 'provider'> = {},
-  ): JSX.Element {
+  ) {
     const { didFirstRender, isInputMode, searchTerm } =
       this.plugin.getPluginState()
     const { i18n } = this.plugin.uppy

+ 2 - 1
packages/@uppy/react/src/Dashboard.ts

@@ -67,7 +67,8 @@ class Dashboard<M extends Meta, B extends Body> extends Component<
     uppy.removePlugin(this.plugin)
   }
 
-  render(): JSX.Element {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  render() {
     return h('div', {
       className: 'uppy-Container',
       ref: (container: HTMLElement): void => {

+ 2 - 1
packages/@uppy/react/src/DashboardModal.ts

@@ -95,7 +95,8 @@ class DashboardModal<M extends Meta, B extends Body> extends Component<
     uppy.removePlugin(this.plugin)
   }
 
-  render(): JSX.Element {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  render() {
     return h('div', {
       className: 'uppy-Container',
       ref: (container: HTMLElement) => {

+ 2 - 1
packages/@uppy/react/src/DragDrop.ts

@@ -65,7 +65,8 @@ class DragDrop<M extends Meta, B extends Body> extends Component<
     uppy.removePlugin(this.plugin)
   }
 
-  render(): JSX.Element {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  render() {
     return h('div', {
       className: 'uppy-Container',
       ref: (container: HTMLElement) => {

+ 2 - 1
packages/@uppy/react/src/FileInput.ts

@@ -67,7 +67,8 @@ class FileInput<M extends Meta, B extends Body> extends Component<
     uppy.removePlugin(this.plugin)
   }
 
-  render(): JSX.Element {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  render() {
     return h('div', {
       className: 'uppy-Container',
       ref: (container: HTMLElement) => {

+ 2 - 1
packages/@uppy/react/src/ProgressBar.ts

@@ -61,7 +61,8 @@ class ProgressBar<M extends Meta, B extends Body> extends Component<
     uppy.removePlugin(this.plugin)
   }
 
-  render(): JSX.Element {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  render() {
     return h('div', {
       className: 'uppy-Container',
       ref: (container: HTMLElement) => {

+ 2 - 1
packages/@uppy/react/src/StatusBar.ts

@@ -76,7 +76,8 @@ class StatusBar<M extends Meta, B extends Body> extends Component<
     uppy.removePlugin(this.plugin)
   }
 
-  render(): JSX.Element {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  render() {
     return h('div', {
       className: 'uppy-Container',
       ref: (container: HTMLElement) => {

+ 14 - 18
packages/@uppy/status-bar/src/Components.tsx

@@ -24,7 +24,7 @@ interface UploadBtnProps<M extends Meta, B extends Body> {
 
 function UploadBtn<M extends Meta, B extends Body>(
   props: UploadBtnProps<M, B>,
-): JSX.Element {
+) {
   const {
     newFiles,
     isUploadStarted,
@@ -70,9 +70,7 @@ interface RetryBtnProps<M extends Meta, B extends Body> {
   uppy: Uppy<M, B>
 }
 
-function RetryBtn<M extends Meta, B extends Body>(
-  props: RetryBtnProps<M, B>,
-): JSX.Element {
+function RetryBtn<M extends Meta, B extends Body>(props: RetryBtnProps<M, B>) {
   const { i18n, uppy } = props
 
   return (
@@ -110,7 +108,7 @@ interface CancelBtnProps<M extends Meta, B extends Body> {
 
 function CancelBtn<M extends Meta, B extends Body>(
   props: CancelBtnProps<M, B>,
-): JSX.Element {
+) {
   const { i18n, uppy } = props
 
   return (
@@ -153,7 +151,7 @@ interface PauseResumeButtonProps<M extends Meta, B extends Body> {
 
 function PauseResumeButton<M extends Meta, B extends Body>(
   props: PauseResumeButtonProps<M, B>,
-): JSX.Element {
+) {
   const { isAllPaused, i18n, isAllComplete, resumableUploads, uppy } = props
   const title = isAllPaused ? i18n('resume') : i18n('pause')
 
@@ -212,7 +210,7 @@ interface DoneBtnProps {
   doneButtonHandler: (() => void) | undefined
 }
 
-function DoneBtn(props: DoneBtnProps): JSX.Element {
+function DoneBtn(props: DoneBtnProps) {
   const { i18n, doneButtonHandler } = props
 
   return (
@@ -227,7 +225,7 @@ function DoneBtn(props: DoneBtnProps): JSX.Element {
   )
 }
 
-function LoadingSpinner(): JSX.Element {
+function LoadingSpinner() {
   return (
     <svg
       className="uppy-StatusBar-spinner"
@@ -248,7 +246,7 @@ interface ProgressBarProcessingProps {
   progress: FileProcessingInfo
 }
 
-function ProgressBarProcessing(props: ProgressBarProcessingProps): JSX.Element {
+function ProgressBarProcessing(props: ProgressBarProcessingProps) {
   const { progress } = props
   const { value, mode, message } = progress
   const dot = `\u00B7`
@@ -271,7 +269,7 @@ interface ProgressDetailsProps {
   totalETA: number
 }
 
-function ProgressDetails(props: ProgressDetailsProps): JSX.Element {
+function ProgressDetails(props: ProgressDetailsProps) {
   const { numUploads, complete, totalUploadedSize, totalSize, totalETA, i18n } =
     props
 
@@ -312,7 +310,7 @@ interface FileUploadCountProps {
   numUploads: number
 }
 
-function FileUploadCount(props: FileUploadCountProps): JSX.Element {
+function FileUploadCount(props: FileUploadCountProps) {
   const { i18n, complete, numUploads } = props
 
   return (
@@ -328,7 +326,7 @@ interface UploadNewlyAddedFilesProps {
   startUpload: () => void
 }
 
-function UploadNewlyAddedFiles(props: UploadNewlyAddedFilesProps): JSX.Element {
+function UploadNewlyAddedFiles(props: UploadNewlyAddedFilesProps) {
   const { i18n, newFiles, startUpload } = props
   const uploadBtnClassNames = classNames(
     'uppy-u-reset',
@@ -371,9 +369,7 @@ interface ProgressBarUploadingProps {
   startUpload: () => void
 }
 
-function ProgressBarUploading(
-  props: ProgressBarUploadingProps,
-): JSX.Element | null {
+function ProgressBarUploading(props: ProgressBarUploadingProps) {
   const {
     i18n,
     supportsUploadProgress,
@@ -399,7 +395,7 @@ function ProgressBarUploading(
 
   const title = isAllPaused ? i18n('paused') : i18n('uploading')
 
-  function renderProgressDetails(): JSX.Element | null {
+  function renderProgressDetails() {
     if (!isAllPaused && !showUploadNewlyAddedFiles && showProgressDetails) {
       if (supportsUploadProgress) {
         return (
@@ -452,7 +448,7 @@ interface ProgressBarCompleteProps {
   i18n: I18n
 }
 
-function ProgressBarComplete(props: ProgressBarCompleteProps): JSX.Element {
+function ProgressBarComplete(props: ProgressBarCompleteProps) {
   const { i18n } = props
 
   return (
@@ -487,7 +483,7 @@ interface ProgressBarErrorProps {
   numUploads: number
 }
 
-function ProgressBarError(props: ProgressBarErrorProps): JSX.Element {
+function ProgressBarError(props: ProgressBarErrorProps) {
   const { error, i18n, complete, numUploads } = props
 
   function displayErrorAlert(): void {

+ 2 - 2
packages/@uppy/status-bar/src/StatusBarUI.tsx

@@ -62,7 +62,7 @@ export interface StatusBarUIProps<M extends Meta, B extends Body> {
 // TODO: rename the function to StatusBarUI on the next major.
 export default function StatusBar<M extends Meta, B extends Body>(
   props: StatusBarUIProps<M, B>,
-): JSX.Element {
+) {
   const {
     newFiles,
     allowNewUpload,
@@ -204,7 +204,7 @@ export default function StatusBar<M extends Meta, B extends Body>(
         aria-valuenow={progressValue!}
       />
 
-      {((): JSX.Element | null => {
+      {(() => {
         switch (uploadState) {
           case STATE_PREPROCESSING:
           case STATE_POSTPROCESSING:

+ 1 - 1
packages/@uppy/svelte/package.json

@@ -42,7 +42,7 @@
     "@uppy/drag-drop": "workspace:^",
     "@uppy/progress-bar": "workspace:^",
     "@uppy/status-bar": "workspace:^",
-    "svelte": "^4.0.0"
+    "svelte": "^4.0.0 || ^5.0.0"
   },
   "publishConfig": {
     "access": "public"

+ 1 - 1
packages/@uppy/unsplash/src/Unsplash.tsx

@@ -24,7 +24,7 @@ export default class Unsplash<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: SearchProvider<M, B>
 

+ 1 - 1
packages/@uppy/url/src/Url.tsx

@@ -81,7 +81,7 @@ export default class Url<M extends Meta, B extends Body> extends UIPlugin<
 
   static requestClientId = Url.name
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   hostname: string
 

+ 3 - 2
packages/@uppy/utils/src/ProgressTimeout.ts

@@ -15,10 +15,11 @@ class ProgressTimeout {
 
   constructor(
     timeout: number,
-    timeoutHandler: Parameters<typeof setTimeout>[0],
+    // eslint-disable-next-line no-shadow
+    timeoutHandler: (timeout: number) => void,
   ) {
     this.#timeout = timeout
-    this.#onTimedOut = timeoutHandler
+    this.#onTimedOut = () => timeoutHandler(timeout)
   }
 
   progress(): void {

+ 13 - 2
packages/@uppy/utils/src/dataURItoBlob.ts

@@ -1,8 +1,17 @@
 const DATA_URL_PATTERN = /^data:([^/]+\/[^,;]+(?:[^,]*?))(;base64)?,([\s\S]*)$/
 
-export default function dataURItoBlob(
+type dataURItoBlobOptions = { mimeType?: string; name?: string }
+
+function dataURItoBlob(dataURI: string, opts: dataURItoBlobOptions): Blob
+function dataURItoBlob(
+  dataURI: string,
+  opts: dataURItoBlobOptions,
+  toFile: true,
+): File
+
+function dataURItoBlob(
   dataURI: string,
-  opts: { mimeType?: string; name?: string },
+  opts: dataURItoBlobOptions,
   toFile?: boolean,
 ): Blob | File {
   // get the base64 data
@@ -30,3 +39,5 @@ export default function dataURItoBlob(
 
   return new Blob(data, { type: mimeType })
 }
+
+export default dataURItoBlob

+ 1 - 1
packages/@uppy/utils/src/dataURItoFile.ts

@@ -3,6 +3,6 @@ import dataURItoBlob from './dataURItoBlob.ts'
 export default function dataURItoFile(
   dataURI: string,
   opts: { mimeType?: string; name?: string },
-): File | Blob {
+): File {
   return dataURItoBlob(dataURI, opts, true)
 }

+ 1 - 1
packages/@uppy/utils/src/fetcher.ts

@@ -44,7 +44,7 @@ export type FetcherOptions = {
   ) => void | Promise<void>
 
   /** Called when no XMLHttpRequest upload progress events have been received for `timeout` ms. */
-  onTimeout?: () => void
+  onTimeout?: (timeout: number) => void
 
   /** Signal to abort the upload. */
   signal?: AbortSignal

+ 1 - 1
packages/@uppy/webcam/src/DiscardButton.tsx

@@ -6,7 +6,7 @@ interface DiscardButtonProps {
   i18n: I18n
 }
 
-function DiscardButton({ onDiscard, i18n }: DiscardButtonProps): JSX.Element {
+function DiscardButton({ onDiscard, i18n }: DiscardButtonProps) {
   return (
     <button
       className="uppy-u-reset uppy-c-btn uppy-Webcam-button uppy-Webcam-button--discard"

+ 1 - 1
packages/@uppy/webcam/src/PermissionsScreen.tsx

@@ -11,7 +11,7 @@ export default function PermissionsScreen({
   icon,
   i18n,
   hasCamera,
-}: PermissionScreenProps): JSX.Element {
+}: PermissionScreenProps) {
   return (
     <div className="uppy-Webcam-permissons">
       <div className="uppy-Webcam-permissonsIcon">{icon()}</div>

+ 1 - 1
packages/@uppy/webcam/src/RecordButton.tsx

@@ -13,7 +13,7 @@ export default function RecordButton({
   onStartRecording,
   onStopRecording,
   i18n,
-}: RecordButtonProps): JSX.Element {
+}: RecordButtonProps) {
   if (recording) {
     return (
       <button

+ 1 - 1
packages/@uppy/webcam/src/RecordingLength.tsx

@@ -10,7 +10,7 @@ interface RecordingLengthProps {
 export default function RecordingLength({
   recordingLengthSeconds,
   i18n,
-}: RecordingLengthProps): JSX.Element {
+}: RecordingLengthProps) {
   const formattedRecordingLengthSeconds = formatSeconds(recordingLengthSeconds)
 
   return (

+ 1 - 1
packages/@uppy/webcam/src/SnapshotButton.tsx

@@ -10,7 +10,7 @@ interface SnapshotButtonProps {
 export default function SnapshotButton({
   onSnapshot,
   i18n,
-}: SnapshotButtonProps): JSX.Element {
+}: SnapshotButtonProps) {
   return (
     <button
       className="uppy-u-reset uppy-c-btn uppy-Webcam-button uppy-Webcam-button--picture"

+ 1 - 1
packages/@uppy/webcam/src/SubmitButton.tsx

@@ -6,7 +6,7 @@ interface SubmitButtonProps {
   i18n: I18n
 }
 
-function SubmitButton({ onSubmit, i18n }: SubmitButtonProps): JSX.Element {
+function SubmitButton({ onSubmit, i18n }: SubmitButtonProps) {
   return (
     <button
       className="uppy-u-reset uppy-c-btn uppy-Webcam-button uppy-Webcam-button--submit"

+ 1 - 1
packages/@uppy/webcam/src/Webcam.tsx

@@ -125,7 +125,7 @@ export default class Webcam<M extends Meta, B extends Body> extends UIPlugin<
 
   private capturedMediaFile: MinimalRequiredUppyFile<M, B> | null
 
-  private icon: () => JSX.Element
+  private icon: () => h.JSX.Element
 
   private webcamActive
 

+ 1 - 2
packages/@uppy/xhr-upload/package.json

@@ -25,8 +25,7 @@
   },
   "dependencies": {
     "@uppy/companion-client": "workspace:^",
-    "@uppy/utils": "workspace:^",
-    "nanoid": "^4.0.0"
+    "@uppy/utils": "workspace:^"
   },
   "devDependencies": {
     "nock": "^13.1.0",

+ 133 - 255
packages/@uppy/xhr-upload/src/index.ts

@@ -1,9 +1,7 @@
 import BasePlugin from '@uppy/core/lib/BasePlugin.js'
 import type { DefinePluginOpts, PluginOpts } from '@uppy/core/lib/BasePlugin.js'
 import type { RequestClient } from '@uppy/companion-client'
-import { nanoid } from 'nanoid/non-secure'
 import EventManager from '@uppy/core/lib/EventManager.js'
-import ProgressTimeout from '@uppy/utils/lib/ProgressTimeout'
 import {
   RateLimitedQueue,
   internalRateLimitedQueue,
@@ -12,6 +10,7 @@ import {
 } from '@uppy/utils/lib/RateLimitedQueue'
 import NetworkError from '@uppy/utils/lib/NetworkError'
 import isNetworkError from '@uppy/utils/lib/isNetworkError'
+import { fetcher } from '@uppy/utils/lib/fetcher'
 import {
   filterNonFailedFiles,
   filterFilesToEmitUploadStarted,
@@ -156,6 +155,8 @@ export default class XHRUpload<
   // eslint-disable-next-line global-require
   static VERSION = packageJson.version
 
+  #getFetcher
+
   requests: RateLimitedQueue
 
   uploaderEvents: Record<string, EventManager<M, B> | null>
@@ -201,6 +202,79 @@ export default class XHRUpload<
     }
 
     this.uploaderEvents = Object.create(null)
+    /**
+     * xhr-upload wrapper for `fetcher` to handle user options
+     * `validateStatus`, `getResponseError`, `getResponseData`
+     * and to emit `upload-progress`, `upload-error`, and `upload-success` events.
+     */
+    this.#getFetcher = (files: UppyFile<M, B>[]) => {
+      return async (
+        url: Parameters<typeof fetcher>[0],
+        options: NonNullable<Parameters<typeof fetcher>[1]>,
+      ) => {
+        try {
+          const res = await fetcher(url, {
+            ...options,
+            onTimeout: (timeout) => {
+              const seconds = Math.ceil(timeout / 1000)
+              const error = new Error(this.i18n('uploadStalled', { seconds }))
+              this.uppy.emit('upload-stalled', error, files)
+            },
+            onUploadProgress: (event) => {
+              if (event.lengthComputable) {
+                for (const file of files) {
+                  this.uppy.emit('upload-progress', file, {
+                    // TODO: do not send `uploader` in next major
+                    // @ts-expect-error we can't type this and we should remove it
+                    uploader: this,
+                    bytesUploaded: (event.loaded / event.total) * file.size!,
+                    bytesTotal: file.size,
+                  })
+                }
+              }
+            },
+          })
+
+          if (!this.opts.validateStatus(res.status, res.responseText, res)) {
+            throw new NetworkError(res.statusText, res)
+          }
+
+          const body = this.opts.getResponseData(res.responseText, res)
+          const uploadURL = body[this.opts.responseUrlFieldName]
+          if (typeof uploadURL !== 'string') {
+            throw new Error(
+              `The received response did not include a valid URL for key ${this.opts.responseUrlFieldName}`,
+            )
+          }
+
+          for (const file of files) {
+            this.uppy.emit('upload-success', file, {
+              status: res.status,
+              body,
+              uploadURL,
+            })
+          }
+
+          return res
+        } catch (error) {
+          if (error.name === 'AbortError') {
+            return undefined
+          }
+          if (error instanceof NetworkError) {
+            const request = error.request!
+            const customError = buildResponseError(
+              request,
+              this.opts.getResponseError(request.responseText, request),
+            )
+            for (const file of files) {
+              this.uppy.emit('upload-error', file, customError)
+            }
+          }
+
+          throw error
+        }
+      }
+    }
   }
 
   getOptions(file: UppyFile<M, B>): OptsWithHeaders<M, B> {
@@ -292,268 +366,75 @@ export default class XHRUpload<
     return formPost
   }
 
-  async #uploadLocalFile(file: UppyFile<M, B>, current: number, total: number) {
-    const opts = this.getOptions(file)
-    const uploadStarted = Date.now()
-
-    this.uppy.log(`uploading ${current} of ${total}`)
-    return new Promise((resolve, reject) => {
-      const data =
+  async #uploadLocalFile(file: UppyFile<M, B>) {
+    const events = new EventManager(this.uppy)
+    const controller = new AbortController()
+    const uppyFetch = this.requests.wrapPromiseFunction(async () => {
+      const opts = this.getOptions(file)
+      const fetch = this.#getFetcher([file])
+      const body =
         opts.formData ? this.createFormDataUpload(file, opts) : file.data
-
-      const xhr = new XMLHttpRequest()
-      const eventManager = new EventManager(this.uppy)
-      this.uploaderEvents[file.id] = eventManager
-      let queuedRequest: { abort: () => void; done: () => void }
-
-      const timer = new ProgressTimeout(opts.timeout, () => {
-        const error = new Error(
-          this.i18n('uploadStalled', {
-            seconds: Math.ceil(opts.timeout / 1000),
-          }),
-        )
-        this.uppy.emit('upload-stalled', error, [file])
-      })
-
-      const id = nanoid()
-
-      xhr.upload.addEventListener('loadstart', () => {
-        this.uppy.log(`[XHRUpload] ${id} started`)
-      })
-
-      xhr.upload.addEventListener('progress', (ev) => {
-        this.uppy.log(`[XHRUpload] ${id} progress: ${ev.loaded} / ${ev.total}`)
-        // Begin checking for timeouts when progress starts, instead of loading,
-        // to avoid timing out requests on browser concurrency queue
-        timer.progress()
-
-        if (ev.lengthComputable) {
-          this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
-            // TODO: do not send `uploader` in next major
-            // @ts-expect-error we can't type this and we should remove it
-            uploader: this,
-            uploadStarted,
-            bytesUploaded: ev.loaded,
-            bytesTotal: ev.total,
-          })
-        }
-      })
-
-      xhr.addEventListener('load', () => {
-        this.uppy.log(`[XHRUpload] ${id} finished`)
-        timer.done()
-        queuedRequest.done()
-        if (this.uploaderEvents[file.id]) {
-          this.uploaderEvents[file.id]!.remove()
-          this.uploaderEvents[file.id] = null
-        }
-
-        if (opts.validateStatus(xhr.status, xhr.responseText, xhr)) {
-          const body = opts.getResponseData(xhr.responseText, xhr)
-          const uploadURL = body?.[opts.responseUrlFieldName] as
-            | string
-            | undefined
-
-          const uploadResp = {
-            status: xhr.status,
-            body,
-            uploadURL,
-          }
-
-          this.uppy.emit(
-            'upload-success',
-            this.uppy.getFile(file.id),
-            uploadResp,
-          )
-
-          if (uploadURL) {
-            this.uppy.log(`Download ${file.name} from ${uploadURL}`)
-          }
-
-          return resolve(file)
-        }
-        const body = opts.getResponseData(xhr.responseText, xhr)
-        const error = buildResponseError(
-          xhr,
-          opts.getResponseError(xhr.responseText, xhr),
-        )
-
-        const response = {
-          status: xhr.status,
-          body,
-        }
-
-        this.uppy.emit('upload-error', file, error, response)
-        return reject(error)
-      })
-
-      xhr.addEventListener('error', () => {
-        this.uppy.log(`[XHRUpload] ${id} errored`)
-        timer.done()
-        queuedRequest.done()
-        if (this.uploaderEvents[file.id]) {
-          this.uploaderEvents[file.id]!.remove()
-          this.uploaderEvents[file.id] = null
-        }
-
-        const error = buildResponseError(
-          xhr,
-          opts.getResponseError(xhr.responseText, xhr),
-        )
-        this.uppy.emit('upload-error', file, error)
-        return reject(error)
+      return fetch(opts.endpoint, {
+        ...opts,
+        body,
+        signal: controller.signal,
       })
+    })
 
-      xhr.open(opts.method.toUpperCase(), opts.endpoint, true)
-      // IE10 does not allow setting `withCredentials` and `responseType`
-      // before `open()` is called.
-      xhr.withCredentials = opts.withCredentials
-      if (opts.responseType !== '') {
-        xhr.responseType = opts.responseType
+    events.onFileRemove(file.id, () => controller.abort())
+    events.onCancelAll(file.id, ({ reason }) => {
+      if (reason === 'user') {
+        controller.abort()
       }
-
-      queuedRequest = this.requests.run(() => {
-        // When using an authentication system like JWT, the bearer token goes as a header. This
-        // header needs to be fresh each time the token is refreshed so computing and setting the
-        // headers just before the upload starts enables this kind of authentication to work properly.
-        // Otherwise, half-way through the list of uploads the token could be stale and the upload would fail.
-        const currentOpts = this.getOptions(file)
-
-        Object.keys(currentOpts.headers).forEach((header) => {
-          xhr.setRequestHeader(header, currentOpts.headers[header])
-        })
-
-        xhr.send(data)
-
-        return () => {
-          timer.done()
-          xhr.abort()
-        }
-      })
-
-      eventManager.onFileRemove(file.id, () => {
-        queuedRequest.abort()
-        reject(new Error('File removed'))
-      })
-
-      eventManager.onCancelAll(file.id, ({ reason }) => {
-        if (reason === 'user') {
-          queuedRequest.abort()
-        }
-        reject(new Error('Upload cancelled'))
-      })
     })
-  }
-
-  #uploadBundle(files: UppyFile<M, B>[]): Promise<void> {
-    return new Promise((resolve, reject) => {
-      const { endpoint } = this.opts
-      const { method } = this.opts
-      const uploadStarted = Date.now()
-
-      const optsFromState = this.uppy.getState().xhrUpload
-      const formData = this.createBundledUpload(files, {
-        ...this.opts,
-        ...(optsFromState || {}),
-      })
-
-      const xhr = new XMLHttpRequest()
 
-      const emitError = (error: Error) => {
-        files.forEach((file) => {
-          this.uppy.emit('upload-error', file, error)
-        })
+    try {
+      await uppyFetch().abortOn(controller.signal)
+    } catch (error) {
+      // TODO: create formal error with name 'AbortError' (this comes from RateLimitedQueue)
+      if (error.message !== 'Cancelled') {
+        throw error
       }
+    } finally {
+      events.remove()
+    }
+  }
 
-      const timer = new ProgressTimeout(this.opts.timeout, () => {
-        const error = new Error(
-          this.i18n('uploadStalled', {
-            seconds: Math.ceil(this.opts.timeout / 1000),
-          }),
-        )
-        this.uppy.emit('upload-stalled', error, files)
-      })
-
-      xhr.upload.addEventListener('loadstart', () => {
-        this.uppy.log('[XHRUpload] started uploading bundle')
-        timer.progress()
-      })
-
-      xhr.upload.addEventListener('progress', (ev) => {
-        timer.progress()
-
-        if (!ev.lengthComputable) return
-
-        files.forEach((file) => {
-          this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
-            // TODO: do not send `uploader` in next major
-            // @ts-expect-error we can't type this and we should remove it
-            uploader: this,
-            uploadStarted,
-            bytesUploaded: (ev.loaded / ev.total) * (file.size as number),
-            bytesTotal: file.size as number,
-          })
-        })
+  async #uploadBundle(files: UppyFile<M, B>[]) {
+    const controller = new AbortController()
+    const uppyFetch = this.requests.wrapPromiseFunction(async () => {
+      const optsFromState = this.uppy.getState().xhrUpload ?? {}
+      const fetch = this.#getFetcher(files)
+      const body = this.createBundledUpload(files, {
+        ...this.opts,
+        ...optsFromState,
       })
-
-      xhr.addEventListener('load', () => {
-        timer.done()
-
-        if (this.opts.validateStatus(xhr.status, xhr.responseText, xhr)) {
-          const body = this.opts.getResponseData(xhr.responseText, xhr)
-          const uploadResp = {
-            status: xhr.status,
-            body,
-          }
-          files.forEach((file) => {
-            this.uppy.emit(
-              'upload-success',
-              this.uppy.getFile(file.id),
-              uploadResp,
-            )
-          })
-          return resolve()
-        }
-
-        const error =
-          this.opts.getResponseError(xhr.responseText, xhr) ||
-          new NetworkError('Upload error', xhr)
-        emitError(error)
-        return reject(error)
+      return fetch(this.opts.endpoint, {
+        // headers can't be a function with bundle: true
+        ...(this.opts as OptsWithHeaders<M, B>),
+        body,
+        signal: controller.signal,
       })
+    })
 
-      xhr.addEventListener('error', () => {
-        timer.done()
-
-        const error =
-          this.opts.getResponseError(xhr.responseText, xhr) ||
-          new Error('Upload error')
-        emitError(error)
-        return reject(error)
-      })
+    function abort() {
+      controller.abort()
+    }
 
-      this.uppy.on('cancel-all', ({ reason } = {}) => {
-        if (reason !== 'user') return
-        timer.done()
-        xhr.abort()
-      })
+    // We only need to abort on cancel all because
+    // individual cancellations are not possible with bundle: true
+    this.uppy.once('cancel-all', abort)
 
-      xhr.open(method.toUpperCase(), endpoint, true)
-      // IE10 does not allow setting `withCredentials` and `responseType`
-      // before `open()` is called.
-      xhr.withCredentials = this.opts.withCredentials
-      if (this.opts.responseType !== '') {
-        xhr.responseType = this.opts.responseType
+    try {
+      await uppyFetch().abortOn(controller.signal)
+    } catch (error) {
+      // TODO: create formal error with name 'AbortError' (this comes from RateLimitedQueue)
+      if (error.message !== 'Cancelled') {
+        throw error
       }
-
-      // In bundle mode headers can not be a function
-      const headers = this.opts.headers as Record<string, string>
-      Object.keys(headers).forEach((header) => {
-        xhr.setRequestHeader(header, headers[header] as string)
-      })
-
-      xhr.send(formData)
-    })
+    } finally {
+      this.uppy.off('cancel-all', abort)
+    }
   }
 
   #getCompanionClientArgs(file: UppyFile<M, B>) {
@@ -579,10 +460,7 @@ export default class XHRUpload<
 
   async #uploadFiles(files: UppyFile<M, B>[]) {
     await Promise.allSettled(
-      files.map((file, i) => {
-        const current = i + 1
-        const total = files.length
-
+      files.map((file) => {
         if (file.isRemote) {
           const getQueue = () => this.requests
           const controller = new AbortController()
@@ -609,7 +487,7 @@ export default class XHRUpload<
           return uploadPromise
         }
 
-        return this.#uploadLocalFile(file, current, total)
+        return this.#uploadLocalFile(file)
       }),
     )
   }

+ 1 - 1
packages/@uppy/zoom/src/Zoom.tsx

@@ -25,7 +25,7 @@ export default class Zoom<M extends Meta, B extends Body> extends UIPlugin<
 > {
   static VERSION = packageJson.version
 
-  icon: () => JSX.Element
+  icon: () => h.JSX.Element
 
   provider: Provider<M, B>
 

+ 4 - 5
yarn.lock

@@ -9915,7 +9915,7 @@ __metadata:
     "@uppy/drag-drop": "workspace:^"
     "@uppy/progress-bar": "workspace:^"
     "@uppy/status-bar": "workspace:^"
-    svelte: ^4.0.0
+    svelte: ^4.0.0 || ^5.0.0
   languageName: unknown
   linkType: soft
 
@@ -10045,7 +10045,6 @@ __metadata:
   dependencies:
     "@uppy/companion-client": "workspace:^"
     "@uppy/utils": "workspace:^"
-    nanoid: ^4.0.0
     nock: ^13.1.0
     vitest: ^1.2.1
   peerDependencies:
@@ -25999,11 +25998,11 @@ __metadata:
   linkType: hard
 
 "prettier@npm:^3.0.3":
-  version: 3.2.4
-  resolution: "prettier@npm:3.2.4"
+  version: 3.2.5
+  resolution: "prettier@npm:3.2.5"
   bin:
     prettier: bin/prettier.cjs
-  checksum: 6ec9385a836e0b9bac549e585101c086d1521c31d7b882d5c8bb7d7646da0693da5f31f4fff6dc080710e5e2d34c85e6fb2f8766876b3645c8be2f33b9c3d1a3
+  checksum: 2ee4e1417572372afb7a13bb446b34f20f1bf1747db77cf6ccaf57a9be005f2f15c40f903d41a6b79eec3f57fff14d32a20fb6dee1f126da48908926fe43c311
   languageName: node
   linkType: hard