Просмотр исходного кода

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

Co-authored-by: Murderlon <merlijn@soverin.net>
Antoine du Hamel 1 год назад
Родитель
Сommit
5fcdd8f275
39 измененных файлов с 1918 добавлено и 1095 удалено
  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.*

Разница между файлами не показана из-за своего большого размера
+ 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 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 (
     <div
       className={classNames('uppy-Dashboard-AddFilesPanel', props.className)}
@@ -10,7 +13,11 @@ const AddFilesPanel = (props) => {
       aria-hidden={!props.showAddFilesPanel}
     >
       <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')}
         </div>
         <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 classNames from 'classnames'
 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
 // https://github.com/ghosh/micromodal
@@ -22,7 +23,9 @@ const HEIGHT_MD = 330
 // const HEIGHT_LG = 400
 // 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 isSingleFile = props.totalFileCount === 1
   const isSizeMD = props.containerWidth > WIDTH_MD
@@ -45,7 +48,8 @@ export default function Dashboard (props) {
     'uppy-Dashboard--isAddFilesPanelVisible': props.showAddFilesPanel,
     'uppy-Dashboard--isInnerWrapVisible': props.areInsidesReadyToBeVisible,
     // 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`.
@@ -60,11 +64,16 @@ export default function Dashboard (props) {
 
   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 = () => {
-    if (numberOfGhosts > 0) {
+    if (numberOfGhosts! > 0) {
       return props.i18n('recoveredXFiles', {
         smart_count: numberOfGhosts,
       })
@@ -78,10 +87,16 @@ export default function Dashboard (props) {
       className={dashboardClassName}
       data-uppy-theme={props.theme}
       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-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}
       onDragOver={props.handleDragOver}
       onDragLeave={props.handleDragLeave}
@@ -97,14 +112,13 @@ export default function Dashboard (props) {
       <div
         className="uppy-Dashboard-inner"
         aria-modal={!props.inline && 'true'}
-        role={!props.inline && 'dialog'}
+        role={props.inline ? undefined : 'dialog'}
         style={{
           width: props.inline && props.width ? props.width : '',
           height: props.inline && props.height ? props.height : '',
         }}
       >
-
-        {!props.inline ? (
+        {!props.inline ?
           <button
             className="uppy-u-reset uppy-Dashboard-close"
             type="button"
@@ -114,7 +128,7 @@ export default function Dashboard (props) {
           >
             <span aria-hidden="true">&times;</span>
           </button>
-        ) : null}
+        : null}
 
         <div className="uppy-Dashboard-innerWrap">
           <div className="uppy-Dashboard-dropFilesHereHint">
@@ -126,9 +140,19 @@ export default function Dashboard (props) {
 
           {numberOfFilesForRecovery && (
             <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">
-                  <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" />
                   <circle fill="#000" cx="12" cy="17" r="1" />
                 </g>
@@ -142,60 +166,70 @@ export default function Dashboard (props) {
             </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>
             {/* 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>
             {/* 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>
             {/* 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>
             {/* eslint-disable-next-line react/jsx-props-no-spreading */}
-            {props.showFileEditor ? <EditorPanel key="Editor" {...props} /> : null}
+            {props.showFileEditor ?
+              <EditorPanel key="Editor" {...props} />
+            : null}
           </Slide>
 
           <div className="uppy-Dashboard-progressindicators">
-            {props.progressindicators.map((target) => {
+            {props.progressindicators.map((target: $TSFixMe) => {
               return props.uppy.getPlugin(target.id).render(props.state)
             })}
           </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 classNames from 'classnames'
 
-function EditorPanel (props) {
+type $TSFixMe = any
+
+function EditorPanel(props: $TSFixMe): JSX.Element {
   const file = props.files[props.fileCardFor]
 
   const handleCancel = () => {
@@ -17,9 +20,17 @@ function EditorPanel (props) {
       id="uppy-DashboardContent-panel--editor"
     >
       <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', {
-            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>
         <button
@@ -38,7 +49,7 @@ function EditorPanel (props) {
         </button>
       </div>
       <div className="uppy-DashboardContent-panelBody">
-        {props.editors.map((target) => {
+        {props.editors.map((target: $TSFixMe) => {
           return props.uppy.getPlugin(target.id).render(props.state)
         })}
       </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 classNames from 'classnames'
 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 {
     files,
     fileCardFor,
@@ -23,8 +25,8 @@ export default function FileCard (props) {
   } = props
 
   const getMetaFields = () => {
-    return typeof metaFields === 'function'
-      ? metaFields(files[fileCardFor])
+    return typeof metaFields === 'function' ?
+        metaFields(files[fileCardFor])
       : metaFields
   }
 
@@ -32,19 +34,22 @@ export default function FileCard (props) {
   const computedMetaFields = getMetaFields() ?? []
   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] ?? ''
   })
 
   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({
       ...formState,
       [name]: newVal,
@@ -81,9 +86,17 @@ export default function FileCard (props) {
       onPaste={ignoreEvent}
     >
       <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', {
-            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>
         <button
@@ -98,14 +111,16 @@ export default function FileCard (props) {
       </div>
 
       <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} />
-          {showEditButton
-            && (
+          {showEditButton && (
             <button
               type="button"
               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.
                 // Otherwise it's confusing for the user to click save in the editor,
                 // 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'). */}
               {i18n('editImage')}
             </button>
-            )}
+          )}
         </div>
 
         <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,
   uploadInProgressOrComplete,
   metaFields,
   canEditFile,
   i18n,
   onClick,
-}) {
+}: $TSFixMe) {
   if (
-    (!uploadInProgressOrComplete && metaFields && metaFields.length > 0)
-    || (!uploadInProgressOrComplete && canEditFile(file))
+    (!uploadInProgressOrComplete && metaFields && metaFields.length > 0) ||
+    (!uploadInProgressOrComplete && canEditFile(file))
   ) {
     return (
       <button
@@ -21,11 +23,24 @@ function EditButton ({
         title={i18n('editFileWithFilename', { file: file.meta.name })}
         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">
-            <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" />
-            <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>
         </svg>
       </button>
@@ -34,7 +49,7 @@ function EditButton ({
   return null
 }
 
-function RemoveButton ({ i18n, onClick, file }) {
+function RemoveButton({ i18n, onClick, file }: $TSFixMe) {
   return (
     <button
       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 })}
       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 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>
     </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(() => {
       props.uppy.log('Link copied to clipboard.')
       props.uppy.info(props.i18n('copyLinkToClipboardSuccess'), 'info', 3000)
@@ -62,7 +90,7 @@ const copyLinkToClipboard = (event, props) => {
     .then(() => event.target.focus({ preventScroll: true }))
 }
 
-function CopyLinkButton (props) {
+function CopyLinkButton(props: $TSFixMe) {
   const { i18n } = props
 
   return (
@@ -73,14 +101,21 @@ function CopyLinkButton (props) {
       title={i18n('copyLink')}
       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" />
       </svg>
     </button>
   )
 }
 
-export default function Buttons (props) {
+export default function Buttons(props: $TSFixMe): ComponentChild {
   const {
     uppy,
     file,
@@ -112,21 +147,17 @@ export default function Buttons (props) {
         metaFields={metaFields}
         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
           i18n={i18n}
           file={file}
           uppy={uppy}
-          onClick={() => props.uppy.removeFile(file.id, 'removed-by-user')}
+          onClick={() => uppy.removeFile(file.id, 'removed-by-user')}
         />
-      ) : null}
+      : null}
     </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 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
 
-  function getMaxNameLength () {
+  function getMaxNameLength() {
     if (props.isSingleFile && props.containerHeight >= 350) {
       return 90
     }
@@ -29,7 +32,7 @@ const renderFileName = (props) => {
   )
 }
 
-const renderAuthor = (props) => {
+const renderAuthor = (props: $TSFixMe) => {
   const { author } = props.file.meta
   const providerName = props.file.remote?.providerName
   const dot = `\u00B7`
@@ -47,37 +50,39 @@ const renderAuthor = (props) => {
       >
         {truncateString(author.name, 13)}
       </a>
-      {providerName ? (
+      {providerName ?
         <>
           {` ${dot} `}
           {providerName}
           {` ${dot} `}
         </>
-      ) : null}
+      : null}
     </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) {
     return (
       <button
@@ -95,7 +100,7 @@ const ErrorButton = ({ file, onClick }) => {
   return null
 }
 
-export default function FileInfo (props) {
+export default function FileInfo(props: $TSFixMe): ComponentChild {
   const { file } = props
   return (
     <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.error && !props.hideRetryButton) {
@@ -15,7 +18,7 @@ function onPauseResumeCancelRetry (props) {
   }
 }
 
-function progressIndicatorTitle (props) {
+function progressIndicatorTitle(props: $TSFixMe) {
   if (props.isUploaded) {
     return props.i18n('uploadComplete')
   }
@@ -29,14 +32,15 @@ function progressIndicatorTitle (props) {
       return props.i18n('resumeUpload')
     }
     return props.i18n('pauseUpload')
-  } if (props.individualCancellation) {
+  }
+  if (props.individualCancellation) {
     return props.i18n('cancelUpload')
   }
 
   return ''
 }
 
-function ProgressIndicatorButton (props) {
+function ProgressIndicatorButton(props: $TSFixMe) {
   return (
     <div className="uppy-Dashboard-Item-progress">
       <button
@@ -52,7 +56,7 @@ function ProgressIndicatorButton (props) {
   )
 }
 
-function ProgressCircleContainer ({ children }) {
+function ProgressCircleContainer({ children }: $TSFixMe) {
   return (
     <svg
       aria-hidden="true"
@@ -67,7 +71,7 @@ function ProgressCircleContainer ({ children }) {
   )
 }
 
-function ProgressCircle ({ progress }) {
+function ProgressCircle({ progress }: $TSFixMe) {
   // circle length equals 2 * PI * R
   const circleLength = 2 * Math.PI * 15
 
@@ -90,13 +94,13 @@ function ProgressCircle ({ progress }) {
         fill="none"
         stroke-width="2"
         stroke-dasharray={circleLength}
-        stroke-dashoffset={circleLength - ((circleLength / 100) * progress)}
+        stroke-dashoffset={circleLength - (circleLength / 100) * progress}
       />
     </g>
   )
 }
 
-export default function FileProgress (props) {
+export default function FileProgress(props: $TSFixMe): ComponentChild {
   // Nothing if upload has not started
   if (!props.file.progress.uploadStarted) {
     return null
@@ -109,7 +113,11 @@ export default function FileProgress (props) {
         <div className="uppy-Dashboard-Item-progressIndicator">
           <ProgressCircleContainer>
             <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>
         </div>
       </div>
@@ -125,7 +133,14 @@ export default function FileProgress (props) {
     return (
       // eslint-disable-next-line react/jsx-props-no-spreading
       <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="M7.9 3H10v2H7.9z" />
           <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}>
         <ProgressCircleContainer>
           <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>
       </ProgressIndicatorButton>
@@ -158,13 +177,21 @@ export default function FileProgress (props) {
   }
 
   // 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 (
       // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
         <ProgressCircleContainer>
           <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>
       </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'
 
-const metaFieldIdToName = (metaFieldId, metaFields) => {
+type $TSFixMe = any
+
+const metaFieldIdToName = (metaFieldId: $TSFixMe, metaFields: $TSFixMe) => {
   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
 }
 
-export default function renderMissingMetaFieldsError (props) {
+export default function MetaErrorMessage(props: $TSFixMe): JSX.Element {
   const { file, toggleFileCard, i18n, metaFields } = props
   const { missingRequiredMetaFields } = file
   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 (
     <div className="uppy-Dashboard-Item-errorMessage">
       {i18n('missingRequiredMetaFields', {
         smart_count: missingRequiredMetaFields.length,
         fields: metaFieldsString,
-      })}
-      {' '}
+      })}{' '}
       <button
         type="button"
         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 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 {
-  componentDidMount () {
+  componentDidMount(): void {
     const { file } = this.props
     if (!file.preview) {
       this.props.handleRequestThumbnail(file)
     }
   }
 
-  shouldComponentUpdate (nextProps) {
+  shouldComponentUpdate(nextProps: $TSFixMe): boolean {
     return !shallowEqual(this.props, nextProps)
   }
 
   // VirtualList mounts FileItems again and they emit `thumbnail:request`
   // Otherwise thumbnails are broken or missing after Golden Retriever restores files
-  componentDidUpdate () {
+  componentDidUpdate(): void {
     const { file } = this.props
     if (!file.preview) {
       this.props.handleRequestThumbnail(file)
     }
   }
 
-  componentWillUnmount () {
+  componentWillUnmount(): void {
     const { file } = this.props
     if (!file.preview) {
       this.props.handleCancelThumbnail(file)
     }
   }
 
-  render () {
+  render(): ComponentChild {
     const { file } = this.props
 
     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
 
     // 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
     const { isGhost } = file
 
-    let showRemoveButton = this.props.individualCancellation
-      ? !isUploaded
+    let showRemoveButton =
+      this.props.individualCancellation ?
+        !isUploaded
       : !uploadInProgress && !isUploaded
 
     if (isUploaded && this.props.showRemoveButtonAfterComplete) {
@@ -89,7 +100,9 @@ export default class FileItem extends Component {
             hideCancelButton={this.props.hideCancelButton}
             hidePauseResumeButton={this.props.hidePauseResumeButton}
             recoveredState={this.props.recoveredState}
-            showRemoveButtonAfterComplete={this.props.showRemoveButtonAfterComplete}
+            showRemoveButtonAfterComplete={
+              this.props.showRemoveButtonAfterComplete
+            }
             resumableUploads={this.props.resumableUploads}
             individualCancellation={this.props.individualCancellation}
             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 { useMemo } from 'preact/hooks'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 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) {
       currentChunk.push(item)
     } else {
@@ -18,37 +22,63 @@ function chunks (list, size) {
   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 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
 
   // Sort files by file.isGhost, ghost files first, only if recoveredState is present
   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)
     if (recoveredState) fileIds.sort(sortByGhostComesFirst)
     return chunks(fileIds, itemsPerRow)
   }, [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.
     // 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]}>
-      {row.map((fileID) => (
+      {row.map((fileID: $TSFixMe) => (
         <FileItem
           key={fileID}
+          // @ts-expect-error it's fine
           uppy={uppy}
           // FIXME This is confusing, it's actually the Dashboard's plugin ID
           id={id}
@@ -86,11 +116,7 @@ export default ({
   )
 
   if (isSingleFile) {
-    return (
-      <div class="uppy-Dashboard-files">
-        {renderRow(rows[0])}
-      </div>
-    )
+    return <div class="uppy-Dashboard-files">{renderRow(rows[0])}</div>
   }
 
   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 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
 
   if (file.preview) {
@@ -18,8 +20,17 @@ export default function FilePreview (props) {
 
   return (
     <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" />
       </svg>
     </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 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 (
     <div
       className={classNames('uppy-DashboardContent-panel', className)}
@@ -15,7 +24,11 @@ function PickerPanelContent ({ activePickerPanel, className, hideAllPanels, i18n
       onPaste={ignoreEvent}
     >
       <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 })}
         </div>
         <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'
 
+type $TSFixMe = any
+
 const uploadStates = {
   STATE_ERROR: 'error',
   STATE_WAITING: 'waiting',
@@ -10,7 +13,12 @@ const uploadStates = {
   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) {
     return uploadStates.STATE_ERROR
   }
@@ -26,7 +34,7 @@ function getUploadingState (isAllErrored, isAllComplete, isAllPaused, files = {}
   let state = uploadStates.STATE_WAITING
   const fileIDs = Object.keys(files)
   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 (progress.uploadStarted && !progress.uploadComplete) {
       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
     // 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
     }
   }
   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(
     isAllErrored,
     isAllComplete,
@@ -58,7 +76,9 @@ function UploadStatus ({
 
   switch (uploadingState) {
     case 'uploading':
-      return i18n('uploadingXFiles', { smart_count: inProgressNotPausedFiles.length })
+      return i18n('uploadingXFiles', {
+        smart_count: inProgressNotPausedFiles.length,
+      })
     case 'preprocessing':
     case 'postprocessing':
       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
   // TODO maybe this should be done in ../Dashboard.jsx, then just pass that down as `allowNewUpload`
   if (allowNewUpload && maxNumberOfFiles) {
@@ -85,7 +112,7 @@ function PanelTopBar (props) {
 
   return (
     <div className="uppy-DashboardContent-bar">
-      {!isAllComplete && !hideCancelButton ? (
+      {!isAllComplete && !hideCancelButton ?
         <button
           className="uppy-DashboardContent-back"
           type="button"
@@ -93,16 +120,18 @@ function PanelTopBar (props) {
         >
           {i18n('cancel')}
         </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 */}
         <UploadStatus {...props} />
       </div>
 
-      {allowNewUpload ? (
+      {allowNewUpload ?
         <button
           className="uppy-DashboardContent-addMore"
           type="button"
@@ -110,14 +139,21 @@ function PanelTopBar (props) {
           title={i18n('addMoreFiles')}
           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" />
           </svg>
-          <span className="uppy-DashboardContent-addMoreCaption">{i18n('addMore')}</span>
+          <span className="uppy-DashboardContent-addMoreCaption">
+            {i18n('addMore')}
+          </span>
         </button>
-      ) : (
-        <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 Core from '@uppy/core'
+import Core, { UIPlugin } from '@uppy/core'
 import StatusBarPlugin from '@uppy/status-bar'
 import GoogleDrivePlugin from '@uppy/google-drive'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import WebcamPlugin from '@uppy/webcam'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import Url from '@uppy/url'
 
 import resizeObserverPolyfill from 'resize-observer-polyfill'
-import DashboardPlugin from '../lib/index.js'
+import DashboardPlugin from './index.ts'
+
+type $TSFixMe = any
 
 describe('Dashboard', () => {
   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(() => {
+    // @ts-expect-error we're touching globals for the test
     delete globalThis.ResizeObserver
   })
 
@@ -48,7 +58,10 @@ describe('Dashboard', () => {
         inline: true,
         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()
 
     core.close()
@@ -75,12 +88,15 @@ describe('Dashboard', () => {
     core.use(DashboardPlugin, { inline: false })
     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
     expect(dashboardPlugins.length).toEqual(4)
     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()
   })
@@ -92,12 +108,15 @@ describe('Dashboard', () => {
     core.use(DashboardPlugin, { inline: false })
     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
     expect(dashboardPlugins.length).toEqual(3)
     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()
   })
@@ -109,13 +128,11 @@ describe('Dashboard', () => {
       target: 'body',
     })
 
-    core.getPlugin('Dashboard').setOptions({
+    core.getPlugin('Dashboard')!.setOptions({
       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()', () => {
@@ -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', () => {
     const core = new Core()
     expect(() => {
       core.use(DashboardPlugin, {
-        metaFields: (file) => {
+        metaFields: (file: any) => {
           const fields = [{ id: 'name', name: 'File name' }]
           if (file.type.startsWith('image/')) {
             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 copyToClipboard from './copyToClipboard.js'
+import copyToClipboard from './copyToClipboard.ts'
 
 describe('copyToClipboard', () => {
   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
  * @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')
     textArea.setAttribute('style', {
       position: 'fixed',
@@ -22,13 +28,13 @@ export default function copyToClipboard (textToCopy, fallbackString = 'Copy the
       outline: 'none',
       boxShadow: 'none',
       background: 'transparent',
-    })
+    } as $TSFixMe as string)
 
     textArea.value = textToCopy
     document.body.appendChild(textArea)
     textArea.select()
 
-    const magicCopyFailed = () => {
+    const magicCopyFailed = (cause?: unknown) => {
       document.body.removeChild(textArea)
       // eslint-disable-next-line no-alert
       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 createSuperFocus from './createSuperFocus.js'
+import createSuperFocus from './createSuperFocus.ts'
 
 describe('createSuperFocus', () => {
   // 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'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 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.
@@ -12,10 +16,10 @@ import getActiveOverlayEl from './getActiveOverlayEl.js'
   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.
 */
-export default function createSuperFocus () {
+export default function createSuperFocus(): $TSFixMe {
   let lastFocusWasOnSuperFocusableEl = false
 
-  const superFocus = (dashboardEl, activeOverlayType) => {
+  const superFocus = (dashboardEl: $TSFixMe, activeOverlayType: $TSFixMe) => {
     const overlayEl = getActiveOverlayEl(dashboardEl, activeOverlayType)
 
     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.
     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.
     // [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.

+ 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 —
 // 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
-  if (tagName === 'INPUT'
-      || tagName === 'TEXTAREA') {
+  if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
     ev.stopPropagation()
     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'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 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]
   if (node) {
     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]
   if (node) {
     node.focus()
@@ -24,13 +28,19 @@ function focusOnLastNode (event, nodes) {
 //    active overlay!
 //    [Practical check] if we use (focusedItemIndex === -1), instagram provider in firefox will never get focus on its pics
 //    in the <ul>.
-function isFocusInOverlay (activeOverlayEl) {
+function isFocusInOverlay(activeOverlayEl: $TSFixMe) {
   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 focusableNodes = toArray(activeOverlayEl.querySelectorAll(FOCUSABLE_ELEMENTS))
+  const focusableNodes = toArray(
+    activeOverlayEl.querySelectorAll(FOCUSABLE_ELEMENTS),
+  )
 
   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.
   if (!isFocusInOverlay(activeOverlayEl)) {
     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) {
     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)
   }
 }
 
 // Traps focus inside of the currently open overlay (e.g. Dashboard, or e.g. Instagram),
 // 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.
-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
   if (activeOverlayType === null) {
     // 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",
+    },
+  ],
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов