Browse Source

@uppy/dashboard: refactor to TypeScript (#4984)

Co-authored-by: Murderlon <merlijn@soverin.net>
Antoine du Hamel 1 year ago
parent
commit
5fcdd8f275
39 changed files with 1918 additions and 1095 deletions
  1. 1 0
      packages/@uppy/dashboard/.npmignore
  2. 376 183
      packages/@uppy/dashboard/src/Dashboard.tsx
  3. 0 325
      packages/@uppy/dashboard/src/components/AddFiles.jsx
  4. 450 0
      packages/@uppy/dashboard/src/components/AddFiles.tsx
  5. 10 3
      packages/@uppy/dashboard/src/components/AddFilesPanel.tsx
  6. 91 57
      packages/@uppy/dashboard/src/components/Dashboard.tsx
  7. 15 4
      packages/@uppy/dashboard/src/components/EditorPanel.tsx
  8. 0 46
      packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx
  9. 54 0
      packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.tsx
  10. 36 21
      packages/@uppy/dashboard/src/components/FileCard/index.tsx
  11. 58 27
      packages/@uppy/dashboard/src/components/FileItem/Buttons/index.tsx
  12. 31 26
      packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.tsx
  13. 0 39
      packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.jsx
  14. 40 0
      packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.tsx
  15. 49 22
      packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.tsx
  16. 12 9
      packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.tsx
  17. 29 16
      packages/@uppy/dashboard/src/components/FileItem/index.tsx
  18. 50 24
      packages/@uppy/dashboard/src/components/FileList.tsx
  19. 15 4
      packages/@uppy/dashboard/src/components/FilePreview.tsx
  20. 16 3
      packages/@uppy/dashboard/src/components/PickerPanelContent.tsx
  21. 57 21
      packages/@uppy/dashboard/src/components/PickerPanelTopBar.tsx
  22. 0 86
      packages/@uppy/dashboard/src/components/Slide.jsx
  23. 96 0
      packages/@uppy/dashboard/src/components/Slide.tsx
  24. 0 1
      packages/@uppy/dashboard/src/index.js
  25. 31 16
      packages/@uppy/dashboard/src/index.test.ts
  26. 1 0
      packages/@uppy/dashboard/src/index.ts
  27. 0 0
      packages/@uppy/dashboard/src/locale.ts
  28. 1 1
      packages/@uppy/dashboard/src/utils/copyToClipboard.test.ts
  29. 10 4
      packages/@uppy/dashboard/src/utils/copyToClipboard.ts
  30. 1 1
      packages/@uppy/dashboard/src/utils/createSuperFocus.test.ts
  31. 10 4
      packages/@uppy/dashboard/src/utils/createSuperFocus.ts
  32. 0 11
      packages/@uppy/dashboard/src/utils/getActiveOverlayEl.js
  33. 18 0
      packages/@uppy/dashboard/src/utils/getActiveOverlayEl.ts
  34. 0 127
      packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx
  35. 212 0
      packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx
  36. 4 3
      packages/@uppy/dashboard/src/utils/ignoreEvent.ts
  37. 28 11
      packages/@uppy/dashboard/src/utils/trapFocus.ts
  38. 60 0
      packages/@uppy/dashboard/tsconfig.build.json
  39. 56 0
      packages/@uppy/dashboard/tsconfig.json

+ 1 - 0
packages/@uppy/dashboard/.npmignore

@@ -0,0 +1 @@
+tsconfig.*

File diff suppressed because it is too large
+ 376 - 183
packages/@uppy/dashboard/src/Dashboard.tsx


+ 0 - 325
packages/@uppy/dashboard/src/components/AddFiles.jsx

@@ -1,325 +0,0 @@
-import { h, Component, Fragment } from 'preact'
-
-class AddFiles extends Component {
-  triggerFileInputClick = () => {
-    this.fileInput.click()
-  }
-
-  triggerFolderInputClick = () => {
-    this.folderInput.click()
-  }
-
-  triggerVideoCameraInputClick = () => {
-    this.mobileVideoFileInput.click()
-  }
-
-  triggerPhotoCameraInputClick = () => {
-    this.mobilePhotoFileInput.click()
-  }
-
-  onFileInputChange = (event) => {
-    this.props.handleInputChange(event)
-
-    // We clear the input after a file is selected, because otherwise
-    // change event is not fired in Chrome and Safari when a file
-    // with the same name is selected.
-    // ___Why not use value="" on <input/> instead?
-    //    Because if we use that method of clearing the input,
-    //    Chrome will not trigger change if we drop the same file twice (Issue #768).
-    event.target.value = null // eslint-disable-line no-param-reassign
-  }
-
-  renderHiddenInput = (isFolder, refCallback) => {
-    return (
-      <input
-        className="uppy-Dashboard-input"
-        hidden
-        aria-hidden="true"
-        tabIndex={-1}
-        webkitdirectory={isFolder}
-        type="file"
-        name="files[]"
-        multiple={this.props.maxNumberOfFiles !== 1}
-        onChange={this.onFileInputChange}
-        accept={this.props.allowedFileTypes}
-        ref={refCallback}
-      />
-    )
-  }
-
-  renderHiddenCameraInput = (type, nativeCameraFacingMode, refCallback) => {
-    const typeToAccept = { photo: 'image/*', video: 'video/*' }
-    const accept = typeToAccept[type]
-
-    return (
-      <input
-        className="uppy-Dashboard-input"
-        hidden
-        aria-hidden="true"
-        tabIndex={-1}
-        type="file"
-        name={`camera-${type}`}
-        onChange={this.onFileInputChange}
-        capture={nativeCameraFacingMode}
-        accept={accept}
-        ref={refCallback}
-      />
-    )
-  }
-
-  renderMyDeviceAcquirer = () => {
-    return (
-      <div
-        className="uppy-DashboardTab"
-        role="presentation"
-        data-uppy-acquirer-id="MyDevice"
-      >
-        <button
-          type="button"
-          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
-          role="tab"
-          tabIndex={0}
-          data-uppy-super-focusable
-          onClick={this.triggerFileInputClick}
-        >
-          <div className="uppy-DashboardTab-inner">
-            <svg className="uppy-DashboardTab-iconMyDevice" aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
-              <path d="M8.45 22.087l-1.305-6.674h17.678l-1.572 6.674H8.45zm4.975-12.412l1.083 1.765a.823.823 0 00.715.386h7.951V13.5H8.587V9.675h4.838zM26.043 13.5h-1.195v-2.598c0-.463-.336-.75-.798-.75h-8.356l-1.082-1.766A.823.823 0 0013.897 8H7.728c-.462 0-.815.256-.815.718V13.5h-.956a.97.97 0 00-.746.37.972.972 0 00-.19.81l1.724 8.565c.095.44.484.755.933.755H24c.44 0 .824-.3.929-.727l2.043-8.568a.972.972 0 00-.176-.825.967.967 0 00-.753-.38z" fill="currentcolor" fill-rule="evenodd" />
-            </svg>
-          </div>
-          <div className="uppy-DashboardTab-name">{this.props.i18n('myDevice')}</div>
-        </button>
-      </div>
-    )
-  }
-
-  renderPhotoCamera = () => {
-    return (
-      <div
-        className="uppy-DashboardTab"
-        role="presentation"
-        data-uppy-acquirer-id="MobilePhotoCamera"
-      >
-        <button
-          type="button"
-          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
-          role="tab"
-          tabIndex={0}
-          data-uppy-super-focusable
-          onClick={this.triggerPhotoCameraInputClick}
-        >
-          <div className="uppy-DashboardTab-inner">
-            <svg aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
-              <path d="M23.5 9.5c1.417 0 2.5 1.083 2.5 2.5v9.167c0 1.416-1.083 2.5-2.5 2.5h-15c-1.417 0-2.5-1.084-2.5-2.5V12c0-1.417 1.083-2.5 2.5-2.5h2.917l1.416-2.167C13 7.167 13.25 7 13.5 7h5c.25 0 .5.167.667.333L20.583 9.5H23.5zM16 11.417a4.706 4.706 0 00-4.75 4.75 4.704 4.704 0 004.75 4.75 4.703 4.703 0 004.75-4.75c0-2.663-2.09-4.75-4.75-4.75zm0 7.825c-1.744 0-3.076-1.332-3.076-3.074 0-1.745 1.333-3.077 3.076-3.077 1.744 0 3.074 1.333 3.074 3.076s-1.33 3.075-3.074 3.075z" fill="#02B383" fill-rule="nonzero" />
-            </svg>
-          </div>
-          <div className="uppy-DashboardTab-name">{this.props.i18n('takePictureBtn')}</div>
-        </button>
-      </div>
-    )
-  }
-
-  renderVideoCamera = () => {
-    return (
-      <div
-        className="uppy-DashboardTab"
-        role="presentation"
-        data-uppy-acquirer-id="MobileVideoCamera"
-      >
-        <button
-          type="button"
-          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
-          role="tab"
-          tabIndex={0}
-          data-uppy-super-focusable
-          onClick={this.triggerVideoCameraInputClick}
-        >
-          <div className="uppy-DashboardTab-inner">
-            <svg aria-hidden="true" width="32" height="32" viewBox="0 0 32 32">
-              <path fill="#FF675E" fillRule="nonzero" d="m21.254 14.277 2.941-2.588c.797-.313 1.243.818 1.09 1.554-.01 2.094.02 4.189-.017 6.282-.126.915-1.145 1.08-1.58.34l-2.434-2.142c-.192.287-.504 1.305-.738.468-.104-1.293-.028-2.596-.05-3.894.047-.312.381.823.426 1.069.063-.384.206-.744.362-1.09zm-12.939-3.73c3.858.013 7.717-.025 11.574.02.912.129 1.492 1.237 1.351 2.217-.019 2.412.04 4.83-.03 7.239-.17 1.025-1.166 1.59-2.029 1.429-3.705-.012-7.41.025-11.114-.019-.913-.129-1.492-1.237-1.352-2.217.018-2.404-.036-4.813.029-7.214.136-.82.83-1.473 1.571-1.454z " />
-            </svg>
-          </div>
-          <div className="uppy-DashboardTab-name">{this.props.i18n('recordVideoBtn')}</div>
-        </button>
-      </div>
-    )
-  }
-
-  renderBrowseButton = (text, onClickFn) => {
-    const numberOfAcquirers = this.props.acquirers.length
-    return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn uppy-Dashboard-browse"
-        onClick={onClickFn}
-        data-uppy-super-focusable={numberOfAcquirers === 0}
-      >
-        {text}
-      </button>
-    )
-  }
-
-  renderDropPasteBrowseTagline = (numberOfAcquirers) => {
-    const browseFiles = this.renderBrowseButton(this.props.i18n('browseFiles'), this.triggerFileInputClick)
-    const browseFolders = this.renderBrowseButton(this.props.i18n('browseFolders'), this.triggerFolderInputClick)
-
-    // in order to keep the i18n CamelCase and options lower (as are defaults) we will want to transform a lower
-    // to Camel
-    const lowerFMSelectionType = this.props.fileManagerSelectionType
-    const camelFMSelectionType = lowerFMSelectionType.charAt(0).toUpperCase() + lowerFMSelectionType.slice(1)
-
-    return (
-      <div class="uppy-Dashboard-AddFiles-title">
-        {
-          // eslint-disable-next-line no-nested-ternary
-          this.props.disableLocalFiles ? this.props.i18n('importFiles')
-            : numberOfAcquirers > 0
-              ? this.props.i18nArray(`dropPasteImport${camelFMSelectionType}`, { browseFiles, browseFolders, browse: browseFiles })
-              : this.props.i18nArray(`dropPaste${camelFMSelectionType}`, { browseFiles, browseFolders, browse: browseFiles })
-        }
-      </div>
-    )
-  }
-
-  [Symbol.for('uppy test: disable unused locale key warning')] () {
-    // Those are actually used in `renderDropPasteBrowseTagline` method.
-    this.props.i18nArray('dropPasteBoth')
-    this.props.i18nArray('dropPasteFiles')
-    this.props.i18nArray('dropPasteFolders')
-    this.props.i18nArray('dropPasteImportBoth')
-    this.props.i18nArray('dropPasteImportFiles')
-    this.props.i18nArray('dropPasteImportFolders')
-  }
-
-  renderAcquirer = (acquirer) => {
-    return (
-      <div
-        className="uppy-DashboardTab"
-        role="presentation"
-        data-uppy-acquirer-id={acquirer.id}
-      >
-        <button
-          type="button"
-          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
-          role="tab"
-          tabIndex={0}
-          data-cy={acquirer.id}
-          aria-controls={`uppy-DashboardContent-panel--${acquirer.id}`}
-          aria-selected={this.props.activePickerPanel.id === acquirer.id}
-          data-uppy-super-focusable
-          onClick={() => this.props.showPanel(acquirer.id)}
-        >
-          <div className="uppy-DashboardTab-inner">
-            {acquirer.icon()}
-          </div>
-          <div className="uppy-DashboardTab-name">{acquirer.name}</div>
-        </button>
-      </div>
-    )
-  }
-
-  renderAcquirers = (acquirers) => {
-    // Group last two buttons, so we don’t end up with
-    // just one button on a new line
-    const acquirersWithoutLastTwo = [...acquirers]
-    const lastTwoAcquirers = acquirersWithoutLastTwo.splice(acquirers.length - 2, acquirers.length)
-
-    return (
-      <>
-        {acquirersWithoutLastTwo.map((acquirer) => this.renderAcquirer(acquirer))}
-        <span role="presentation" style={{ 'white-space': 'nowrap' }}>
-          {lastTwoAcquirers.map((acquirer) => this.renderAcquirer(acquirer))}
-        </span>
-      </>
-    )
-  }
-
-  renderSourcesList = (acquirers, disableLocalFiles) => {
-    const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props
-
-    let list = []
-
-    const myDeviceKey = 'myDevice'
-
-    if (!disableLocalFiles) list.push({ key: myDeviceKey, elements: this.renderMyDeviceAcquirer() })
-    if (showNativePhotoCameraButton) list.push({ key: 'nativePhotoCameraButton', elements: this.renderPhotoCamera() })
-    if (showNativeVideoCameraButton) list.push({ key: 'nativePhotoCameraButton', elements: this.renderVideoCamera() })
-    list.push(...acquirers.map((acquirer) => ({ key: acquirer.id, elements: this.renderAcquirer(acquirer) })))
-
-    // doesn't make sense to show only a lonely "My Device"
-    const hasOnlyMyDevice = list.length === 1 && list[0].key === myDeviceKey
-    if (hasOnlyMyDevice) list = []
-
-    // Group last two buttons, so we don’t end up with
-    // just one button on a new line
-    const listWithoutLastTwo = [...list]
-    const lastTwo = listWithoutLastTwo.splice(list.length - 2, list.length)
-
-    const renderList = (l) => l.map(({ key, elements }) => <Fragment key={key}>{elements}</Fragment>)
-
-    return (
-      <>
-        {this.renderDropPasteBrowseTagline(list.length)}
-
-        <div className="uppy-Dashboard-AddFiles-list" role="tablist">
-          {renderList(listWithoutLastTwo)}
-
-          <span role="presentation" style={{ 'white-space': 'nowrap' }}>
-            {renderList(lastTwo)}
-          </span>
-        </div>
-      </>
-    )
-  }
-
-  renderPoweredByUppy () {
-    const { i18nArray } = this.props
-
-    const uppyBranding = (
-      <span>
-        <svg aria-hidden="true" focusable="false" className="uppy-c-icon uppy-Dashboard-poweredByIcon" width="11" height="11" viewBox="0 0 11 11">
-          <path d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z" fillRule="evenodd" />
-        </svg>
-        <span className="uppy-Dashboard-poweredByUppy">Uppy</span>
-      </span>
-    )
-
-    const linkText = i18nArray('poweredBy', { uppy: uppyBranding })
-
-    return (
-      <a
-        tabIndex="-1"
-        href="https://uppy.io"
-        rel="noreferrer noopener"
-        target="_blank"
-        className="uppy-Dashboard-poweredBy"
-      >
-        {linkText}
-      </a>
-    )
-  }
-
-  render () {
-    const {
-      showNativePhotoCameraButton,
-      showNativeVideoCameraButton,
-      nativeCameraFacingMode,
-    } = this.props
-
-    return (
-      <div className="uppy-Dashboard-AddFiles">
-        {this.renderHiddenInput(false, (ref) => { this.fileInput = ref })}
-        {this.renderHiddenInput(true, (ref) => { this.folderInput = ref })}
-        {showNativePhotoCameraButton && this.renderHiddenCameraInput('photo', nativeCameraFacingMode, (ref) => { this.mobilePhotoFileInput = ref })}
-        {showNativeVideoCameraButton && this.renderHiddenCameraInput('video', nativeCameraFacingMode, (ref) => { this.mobileVideoFileInput = ref })}
-        {this.renderSourcesList(this.props.acquirers, this.props.disableLocalFiles)}
-        <div className="uppy-Dashboard-AddFiles-info">
-          {this.props.note && <div className="uppy-Dashboard-note">{this.props.note}</div>}
-          {this.props.proudlyDisplayPoweredByUppy && this.renderPoweredByUppy(this.props)}
-        </div>
-      </div>
-    )
-  }
-}
-
-export default AddFiles

+ 450 - 0
packages/@uppy/dashboard/src/components/AddFiles.tsx

@@ -0,0 +1,450 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck Typing this file requires more work, skipping it to unblock the rest of the transition.
+
+/* eslint-disable react/destructuring-assignment */
+import { h, Component, Fragment, type ComponentChild } from 'preact'
+
+type $TSFixMe = any
+
+class AddFiles extends Component {
+  fileInput: $TSFixMe
+
+  folderInput: $TSFixMe
+
+  mobilePhotoFileInput: $TSFixMe
+
+  mobileVideoFileInput: $TSFixMe
+
+  private triggerFileInputClick = () => {
+    this.fileInput.click()
+  }
+
+  private triggerFolderInputClick = () => {
+    this.folderInput.click()
+  }
+
+  private triggerVideoCameraInputClick = () => {
+    this.mobileVideoFileInput.click()
+  }
+
+  private triggerPhotoCameraInputClick = () => {
+    this.mobilePhotoFileInput.click()
+  }
+
+  private onFileInputChange = (event: $TSFixMe) => {
+    this.props.handleInputChange(event)
+
+    // We clear the input after a file is selected, because otherwise
+    // change event is not fired in Chrome and Safari when a file
+    // with the same name is selected.
+    // ___Why not use value="" on <input/> instead?
+    //    Because if we use that method of clearing the input,
+    //    Chrome will not trigger change if we drop the same file twice (Issue #768).
+    event.target.value = null // eslint-disable-line no-param-reassign
+  }
+
+  private renderHiddenInput = (isFolder: $TSFixMe, refCallback: $TSFixMe) => {
+    return (
+      <input
+        className="uppy-Dashboard-input"
+        hidden
+        aria-hidden="true"
+        tabIndex={-1}
+        webkitdirectory={isFolder}
+        type="file"
+        name="files[]"
+        multiple={this.props.maxNumberOfFiles !== 1}
+        onChange={this.onFileInputChange}
+        accept={this.props.allowedFileTypes}
+        ref={refCallback}
+      />
+    )
+  }
+
+  private renderHiddenCameraInput = (
+    type: $TSFixMe,
+    nativeCameraFacingMode: $TSFixMe,
+    refCallback: $TSFixMe,
+  ) => {
+    const typeToAccept = { photo: 'image/*', video: 'video/*' }
+    const accept = typeToAccept[type]
+
+    return (
+      <input
+        className="uppy-Dashboard-input"
+        hidden
+        aria-hidden="true"
+        tabIndex={-1}
+        type="file"
+        name={`camera-${type}`}
+        onChange={this.onFileInputChange}
+        capture={nativeCameraFacingMode}
+        accept={accept}
+        ref={refCallback}
+      />
+    )
+  }
+
+  private renderMyDeviceAcquirer = () => {
+    return (
+      <div
+        className="uppy-DashboardTab"
+        role="presentation"
+        data-uppy-acquirer-id="MyDevice"
+      >
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
+          role="tab"
+          tabIndex={0}
+          data-uppy-super-focusable
+          onClick={this.triggerFileInputClick}
+        >
+          <div className="uppy-DashboardTab-inner">
+            <svg
+              className="uppy-DashboardTab-iconMyDevice"
+              aria-hidden="true"
+              focusable="false"
+              width="32"
+              height="32"
+              viewBox="0 0 32 32"
+            >
+              <path
+                d="M8.45 22.087l-1.305-6.674h17.678l-1.572 6.674H8.45zm4.975-12.412l1.083 1.765a.823.823 0 00.715.386h7.951V13.5H8.587V9.675h4.838zM26.043 13.5h-1.195v-2.598c0-.463-.336-.75-.798-.75h-8.356l-1.082-1.766A.823.823 0 0013.897 8H7.728c-.462 0-.815.256-.815.718V13.5h-.956a.97.97 0 00-.746.37.972.972 0 00-.19.81l1.724 8.565c.095.44.484.755.933.755H24c.44 0 .824-.3.929-.727l2.043-8.568a.972.972 0 00-.176-.825.967.967 0 00-.753-.38z"
+                fill="currentcolor"
+                fill-rule="evenodd"
+              />
+            </svg>
+          </div>
+          <div className="uppy-DashboardTab-name">
+            {this.props.i18n('myDevice')}
+          </div>
+        </button>
+      </div>
+    )
+  }
+
+  private renderPhotoCamera = () => {
+    return (
+      <div
+        className="uppy-DashboardTab"
+        role="presentation"
+        data-uppy-acquirer-id="MobilePhotoCamera"
+      >
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
+          role="tab"
+          tabIndex={0}
+          data-uppy-super-focusable
+          onClick={this.triggerPhotoCameraInputClick}
+        >
+          <div className="uppy-DashboardTab-inner">
+            <svg
+              aria-hidden="true"
+              focusable="false"
+              width="32"
+              height="32"
+              viewBox="0 0 32 32"
+            >
+              <path
+                d="M23.5 9.5c1.417 0 2.5 1.083 2.5 2.5v9.167c0 1.416-1.083 2.5-2.5 2.5h-15c-1.417 0-2.5-1.084-2.5-2.5V12c0-1.417 1.083-2.5 2.5-2.5h2.917l1.416-2.167C13 7.167 13.25 7 13.5 7h5c.25 0 .5.167.667.333L20.583 9.5H23.5zM16 11.417a4.706 4.706 0 00-4.75 4.75 4.704 4.704 0 004.75 4.75 4.703 4.703 0 004.75-4.75c0-2.663-2.09-4.75-4.75-4.75zm0 7.825c-1.744 0-3.076-1.332-3.076-3.074 0-1.745 1.333-3.077 3.076-3.077 1.744 0 3.074 1.333 3.074 3.076s-1.33 3.075-3.074 3.075z"
+                fill="#02B383"
+                fill-rule="nonzero"
+              />
+            </svg>
+          </div>
+          <div className="uppy-DashboardTab-name">
+            {this.props.i18n('takePictureBtn')}
+          </div>
+        </button>
+      </div>
+    )
+  }
+
+  private renderVideoCamera = () => {
+    return (
+      <div
+        className="uppy-DashboardTab"
+        role="presentation"
+        data-uppy-acquirer-id="MobileVideoCamera"
+      >
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
+          role="tab"
+          tabIndex={0}
+          data-uppy-super-focusable
+          onClick={this.triggerVideoCameraInputClick}
+        >
+          <div className="uppy-DashboardTab-inner">
+            <svg aria-hidden="true" width="32" height="32" viewBox="0 0 32 32">
+              <path
+                fill="#FF675E"
+                fillRule="nonzero"
+                d="m21.254 14.277 2.941-2.588c.797-.313 1.243.818 1.09 1.554-.01 2.094.02 4.189-.017 6.282-.126.915-1.145 1.08-1.58.34l-2.434-2.142c-.192.287-.504 1.305-.738.468-.104-1.293-.028-2.596-.05-3.894.047-.312.381.823.426 1.069.063-.384.206-.744.362-1.09zm-12.939-3.73c3.858.013 7.717-.025 11.574.02.912.129 1.492 1.237 1.351 2.217-.019 2.412.04 4.83-.03 7.239-.17 1.025-1.166 1.59-2.029 1.429-3.705-.012-7.41.025-11.114-.019-.913-.129-1.492-1.237-1.352-2.217.018-2.404-.036-4.813.029-7.214.136-.82.83-1.473 1.571-1.454z "
+              />
+            </svg>
+          </div>
+          <div className="uppy-DashboardTab-name">
+            {this.props.i18n('recordVideoBtn')}
+          </div>
+        </button>
+      </div>
+    )
+  }
+
+  private renderBrowseButton = (text: $TSFixMe, onClickFn: $TSFixMe) => {
+    const numberOfAcquirers = this.props.acquirers.length
+    return (
+      <button
+        type="button"
+        className="uppy-u-reset uppy-c-btn uppy-Dashboard-browse"
+        onClick={onClickFn}
+        data-uppy-super-focusable={numberOfAcquirers === 0}
+      >
+        {text}
+      </button>
+    )
+  }
+
+  private renderDropPasteBrowseTagline = (numberOfAcquirers: $TSFixMe) => {
+    const browseFiles = this.renderBrowseButton(
+      this.props.i18n('browseFiles'),
+      this.triggerFileInputClick,
+    )
+    const browseFolders = this.renderBrowseButton(
+      this.props.i18n('browseFolders'),
+      this.triggerFolderInputClick,
+    )
+
+    // in order to keep the i18n CamelCase and options lower (as are defaults) we will want to transform a lower
+    // to Camel
+    const lowerFMSelectionType = this.props.fileManagerSelectionType
+    const camelFMSelectionType =
+      lowerFMSelectionType.charAt(0).toUpperCase() +
+      lowerFMSelectionType.slice(1)
+
+    return (
+      <div class="uppy-Dashboard-AddFiles-title">
+        {
+          // eslint-disable-next-line no-nested-ternary
+          this.props.disableLocalFiles ?
+            this.props.i18n('importFiles')
+          : numberOfAcquirers > 0 ?
+            this.props.i18nArray(`dropPasteImport${camelFMSelectionType}`, {
+              browseFiles,
+              browseFolders,
+              browse: browseFiles,
+            })
+          : this.props.i18nArray(`dropPaste${camelFMSelectionType}`, {
+              browseFiles,
+              browseFolders,
+              browse: browseFiles,
+            })
+
+        }
+      </div>
+    )
+  }
+
+  private [Symbol.for('uppy test: disable unused locale key warning')]() {
+    // Those are actually used in `renderDropPasteBrowseTagline` method.
+    this.props.i18nArray('dropPasteBoth')
+    this.props.i18nArray('dropPasteFiles')
+    this.props.i18nArray('dropPasteFolders')
+    this.props.i18nArray('dropPasteImportBoth')
+    this.props.i18nArray('dropPasteImportFiles')
+    this.props.i18nArray('dropPasteImportFolders')
+  }
+
+  private renderAcquirer = (acquirer: $TSFixMe) => {
+    return (
+      <div
+        className="uppy-DashboardTab"
+        role="presentation"
+        data-uppy-acquirer-id={acquirer.id}
+      >
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
+          role="tab"
+          tabIndex={0}
+          data-cy={acquirer.id}
+          aria-controls={`uppy-DashboardContent-panel--${acquirer.id}`}
+          aria-selected={this.props.activePickerPanel?.id === acquirer.id}
+          data-uppy-super-focusable
+          onClick={() => this.props.showPanel(acquirer.id)}
+        >
+          <div className="uppy-DashboardTab-inner">{acquirer.icon()}</div>
+          <div className="uppy-DashboardTab-name">{acquirer.name}</div>
+        </button>
+      </div>
+    )
+  }
+
+  private renderAcquirers = (acquirers: $TSFixMe) => {
+    // Group last two buttons, so we don’t end up with
+    // just one button on a new line
+    const acquirersWithoutLastTwo = [...acquirers]
+    const lastTwoAcquirers = acquirersWithoutLastTwo.splice(
+      acquirers.length - 2,
+      acquirers.length,
+    )
+
+    return (
+      <>
+        {acquirersWithoutLastTwo.map((acquirer) =>
+          this.renderAcquirer(acquirer),
+        )}
+        <span role="presentation" style={{ 'white-space': 'nowrap' }}>
+          {lastTwoAcquirers.map((acquirer) => this.renderAcquirer(acquirer))}
+        </span>
+      </>
+    )
+  }
+
+  private renderSourcesList = (
+    acquirers: $TSFixMe,
+    disableLocalFiles: $TSFixMe,
+  ) => {
+    const { showNativePhotoCameraButton, showNativeVideoCameraButton } =
+      this.props
+
+    let list = []
+
+    const myDeviceKey = 'myDevice'
+
+    if (!disableLocalFiles)
+      list.push({ key: myDeviceKey, elements: this.renderMyDeviceAcquirer() })
+    if (showNativePhotoCameraButton)
+      list.push({
+        key: 'nativePhotoCameraButton',
+        elements: this.renderPhotoCamera(),
+      })
+    if (showNativeVideoCameraButton)
+      list.push({
+        key: 'nativePhotoCameraButton',
+        elements: this.renderVideoCamera(),
+      })
+    list.push(
+      ...acquirers.map((acquirer: $TSFixMe) => ({
+        key: acquirer.id,
+        elements: this.renderAcquirer(acquirer),
+      })),
+    )
+
+    // doesn't make sense to show only a lonely "My Device"
+    const hasOnlyMyDevice = list.length === 1 && list[0].key === myDeviceKey
+    if (hasOnlyMyDevice) list = []
+
+    // Group last two buttons, so we don’t end up with
+    // just one button on a new line
+    const listWithoutLastTwo = [...list]
+    const lastTwo = listWithoutLastTwo.splice(list.length - 2, list.length)
+
+    const renderList = (l: $TSFixMe) =>
+      l.map(({ key, elements }: $TSFixMe) => (
+        <Fragment key={key}>{elements}</Fragment>
+      ))
+
+    return (
+      <>
+        {this.renderDropPasteBrowseTagline(list.length)}
+
+        <div className="uppy-Dashboard-AddFiles-list" role="tablist">
+          {renderList(listWithoutLastTwo)}
+
+          <span role="presentation" style={{ 'white-space': 'nowrap' }}>
+            {renderList(lastTwo)}
+          </span>
+        </div>
+      </>
+    )
+  }
+
+  private renderPoweredByUppy() {
+    const { i18nArray } = this.props as $TSFixMe
+
+    const uppyBranding = (
+      <span>
+        <svg
+          aria-hidden="true"
+          focusable="false"
+          className="uppy-c-icon uppy-Dashboard-poweredByIcon"
+          width="11"
+          height="11"
+          viewBox="0 0 11 11"
+        >
+          <path
+            d="M7.365 10.5l-.01-4.045h2.612L5.5.806l-4.467 5.65h2.604l.01 4.044h3.718z"
+            fillRule="evenodd"
+          />
+        </svg>
+        <span className="uppy-Dashboard-poweredByUppy">Uppy</span>
+      </span>
+    )
+
+    const linkText = i18nArray('poweredBy', { uppy: uppyBranding })
+
+    return (
+      <a
+        tabIndex={-1}
+        href="https://uppy.io"
+        rel="noreferrer noopener"
+        target="_blank"
+        className="uppy-Dashboard-poweredBy"
+      >
+        {linkText}
+      </a>
+    )
+  }
+
+  render(): ComponentChild {
+    const {
+      showNativePhotoCameraButton,
+      showNativeVideoCameraButton,
+      nativeCameraFacingMode,
+    } = this.props
+
+    return (
+      <div className="uppy-Dashboard-AddFiles">
+        {this.renderHiddenInput(false, (ref: $TSFixMe) => {
+          this.fileInput = ref
+        })}
+        {this.renderHiddenInput(true, (ref: $TSFixMe) => {
+          this.folderInput = ref
+        })}
+        {showNativePhotoCameraButton &&
+          this.renderHiddenCameraInput(
+            'photo',
+            nativeCameraFacingMode,
+            (ref: $TSFixMe) => {
+              this.mobilePhotoFileInput = ref
+            },
+          )}
+        {showNativeVideoCameraButton &&
+          this.renderHiddenCameraInput(
+            'video',
+            nativeCameraFacingMode,
+            (ref: $TSFixMe) => {
+              this.mobileVideoFileInput = ref
+            },
+          )}
+        {this.renderSourcesList(
+          this.props.acquirers,
+          this.props.disableLocalFiles,
+        )}
+        <div className="uppy-Dashboard-AddFiles-info">
+          {this.props.note && (
+            <div className="uppy-Dashboard-note">{this.props.note}</div>
+          )}
+          {this.props.proudlyDisplayPoweredByUppy &&
+            this.renderPoweredByUppy(this.props)}
+        </div>
+      </div>
+    )
+  }
+}
+
+export default AddFiles

+ 10 - 3
packages/@uppy/dashboard/src/components/AddFilesPanel.jsx → packages/@uppy/dashboard/src/components/AddFilesPanel.tsx

@@ -1,8 +1,11 @@
+/* eslint-disable react/destructuring-assignment */
 import { h } from 'preact'
 import { h } from 'preact'
 import classNames from 'classnames'
 import classNames from 'classnames'
-import AddFiles from './AddFiles.jsx'
+import AddFiles from './AddFiles.tsx'
 
 
-const AddFilesPanel = (props) => {
+type $TSFixMe = any
+
+const AddFilesPanel = (props: $TSFixMe): $TSFixMe => {
   return (
   return (
     <div
     <div
       className={classNames('uppy-Dashboard-AddFilesPanel', props.className)}
       className={classNames('uppy-Dashboard-AddFilesPanel', props.className)}
@@ -10,7 +13,11 @@ const AddFilesPanel = (props) => {
       aria-hidden={!props.showAddFilesPanel}
       aria-hidden={!props.showAddFilesPanel}
     >
     >
       <div className="uppy-DashboardContent-bar">
       <div className="uppy-DashboardContent-bar">
-        <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+        <div
+          className="uppy-DashboardContent-title"
+          role="heading"
+          aria-level="1"
+        >
           {props.i18n('addingMoreFiles')}
           {props.i18n('addingMoreFiles')}
         </div>
         </div>
         <button
         <button

+ 91 - 57
packages/@uppy/dashboard/src/components/Dashboard.jsx → packages/@uppy/dashboard/src/components/Dashboard.tsx

@@ -1,14 +1,15 @@
+/* eslint-disable react/destructuring-assignment, react/jsx-props-no-spreading */
 import { h } from 'preact'
 import { h } from 'preact'
 import classNames from 'classnames'
 import classNames from 'classnames'
 import isDragDropSupported from '@uppy/utils/lib/isDragDropSupported'
 import isDragDropSupported from '@uppy/utils/lib/isDragDropSupported'
-import FileList from './FileList.jsx'
-import AddFiles from './AddFiles.jsx'
-import AddFilesPanel from './AddFilesPanel.jsx'
-import PickerPanelContent from './PickerPanelContent.jsx'
-import EditorPanel from './EditorPanel.jsx'
-import PanelTopBar from './PickerPanelTopBar.jsx'
-import FileCard from './FileCard/index.jsx'
-import Slide from './Slide.jsx'
+import FileList from './FileList.tsx'
+import AddFiles from './AddFiles.tsx'
+import AddFilesPanel from './AddFilesPanel.tsx'
+import PickerPanelContent from './PickerPanelContent.tsx'
+import EditorPanel from './EditorPanel.tsx'
+import PanelTopBar from './PickerPanelTopBar.tsx'
+import FileCard from './FileCard/index.tsx'
+import Slide from './Slide.tsx'
 
 
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal
 // https://github.com/ghosh/micromodal
@@ -22,7 +23,9 @@ const HEIGHT_MD = 330
 // const HEIGHT_LG = 400
 // const HEIGHT_LG = 400
 // const HEIGHT_XL = 460
 // const HEIGHT_XL = 460
 
 
-export default function Dashboard (props) {
+type $TSFixMe = any
+
+export default function Dashboard(props: $TSFixMe): JSX.Element {
   const isNoFiles = props.totalFileCount === 0
   const isNoFiles = props.totalFileCount === 0
   const isSingleFile = props.totalFileCount === 1
   const isSingleFile = props.totalFileCount === 1
   const isSizeMD = props.containerWidth > WIDTH_MD
   const isSizeMD = props.containerWidth > WIDTH_MD
@@ -45,7 +48,8 @@ export default function Dashboard (props) {
     'uppy-Dashboard--isAddFilesPanelVisible': props.showAddFilesPanel,
     'uppy-Dashboard--isAddFilesPanelVisible': props.showAddFilesPanel,
     'uppy-Dashboard--isInnerWrapVisible': props.areInsidesReadyToBeVisible,
     'uppy-Dashboard--isInnerWrapVisible': props.areInsidesReadyToBeVisible,
     // Only enable “centered single file” mode when Dashboard is tall enough
     // Only enable “centered single file” mode when Dashboard is tall enough
-    'uppy-Dashboard--singleFile': props.singleFileFullScreen && isSingleFile && isSizeHeightMD,
+    'uppy-Dashboard--singleFile':
+      props.singleFileFullScreen && isSingleFile && isSizeHeightMD,
   })
   })
 
 
   // Important: keep these in sync with the percent width values in `src/components/FileItem/index.scss`.
   // Important: keep these in sync with the percent width values in `src/components/FileItem/index.scss`.
@@ -60,11 +64,16 @@ export default function Dashboard (props) {
 
 
   const showFileList = props.showSelectedFiles && !isNoFiles
   const showFileList = props.showSelectedFiles && !isNoFiles
 
 
-  const numberOfFilesForRecovery = props.recoveredState ? Object.keys(props.recoveredState.files).length : null
-  const numberOfGhosts = props.files ? Object.keys(props.files).filter((fileID) => props.files[fileID].isGhost).length : null
+  const numberOfFilesForRecovery =
+    props.recoveredState ? Object.keys(props.recoveredState.files).length : null
+  const numberOfGhosts =
+    props.files ?
+      Object.keys(props.files).filter((fileID) => props.files[fileID].isGhost)
+        .length
+    : null
 
 
   const renderRestoredText = () => {
   const renderRestoredText = () => {
-    if (numberOfGhosts > 0) {
+    if (numberOfGhosts! > 0) {
       return props.i18n('recoveredXFiles', {
       return props.i18n('recoveredXFiles', {
         smart_count: numberOfGhosts,
         smart_count: numberOfGhosts,
       })
       })
@@ -78,10 +87,16 @@ export default function Dashboard (props) {
       className={dashboardClassName}
       className={dashboardClassName}
       data-uppy-theme={props.theme}
       data-uppy-theme={props.theme}
       data-uppy-num-acquirers={props.acquirers.length}
       data-uppy-num-acquirers={props.acquirers.length}
-      data-uppy-drag-drop-supported={!props.disableLocalFiles && isDragDropSupported()}
+      data-uppy-drag-drop-supported={
+        !props.disableLocalFiles && isDragDropSupported()
+      }
       aria-hidden={props.inline ? 'false' : props.isHidden}
       aria-hidden={props.inline ? 'false' : props.isHidden}
       aria-disabled={props.disabled}
       aria-disabled={props.disabled}
-      aria-label={!props.inline ? props.i18n('dashboardWindowTitle') : props.i18n('dashboardTitle')}
+      aria-label={
+        !props.inline ?
+          props.i18n('dashboardWindowTitle')
+        : props.i18n('dashboardTitle')
+      }
       onPaste={props.handlePaste}
       onPaste={props.handlePaste}
       onDragOver={props.handleDragOver}
       onDragOver={props.handleDragOver}
       onDragLeave={props.handleDragLeave}
       onDragLeave={props.handleDragLeave}
@@ -97,14 +112,13 @@ export default function Dashboard (props) {
       <div
       <div
         className="uppy-Dashboard-inner"
         className="uppy-Dashboard-inner"
         aria-modal={!props.inline && 'true'}
         aria-modal={!props.inline && 'true'}
-        role={!props.inline && 'dialog'}
+        role={props.inline ? undefined : 'dialog'}
         style={{
         style={{
           width: props.inline && props.width ? props.width : '',
           width: props.inline && props.width ? props.width : '',
           height: props.inline && props.height ? props.height : '',
           height: props.inline && props.height ? props.height : '',
         }}
         }}
       >
       >
-
-        {!props.inline ? (
+        {!props.inline ?
           <button
           <button
             className="uppy-u-reset uppy-Dashboard-close"
             className="uppy-u-reset uppy-Dashboard-close"
             type="button"
             type="button"
@@ -114,7 +128,7 @@ export default function Dashboard (props) {
           >
           >
             <span aria-hidden="true">&times;</span>
             <span aria-hidden="true">&times;</span>
           </button>
           </button>
-        ) : null}
+        : null}
 
 
         <div className="uppy-Dashboard-innerWrap">
         <div className="uppy-Dashboard-innerWrap">
           <div className="uppy-Dashboard-dropFilesHereHint">
           <div className="uppy-Dashboard-dropFilesHereHint">
@@ -126,9 +140,19 @@ export default function Dashboard (props) {
 
 
           {numberOfFilesForRecovery && (
           {numberOfFilesForRecovery && (
             <div className="uppy-Dashboard-serviceMsg">
             <div className="uppy-Dashboard-serviceMsg">
-              <svg className="uppy-Dashboard-serviceMsg-icon" aria-hidden="true" focusable="false" width="21" height="16" viewBox="0 0 24 19">
+              <svg
+                className="uppy-Dashboard-serviceMsg-icon"
+                aria-hidden="true"
+                focusable="false"
+                width="21"
+                height="16"
+                viewBox="0 0 24 19"
+              >
                 <g transform="translate(0 -1)" fill="none" fillRule="evenodd">
                 <g transform="translate(0 -1)" fill="none" fillRule="evenodd">
-                  <path d="M12.857 1.43l10.234 17.056A1 1 0 0122.234 20H1.766a1 1 0 01-.857-1.514L11.143 1.429a1 1 0 011.714 0z" fill="#FFD300" />
+                  <path
+                    d="M12.857 1.43l10.234 17.056A1 1 0 0122.234 20H1.766a1 1 0 01-.857-1.514L11.143 1.429a1 1 0 011.714 0z"
+                    fill="#FFD300"
+                  />
                   <path fill="#000" d="M11 6h2l-.3 8h-1.4z" />
                   <path fill="#000" d="M11 6h2l-.3 8h-1.4z" />
                   <circle fill="#000" cx="12" cy="17" r="1" />
                   <circle fill="#000" cx="12" cy="17" r="1" />
                 </g>
                 </g>
@@ -142,60 +166,70 @@ export default function Dashboard (props) {
             </div>
             </div>
           )}
           )}
 
 
-          {showFileList ? (
-            <FileList
-              id={props.id}
-              error={props.error}
-              i18n={props.i18n}
-              uppy={props.uppy}
-              files={props.files}
-              acquirers={props.acquirers}
-              resumableUploads={props.resumableUploads}
-              hideRetryButton={props.hideRetryButton}
-              hidePauseResumeButton={props.hidePauseResumeButton}
-              hideCancelButton={props.hideCancelButton}
-              showLinkToFileUploadResult={props.showLinkToFileUploadResult}
-              showRemoveButtonAfterComplete={props.showRemoveButtonAfterComplete}
-              isWide={props.isWide}
-              metaFields={props.metaFields}
-              toggleFileCard={props.toggleFileCard}
-              handleRequestThumbnail={props.handleRequestThumbnail}
-              handleCancelThumbnail={props.handleCancelThumbnail}
-              recoveredState={props.recoveredState}
-              individualCancellation={props.individualCancellation}
-              openFileEditor={props.openFileEditor}
-              canEditFile={props.canEditFile}
-              toggleAddFilesPanel={props.toggleAddFilesPanel}
-              isSingleFile={isSingleFile}
-              itemsPerRow={itemsPerRow}
-            />
-          ) : (
-            // eslint-disable-next-line react/jsx-props-no-spreading
-            <AddFiles {...props} isSizeMD={isSizeMD} />
-          )}
+          {
+            showFileList ?
+              <FileList
+                id={props.id}
+                error={props.error}
+                i18n={props.i18n}
+                uppy={props.uppy}
+                files={props.files}
+                acquirers={props.acquirers}
+                resumableUploads={props.resumableUploads}
+                hideRetryButton={props.hideRetryButton}
+                hidePauseResumeButton={props.hidePauseResumeButton}
+                hideCancelButton={props.hideCancelButton}
+                showLinkToFileUploadResult={props.showLinkToFileUploadResult}
+                showRemoveButtonAfterComplete={
+                  props.showRemoveButtonAfterComplete
+                }
+                isWide={props.isWide}
+                metaFields={props.metaFields}
+                toggleFileCard={props.toggleFileCard}
+                handleRequestThumbnail={props.handleRequestThumbnail}
+                handleCancelThumbnail={props.handleCancelThumbnail}
+                recoveredState={props.recoveredState}
+                individualCancellation={props.individualCancellation}
+                openFileEditor={props.openFileEditor}
+                canEditFile={props.canEditFile}
+                toggleAddFilesPanel={props.toggleAddFilesPanel}
+                isSingleFile={isSingleFile}
+                itemsPerRow={itemsPerRow}
+              />
+              // eslint-disable-next-line react/jsx-props-no-spreading
+            : <AddFiles {...props} isSizeMD={isSizeMD} />
+          }
 
 
           <Slide>
           <Slide>
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-            {props.showAddFilesPanel ? <AddFilesPanel key="AddFiles" {...props} isSizeMD={isSizeMD} /> : null}
+            {props.showAddFilesPanel ?
+              <AddFilesPanel key="AddFiles" {...props} isSizeMD={isSizeMD} />
+            : null}
           </Slide>
           </Slide>
 
 
           <Slide>
           <Slide>
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-            {props.fileCardFor ? <FileCard key="FileCard" {...props} /> : null}
+            {props.fileCardFor ?
+              <FileCard key="FileCard" {...props} />
+            : null}
           </Slide>
           </Slide>
 
 
           <Slide>
           <Slide>
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-            {props.activePickerPanel ? <PickerPanelContent key="Picker" {...props} /> : null}
+            {props.activePickerPanel ?
+              <PickerPanelContent key="Picker" {...props} />
+            : null}
           </Slide>
           </Slide>
 
 
           <Slide>
           <Slide>
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-            {props.showFileEditor ? <EditorPanel key="Editor" {...props} /> : null}
+            {props.showFileEditor ?
+              <EditorPanel key="Editor" {...props} />
+            : null}
           </Slide>
           </Slide>
 
 
           <div className="uppy-Dashboard-progressindicators">
           <div className="uppy-Dashboard-progressindicators">
-            {props.progressindicators.map((target) => {
+            {props.progressindicators.map((target: $TSFixMe) => {
               return props.uppy.getPlugin(target.id).render(props.state)
               return props.uppy.getPlugin(target.id).render(props.state)
             })}
             })}
           </div>
           </div>

+ 15 - 4
packages/@uppy/dashboard/src/components/EditorPanel.jsx → packages/@uppy/dashboard/src/components/EditorPanel.tsx

@@ -1,7 +1,10 @@
+/* eslint-disable react/destructuring-assignment */
 import { h } from 'preact'
 import { h } from 'preact'
 import classNames from 'classnames'
 import classNames from 'classnames'
 
 
-function EditorPanel (props) {
+type $TSFixMe = any
+
+function EditorPanel(props: $TSFixMe): JSX.Element {
   const file = props.files[props.fileCardFor]
   const file = props.files[props.fileCardFor]
 
 
   const handleCancel = () => {
   const handleCancel = () => {
@@ -17,9 +20,17 @@ function EditorPanel (props) {
       id="uppy-DashboardContent-panel--editor"
       id="uppy-DashboardContent-panel--editor"
     >
     >
       <div className="uppy-DashboardContent-bar">
       <div className="uppy-DashboardContent-bar">
-        <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+        <div
+          className="uppy-DashboardContent-title"
+          role="heading"
+          aria-level="1"
+        >
           {props.i18nArray('editing', {
           {props.i18nArray('editing', {
-            file: <span className="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>,
+            file: (
+              <span className="uppy-DashboardContent-titleFile">
+                {file.meta ? file.meta.name : file.name}
+              </span>
+            ),
           })}
           })}
         </div>
         </div>
         <button
         <button
@@ -38,7 +49,7 @@ function EditorPanel (props) {
         </button>
         </button>
       </div>
       </div>
       <div className="uppy-DashboardContent-panelBody">
       <div className="uppy-DashboardContent-panelBody">
-        {props.editors.map((target) => {
+        {props.editors.map((target: $TSFixMe) => {
           return props.uppy.getPlugin(target.id).render(props.state)
           return props.uppy.getPlugin(target.id).render(props.state)
         })}
         })}
       </div>
       </div>

+ 0 - 46
packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx

@@ -1,46 +0,0 @@
-import { h } from 'preact'
-
-export default function RenderMetaFields (props)  {
-  const {
-    computedMetaFields,
-    requiredMetaFields,
-    updateMeta,
-    form,
-    formState,
-  } = props
-
-  const fieldCSSClasses = {
-    text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input',
-  }
-
-  return computedMetaFields.map((field) => {
-    const id = `uppy-Dashboard-FileCard-input-${field.id}`
-    const required = requiredMetaFields.includes(field.id)
-    return (
-      <fieldset key={field.id} className="uppy-Dashboard-FileCard-fieldset">
-        <label className="uppy-Dashboard-FileCard-label" htmlFor={id}>{field.name}</label>
-        {field.render !== undefined
-          ? field.render({
-            value: formState[field.id],
-            onChange: (newVal) => updateMeta(newVal, field.id),
-            fieldCSSClasses,
-            required,
-            form: form.id,
-          }, h)
-          : (
-            <input
-              className={fieldCSSClasses.text}
-              id={id}
-              form={form.id}
-              type={field.type || 'text'}
-              required={required}
-              value={formState[field.id]}
-              placeholder={field.placeholder}
-              onInput={ev => updateMeta(ev.target.value, field.id)}
-              data-uppy-super-focusable
-            />
-          )}
-      </fieldset>
-    )
-  })
-}

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

@@ -0,0 +1,54 @@
+import { h } from 'preact'
+
+type $TSFixMe = any
+
+export default function RenderMetaFields(props: $TSFixMe): JSX.Element {
+  const {
+    computedMetaFields,
+    requiredMetaFields,
+    updateMeta,
+    form,
+    formState,
+  } = props
+
+  const fieldCSSClasses = {
+    text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input',
+  }
+
+  return computedMetaFields.map((field: $TSFixMe) => {
+    const id = `uppy-Dashboard-FileCard-input-${field.id}`
+    const required = requiredMetaFields.includes(field.id)
+    return (
+      <fieldset key={field.id} className="uppy-Dashboard-FileCard-fieldset">
+        <label className="uppy-Dashboard-FileCard-label" htmlFor={id}>
+          {field.name}
+        </label>
+        {field.render !== undefined ?
+          field.render(
+            {
+              value: formState[field.id],
+              onChange: (newVal: $TSFixMe) => updateMeta(newVal, field.id),
+              fieldCSSClasses,
+              required,
+              form: form.id,
+            },
+            h,
+          )
+        : <input
+            className={fieldCSSClasses.text}
+            id={id}
+            form={form.id}
+            type={field.type || 'text'}
+            required={required}
+            value={formState[field.id]}
+            placeholder={field.placeholder}
+            onInput={(ev) =>
+              updateMeta((ev.target as HTMLInputElement).value, field.id)
+            }
+            data-uppy-super-focusable
+          />
+        }
+      </fieldset>
+    )
+  })
+}

+ 36 - 21
packages/@uppy/dashboard/src/components/FileCard/index.jsx → packages/@uppy/dashboard/src/components/FileCard/index.tsx

@@ -2,12 +2,14 @@ import { h } from 'preact'
 import { useEffect, useState, useCallback } from 'preact/hooks'
 import { useEffect, useState, useCallback } from 'preact/hooks'
 import classNames from 'classnames'
 import classNames from 'classnames'
 import { nanoid } from 'nanoid/non-secure'
 import { nanoid } from 'nanoid/non-secure'
-import getFileTypeIcon from '../../utils/getFileTypeIcon.jsx'
-import ignoreEvent from '../../utils/ignoreEvent.js'
-import FilePreview from '../FilePreview.jsx'
-import RenderMetaFields from './RenderMetaFields.jsx'
+import getFileTypeIcon from '../../utils/getFileTypeIcon.tsx'
+import ignoreEvent from '../../utils/ignoreEvent.ts'
+import FilePreview from '../FilePreview.tsx'
+import RenderMetaFields from './RenderMetaFields.tsx'
 
 
-export default function FileCard (props) {
+type $TSFixMe = any
+
+export default function FileCard(props: $TSFixMe): JSX.Element {
   const {
   const {
     files,
     files,
     fileCardFor,
     fileCardFor,
@@ -23,8 +25,8 @@ export default function FileCard (props) {
   } = props
   } = props
 
 
   const getMetaFields = () => {
   const getMetaFields = () => {
-    return typeof metaFields === 'function'
-      ? metaFields(files[fileCardFor])
+    return typeof metaFields === 'function' ?
+        metaFields(files[fileCardFor])
       : metaFields
       : metaFields
   }
   }
 
 
@@ -32,19 +34,22 @@ export default function FileCard (props) {
   const computedMetaFields = getMetaFields() ?? []
   const computedMetaFields = getMetaFields() ?? []
   const showEditButton = canEditFile(file)
   const showEditButton = canEditFile(file)
 
 
-  const storedMetaData = {}
-  computedMetaFields.forEach((field) => {
+  const storedMetaData: Record<string, string> = {}
+  computedMetaFields.forEach((field: $TSFixMe) => {
     storedMetaData[field.id] = file.meta[field.id] ?? ''
     storedMetaData[field.id] = file.meta[field.id] ?? ''
   })
   })
 
 
   const [formState, setFormState] = useState(storedMetaData)
   const [formState, setFormState] = useState(storedMetaData)
 
 
-  const handleSave = useCallback((ev) => {
-    ev.preventDefault()
-    saveFileCard(formState, fileCardFor)
-  }, [saveFileCard, formState, fileCardFor])
+  const handleSave = useCallback(
+    (ev: $TSFixMe) => {
+      ev.preventDefault()
+      saveFileCard(formState, fileCardFor)
+    },
+    [saveFileCard, formState, fileCardFor],
+  )
 
 
-  const updateMeta = (newVal, name) => {
+  const updateMeta = (newVal: $TSFixMe, name: $TSFixMe) => {
     setFormState({
     setFormState({
       ...formState,
       ...formState,
       [name]: newVal,
       [name]: newVal,
@@ -81,9 +86,17 @@ export default function FileCard (props) {
       onPaste={ignoreEvent}
       onPaste={ignoreEvent}
     >
     >
       <div className="uppy-DashboardContent-bar">
       <div className="uppy-DashboardContent-bar">
-        <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+        <div
+          className="uppy-DashboardContent-title"
+          role="heading"
+          aria-level="1"
+        >
           {i18nArray('editing', {
           {i18nArray('editing', {
-            file: <span className="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>,
+            file: (
+              <span className="uppy-DashboardContent-titleFile">
+                {file.meta ? file.meta.name : file.name}
+              </span>
+            ),
           })}
           })}
         </div>
         </div>
         <button
         <button
@@ -98,14 +111,16 @@ export default function FileCard (props) {
       </div>
       </div>
 
 
       <div className="uppy-Dashboard-FileCard-inner">
       <div className="uppy-Dashboard-FileCard-inner">
-        <div className="uppy-Dashboard-FileCard-preview" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
+        <div
+          className="uppy-Dashboard-FileCard-preview"
+          style={{ backgroundColor: getFileTypeIcon(file.type).color }}
+        >
           <FilePreview file={file} />
           <FilePreview file={file} />
-          {showEditButton
-            && (
+          {showEditButton && (
             <button
             <button
               type="button"
               type="button"
               className="uppy-u-reset uppy-c-btn uppy-Dashboard-FileCard-edit"
               className="uppy-u-reset uppy-c-btn uppy-Dashboard-FileCard-edit"
-              onClick={(event) => {
+              onClick={(event: $TSFixMe) => {
                 // When opening the image editor we want to save any meta fields changes.
                 // When opening the image editor we want to save any meta fields changes.
                 // Otherwise it's confusing for the user to click save in the editor,
                 // Otherwise it's confusing for the user to click save in the editor,
                 // but the changes here are discarded. This bypasses validation,
                 // but the changes here are discarded. This bypasses validation,
@@ -119,7 +134,7 @@ export default function FileCard (props) {
               we can conditionally display i18n('editFile')/i18n('editImage'). */}
               we can conditionally display i18n('editFile')/i18n('editImage'). */}
               {i18n('editImage')}
               {i18n('editImage')}
             </button>
             </button>
-            )}
+          )}
         </div>
         </div>
 
 
         <div className="uppy-Dashboard-FileCard-info">
         <div className="uppy-Dashboard-FileCard-info">

+ 58 - 27
packages/@uppy/dashboard/src/components/FileItem/Buttons/index.jsx → packages/@uppy/dashboard/src/components/FileItem/Buttons/index.tsx

@@ -1,17 +1,19 @@
-import { h } from 'preact'
-import copyToClipboard from '../../../utils/copyToClipboard.js'
+import { h, type ComponentChild } from 'preact'
+import copyToClipboard from '../../../utils/copyToClipboard.ts'
 
 
-function EditButton ({
+type $TSFixMe = any
+
+function EditButton({
   file,
   file,
   uploadInProgressOrComplete,
   uploadInProgressOrComplete,
   metaFields,
   metaFields,
   canEditFile,
   canEditFile,
   i18n,
   i18n,
   onClick,
   onClick,
-}) {
+}: $TSFixMe) {
   if (
   if (
-    (!uploadInProgressOrComplete && metaFields && metaFields.length > 0)
-    || (!uploadInProgressOrComplete && canEditFile(file))
+    (!uploadInProgressOrComplete && metaFields && metaFields.length > 0) ||
+    (!uploadInProgressOrComplete && canEditFile(file))
   ) {
   ) {
     return (
     return (
       <button
       <button
@@ -21,11 +23,24 @@ function EditButton ({
         title={i18n('editFileWithFilename', { file: file.meta.name })}
         title={i18n('editFileWithFilename', { file: file.meta.name })}
         onClick={() => onClick()}
         onClick={() => onClick()}
       >
       >
-        <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="14" height="14" viewBox="0 0 14 14">
+        <svg
+          aria-hidden="true"
+          focusable="false"
+          className="uppy-c-icon"
+          width="14"
+          height="14"
+          viewBox="0 0 14 14"
+        >
           <g fillRule="evenodd">
           <g fillRule="evenodd">
-            <path d="M1.5 10.793h2.793A1 1 0 0 0 5 10.5L11.5 4a1 1 0 0 0 0-1.414L9.707.793a1 1 0 0 0-1.414 0l-6.5 6.5A1 1 0 0 0 1.5 8v2.793zm1-1V8L9 1.5l1.793 1.793-6.5 6.5H2.5z" fillRule="nonzero" />
+            <path
+              d="M1.5 10.793h2.793A1 1 0 0 0 5 10.5L11.5 4a1 1 0 0 0 0-1.414L9.707.793a1 1 0 0 0-1.414 0l-6.5 6.5A1 1 0 0 0 1.5 8v2.793zm1-1V8L9 1.5l1.793 1.793-6.5 6.5H2.5z"
+              fillRule="nonzero"
+            />
             <rect x="1" y="12.293" width="11" height="1" rx=".5" />
             <rect x="1" y="12.293" width="11" height="1" rx=".5" />
-            <path fillRule="nonzero" d="M6.793 2.5L9.5 5.207l.707-.707L7.5 1.793z" />
+            <path
+              fillRule="nonzero"
+              d="M6.793 2.5L9.5 5.207l.707-.707L7.5 1.793z"
+            />
           </g>
           </g>
         </svg>
         </svg>
       </button>
       </button>
@@ -34,7 +49,7 @@ function EditButton ({
   return null
   return null
 }
 }
 
 
-function RemoveButton ({ i18n, onClick, file }) {
+function RemoveButton({ i18n, onClick, file }: $TSFixMe) {
   return (
   return (
     <button
     <button
       className="uppy-u-reset uppy-Dashboard-Item-action uppy-Dashboard-Item-action--remove"
       className="uppy-u-reset uppy-Dashboard-Item-action uppy-Dashboard-Item-action--remove"
@@ -43,16 +58,29 @@ function RemoveButton ({ i18n, onClick, file }) {
       title={i18n('removeFile', { file: file.meta.name })}
       title={i18n('removeFile', { file: file.meta.name })}
       onClick={() => onClick()}
       onClick={() => onClick()}
     >
     >
-      <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="18" height="18" viewBox="0 0 18 18">
+      <svg
+        aria-hidden="true"
+        focusable="false"
+        className="uppy-c-icon"
+        width="18"
+        height="18"
+        viewBox="0 0 18 18"
+      >
         <path d="M9 0C4.034 0 0 4.034 0 9s4.034 9 9 9 9-4.034 9-9-4.034-9-9-9z" />
         <path d="M9 0C4.034 0 0 4.034 0 9s4.034 9 9 9 9-4.034 9-9-4.034-9-9-9z" />
-        <path fill="#FFF" d="M13 12.222l-.778.778L9 9.778 5.778 13 5 12.222 8.222 9 5 5.778 5.778 5 9 8.222 12.222 5l.778.778L9.778 9z" />
+        <path
+          fill="#FFF"
+          d="M13 12.222l-.778.778L9 9.778 5.778 13 5 12.222 8.222 9 5 5.778 5.778 5 9 8.222 12.222 5l.778.778L9.778 9z"
+        />
       </svg>
       </svg>
     </button>
     </button>
   )
   )
 }
 }
 
 
-const copyLinkToClipboard = (event, props) => {
-  copyToClipboard(props.file.uploadURL, props.i18n('copyLinkToClipboardFallback'))
+const copyLinkToClipboard = (event: $TSFixMe, props: $TSFixMe) => {
+  copyToClipboard(
+    props.file.uploadURL,
+    props.i18n('copyLinkToClipboardFallback'),
+  )
     .then(() => {
     .then(() => {
       props.uppy.log('Link copied to clipboard.')
       props.uppy.log('Link copied to clipboard.')
       props.uppy.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
       props.uppy.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
@@ -62,7 +90,7 @@ const copyLinkToClipboard = (event, props) => {
     .then(() => event.target.focus({ preventScroll: true }))
     .then(() => event.target.focus({ preventScroll: true }))
 }
 }
 
 
-function CopyLinkButton (props) {
+function CopyLinkButton(props: $TSFixMe) {
   const { i18n } = props
   const { i18n } = props
 
 
   return (
   return (
@@ -73,14 +101,21 @@ function CopyLinkButton (props) {
       title={i18n('copyLink')}
       title={i18n('copyLink')}
       onClick={(event) => copyLinkToClipboard(event, props)}
       onClick={(event) => copyLinkToClipboard(event, props)}
     >
     >
-      <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="14" height="14" viewBox="0 0 14 12">
+      <svg
+        aria-hidden="true"
+        focusable="false"
+        className="uppy-c-icon"
+        width="14"
+        height="14"
+        viewBox="0 0 14 12"
+      >
         <path d="M7.94 7.703a2.613 2.613 0 0 1-.626 2.681l-.852.851a2.597 2.597 0 0 1-1.849.766A2.616 2.616 0 0 1 2.764 7.54l.852-.852a2.596 2.596 0 0 1 2.69-.625L5.267 7.099a1.44 1.44 0 0 0-.833.407l-.852.851a1.458 1.458 0 0 0 1.03 2.486c.39 0 .755-.152 1.03-.426l.852-.852c.231-.231.363-.522.406-.824l1.04-1.038zm4.295-5.937A2.596 2.596 0 0 0 10.387 1c-.698 0-1.355.272-1.849.766l-.852.851a2.614 2.614 0 0 0-.624 2.688l1.036-1.036c.041-.304.173-.6.407-.833l.852-.852c.275-.275.64-.426 1.03-.426a1.458 1.458 0 0 1 1.03 2.486l-.852.851a1.442 1.442 0 0 1-.824.406l-1.04 1.04a2.596 2.596 0 0 0 2.683-.628l.851-.85a2.616 2.616 0 0 0 0-3.697zm-6.88 6.883a.577.577 0 0 0 .82 0l3.474-3.474a.579.579 0 1 0-.819-.82L5.355 7.83a.579.579 0 0 0 0 .819z" />
         <path d="M7.94 7.703a2.613 2.613 0 0 1-.626 2.681l-.852.851a2.597 2.597 0 0 1-1.849.766A2.616 2.616 0 0 1 2.764 7.54l.852-.852a2.596 2.596 0 0 1 2.69-.625L5.267 7.099a1.44 1.44 0 0 0-.833.407l-.852.851a1.458 1.458 0 0 0 1.03 2.486c.39 0 .755-.152 1.03-.426l.852-.852c.231-.231.363-.522.406-.824l1.04-1.038zm4.295-5.937A2.596 2.596 0 0 0 10.387 1c-.698 0-1.355.272-1.849.766l-.852.851a2.614 2.614 0 0 0-.624 2.688l1.036-1.036c.041-.304.173-.6.407-.833l.852-.852c.275-.275.64-.426 1.03-.426a1.458 1.458 0 0 1 1.03 2.486l-.852.851a1.442 1.442 0 0 1-.824.406l-1.04 1.04a2.596 2.596 0 0 0 2.683-.628l.851-.85a2.616 2.616 0 0 0 0-3.697zm-6.88 6.883a.577.577 0 0 0 .82 0l3.474-3.474a.579.579 0 1 0-.819-.82L5.355 7.83a.579.579 0 0 0 0 .819z" />
       </svg>
       </svg>
     </button>
     </button>
   )
   )
 }
 }
 
 
-export default function Buttons (props) {
+export default function Buttons(props: $TSFixMe): ComponentChild {
   const {
   const {
     uppy,
     uppy,
     file,
     file,
@@ -112,21 +147,17 @@ export default function Buttons (props) {
         metaFields={metaFields}
         metaFields={metaFields}
         onClick={editAction}
         onClick={editAction}
       />
       />
-      {showLinkToFileUploadResult && file.uploadURL ? (
-        <CopyLinkButton
-          file={file}
-          uppy={uppy}
-          i18n={i18n}
-        />
-      ) : null}
-      {showRemoveButton ? (
+      {showLinkToFileUploadResult && file.uploadURL ?
+        <CopyLinkButton file={file} uppy={uppy} i18n={i18n} />
+      : null}
+      {showRemoveButton ?
         <RemoveButton
         <RemoveButton
           i18n={i18n}
           i18n={i18n}
           file={file}
           file={file}
           uppy={uppy}
           uppy={uppy}
-          onClick={() => props.uppy.removeFile(file.id, 'removed-by-user')}
+          onClick={() => uppy.removeFile(file.id, 'removed-by-user')}
         />
         />
-      ) : null}
+      : null}
     </div>
     </div>
   )
   )
 }
 }

+ 31 - 26
packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.jsx → packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.tsx

@@ -1,12 +1,15 @@
-import {  h, Fragment  } from 'preact'
+/* eslint-disable react/destructuring-assignment */
+import { h, Fragment, type ComponentChild } from 'preact'
 import prettierBytes from '@transloadit/prettier-bytes'
 import prettierBytes from '@transloadit/prettier-bytes'
 import truncateString from '@uppy/utils/lib/truncateString'
 import truncateString from '@uppy/utils/lib/truncateString'
-import MetaErrorMessage from '../MetaErrorMessage.jsx'
+import MetaErrorMessage from '../MetaErrorMessage.tsx'
 
 
-const renderFileName = (props) => {
+type $TSFixMe = any
+
+const renderFileName = (props: $TSFixMe) => {
   const { author, name } = props.file.meta
   const { author, name } = props.file.meta
 
 
-  function getMaxNameLength () {
+  function getMaxNameLength() {
     if (props.isSingleFile && props.containerHeight >= 350) {
     if (props.isSingleFile && props.containerHeight >= 350) {
       return 90
       return 90
     }
     }
@@ -29,7 +32,7 @@ const renderFileName = (props) => {
   )
   )
 }
 }
 
 
-const renderAuthor = (props) => {
+const renderAuthor = (props: $TSFixMe) => {
   const { author } = props.file.meta
   const { author } = props.file.meta
   const providerName = props.file.remote?.providerName
   const providerName = props.file.remote?.providerName
   const dot = `\u00B7`
   const dot = `\u00B7`
@@ -47,37 +50,39 @@ const renderAuthor = (props) => {
       >
       >
         {truncateString(author.name, 13)}
         {truncateString(author.name, 13)}
       </a>
       </a>
-      {providerName ? (
+      {providerName ?
         <>
         <>
           {` ${dot} `}
           {` ${dot} `}
           {providerName}
           {providerName}
           {` ${dot} `}
           {` ${dot} `}
         </>
         </>
-      ) : null}
+      : null}
     </div>
     </div>
   )
   )
 }
 }
 
 
-const renderFileSize = (props) => props.file.size && (
-  <div className="uppy-Dashboard-Item-statusSize">
-    {prettierBytes(props.file.size)}
-  </div>
-)
+const renderFileSize = (props: $TSFixMe) =>
+  props.file.size && (
+    <div className="uppy-Dashboard-Item-statusSize">
+      {prettierBytes(props.file.size)}
+    </div>
+  )
 
 
-const ReSelectButton = (props) => props.file.isGhost && (
-  <span>
-    {' \u2022 '}
-    <button
-      className="uppy-u-reset uppy-c-btn uppy-Dashboard-Item-reSelect"
-      type="button"
-      onClick={props.toggleAddFilesPanel}
-    >
-      {props.i18n('reSelect')}
-    </button>
-  </span>
-)
+const ReSelectButton = (props: $TSFixMe) =>
+  props.file.isGhost && (
+    <span>
+      {' \u2022 '}
+      <button
+        className="uppy-u-reset uppy-c-btn uppy-Dashboard-Item-reSelect"
+        type="button"
+        onClick={props.toggleAddFilesPanel}
+      >
+        {props.i18n('reSelect')}
+      </button>
+    </span>
+  )
 
 
-const ErrorButton = ({ file, onClick }) => {
+const ErrorButton = ({ file, onClick }: $TSFixMe) => {
   if (file.error) {
   if (file.error) {
     return (
     return (
       <button
       <button
@@ -95,7 +100,7 @@ const ErrorButton = ({ file, onClick }) => {
   return null
   return null
 }
 }
 
 
-export default function FileInfo (props) {
+export default function FileInfo(props: $TSFixMe): ComponentChild {
   const { file } = props
   const { file } = props
   return (
   return (
     <div
     <div

+ 0 - 39
packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.jsx

@@ -1,39 +0,0 @@
-import { h } from 'preact'
-import FilePreview from '../../FilePreview.jsx'
-import MetaErrorMessage from '../MetaErrorMessage.jsx'
-import getFileTypeIcon from '../../../utils/getFileTypeIcon.jsx'
-
-export default function FilePreviewAndLink (props) {
-  const { file, i18n, toggleFileCard, metaFields, showLinkToFileUploadResult } = props
-  const white = 'rgba(255, 255, 255, 0.5)'
-  const previewBackgroundColor = file.preview ? white : getFileTypeIcon(props.file.type).color
-
-  return (
-    <div
-      className="uppy-Dashboard-Item-previewInnerWrap"
-      style={{ backgroundColor: previewBackgroundColor }}
-    >
-      {
-        showLinkToFileUploadResult && file.uploadURL
-          && (
-          <a
-            className="uppy-Dashboard-Item-previewLink"
-            href={file.uploadURL}
-            rel="noreferrer noopener"
-            target="_blank"
-            aria-label={file.meta.name}
-          >
-            <span hidden>{file.meta.name}</span>
-          </a>
-          )
-      }
-      <FilePreview file={file} />
-      <MetaErrorMessage
-        file={file}
-        i18n={i18n}
-        toggleFileCard={toggleFileCard}
-        metaFields={metaFields}
-      />
-    </div>
-  )
-}

+ 40 - 0
packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.tsx

@@ -0,0 +1,40 @@
+import { h, type ComponentChild } from 'preact'
+import FilePreview from '../../FilePreview.tsx'
+import MetaErrorMessage from '../MetaErrorMessage.tsx'
+import getFileTypeIcon from '../../../utils/getFileTypeIcon.tsx'
+
+type $TSFixMe = any
+
+export default function FilePreviewAndLink(props: $TSFixMe): ComponentChild {
+  const { file, i18n, toggleFileCard, metaFields, showLinkToFileUploadResult } =
+    props
+  const white = 'rgba(255, 255, 255, 0.5)'
+  const previewBackgroundColor =
+    file.preview ? white : getFileTypeIcon(file.type).color
+
+  return (
+    <div
+      className="uppy-Dashboard-Item-previewInnerWrap"
+      style={{ backgroundColor: previewBackgroundColor }}
+    >
+      {showLinkToFileUploadResult && file.uploadURL && (
+        <a
+          className="uppy-Dashboard-Item-previewLink"
+          href={file.uploadURL}
+          rel="noreferrer noopener"
+          target="_blank"
+          aria-label={file.meta.name}
+        >
+          <span hidden>{file.meta.name}</span>
+        </a>
+      )}
+      <FilePreview file={file} />
+      <MetaErrorMessage
+        file={file}
+        i18n={i18n}
+        toggleFileCard={toggleFileCard}
+        metaFields={metaFields}
+      />
+    </div>
+  )
+}

+ 49 - 22
packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.jsx → packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.tsx

@@ -1,6 +1,9 @@
-import { h } from 'preact'
+/* eslint-disable react/destructuring-assignment */
+import { h, type ComponentChild } from 'preact'
 
 
-function onPauseResumeCancelRetry (props) {
+type $TSFixMe = any
+
+function onPauseResumeCancelRetry(props: $TSFixMe) {
   if (props.isUploaded) return
   if (props.isUploaded) return
 
 
   if (props.error && !props.hideRetryButton) {
   if (props.error && !props.hideRetryButton) {
@@ -15,7 +18,7 @@ function onPauseResumeCancelRetry (props) {
   }
   }
 }
 }
 
 
-function progressIndicatorTitle (props) {
+function progressIndicatorTitle(props: $TSFixMe) {
   if (props.isUploaded) {
   if (props.isUploaded) {
     return props.i18n('uploadComplete')
     return props.i18n('uploadComplete')
   }
   }
@@ -29,14 +32,15 @@ function progressIndicatorTitle (props) {
       return props.i18n('resumeUpload')
       return props.i18n('resumeUpload')
     }
     }
     return props.i18n('pauseUpload')
     return props.i18n('pauseUpload')
-  } if (props.individualCancellation) {
+  }
+  if (props.individualCancellation) {
     return props.i18n('cancelUpload')
     return props.i18n('cancelUpload')
   }
   }
 
 
   return ''
   return ''
 }
 }
 
 
-function ProgressIndicatorButton (props) {
+function ProgressIndicatorButton(props: $TSFixMe) {
   return (
   return (
     <div className="uppy-Dashboard-Item-progress">
     <div className="uppy-Dashboard-Item-progress">
       <button
       <button
@@ -52,7 +56,7 @@ function ProgressIndicatorButton (props) {
   )
   )
 }
 }
 
 
-function ProgressCircleContainer ({ children }) {
+function ProgressCircleContainer({ children }: $TSFixMe) {
   return (
   return (
     <svg
     <svg
       aria-hidden="true"
       aria-hidden="true"
@@ -67,7 +71,7 @@ function ProgressCircleContainer ({ children }) {
   )
   )
 }
 }
 
 
-function ProgressCircle ({ progress }) {
+function ProgressCircle({ progress }: $TSFixMe) {
   // circle length equals 2 * PI * R
   // circle length equals 2 * PI * R
   const circleLength = 2 * Math.PI * 15
   const circleLength = 2 * Math.PI * 15
 
 
@@ -90,13 +94,13 @@ function ProgressCircle ({ progress }) {
         fill="none"
         fill="none"
         stroke-width="2"
         stroke-width="2"
         stroke-dasharray={circleLength}
         stroke-dasharray={circleLength}
-        stroke-dashoffset={circleLength - ((circleLength / 100) * progress)}
+        stroke-dashoffset={circleLength - (circleLength / 100) * progress}
       />
       />
     </g>
     </g>
   )
   )
 }
 }
 
 
-export default function FileProgress (props) {
+export default function FileProgress(props: $TSFixMe): ComponentChild {
   // Nothing if upload has not started
   // Nothing if upload has not started
   if (!props.file.progress.uploadStarted) {
   if (!props.file.progress.uploadStarted) {
     return null
     return null
@@ -109,7 +113,11 @@ export default function FileProgress (props) {
         <div className="uppy-Dashboard-Item-progressIndicator">
         <div className="uppy-Dashboard-Item-progressIndicator">
           <ProgressCircleContainer>
           <ProgressCircleContainer>
             <circle r="15" cx="18" cy="18" fill="#1bb240" />
             <circle r="15" cx="18" cy="18" fill="#1bb240" />
-            <polygon className="uppy-Dashboard-Item-progressIcon--check" transform="translate(2, 3)" points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634" />
+            <polygon
+              className="uppy-Dashboard-Item-progressIcon--check"
+              transform="translate(2, 3)"
+              points="14 22.5 7 15.2457065 8.99985857 13.1732815 14 18.3547104 22.9729883 9 25 11.1005634"
+            />
           </ProgressCircleContainer>
           </ProgressCircleContainer>
         </div>
         </div>
       </div>
       </div>
@@ -125,7 +133,14 @@ export default function FileProgress (props) {
     return (
     return (
       // eslint-disable-next-line react/jsx-props-no-spreading
       // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
       <ProgressIndicatorButton {...props}>
-        <svg aria-hidden="true" focusable="false" className="uppy-c-icon uppy-Dashboard-Item-progressIcon--retry" width="28" height="31" viewBox="0 0 16 19">
+        <svg
+          aria-hidden="true"
+          focusable="false"
+          className="uppy-c-icon uppy-Dashboard-Item-progressIcon--retry"
+          width="28"
+          height="31"
+          viewBox="0 0 16 19"
+        >
           <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
           <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
           <path d="M7.9 3H10v2H7.9z" />
           <path d="M7.9 3H10v2H7.9z" />
           <path d="M8.536.5l3.535 3.536-1.414 1.414L7.12 1.914z" />
           <path d="M8.536.5l3.535 3.536-1.414 1.414L7.12 1.914z" />
@@ -142,15 +157,19 @@ export default function FileProgress (props) {
       <ProgressIndicatorButton {...props}>
       <ProgressIndicatorButton {...props}>
         <ProgressCircleContainer>
         <ProgressCircleContainer>
           <ProgressCircle progress={props.file.progress.percentage} />
           <ProgressCircle progress={props.file.progress.percentage} />
-          {
-            props.file.isPaused
-              ? <polygon className="uppy-Dashboard-Item-progressIcon--play" transform="translate(3, 3)" points="12 20 12 10 20 15" />
-              : (
-                <g className="uppy-Dashboard-Item-progressIcon--pause" transform="translate(14.5, 13)">
-                  <rect x="0" y="0" width="2" height="10" rx="0" />
-                  <rect x="5" y="0" width="2" height="10" rx="0" />
-                </g>
-              )
+          {props.file.isPaused ?
+            <polygon
+              className="uppy-Dashboard-Item-progressIcon--play"
+              transform="translate(3, 3)"
+              points="12 20 12 10 20 15"
+            />
+          : <g
+              className="uppy-Dashboard-Item-progressIcon--pause"
+              transform="translate(14.5, 13)"
+            >
+              <rect x="0" y="0" width="2" height="10" rx="0" />
+              <rect x="5" y="0" width="2" height="10" rx="0" />
+            </g>
           }
           }
         </ProgressCircleContainer>
         </ProgressCircleContainer>
       </ProgressIndicatorButton>
       </ProgressIndicatorButton>
@@ -158,13 +177,21 @@ export default function FileProgress (props) {
   }
   }
 
 
   // Cancel button for non-resumable uploads if individualCancellation is supported (not bundled)
   // Cancel button for non-resumable uploads if individualCancellation is supported (not bundled)
-  if (!props.resumableUploads && props.individualCancellation && !props.hideCancelButton) {
+  if (
+    !props.resumableUploads &&
+    props.individualCancellation &&
+    !props.hideCancelButton
+  ) {
     return (
     return (
       // eslint-disable-next-line react/jsx-props-no-spreading
       // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
       <ProgressIndicatorButton {...props}>
         <ProgressCircleContainer>
         <ProgressCircleContainer>
           <ProgressCircle progress={props.file.progress.percentage} />
           <ProgressCircle progress={props.file.progress.percentage} />
-          <polygon className="cancel" transform="translate(2, 2)" points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12" />
+          <polygon
+            className="cancel"
+            transform="translate(2, 2)"
+            points="19.8856516 11.0625 16 14.9481516 12.1019737 11.0625 11.0625 12.1143484 14.9481516 16 11.0625 19.8980263 12.1019737 20.9375 16 17.0518484 19.8856516 20.9375 20.9375 19.8980263 17.0518484 16 20.9375 12"
+          />
         </ProgressCircleContainer>
         </ProgressCircleContainer>
       </ProgressIndicatorButton>
       </ProgressIndicatorButton>
     )
     )

+ 12 - 9
packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.jsx → packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.tsx

@@ -1,29 +1,32 @@
 import { h } from 'preact'
 import { h } from 'preact'
 
 
-const metaFieldIdToName = (metaFieldId, metaFields) => {
+type $TSFixMe = any
+
+const metaFieldIdToName = (metaFieldId: $TSFixMe, metaFields: $TSFixMe) => {
   const fields = typeof metaFields === 'function' ? metaFields() : metaFields
   const fields = typeof metaFields === 'function' ? metaFields() : metaFields
-  const field = fields.filter(f => f.id === metaFieldId)
+  const field = fields.filter((f: $TSFixMe) => f.id === metaFieldId)
   return field[0].name
   return field[0].name
 }
 }
 
 
-export default function renderMissingMetaFieldsError (props) {
+export default function MetaErrorMessage(props: $TSFixMe): JSX.Element {
   const { file, toggleFileCard, i18n, metaFields } = props
   const { file, toggleFileCard, i18n, metaFields } = props
   const { missingRequiredMetaFields } = file
   const { missingRequiredMetaFields } = file
   if (!missingRequiredMetaFields?.length) {
   if (!missingRequiredMetaFields?.length) {
-    return null
+    return null as $TSFixMe
   }
   }
 
 
-  const metaFieldsString = missingRequiredMetaFields.map(missingMetaField => (
-    metaFieldIdToName(missingMetaField, metaFields)
-  )).join(', ')
+  const metaFieldsString = missingRequiredMetaFields
+    .map((missingMetaField: $TSFixMe) =>
+      metaFieldIdToName(missingMetaField, metaFields),
+    )
+    .join(', ')
 
 
   return (
   return (
     <div className="uppy-Dashboard-Item-errorMessage">
     <div className="uppy-Dashboard-Item-errorMessage">
       {i18n('missingRequiredMetaFields', {
       {i18n('missingRequiredMetaFields', {
         smart_count: missingRequiredMetaFields.length,
         smart_count: missingRequiredMetaFields.length,
         fields: metaFieldsString,
         fields: metaFieldsString,
-      })}
-      {' '}
+      })}{' '}
       <button
       <button
         type="button"
         type="button"
         class="uppy-u-reset uppy-Dashboard-Item-errorMessageBtn"
         class="uppy-u-reset uppy-Dashboard-Item-errorMessageBtn"

+ 29 - 16
packages/@uppy/dashboard/src/components/FileItem/index.jsx → packages/@uppy/dashboard/src/components/FileItem/index.tsx

@@ -1,54 +1,65 @@
-import {  h, Component  } from 'preact'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck Typing this file requires more work, skipping it to unblock the rest of the transition.
+
+/* eslint-disable react/destructuring-assignment */
+import { h, Component, type ComponentChild } from 'preact'
 import classNames from 'classnames'
 import classNames from 'classnames'
 import shallowEqual from 'is-shallow-equal'
 import shallowEqual from 'is-shallow-equal'
-import FilePreviewAndLink from './FilePreviewAndLink/index.jsx'
-import FileProgress from './FileProgress/index.jsx'
-import FileInfo from './FileInfo/index.jsx'
-import Buttons from './Buttons/index.jsx'
+import FilePreviewAndLink from './FilePreviewAndLink/index.tsx'
+import FileProgress from './FileProgress/index.tsx'
+import FileInfo from './FileInfo/index.tsx'
+import Buttons from './Buttons/index.tsx'
+
+type $TSFixMe = any
 
 
 export default class FileItem extends Component {
 export default class FileItem extends Component {
-  componentDidMount () {
+  componentDidMount(): void {
     const { file } = this.props
     const { file } = this.props
     if (!file.preview) {
     if (!file.preview) {
       this.props.handleRequestThumbnail(file)
       this.props.handleRequestThumbnail(file)
     }
     }
   }
   }
 
 
-  shouldComponentUpdate (nextProps) {
+  shouldComponentUpdate(nextProps: $TSFixMe): boolean {
     return !shallowEqual(this.props, nextProps)
     return !shallowEqual(this.props, nextProps)
   }
   }
 
 
   // VirtualList mounts FileItems again and they emit `thumbnail:request`
   // VirtualList mounts FileItems again and they emit `thumbnail:request`
   // Otherwise thumbnails are broken or missing after Golden Retriever restores files
   // Otherwise thumbnails are broken or missing after Golden Retriever restores files
-  componentDidUpdate () {
+  componentDidUpdate(): void {
     const { file } = this.props
     const { file } = this.props
     if (!file.preview) {
     if (!file.preview) {
       this.props.handleRequestThumbnail(file)
       this.props.handleRequestThumbnail(file)
     }
     }
   }
   }
 
 
-  componentWillUnmount () {
+  componentWillUnmount(): void {
     const { file } = this.props
     const { file } = this.props
     if (!file.preview) {
     if (!file.preview) {
       this.props.handleCancelThumbnail(file)
       this.props.handleCancelThumbnail(file)
     }
     }
   }
   }
 
 
-  render () {
+  render(): ComponentChild {
     const { file } = this.props
     const { file } = this.props
 
 
     const isProcessing = file.progress.preprocess || file.progress.postprocess
     const isProcessing = file.progress.preprocess || file.progress.postprocess
-    const isUploaded = file.progress.uploadComplete && !isProcessing && !file.error
-    const uploadInProgressOrComplete = file.progress.uploadStarted || isProcessing
-    const uploadInProgress = (file.progress.uploadStarted && !file.progress.uploadComplete) || isProcessing
+    const isUploaded =
+      file.progress.uploadComplete && !isProcessing && !file.error
+    const uploadInProgressOrComplete =
+      file.progress.uploadStarted || isProcessing
+    const uploadInProgress =
+      (file.progress.uploadStarted && !file.progress.uploadComplete) ||
+      isProcessing
     const error = file.error || false
     const error = file.error || false
 
 
     // File that Golden Retriever was able to partly restore (only meta, not blob),
     // File that Golden Retriever was able to partly restore (only meta, not blob),
     // users still need to re-add it, so it’s a ghost
     // users still need to re-add it, so it’s a ghost
     const { isGhost } = file
     const { isGhost } = file
 
 
-    let showRemoveButton = this.props.individualCancellation
-      ? !isUploaded
+    let showRemoveButton =
+      this.props.individualCancellation ?
+        !isUploaded
       : !uploadInProgress && !isUploaded
       : !uploadInProgress && !isUploaded
 
 
     if (isUploaded && this.props.showRemoveButtonAfterComplete) {
     if (isUploaded && this.props.showRemoveButtonAfterComplete) {
@@ -89,7 +100,9 @@ export default class FileItem extends Component {
             hideCancelButton={this.props.hideCancelButton}
             hideCancelButton={this.props.hideCancelButton}
             hidePauseResumeButton={this.props.hidePauseResumeButton}
             hidePauseResumeButton={this.props.hidePauseResumeButton}
             recoveredState={this.props.recoveredState}
             recoveredState={this.props.recoveredState}
-            showRemoveButtonAfterComplete={this.props.showRemoveButtonAfterComplete}
+            showRemoveButtonAfterComplete={
+              this.props.showRemoveButtonAfterComplete
+            }
             resumableUploads={this.props.resumableUploads}
             resumableUploads={this.props.resumableUploads}
             individualCancellation={this.props.individualCancellation}
             individualCancellation={this.props.individualCancellation}
             i18n={this.props.i18n}
             i18n={this.props.i18n}

+ 50 - 24
packages/@uppy/dashboard/src/components/FileList.jsx → packages/@uppy/dashboard/src/components/FileList.tsx

@@ -1,12 +1,16 @@
 import { h } from 'preact'
 import { h } from 'preact'
 import { useMemo } from 'preact/hooks'
 import { useMemo } from 'preact/hooks'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import VirtualList from '@uppy/utils/lib/VirtualList'
 import VirtualList from '@uppy/utils/lib/VirtualList'
-import FileItem from './FileItem/index.jsx'
+import FileItem from './FileItem/index.tsx'
 
 
-function chunks (list, size) {
-  const chunked = []
-  let currentChunk = []
-  list.forEach((item) => {
+type $TSFixMe = any
+
+function chunks(list: $TSFixMe, size: $TSFixMe) {
+  const chunked: $TSFixMe[] = []
+  let currentChunk: $TSFixMe[] = []
+  list.forEach((item: $TSFixMe) => {
     if (currentChunk.length < size) {
     if (currentChunk.length < size) {
       currentChunk.push(item)
       currentChunk.push(item)
     } else {
     } else {
@@ -18,37 +22,63 @@ function chunks (list, size) {
   return chunked
   return chunked
 }
 }
 
 
-export default ({
-  id, error, i18n, uppy, files, acquirers, resumableUploads, hideRetryButton, hidePauseResumeButton, hideCancelButton,
-  showLinkToFileUploadResult, showRemoveButtonAfterComplete, isWide, metaFields, isSingleFile, toggleFileCard,
-  handleRequestThumbnail, handleCancelThumbnail, recoveredState, individualCancellation, itemsPerRow, openFileEditor,
-  canEditFile, toggleAddFilesPanel, containerWidth, containerHeight,
-}) => {
+export default function FileList({
+  id,
+  error,
+  i18n,
+  uppy,
+  files,
+  acquirers,
+  resumableUploads,
+  hideRetryButton,
+  hidePauseResumeButton,
+  hideCancelButton,
+  showLinkToFileUploadResult,
+  showRemoveButtonAfterComplete,
+  isWide,
+  metaFields,
+  isSingleFile,
+  toggleFileCard,
+  handleRequestThumbnail,
+  handleCancelThumbnail,
+  recoveredState,
+  individualCancellation,
+  itemsPerRow,
+  openFileEditor,
+  canEditFile,
+  toggleAddFilesPanel,
+  containerWidth,
+  containerHeight,
+}: $TSFixMe): JSX.Element {
   // It's not great that this is hardcoded!
   // It's not great that this is hardcoded!
   // It's ESPECIALLY not great that this is checking against `itemsPerRow`!
   // It's ESPECIALLY not great that this is checking against `itemsPerRow`!
-  const rowHeight = itemsPerRow === 1
-    // Mobile
-    ? 71
-    // 190px height + 2 * 5px margin
+  const rowHeight =
+    itemsPerRow === 1 ?
+      // Mobile
+      71
+      // 190px height + 2 * 5px margin
     : 200
     : 200
 
 
   // Sort files by file.isGhost, ghost files first, only if recoveredState is present
   // Sort files by file.isGhost, ghost files first, only if recoveredState is present
   const rows = useMemo(() => {
   const rows = useMemo(() => {
-    const sortByGhostComesFirst = (file1, file2) => files[file2].isGhost - files[file1].isGhost
+    const sortByGhostComesFirst = (file1: $TSFixMe, file2: $TSFixMe) =>
+      files[file2].isGhost - files[file1].isGhost
 
 
     const fileIds = Object.keys(files)
     const fileIds = Object.keys(files)
     if (recoveredState) fileIds.sort(sortByGhostComesFirst)
     if (recoveredState) fileIds.sort(sortByGhostComesFirst)
     return chunks(fileIds, itemsPerRow)
     return chunks(fileIds, itemsPerRow)
   }, [files, itemsPerRow, recoveredState])
   }, [files, itemsPerRow, recoveredState])
 
 
-  const renderRow = (row) => (
-    // The `role="presentation` attribute ensures that the list items are properly
+  const renderRow = (
+    row: $TSFixMe, // The `role="presentation` attribute ensures that the list items are properly
+  ) => (
     // associated with the `VirtualList` element.
     // associated with the `VirtualList` element.
     // We use the first file ID as the key—this should not change across scroll rerenders
     // We use the first file ID as the key—this should not change across scroll rerenders
     <div class="uppy-Dashboard-filesInner" role="presentation" key={row[0]}>
     <div class="uppy-Dashboard-filesInner" role="presentation" key={row[0]}>
-      {row.map((fileID) => (
+      {row.map((fileID: $TSFixMe) => (
         <FileItem
         <FileItem
           key={fileID}
           key={fileID}
+          // @ts-expect-error it's fine
           uppy={uppy}
           uppy={uppy}
           // FIXME This is confusing, it's actually the Dashboard's plugin ID
           // FIXME This is confusing, it's actually the Dashboard's plugin ID
           id={id}
           id={id}
@@ -86,11 +116,7 @@ export default ({
   )
   )
 
 
   if (isSingleFile) {
   if (isSingleFile) {
-    return (
-      <div class="uppy-Dashboard-files">
-        {renderRow(rows[0])}
-      </div>
-    )
+    return <div class="uppy-Dashboard-files">{renderRow(rows[0])}</div>
   }
   }
 
 
   return (
   return (

+ 15 - 4
packages/@uppy/dashboard/src/components/FilePreview.jsx → packages/@uppy/dashboard/src/components/FilePreview.tsx

@@ -1,7 +1,9 @@
 import { h } from 'preact'
 import { h } from 'preact'
-import getFileTypeIcon from '../utils/getFileTypeIcon.jsx'
+import getFileTypeIcon from '../utils/getFileTypeIcon.tsx'
 
 
-export default function FilePreview (props) {
+type $TSFixMe = any
+
+export default function FilePreview(props: $TSFixMe): JSX.Element {
   const { file } = props
   const { file } = props
 
 
   if (file.preview) {
   if (file.preview) {
@@ -18,8 +20,17 @@ export default function FilePreview (props) {
 
 
   return (
   return (
     <div className="uppy-Dashboard-Item-previewIconWrap">
     <div className="uppy-Dashboard-Item-previewIconWrap">
-      <span className="uppy-Dashboard-Item-previewIcon" style={{ color }}>{icon}</span>
-      <svg aria-hidden="true" focusable="false" className="uppy-Dashboard-Item-previewIconBg" width="58" height="76" viewBox="0 0 58 76">
+      <span className="uppy-Dashboard-Item-previewIcon" style={{ color }}>
+        {icon}
+      </span>
+      <svg
+        aria-hidden="true"
+        focusable="false"
+        className="uppy-Dashboard-Item-previewIconBg"
+        width="58"
+        height="76"
+        viewBox="0 0 58 76"
+      >
         <rect fill="#FFF" width="58" height="76" rx="3" fillRule="evenodd" />
         <rect fill="#FFF" width="58" height="76" rx="3" fillRule="evenodd" />
       </svg>
       </svg>
     </div>
     </div>

+ 16 - 3
packages/@uppy/dashboard/src/components/PickerPanelContent.jsx → packages/@uppy/dashboard/src/components/PickerPanelContent.tsx

@@ -1,8 +1,17 @@
 import { h } from 'preact'
 import { h } from 'preact'
 import classNames from 'classnames'
 import classNames from 'classnames'
-import ignoreEvent from '../utils/ignoreEvent.js'
+import ignoreEvent from '../utils/ignoreEvent.ts'
 
 
-function PickerPanelContent ({ activePickerPanel, className, hideAllPanels, i18n, state, uppy }) {
+type $TSFixMe = any
+
+function PickerPanelContent({
+  activePickerPanel,
+  className,
+  hideAllPanels,
+  i18n,
+  state,
+  uppy,
+}: $TSFixMe): JSX.Element {
   return (
   return (
     <div
     <div
       className={classNames('uppy-DashboardContent-panel', className)}
       className={classNames('uppy-DashboardContent-panel', className)}
@@ -15,7 +24,11 @@ function PickerPanelContent ({ activePickerPanel, className, hideAllPanels, i18n
       onPaste={ignoreEvent}
       onPaste={ignoreEvent}
     >
     >
       <div className="uppy-DashboardContent-bar">
       <div className="uppy-DashboardContent-bar">
-        <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+        <div
+          className="uppy-DashboardContent-title"
+          role="heading"
+          aria-level="1"
+        >
           {i18n('importFrom', { name: activePickerPanel.name })}
           {i18n('importFrom', { name: activePickerPanel.name })}
         </div>
         </div>
         <button
         <button

+ 57 - 21
packages/@uppy/dashboard/src/components/PickerPanelTopBar.jsx → packages/@uppy/dashboard/src/components/PickerPanelTopBar.tsx

@@ -1,5 +1,8 @@
+import type { UppyFile } from '@uppy/utils/lib/UppyFile'
 import { h } from 'preact'
 import { h } from 'preact'
 
 
+type $TSFixMe = any
+
 const uploadStates = {
 const uploadStates = {
   STATE_ERROR: 'error',
   STATE_ERROR: 'error',
   STATE_WAITING: 'waiting',
   STATE_WAITING: 'waiting',
@@ -10,7 +13,12 @@ const uploadStates = {
   STATE_PAUSED: 'paused',
   STATE_PAUSED: 'paused',
 }
 }
 
 
-function getUploadingState (isAllErrored, isAllComplete, isAllPaused, files = {}) {
+function getUploadingState(
+  isAllErrored: $TSFixMe,
+  isAllComplete: $TSFixMe,
+  isAllPaused: $TSFixMe,
+  files: Record<string, UppyFile<any, any>> = {},
+): $TSFixMe {
   if (isAllErrored) {
   if (isAllErrored) {
     return uploadStates.STATE_ERROR
     return uploadStates.STATE_ERROR
   }
   }
@@ -26,7 +34,7 @@ function getUploadingState (isAllErrored, isAllComplete, isAllPaused, files = {}
   let state = uploadStates.STATE_WAITING
   let state = uploadStates.STATE_WAITING
   const fileIDs = Object.keys(files)
   const fileIDs = Object.keys(files)
   for (let i = 0; i < fileIDs.length; i++) {
   for (let i = 0; i < fileIDs.length; i++) {
-    const { progress } = files[fileIDs[i]]
+    const { progress } = files[fileIDs[i] as keyof typeof files]
     // If ANY files are being uploaded right now, show the uploading state.
     // If ANY files are being uploaded right now, show the uploading state.
     if (progress.uploadStarted && !progress.uploadComplete) {
     if (progress.uploadStarted && !progress.uploadComplete) {
       return uploadStates.STATE_UPLOADING
       return uploadStates.STATE_UPLOADING
@@ -38,17 +46,27 @@ function getUploadingState (isAllErrored, isAllComplete, isAllPaused, files = {}
     }
     }
     // If NO files are being preprocessed or uploaded right now, but some files are
     // If NO files are being preprocessed or uploaded right now, but some files are
     // being postprocessed, show the postprocess state.
     // being postprocessed, show the postprocess state.
-    if (progress.postprocess && state !== uploadStates.STATE_UPLOADING && state !== uploadStates.STATE_PREPROCESSING) {
+    if (
+      progress.postprocess &&
+      state !== uploadStates.STATE_UPLOADING &&
+      state !== uploadStates.STATE_PREPROCESSING
+    ) {
       state = uploadStates.STATE_POSTPROCESSING
       state = uploadStates.STATE_POSTPROCESSING
     }
     }
   }
   }
   return state
   return state
 }
 }
 
 
-function UploadStatus ({
-  files, i18n, isAllComplete, isAllErrored, isAllPaused,
-  inProgressNotPausedFiles, newFiles, processingFiles,
-}) {
+function UploadStatus({
+  files,
+  i18n,
+  isAllComplete,
+  isAllErrored,
+  isAllPaused,
+  inProgressNotPausedFiles,
+  newFiles,
+  processingFiles,
+}: $TSFixMe) {
   const uploadingState = getUploadingState(
   const uploadingState = getUploadingState(
     isAllErrored,
     isAllErrored,
     isAllComplete,
     isAllComplete,
@@ -58,7 +76,9 @@ function UploadStatus ({
 
 
   switch (uploadingState) {
   switch (uploadingState) {
     case 'uploading':
     case 'uploading':
-      return i18n('uploadingXFiles', { smart_count: inProgressNotPausedFiles.length })
+      return i18n('uploadingXFiles', {
+        smart_count: inProgressNotPausedFiles.length,
+      })
     case 'preprocessing':
     case 'preprocessing':
     case 'postprocessing':
     case 'postprocessing':
       return i18n('processingXFiles', { smart_count: processingFiles.length })
       return i18n('processingXFiles', { smart_count: processingFiles.length })
@@ -74,8 +94,15 @@ function UploadStatus ({
   }
   }
 }
 }
 
 
-function PanelTopBar (props) {
-  const { i18n, isAllComplete, hideCancelButton, maxNumberOfFiles, toggleAddFilesPanel, uppy } = props
+function PanelTopBar(props: $TSFixMe): JSX.Element {
+  const {
+    i18n,
+    isAllComplete,
+    hideCancelButton,
+    maxNumberOfFiles,
+    toggleAddFilesPanel,
+    uppy,
+  } = props
   let { allowNewUpload } = props
   let { allowNewUpload } = props
   // TODO maybe this should be done in ../Dashboard.jsx, then just pass that down as `allowNewUpload`
   // TODO maybe this should be done in ../Dashboard.jsx, then just pass that down as `allowNewUpload`
   if (allowNewUpload && maxNumberOfFiles) {
   if (allowNewUpload && maxNumberOfFiles) {
@@ -85,7 +112,7 @@ function PanelTopBar (props) {
 
 
   return (
   return (
     <div className="uppy-DashboardContent-bar">
     <div className="uppy-DashboardContent-bar">
-      {!isAllComplete && !hideCancelButton ? (
+      {!isAllComplete && !hideCancelButton ?
         <button
         <button
           className="uppy-DashboardContent-back"
           className="uppy-DashboardContent-back"
           type="button"
           type="button"
@@ -93,16 +120,18 @@ function PanelTopBar (props) {
         >
         >
           {i18n('cancel')}
           {i18n('cancel')}
         </button>
         </button>
-      ) : (
-        <div />
-      )}
+      : <div />}
 
 
-      <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+      <div
+        className="uppy-DashboardContent-title"
+        role="heading"
+        aria-level="1"
+      >
         {/* eslint-disable-next-line react/jsx-props-no-spreading */}
         {/* eslint-disable-next-line react/jsx-props-no-spreading */}
         <UploadStatus {...props} />
         <UploadStatus {...props} />
       </div>
       </div>
 
 
-      {allowNewUpload ? (
+      {allowNewUpload ?
         <button
         <button
           className="uppy-DashboardContent-addMore"
           className="uppy-DashboardContent-addMore"
           type="button"
           type="button"
@@ -110,14 +139,21 @@ function PanelTopBar (props) {
           title={i18n('addMoreFiles')}
           title={i18n('addMoreFiles')}
           onClick={() => toggleAddFilesPanel(true)}
           onClick={() => toggleAddFilesPanel(true)}
         >
         >
-          <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="15" height="15" viewBox="0 0 15 15">
+          <svg
+            aria-hidden="true"
+            focusable="false"
+            className="uppy-c-icon"
+            width="15"
+            height="15"
+            viewBox="0 0 15 15"
+          >
             <path d="M8 6.5h6a.5.5 0 0 1 .5.5v.5a.5.5 0 0 1-.5.5H8v6a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V8h-6a.5.5 0 0 1-.5-.5V7a.5.5 0 0 1 .5-.5h6v-6A.5.5 0 0 1 7 0h.5a.5.5 0 0 1 .5.5v6z" />
             <path d="M8 6.5h6a.5.5 0 0 1 .5.5v.5a.5.5 0 0 1-.5.5H8v6a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V8h-6a.5.5 0 0 1-.5-.5V7a.5.5 0 0 1 .5-.5h6v-6A.5.5 0 0 1 7 0h.5a.5.5 0 0 1 .5.5v6z" />
           </svg>
           </svg>
-          <span className="uppy-DashboardContent-addMoreCaption">{i18n('addMore')}</span>
+          <span className="uppy-DashboardContent-addMoreCaption">
+            {i18n('addMore')}
+          </span>
         </button>
         </button>
-      ) : (
-        <div />
-      )}
+      : <div />}
     </div>
     </div>
   )
   )
 }
 }

+ 0 - 86
packages/@uppy/dashboard/src/components/Slide.jsx

@@ -1,86 +0,0 @@
-import { cloneElement, toChildArray } from 'preact'
-import { useEffect, useState, useRef } from 'preact/hooks'
-import classNames from 'classnames'
-
-const transitionName = 'uppy-transition-slideDownUp'
-const duration = 250
-
-/**
- * Vertical slide transition.
- *
- * This can take a _single_ child component, which _must_ accept a `className` prop.
- *
- * Currently this is specific to the `uppy-transition-slideDownUp` transition,
- * 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 }) {
-  const [cachedChildren, setCachedChildren] = useState(null);
-  const [className, setClassName] = useState('');
-  const enterTimeoutRef = useRef();
-  const leaveTimeoutRef = useRef();
-  const animationFrameRef = useRef();
-
-  const handleEnterTransition = () => {
-    setClassName(`${transitionName}-enter`);
-
-    cancelAnimationFrame(animationFrameRef.current);
-    clearTimeout(leaveTimeoutRef.current);
-    leaveTimeoutRef.current = undefined;
-
-    animationFrameRef.current = requestAnimationFrame(() => {
-      setClassName(`${transitionName}-enter ${transitionName}-enter-active`);
-
-      enterTimeoutRef.current = setTimeout(() => {
-        setClassName('');
-      }, duration);
-    });
-  };
-
-  const handleLeaveTransition = () => {
-    setClassName(`${transitionName}-leave`);
-
-    cancelAnimationFrame(animationFrameRef.current);
-    clearTimeout(enterTimeoutRef.current);
-    enterTimeoutRef.current = undefined;
-
-    animationFrameRef.current = requestAnimationFrame(() => {
-      setClassName(`${transitionName}-leave ${transitionName}-leave-active`);
-
-      leaveTimeoutRef.current = setTimeout(() => {
-        setCachedChildren(null);
-        setClassName('');
-      }, duration);
-    });
-  };
-
-  useEffect(() => {
-    const child = toChildArray(children)[0];
-    if (cachedChildren === child) return;
-
-    if (child && !cachedChildren) {
-      handleEnterTransition();
-    } else if (cachedChildren && !child && !leaveTimeoutRef.current) {
-      handleLeaveTransition();
-    }
-
-    setCachedChildren(child);
-  }, [children, cachedChildren]); // Dependency array to trigger effect on children change
-
-
-  useEffect(() => {
-    return () => {
-      clearTimeout(enterTimeoutRef.current);
-      clearTimeout(leaveTimeoutRef.current);
-      cancelAnimationFrame(animationFrameRef.current);
-    };
-  }, []); // Cleanup useEffect
-
-  if (!cachedChildren) return null;
-
-  return cloneElement(cachedChildren, {
-    className: classNames(className, cachedChildren.props.className),
-  });
-};
-
-export default Slide

+ 96 - 0
packages/@uppy/dashboard/src/components/Slide.tsx

@@ -0,0 +1,96 @@
+import {
+  cloneElement,
+  toChildArray,
+  type VNode,
+  type ComponentChildren,
+} from 'preact'
+import { useEffect, useState, useRef } from 'preact/hooks'
+import classNames from 'classnames'
+
+const transitionName = 'uppy-transition-slideDownUp'
+const duration = 250
+
+/**
+ * Vertical slide transition.
+ *
+ * This can take a _single_ child component, which _must_ accept a `className` prop.
+ *
+ * Currently this is specific to the `uppy-transition-slideDownUp` transition,
+ * 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 {
+  const [cachedChildren, setCachedChildren] = useState<VNode<{
+    className?: string
+  }> | null>(null)
+  const [className, setClassName] = useState('')
+  const enterTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
+  const leaveTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
+  const animationFrameRef = useRef<ReturnType<typeof requestAnimationFrame>>()
+
+  const handleEnterTransition = () => {
+    setClassName(`${transitionName}-enter`)
+
+    cancelAnimationFrame(animationFrameRef.current!)
+    clearTimeout(leaveTimeoutRef.current)
+    leaveTimeoutRef.current = undefined
+
+    animationFrameRef.current = requestAnimationFrame(() => {
+      setClassName(`${transitionName}-enter ${transitionName}-enter-active`)
+
+      enterTimeoutRef.current = setTimeout(() => {
+        setClassName('')
+      }, duration)
+    })
+  }
+
+  const handleLeaveTransition = () => {
+    setClassName(`${transitionName}-leave`)
+
+    cancelAnimationFrame(animationFrameRef.current!)
+    clearTimeout(enterTimeoutRef.current)
+    enterTimeoutRef.current = undefined
+
+    animationFrameRef.current = requestAnimationFrame(() => {
+      setClassName(`${transitionName}-leave ${transitionName}-leave-active`)
+
+      leaveTimeoutRef.current = setTimeout(() => {
+        setCachedChildren(null)
+        setClassName('')
+      }, duration)
+    })
+  }
+
+  useEffect(() => {
+    const child = toChildArray(children)[0] as VNode
+    if (cachedChildren === child) return
+
+    if (child && !cachedChildren) {
+      handleEnterTransition()
+    } else if (cachedChildren && !child && !leaveTimeoutRef.current) {
+      handleLeaveTransition()
+    }
+
+    setCachedChildren(child)
+  }, [children, cachedChildren]) // Dependency array to trigger effect on children change
+
+  useEffect(() => {
+    return () => {
+      clearTimeout(enterTimeoutRef.current)
+      clearTimeout(leaveTimeoutRef.current)
+      cancelAnimationFrame(animationFrameRef.current!)
+    }
+  }, []) // Cleanup useEffect
+
+  if (!cachedChildren) return null
+
+  return cloneElement(cachedChildren, {
+    className: classNames(className, cachedChildren.props.className),
+  })
+}
+
+export default Slide

+ 0 - 1
packages/@uppy/dashboard/src/index.js

@@ -1 +0,0 @@
-export { default } from './Dashboard.jsx'

+ 31 - 16
packages/@uppy/dashboard/src/index.test.js → packages/@uppy/dashboard/src/index.test.ts

@@ -1,19 +1,29 @@
 import { afterAll, beforeAll, describe, it, expect } from 'vitest'
 import { afterAll, beforeAll, describe, it, expect } from 'vitest'
 
 
-import Core from '@uppy/core'
+import Core, { UIPlugin } from '@uppy/core'
 import StatusBarPlugin from '@uppy/status-bar'
 import StatusBarPlugin from '@uppy/status-bar'
 import GoogleDrivePlugin from '@uppy/google-drive'
 import GoogleDrivePlugin from '@uppy/google-drive'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import WebcamPlugin from '@uppy/webcam'
 import WebcamPlugin from '@uppy/webcam'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import Url from '@uppy/url'
 import Url from '@uppy/url'
 
 
 import resizeObserverPolyfill from 'resize-observer-polyfill'
 import resizeObserverPolyfill from 'resize-observer-polyfill'
-import DashboardPlugin from '../lib/index.js'
+import DashboardPlugin from './index.ts'
+
+type $TSFixMe = any
 
 
 describe('Dashboard', () => {
 describe('Dashboard', () => {
   beforeAll(() => {
   beforeAll(() => {
-    globalThis.ResizeObserver = resizeObserverPolyfill.default || resizeObserverPolyfill
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore we're touching globals for the test
+    globalThis.ResizeObserver =
+      (resizeObserverPolyfill as any).default || resizeObserverPolyfill
   })
   })
   afterAll(() => {
   afterAll(() => {
+    // @ts-expect-error we're touching globals for the test
     delete globalThis.ResizeObserver
     delete globalThis.ResizeObserver
   })
   })
 
 
@@ -48,7 +58,10 @@ describe('Dashboard', () => {
         inline: true,
         inline: true,
         target: 'body',
         target: 'body',
       })
       })
-      core.use(GoogleDrivePlugin, { target: DashboardPlugin, companionUrl: 'https://fake.uppy.io/' })
+      core.use(GoogleDrivePlugin, {
+        target: DashboardPlugin as $TSFixMe,
+        companionUrl: 'https://fake.uppy.io/',
+      })
     }).not.toThrow()
     }).not.toThrow()
 
 
     core.close()
     core.close()
@@ -75,12 +88,15 @@ describe('Dashboard', () => {
     core.use(DashboardPlugin, { inline: false })
     core.use(DashboardPlugin, { inline: false })
     core.use(WebcamPlugin)
     core.use(WebcamPlugin)
 
 
-    const dashboardPlugins = core.getState().plugins['Dashboard'].targets
+    const dashboardPlugins = core.getState().plugins['Dashboard']!
+      .targets as UIPlugin<any, any, any>[]
 
 
     // two built-in plugins + these ones below
     // two built-in plugins + these ones below
     expect(dashboardPlugins.length).toEqual(4)
     expect(dashboardPlugins.length).toEqual(4)
     expect(dashboardPlugins.some((plugin) => plugin.id === 'Url')).toEqual(true)
     expect(dashboardPlugins.some((plugin) => plugin.id === 'Url')).toEqual(true)
-    expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual(true)
+    expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual(
+      true,
+    )
 
 
     core.close()
     core.close()
   })
   })
@@ -92,12 +108,15 @@ describe('Dashboard', () => {
     core.use(DashboardPlugin, { inline: false })
     core.use(DashboardPlugin, { inline: false })
     core.use(WebcamPlugin, { target: 'body' })
     core.use(WebcamPlugin, { target: 'body' })
 
 
-    const dashboardPlugins = core.getState().plugins['Dashboard'].targets
+    const dashboardPlugins = core.getState().plugins['Dashboard']!
+      .targets as UIPlugin<any, any, any>[]
 
 
     // two built-in plugins + these ones below
     // two built-in plugins + these ones below
     expect(dashboardPlugins.length).toEqual(3)
     expect(dashboardPlugins.length).toEqual(3)
     expect(dashboardPlugins.some((plugin) => plugin.id === 'Url')).toEqual(true)
     expect(dashboardPlugins.some((plugin) => plugin.id === 'Url')).toEqual(true)
-    expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual(false)
+    expect(dashboardPlugins.some((plugin) => plugin.id === 'Webcam')).toEqual(
+      false,
+    )
 
 
     core.close()
     core.close()
   })
   })
@@ -109,13 +128,11 @@ describe('Dashboard', () => {
       target: 'body',
       target: 'body',
     })
     })
 
 
-    core.getPlugin('Dashboard').setOptions({
+    core.getPlugin('Dashboard')!.setOptions({
       width: 300,
       width: 300,
     })
     })
 
 
-    expect(
-      core.getPlugin('Dashboard').opts.width,
-    ).toEqual(300)
+    expect(core.getPlugin('Dashboard')!.opts.width).toEqual(300)
   })
   })
 
 
   it('should use updated locale from Core, when it’s set via Core’s setOptions()', () => {
   it('should use updated locale from Core, when it’s set via Core’s setOptions()', () => {
@@ -133,16 +150,14 @@ describe('Dashboard', () => {
       },
       },
     })
     })
 
 
-    expect(
-      core.getPlugin('Dashboard').i18n('myDevice'),
-    ).toEqual('Май дивайс')
+    expect(core.getPlugin('Dashboard')!.i18n('myDevice')).toEqual('Май дивайс')
   })
   })
 
 
   it('should accept a callback as `metaFields` option', () => {
   it('should accept a callback as `metaFields` option', () => {
     const core = new Core()
     const core = new Core()
     expect(() => {
     expect(() => {
       core.use(DashboardPlugin, {
       core.use(DashboardPlugin, {
-        metaFields: (file) => {
+        metaFields: (file: any) => {
           const fields = [{ id: 'name', name: 'File name' }]
           const fields = [{ id: 'name', name: 'File name' }]
           if (file.type.startsWith('image/')) {
           if (file.type.startsWith('image/')) {
             fields.push({ id: 'location', name: 'Photo Location' })
             fields.push({ id: 'location', name: 'Photo Location' })

+ 1 - 0
packages/@uppy/dashboard/src/index.ts

@@ -0,0 +1 @@
+export { default } from './Dashboard.tsx'

+ 0 - 0
packages/@uppy/dashboard/src/locale.js → packages/@uppy/dashboard/src/locale.ts


+ 1 - 1
packages/@uppy/dashboard/src/utils/copyToClipboard.test.js → packages/@uppy/dashboard/src/utils/copyToClipboard.test.ts

@@ -1,5 +1,5 @@
 import { describe, it, expect } from 'vitest'
 import { describe, it, expect } from 'vitest'
-import copyToClipboard from './copyToClipboard.js'
+import copyToClipboard from './copyToClipboard.ts'
 
 
 describe('copyToClipboard', () => {
 describe('copyToClipboard', () => {
   it.skip('should copy the specified text to the clipboard', () => {
   it.skip('should copy the specified text to the clipboard', () => {

+ 10 - 4
packages/@uppy/dashboard/src/utils/copyToClipboard.js → packages/@uppy/dashboard/src/utils/copyToClipboard.ts

@@ -8,8 +8,14 @@
  * @param {string} fallbackString
  * @param {string} fallbackString
  * @returns {Promise}
  * @returns {Promise}
  */
  */
-export default function copyToClipboard (textToCopy, fallbackString = 'Copy the URL below') {
-  return new Promise((resolve) => {
+
+type $TSFixMe = any
+
+export default function copyToClipboard(
+  textToCopy: $TSFixMe,
+  fallbackString = 'Copy the URL below',
+): $TSFixMe {
+  return new Promise<void>((resolve) => {
     const textArea = document.createElement('textarea')
     const textArea = document.createElement('textarea')
     textArea.setAttribute('style', {
     textArea.setAttribute('style', {
       position: 'fixed',
       position: 'fixed',
@@ -22,13 +28,13 @@ export default function copyToClipboard (textToCopy, fallbackString = 'Copy the
       outline: 'none',
       outline: 'none',
       boxShadow: 'none',
       boxShadow: 'none',
       background: 'transparent',
       background: 'transparent',
-    })
+    } as $TSFixMe as string)
 
 
     textArea.value = textToCopy
     textArea.value = textToCopy
     document.body.appendChild(textArea)
     document.body.appendChild(textArea)
     textArea.select()
     textArea.select()
 
 
-    const magicCopyFailed = () => {
+    const magicCopyFailed = (cause?: unknown) => {
       document.body.removeChild(textArea)
       document.body.removeChild(textArea)
       // eslint-disable-next-line no-alert
       // eslint-disable-next-line no-alert
       window.prompt(fallbackString, textToCopy)
       window.prompt(fallbackString, textToCopy)

+ 1 - 1
packages/@uppy/dashboard/src/utils/createSuperFocus.test.js → packages/@uppy/dashboard/src/utils/createSuperFocus.test.ts

@@ -1,5 +1,5 @@
 import { describe, it, expect } from 'vitest'
 import { describe, it, expect } from 'vitest'
-import createSuperFocus from './createSuperFocus.js'
+import createSuperFocus from './createSuperFocus.ts'
 
 
 describe('createSuperFocus', () => {
 describe('createSuperFocus', () => {
   // superFocus.cancel() is used in dashboard
   // superFocus.cancel() is used in dashboard

+ 10 - 4
packages/@uppy/dashboard/src/utils/createSuperFocus.js → packages/@uppy/dashboard/src/utils/createSuperFocus.ts

@@ -1,6 +1,10 @@
 import debounce from 'lodash/debounce.js'
 import debounce from 'lodash/debounce.js'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS'
 import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS'
-import getActiveOverlayEl from './getActiveOverlayEl.js'
+import getActiveOverlayEl from './getActiveOverlayEl.ts'
+
+type $TSFixMe = any
 
 
 /*
 /*
   Focuses on some element in the currently topmost overlay.
   Focuses on some element in the currently topmost overlay.
@@ -12,10 +16,10 @@ import getActiveOverlayEl from './getActiveOverlayEl.js'
   2. If there are no [data-uppy-super-focusable] elements yet (or ever) - focuses
   2. If there are no [data-uppy-super-focusable] elements yet (or ever) - focuses
      on the first focusable element, but switches focus if superfocusable elements appear on next render.
      on the first focusable element, but switches focus if superfocusable elements appear on next render.
 */
 */
-export default function createSuperFocus () {
+export default function createSuperFocus(): $TSFixMe {
   let lastFocusWasOnSuperFocusableEl = false
   let lastFocusWasOnSuperFocusableEl = false
 
 
-  const superFocus = (dashboardEl, activeOverlayType) => {
+  const superFocus = (dashboardEl: $TSFixMe, activeOverlayType: $TSFixMe) => {
     const overlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType)
     const overlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType)
 
 
     const isFocusInOverlay = overlayEl.contains(document.activeElement)
     const isFocusInOverlay = overlayEl.contains(document.activeElement)
@@ -24,7 +28,9 @@ export default function createSuperFocus () {
     // [Practical check] without this line, typing in the search input in googledrive overlay won't work.
     // [Practical check] without this line, typing in the search input in googledrive overlay won't work.
     if (isFocusInOverlay && lastFocusWasOnSuperFocusableEl) return
     if (isFocusInOverlay && lastFocusWasOnSuperFocusableEl) return
 
 
-    const superFocusableEl = overlayEl.querySelector('[data-uppy-super-focusable]')
+    const superFocusableEl = overlayEl.querySelector(
+      '[data-uppy-super-focusable]',
+    )
     // If we are already in the topmost overlay, AND there are no super focusable elements yet, - leave focus up to the user.
     // If we are already in the topmost overlay, AND there are no super focusable elements yet, - leave focus up to the user.
     // [Practical check] without this line, if you are in an empty folder in google drive, and something's uploading in the
     // [Practical check] without this line, if you are in an empty folder in google drive, and something's uploading in the
     // bg, - focus will be jumping to Done all the time.
     // bg, - focus will be jumping to Done all the time.

+ 0 - 11
packages/@uppy/dashboard/src/utils/getActiveOverlayEl.js

@@ -1,11 +0,0 @@
-/**
- * @returns {HTMLElement} - either dashboard element, or the overlay that's most on top
- */
-export default function getActiveOverlayEl (dashboardEl, activeOverlayType) {
-  if (activeOverlayType) {
-    const overlayEl = dashboardEl.querySelector(`[data-uppy-paneltype="${activeOverlayType}"]`)
-    // if an overlay is already mounted
-    if (overlayEl) return overlayEl
-  }
-  return dashboardEl
-}

+ 18 - 0
packages/@uppy/dashboard/src/utils/getActiveOverlayEl.ts

@@ -0,0 +1,18 @@
+type $TSFixMe = any
+
+/**
+ * @returns {HTMLElement} - either dashboard element, or the overlay that's most on top
+ */
+export default function getActiveOverlayEl(
+  dashboardEl: $TSFixMe,
+  activeOverlayType: $TSFixMe,
+): $TSFixMe {
+  if (activeOverlayType) {
+    const overlayEl = dashboardEl.querySelector(
+      `[data-uppy-paneltype="${activeOverlayType}"]`,
+    )
+    // if an overlay is already mounted
+    if (overlayEl) return overlayEl
+  }
+  return dashboardEl
+}

+ 0 - 127
packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx

@@ -1,127 +0,0 @@
-import { h } from 'preact'
-
-function iconImage () {
-  return (
-    <svg aria-hidden="true" focusable="false" width="25" height="25" viewBox="0 0 25 25">
-      <g fill="#686DE0" fillRule="evenodd">
-        <path d="M5 7v10h15V7H5zm0-1h15a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z" fillRule="nonzero" />
-        <path d="M6.35 17.172l4.994-5.026a.5.5 0 0 1 .707 0l2.16 2.16 3.505-3.505a.5.5 0 0 1 .707 0l2.336 2.31-.707.72-1.983-1.97-3.505 3.505a.5.5 0 0 1-.707 0l-2.16-2.159-3.938 3.939-1.409.026z" fillRule="nonzero" />
-        <circle cx="7.5" cy="9.5" r="1.5" />
-      </g>
-    </svg>
-  )
-}
-
-function iconAudio () {
-  return (
-    <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="25" height="25" viewBox="0 0 25 25">
-      <path d="M9.5 18.64c0 1.14-1.145 2-2.5 2s-2.5-.86-2.5-2c0-1.14 1.145-2 2.5-2 .557 0 1.079.145 1.5.396V7.25a.5.5 0 0 1 .379-.485l9-2.25A.5.5 0 0 1 18.5 5v11.64c0 1.14-1.145 2-2.5 2s-2.5-.86-2.5-2c0-1.14 1.145-2 2.5-2 .557 0 1.079.145 1.5.396V8.67l-8 2v7.97zm8-11v-2l-8 2v2l8-2zM7 19.64c.855 0 1.5-.484 1.5-1s-.645-1-1.5-1-1.5.484-1.5 1 .645 1 1.5 1zm9-2c.855 0 1.5-.484 1.5-1s-.645-1-1.5-1-1.5.484-1.5 1 .645 1 1.5 1z" fill="#049BCF" fillRule="nonzero" />
-    </svg>
-  )
-}
-
-function iconVideo () {
-  return (
-    <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="25" height="25" viewBox="0 0 25 25">
-      <path d="M16 11.834l4.486-2.691A1 1 0 0 1 22 10v6a1 1 0 0 1-1.514.857L16 14.167V17a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2.834zM15 9H5v8h10V9zm1 4l5 3v-6l-5 3z" fill="#19AF67" fillRule="nonzero" />
-    </svg>
-  )
-}
-
-function iconPDF () {
-  return (
-    <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="25" height="25" viewBox="0 0 25 25">
-      <path d="M9.766 8.295c-.691-1.843-.539-3.401.747-3.726 1.643-.414 2.505.938 2.39 3.299-.039.79-.194 1.662-.537 3.148.324.49.66.967 1.055 1.51.17.231.382.488.629.757 1.866-.128 3.653.114 4.918.655 1.487.635 2.192 1.685 1.614 2.84-.566 1.133-1.839 1.084-3.416.249-1.141-.604-2.457-1.634-3.51-2.707a13.467 13.467 0 0 0-2.238.426c-1.392 4.051-4.534 6.453-5.707 4.572-.986-1.58 1.38-4.206 4.914-5.375.097-.322.185-.656.264-1.001.08-.353.306-1.31.407-1.737-.678-1.059-1.2-2.031-1.53-2.91zm2.098 4.87c-.033.144-.068.287-.104.427l.033-.01-.012.038a14.065 14.065 0 0 1 1.02-.197l-.032-.033.052-.004a7.902 7.902 0 0 1-.208-.271c-.197-.27-.38-.526-.555-.775l-.006.028-.002-.003c-.076.323-.148.632-.186.8zm5.77 2.978c1.143.605 1.832.632 2.054.187.26-.519-.087-1.034-1.113-1.473-.911-.39-2.175-.608-3.55-.608.845.766 1.787 1.459 2.609 1.894zM6.559 18.789c.14.223.693.16 1.425-.413.827-.648 1.61-1.747 2.208-3.206-2.563 1.064-4.102 2.867-3.633 3.62zm5.345-10.97c.088-1.793-.351-2.48-1.146-2.28-.473.119-.564 1.05-.056 2.405.213.566.52 1.188.908 1.859.18-.858.268-1.453.294-1.984z" fill="#E2514A" fillRule="nonzero" />
-    </svg>
-  )
-}
-
-function iconArchive () {
-  return (
-    <svg aria-hidden="true" focusable="false" width="25" height="25" viewBox="0 0 25 25">
-      <path d="M10.45 2.05h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5V2.55a.5.5 0 0 1 .5-.5zm2.05 1.024h1.05a.5.5 0 0 1 .5.5V3.6a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5v-.001zM10.45 0h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5V.5a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-2.05 3.074h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-2.05 1.024h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm-2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-1.656 3.074l-.82 5.946c.52.302 1.174.458 1.976.458.803 0 1.455-.156 1.975-.458l-.82-5.946h-2.311zm0-1.025h2.312c.512 0 .946.378 1.015.885l.82 5.946c.056.412-.142.817-.501 1.026-.686.398-1.515.597-2.49.597-.974 0-1.804-.199-2.49-.597a1.025 1.025 0 0 1-.5-1.026l.819-5.946c.07-.507.503-.885 1.015-.885zm.545 6.6a.5.5 0 0 1-.397-.561l.143-.999a.5.5 0 0 1 .495-.429h.74a.5.5 0 0 1 .495.43l.143.998a.5.5 0 0 1-.397.561c-.404.08-.819.08-1.222 0z" fill="#00C469" fillRule="nonzero" />
-    </svg>
-  )
-}
-
-function iconFile () {
-  return (
-    <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="25" height="25" viewBox="0 0 25 25">
-      <g fill="#A7AFB7" fillRule="nonzero">
-        <path d="M5.5 22a.5.5 0 0 1-.5-.5v-18a.5.5 0 0 1 .5-.5h10.719a.5.5 0 0 1 .367.16l3.281 3.556a.5.5 0 0 1 .133.339V21.5a.5.5 0 0 1-.5.5h-14zm.5-1h13V7.25L16 4H6v17z" />
-        <path d="M15 4v3a1 1 0 0 0 1 1h3V7h-3V4h-1z" />
-      </g>
-    </svg>
-  )
-}
-
-function iconText () {
-  return (
-    <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="25" height="25" viewBox="0 0 25 25">
-      <path d="M4.5 7h13a.5.5 0 1 1 0 1h-13a.5.5 0 0 1 0-1zm0 3h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 3h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 3h10a.5.5 0 1 1 0 1h-10a.5.5 0 1 1 0-1z" fill="#5A5E69" fillRule="nonzero" />
-    </svg>
-  )
-}
-
-export default function getIconByMime (fileType) {
-  const defaultChoice = {
-    color: '#838999',
-    icon: iconFile(),
-  }
-
-  if (!fileType) return defaultChoice
-
-  const fileTypeGeneral = fileType.split('/')[0]
-  const fileTypeSpecific = fileType.split('/')[1]
-
-  // Text
-  if (fileTypeGeneral === 'text') {
-    return {
-      color: '#5a5e69',
-      icon: iconText(),
-    }
-  }
-
-  // Image
-  if (fileTypeGeneral === 'image') {
-    return {
-      color: '#686de0',
-      icon: iconImage(),
-    }
-  }
-
-  // Audio
-  if (fileTypeGeneral === 'audio') {
-    return {
-      color: '#068dbb',
-      icon: iconAudio(),
-    }
-  }
-
-  // Video
-  if (fileTypeGeneral === 'video') {
-    return {
-      color: '#19af67',
-      icon: iconVideo(),
-    }
-  }
-
-  // PDF
-  if (fileTypeGeneral === 'application' && fileTypeSpecific === 'pdf') {
-    return {
-      color: '#e25149',
-      icon: iconPDF(),
-    }
-  }
-
-  // Archive
-  const archiveTypes = ['zip', 'x-7z-compressed', 'x-rar-compressed', 'x-tar', 'x-gzip', 'x-apple-diskimage']
-  if (fileTypeGeneral === 'application' && archiveTypes.indexOf(fileTypeSpecific) !== -1) {
-    return {
-      color: '#00C469',
-      icon: iconArchive(),
-    }
-  }
-
-  return defaultChoice
-}

+ 212 - 0
packages/@uppy/dashboard/src/utils/getFileTypeIcon.tsx

@@ -0,0 +1,212 @@
+import { h } from 'preact'
+
+type $TSFixMe = any
+
+function iconImage() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <g fill="#686DE0" fillRule="evenodd">
+        <path
+          d="M5 7v10h15V7H5zm0-1h15a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"
+          fillRule="nonzero"
+        />
+        <path
+          d="M6.35 17.172l4.994-5.026a.5.5 0 0 1 .707 0l2.16 2.16 3.505-3.505a.5.5 0 0 1 .707 0l2.336 2.31-.707.72-1.983-1.97-3.505 3.505a.5.5 0 0 1-.707 0l-2.16-2.159-3.938 3.939-1.409.026z"
+          fillRule="nonzero"
+        />
+        <circle cx="7.5" cy="9.5" r="1.5" />
+      </g>
+    </svg>
+  )
+}
+
+function iconAudio() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      className="uppy-c-icon"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <path
+        d="M9.5 18.64c0 1.14-1.145 2-2.5 2s-2.5-.86-2.5-2c0-1.14 1.145-2 2.5-2 .557 0 1.079.145 1.5.396V7.25a.5.5 0 0 1 .379-.485l9-2.25A.5.5 0 0 1 18.5 5v11.64c0 1.14-1.145 2-2.5 2s-2.5-.86-2.5-2c0-1.14 1.145-2 2.5-2 .557 0 1.079.145 1.5.396V8.67l-8 2v7.97zm8-11v-2l-8 2v2l8-2zM7 19.64c.855 0 1.5-.484 1.5-1s-.645-1-1.5-1-1.5.484-1.5 1 .645 1 1.5 1zm9-2c.855 0 1.5-.484 1.5-1s-.645-1-1.5-1-1.5.484-1.5 1 .645 1 1.5 1z"
+        fill="#049BCF"
+        fillRule="nonzero"
+      />
+    </svg>
+  )
+}
+
+function iconVideo() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      className="uppy-c-icon"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <path
+        d="M16 11.834l4.486-2.691A1 1 0 0 1 22 10v6a1 1 0 0 1-1.514.857L16 14.167V17a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2.834zM15 9H5v8h10V9zm1 4l5 3v-6l-5 3z"
+        fill="#19AF67"
+        fillRule="nonzero"
+      />
+    </svg>
+  )
+}
+
+function iconPDF() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      className="uppy-c-icon"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <path
+        d="M9.766 8.295c-.691-1.843-.539-3.401.747-3.726 1.643-.414 2.505.938 2.39 3.299-.039.79-.194 1.662-.537 3.148.324.49.66.967 1.055 1.51.17.231.382.488.629.757 1.866-.128 3.653.114 4.918.655 1.487.635 2.192 1.685 1.614 2.84-.566 1.133-1.839 1.084-3.416.249-1.141-.604-2.457-1.634-3.51-2.707a13.467 13.467 0 0 0-2.238.426c-1.392 4.051-4.534 6.453-5.707 4.572-.986-1.58 1.38-4.206 4.914-5.375.097-.322.185-.656.264-1.001.08-.353.306-1.31.407-1.737-.678-1.059-1.2-2.031-1.53-2.91zm2.098 4.87c-.033.144-.068.287-.104.427l.033-.01-.012.038a14.065 14.065 0 0 1 1.02-.197l-.032-.033.052-.004a7.902 7.902 0 0 1-.208-.271c-.197-.27-.38-.526-.555-.775l-.006.028-.002-.003c-.076.323-.148.632-.186.8zm5.77 2.978c1.143.605 1.832.632 2.054.187.26-.519-.087-1.034-1.113-1.473-.911-.39-2.175-.608-3.55-.608.845.766 1.787 1.459 2.609 1.894zM6.559 18.789c.14.223.693.16 1.425-.413.827-.648 1.61-1.747 2.208-3.206-2.563 1.064-4.102 2.867-3.633 3.62zm5.345-10.97c.088-1.793-.351-2.48-1.146-2.28-.473.119-.564 1.05-.056 2.405.213.566.52 1.188.908 1.859.18-.858.268-1.453.294-1.984z"
+        fill="#E2514A"
+        fillRule="nonzero"
+      />
+    </svg>
+  )
+}
+
+function iconArchive() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <path
+        d="M10.45 2.05h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5V2.55a.5.5 0 0 1 .5-.5zm2.05 1.024h1.05a.5.5 0 0 1 .5.5V3.6a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5v-.001zM10.45 0h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5V.5a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-2.05 3.074h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-2.05 1.024h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm-2.05 1.025h1.05a.5.5 0 0 1 .5.5v.025a.5.5 0 0 1-.5.5h-1.05a.5.5 0 0 1-.5-.5v-.025a.5.5 0 0 1 .5-.5zm2.05 1.025h1.05a.5.5 0 0 1 .5.5v.024a.5.5 0 0 1-.5.5H12.5a.5.5 0 0 1-.5-.5v-.024a.5.5 0 0 1 .5-.5zm-1.656 3.074l-.82 5.946c.52.302 1.174.458 1.976.458.803 0 1.455-.156 1.975-.458l-.82-5.946h-2.311zm0-1.025h2.312c.512 0 .946.378 1.015.885l.82 5.946c.056.412-.142.817-.501 1.026-.686.398-1.515.597-2.49.597-.974 0-1.804-.199-2.49-.597a1.025 1.025 0 0 1-.5-1.026l.819-5.946c.07-.507.503-.885 1.015-.885zm.545 6.6a.5.5 0 0 1-.397-.561l.143-.999a.5.5 0 0 1 .495-.429h.74a.5.5 0 0 1 .495.43l.143.998a.5.5 0 0 1-.397.561c-.404.08-.819.08-1.222 0z"
+        fill="#00C469"
+        fillRule="nonzero"
+      />
+    </svg>
+  )
+}
+
+function iconFile() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      className="uppy-c-icon"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <g fill="#A7AFB7" fillRule="nonzero">
+        <path d="M5.5 22a.5.5 0 0 1-.5-.5v-18a.5.5 0 0 1 .5-.5h10.719a.5.5 0 0 1 .367.16l3.281 3.556a.5.5 0 0 1 .133.339V21.5a.5.5 0 0 1-.5.5h-14zm.5-1h13V7.25L16 4H6v17z" />
+        <path d="M15 4v3a1 1 0 0 0 1 1h3V7h-3V4h-1z" />
+      </g>
+    </svg>
+  )
+}
+
+function iconText() {
+  return (
+    <svg
+      aria-hidden="true"
+      focusable="false"
+      className="uppy-c-icon"
+      width="25"
+      height="25"
+      viewBox="0 0 25 25"
+    >
+      <path
+        d="M4.5 7h13a.5.5 0 1 1 0 1h-13a.5.5 0 0 1 0-1zm0 3h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 3h15a.5.5 0 1 1 0 1h-15a.5.5 0 1 1 0-1zm0 3h10a.5.5 0 1 1 0 1h-10a.5.5 0 1 1 0-1z"
+        fill="#5A5E69"
+        fillRule="nonzero"
+      />
+    </svg>
+  )
+}
+
+export default function getIconByMime(fileType: $TSFixMe): $TSFixMe {
+  const defaultChoice = {
+    color: '#838999',
+    icon: iconFile(),
+  }
+
+  if (!fileType) return defaultChoice
+
+  const fileTypeGeneral = fileType.split('/')[0]
+  const fileTypeSpecific = fileType.split('/')[1]
+
+  // Text
+  if (fileTypeGeneral === 'text') {
+    return {
+      color: '#5a5e69',
+      icon: iconText(),
+    }
+  }
+
+  // Image
+  if (fileTypeGeneral === 'image') {
+    return {
+      color: '#686de0',
+      icon: iconImage(),
+    }
+  }
+
+  // Audio
+  if (fileTypeGeneral === 'audio') {
+    return {
+      color: '#068dbb',
+      icon: iconAudio(),
+    }
+  }
+
+  // Video
+  if (fileTypeGeneral === 'video') {
+    return {
+      color: '#19af67',
+      icon: iconVideo(),
+    }
+  }
+
+  // PDF
+  if (fileTypeGeneral === 'application' && fileTypeSpecific === 'pdf') {
+    return {
+      color: '#e25149',
+      icon: iconPDF(),
+    }
+  }
+
+  // Archive
+  const archiveTypes = [
+    'zip',
+    'x-7z-compressed',
+    'x-rar-compressed',
+    'x-tar',
+    'x-gzip',
+    'x-apple-diskimage',
+  ]
+  if (
+    fileTypeGeneral === 'application' &&
+    archiveTypes.indexOf(fileTypeSpecific) !== -1
+  ) {
+    return {
+      color: '#00C469',
+      icon: iconArchive(),
+    }
+  }
+
+  return defaultChoice
+}

+ 4 - 3
packages/@uppy/dashboard/src/utils/ignoreEvent.js → packages/@uppy/dashboard/src/utils/ignoreEvent.ts

@@ -3,10 +3,11 @@
 // draging UI elements or pasting anything into any field triggers those events —
 // draging UI elements or pasting anything into any field triggers those events —
 // Url treats them as URLs that need to be imported
 // Url treats them as URLs that need to be imported
 
 
-function ignoreEvent (ev) {
+type $TSFixMe = any
+
+function ignoreEvent(ev: $TSFixMe): void {
   const { tagName } = ev.target
   const { tagName } = ev.target
-  if (tagName === 'INPUT'
-      || tagName === 'TEXTAREA') {
+  if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
     ev.stopPropagation()
     ev.stopPropagation()
     return
     return
   }
   }

+ 28 - 11
packages/@uppy/dashboard/src/utils/trapFocus.js → packages/@uppy/dashboard/src/utils/trapFocus.ts

@@ -1,8 +1,12 @@
 import toArray from '@uppy/utils/lib/toArray'
 import toArray from '@uppy/utils/lib/toArray'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS'
 import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS'
-import getActiveOverlayEl from './getActiveOverlayEl.js'
+import getActiveOverlayEl from './getActiveOverlayEl.ts'
 
 
-function focusOnFirstNode (event, nodes) {
+type $TSFixMe = any
+
+function focusOnFirstNode(event: $TSFixMe, nodes: $TSFixMe) {
   const node = nodes[0]
   const node = nodes[0]
   if (node) {
   if (node) {
     node.focus()
     node.focus()
@@ -10,7 +14,7 @@ function focusOnFirstNode (event, nodes) {
   }
   }
 }
 }
 
 
-function focusOnLastNode (event, nodes) {
+function focusOnLastNode(event: $TSFixMe, nodes: $TSFixMe) {
   const node = nodes[nodes.length - 1]
   const node = nodes[nodes.length - 1]
   if (node) {
   if (node) {
     node.focus()
     node.focus()
@@ -24,13 +28,19 @@ function focusOnLastNode (event, nodes) {
 //    active overlay!
 //    active overlay!
 //    [Practical check] if we use (focusedItemIndex === -1), instagram provider in firefox will never get focus on its pics
 //    [Practical check] if we use (focusedItemIndex === -1), instagram provider in firefox will never get focus on its pics
 //    in the <ul>.
 //    in the <ul>.
-function isFocusInOverlay (activeOverlayEl) {
+function isFocusInOverlay(activeOverlayEl: $TSFixMe) {
   return activeOverlayEl.contains(document.activeElement)
   return activeOverlayEl.contains(document.activeElement)
 }
 }
 
 
-function trapFocus (event, activeOverlayType, dashboardEl) {
+function trapFocus(
+  event: $TSFixMe,
+  activeOverlayType: $TSFixMe,
+  dashboardEl: $TSFixMe,
+): void {
   const activeOverlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType)
   const activeOverlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType)
-  const focusableNodes = toArray(activeOverlayEl.querySelectorAll(FOCUSABLE_ELEMENTS))
+  const focusableNodes = toArray(
+    activeOverlayEl.querySelectorAll(FOCUSABLE_ELEMENTS),
+  )
 
 
   const focusedItemIndex = focusableNodes.indexOf(document.activeElement)
   const focusedItemIndex = focusableNodes.indexOf(document.activeElement)
 
 
@@ -40,21 +50,28 @@ function trapFocus (event, activeOverlayType, dashboardEl) {
   // plugins will try to focus on some important element as it loads.
   // plugins will try to focus on some important element as it loads.
   if (!isFocusInOverlay(activeOverlayEl)) {
   if (!isFocusInOverlay(activeOverlayEl)) {
     focusOnFirstNode(event, focusableNodes)
     focusOnFirstNode(event, focusableNodes)
-  // If we pressed shift + tab, and we're on the first element of a modal
+    // If we pressed shift + tab, and we're on the first element of a modal
   } else if (event.shiftKey && focusedItemIndex === 0) {
   } else if (event.shiftKey && focusedItemIndex === 0) {
     focusOnLastNode(event, focusableNodes)
     focusOnLastNode(event, focusableNodes)
-  // If we pressed tab, and we're on the last element of the modal
-  } else if (!event.shiftKey && focusedItemIndex === focusableNodes.length - 1) {
+    // If we pressed tab, and we're on the last element of the modal
+  } else if (
+    !event.shiftKey &&
+    focusedItemIndex === focusableNodes.length - 1
+  ) {
     focusOnFirstNode(event, focusableNodes)
     focusOnFirstNode(event, focusableNodes)
   }
   }
 }
 }
 
 
 // Traps focus inside of the currently open overlay (e.g. Dashboard, or e.g. Instagram),
 // Traps focus inside of the currently open overlay (e.g. Dashboard, or e.g. Instagram),
 // never lets focus disappear from the modal.
 // never lets focus disappear from the modal.
-export  { trapFocus as forModal }
+export { trapFocus as forModal }
 
 
 // Traps focus inside of the currently open overlay, unless overlay is null - then let the user tab away.
 // Traps focus inside of the currently open overlay, unless overlay is null - then let the user tab away.
-export function forInline (event, activeOverlayType, dashboardEl) {
+export function forInline(
+  event: $TSFixMe,
+  activeOverlayType: $TSFixMe,
+  dashboardEl: $TSFixMe,
+): void {
   // ___When we're in the bare 'Drop files here, paste, browse or import from' screen
   // ___When we're in the bare 'Drop files here, paste, browse or import from' screen
   if (activeOverlayType === null) {
   if (activeOverlayType === null) {
     // Do nothing and let the browser handle it, user can tab away from Uppy to other elements on the page
     // Do nothing and let the browser handle it, user can tab away from Uppy to other elements on the page

+ 60 - 0
packages/@uppy/dashboard/tsconfig.build.json

@@ -0,0 +1,60 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "noImplicitAny": false,
+    "outDir": "./lib",
+    "paths": {
+      "@uppy/informer": ["../informer/src/index.js"],
+      "@uppy/informer/lib/*": ["../informer/src/*"],
+      "@uppy/provider-views": ["../provider-views/src/index.js"],
+      "@uppy/provider-views/lib/*": ["../provider-views/src/*"],
+      "@uppy/status-bar": ["../status-bar/src/index.js"],
+      "@uppy/status-bar/lib/*": ["../status-bar/src/*"],
+      "@uppy/thumbnail-generator": ["../thumbnail-generator/src/index.js"],
+      "@uppy/thumbnail-generator/lib/*": ["../thumbnail-generator/src/*"],
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+      "@uppy/google-drive": ["../google-drive/src/index.js"],
+      "@uppy/google-drive/lib/*": ["../google-drive/src/*"],
+      "@uppy/url": ["../url/src/index.js"],
+      "@uppy/url/lib/*": ["../url/src/*"],
+      "@uppy/webcam": ["../webcam/src/index.js"],
+      "@uppy/webcam/lib/*": ["../webcam/src/*"]
+    },
+    "resolveJsonModule": false,
+    "rootDir": "./src",
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../informer/tsconfig.build.json"
+    },
+    {
+      "path": "../provider-views/tsconfig.build.json"
+    },
+    {
+      "path": "../status-bar/tsconfig.build.json"
+    },
+    {
+      "path": "../thumbnail-generator/tsconfig.build.json"
+    },
+    {
+      "path": "../utils/tsconfig.build.json"
+    },
+    {
+      "path": "../core/tsconfig.build.json"
+    },
+    {
+      "path": "../google-drive/tsconfig.build.json"
+    },
+    {
+      "path": "../url/tsconfig.build.json"
+    },
+    {
+      "path": "../webcam/tsconfig.build.json"
+    }
+  ]
+}

+ 56 - 0
packages/@uppy/dashboard/tsconfig.json

@@ -0,0 +1,56 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true,
+    "paths": {
+      "@uppy/informer": ["../informer/src/index.js"],
+      "@uppy/informer/lib/*": ["../informer/src/*"],
+      "@uppy/provider-views": ["../provider-views/src/index.js"],
+      "@uppy/provider-views/lib/*": ["../provider-views/src/*"],
+      "@uppy/status-bar": ["../status-bar/src/index.js"],
+      "@uppy/status-bar/lib/*": ["../status-bar/src/*"],
+      "@uppy/thumbnail-generator": ["../thumbnail-generator/src/index.js"],
+      "@uppy/thumbnail-generator/lib/*": ["../thumbnail-generator/src/*"],
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+      "@uppy/google-drive": ["../google-drive/src/index.js"],
+      "@uppy/google-drive/lib/*": ["../google-drive/src/*"],
+      "@uppy/url": ["../url/src/index.js"],
+      "@uppy/url/lib/*": ["../url/src/*"],
+      "@uppy/webcam": ["../webcam/src/index.js"],
+      "@uppy/webcam/lib/*": ["../webcam/src/*"],
+    },
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../informer/tsconfig.build.json",
+    },
+    {
+      "path": "../provider-views/tsconfig.build.json",
+    },
+    {
+      "path": "../status-bar/tsconfig.build.json",
+    },
+    {
+      "path": "../thumbnail-generator/tsconfig.build.json",
+    },
+    {
+      "path": "../utils/tsconfig.build.json",
+    },
+    {
+      "path": "../core/tsconfig.build.json",
+    },
+    {
+      "path": "../google-drive/tsconfig.build.json",
+    },
+    {
+      "path": "../url/tsconfig.build.json",
+    },
+    {
+      "path": "../webcam/tsconfig.build.json",
+    },
+  ],
+}

Some files were not shown because too many files changed in this diff