فهرست منبع

@uppy/dashboard: refactor to ESM (#3701)

Antoine du Hamel 2 سال پیش
والد
کامیت
327509a7dc
34فایلهای تغییر یافته به همراه1311 افزوده شده و 1274 حذف شده
  1. 9 0
      .eslintrc.js
  2. 2 0
      packages/@uppy/dashboard/package.json
  3. 1112 0
      packages/@uppy/dashboard/src/Dashboard.jsx
  4. 3 3
      packages/@uppy/dashboard/src/components/AddFiles.jsx
  5. 5 4
      packages/@uppy/dashboard/src/components/AddFilesPanel.jsx
  6. 19 12
      packages/@uppy/dashboard/src/components/Dashboard.jsx
  7. 3 3
      packages/@uppy/dashboard/src/components/EditorPanel.jsx
  8. 7 7
      packages/@uppy/dashboard/src/components/FileCard/index.jsx
  9. 3 3
      packages/@uppy/dashboard/src/components/FileItem/Buttons/index.jsx
  10. 5 5
      packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.jsx
  11. 5 5
      packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.jsx
  12. 6 3
      packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.jsx
  13. 2 2
      packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.jsx
  14. 8 8
      packages/@uppy/dashboard/src/components/FileItem/index.jsx
  15. 6 6
      packages/@uppy/dashboard/src/components/FileList.jsx
  16. 3 3
      packages/@uppy/dashboard/src/components/FilePreview.jsx
  17. 0 36
      packages/@uppy/dashboard/src/components/PickerPanelContent.js
  18. 36 0
      packages/@uppy/dashboard/src/components/PickerPanelContent.jsx
  19. 28 21
      packages/@uppy/dashboard/src/components/PickerPanelTopBar.jsx
  20. 3 3
      packages/@uppy/dashboard/src/components/Slide.jsx
  21. 3 2
      packages/@uppy/dashboard/src/components/VirtualList.jsx
  22. 1 1109
      packages/@uppy/dashboard/src/index.js
  23. 8 5
      packages/@uppy/dashboard/src/index.test.js
  24. 1 1
      packages/@uppy/dashboard/src/locale.js
  25. 3 2
      packages/@uppy/dashboard/src/utils/copyToClipboard.js
  26. 2 1
      packages/@uppy/dashboard/src/utils/copyToClipboard.test.js
  27. 4 4
      packages/@uppy/dashboard/src/utils/createSuperFocus.js
  28. 2 1
      packages/@uppy/dashboard/src/utils/createSuperFocus.test.js
  29. 1 1
      packages/@uppy/dashboard/src/utils/getActiveOverlayEl.js
  30. 2 2
      packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx
  31. 1 1
      packages/@uppy/dashboard/src/utils/ignoreEvent.js
  32. 16 20
      packages/@uppy/dashboard/src/utils/trapFocus.js
  33. 1 1
      website/src/docs/dashboard.md
  34. 1 0
      yarn.lock

+ 9 - 0
.eslintrc.js

@@ -201,6 +201,7 @@ module.exports = {
         'packages/@uppy/aws-s3-multipart/src/**/*.js',
         'packages/@uppy/box/src/**/*.js',
         'packages/@uppy/compressor/src/**/*.js',
+        'packages/@uppy/dashboard/src/**/*.js',
         'packages/@uppy/drag-drop/src/**/*.js',
         'packages/@uppy/drop-target/src/**/*.js',
         'packages/@uppy/dropbox/src/**/*.js',
@@ -269,6 +270,14 @@ module.exports = {
         'import/extensions': ['error', 'ignorePackages'],
       },
     },
+    {
+      files: [
+        'packages/@uppy/dashboard/src/components/**/*.jsx',
+      ],
+      rules: {
+        'react/destructuring-assignment': 'off',
+      },
+    },
     {
       files: [
         // Those need looser rules, and cannot be made part of the stricter rules above.

+ 2 - 0
packages/@uppy/dashboard/package.json

@@ -6,6 +6,7 @@
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "types": "types/index.d.ts",
+  "type": "module",
   "keywords": [
     "file uploader",
     "uppy",
@@ -36,6 +37,7 @@
     "preact": "^10.5.13"
   },
   "devDependencies": {
+    "@jest/globals": "^27.4.2",
     "@uppy/google-drive": "workspace:^",
     "@uppy/status-bar": "workspace:^",
     "resize-observer-polyfill": "^1.5.0"

+ 1112 - 0
packages/@uppy/dashboard/src/Dashboard.jsx

@@ -0,0 +1,1112 @@
+import { h } from 'preact'
+import { UIPlugin } from '@uppy/core'
+import StatusBar from '@uppy/status-bar'
+import Informer from '@uppy/informer'
+import ThumbnailGenerator from '@uppy/thumbnail-generator'
+import findAllDOMElements from '@uppy/utils/lib/findAllDOMElements'
+import toArray from '@uppy/utils/lib/toArray'
+import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
+import { nanoid } from 'nanoid/non-secure'
+import memoizeOne from 'memoize-one'
+import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS.js'
+import * as trapFocus from './utils/trapFocus.js'
+import createSuperFocus from './utils/createSuperFocus.js'
+import DashboardUI from './components/Dashboard.jsx'
+
+import packageJson from '../package.json'
+import locale from './locale.js'
+
+const memoize = memoizeOne.default || memoizeOne
+
+const TAB_KEY = 9
+const ESC_KEY = 27
+
+function createPromise () {
+  const o = {}
+  o.promise = new Promise((resolve, reject) => {
+    o.resolve = resolve
+    o.reject = reject
+  })
+  return o
+}
+
+function defaultPickerIcon () {
+  return (
+    <svg aria-hidden="true" focusable="false" width="30" height="30" viewBox="0 0 30 30">
+      <path d="M15 30c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15C6.716 0 0 6.716 0 15c0 8.284 6.716 15 15 15zm4.258-12.676v6.846h-8.426v-6.846H5.204l9.82-12.364 9.82 12.364H19.26z" />
+    </svg>
+  )
+}
+
+/**
+ * Dashboard UI with previews, metadata editing, tabs for various services and more
+ */
+export default class Dashboard extends UIPlugin {
+  static VERSION = packageJson.version
+
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'Dashboard'
+    this.title = 'Dashboard'
+    this.type = 'orchestrator'
+    this.modalName = `uppy-Dashboard-${nanoid()}`
+
+    this.defaultLocale = locale
+
+    // set default options
+    const defaultOptions = {
+      target: 'body',
+      metaFields: [],
+      trigger: null,
+      inline: false,
+      width: 750,
+      height: 550,
+      thumbnailWidth: 280,
+      thumbnailType: 'image/jpeg',
+      waitForThumbnailsBeforeUpload: false,
+      defaultPickerIcon,
+      showLinkToFileUploadResult: false,
+      showProgressDetails: false,
+      hideUploadButton: false,
+      hideCancelButton: false,
+      hideRetryButton: false,
+      hidePauseResumeButton: false,
+      hideProgressAfterFinish: false,
+      doneButtonHandler: () => {
+        this.uppy.reset()
+        this.requestCloseModal()
+      },
+      note: null,
+      closeModalOnClickOutside: false,
+      closeAfterFinish: false,
+      disableStatusBar: false,
+      disableInformer: false,
+      disableThumbnailGenerator: false,
+      disablePageScrollWhenModalOpen: true,
+      animateOpenClose: true,
+      fileManagerSelectionType: 'files',
+      proudlyDisplayPoweredByUppy: true,
+      onRequestCloseModal: () => this.closeModal(),
+      showSelectedFiles: true,
+      showRemoveButtonAfterComplete: false,
+      browserBackButtonClose: false,
+      theme: 'light',
+      autoOpenFileEditor: false,
+      disabled: false,
+      disableLocalFiles: false,
+    }
+
+    // merge default options with the ones set by user
+    this.opts = { ...defaultOptions, ...opts }
+
+    this.i18nInit()
+
+    this.superFocus = createSuperFocus()
+    this.ifFocusedOnUppyRecently = false
+
+    // Timeouts
+    this.makeDashboardInsidesVisibleAnywayTimeout = null
+    this.removeDragOverClassTimeout = null
+  }
+
+  removeTarget = (plugin) => {
+    const pluginState = this.getPluginState()
+    // filter out the one we want to remove
+    const newTargets = pluginState.targets.filter(target => target.id !== plugin.id)
+
+    this.setPluginState({
+      targets: newTargets,
+    })
+  }
+
+  addTarget = (plugin) => {
+    const callerPluginId = plugin.id || plugin.constructor.name
+    const callerPluginName = plugin.title || callerPluginId
+    const callerPluginType = plugin.type
+
+    if (callerPluginType !== 'acquirer'
+        && callerPluginType !== 'progressindicator'
+        && callerPluginType !== 'editor') {
+      const msg = 'Dashboard: can only be targeted by plugins of types: acquirer, progressindicator, editor'
+      this.uppy.log(msg, 'error')
+      return undefined
+    }
+
+    const target = {
+      id: callerPluginId,
+      name: callerPluginName,
+      type: callerPluginType,
+    }
+
+    const state = this.getPluginState()
+    const newTargets = state.targets.slice()
+    newTargets.push(target)
+
+    this.setPluginState({
+      targets: newTargets,
+    })
+
+    return this.el
+  }
+
+  hideAllPanels = () => {
+    const state = this.getPluginState()
+    const update = {
+      activePickerPanel: false,
+      showAddFilesPanel: false,
+      activeOverlayType: null,
+      fileCardFor: null,
+      showFileEditor: false,
+    }
+
+    if (state.activePickerPanel === update.activePickerPanel
+        && state.showAddFilesPanel === update.showAddFilesPanel
+        && state.showFileEditor === update.showFileEditor
+        && state.activeOverlayType === update.activeOverlayType) {
+      // avoid doing a state update if nothing changed
+      return
+    }
+
+    this.setPluginState(update)
+  }
+
+  showPanel = (id) => {
+    const { targets } = this.getPluginState()
+
+    const activePickerPanel = targets.filter((target) => {
+      return target.type === 'acquirer' && target.id === id
+    })[0]
+
+    this.setPluginState({
+      activePickerPanel,
+      activeOverlayType: 'PickerPanel',
+    })
+  }
+
+  canEditFile = (file) => {
+    const { targets } = this.getPluginState()
+    const editors = this.#getEditors(targets)
+
+    return editors.some((target) => (
+      this.uppy.getPlugin(target.id).canEditFile(file)
+    ))
+  }
+
+  openFileEditor = (file) => {
+    const { targets } = this.getPluginState()
+    const editors = this.#getEditors(targets)
+
+    this.setPluginState({
+      showFileEditor: true,
+      fileCardFor: file.id || null,
+      activeOverlayType: 'FileEditor',
+    })
+
+    editors.forEach((editor) => {
+      this.uppy.getPlugin(editor.id).selectFile(file)
+    })
+  }
+
+  saveFileEditor = () => {
+    const { targets } = this.getPluginState()
+    const editors = this.#getEditors(targets)
+
+    editors.forEach((editor) => {
+      this.uppy.getPlugin(editor.id).save()
+    })
+
+    this.hideAllPanels()
+  }
+
+  openModal = () => {
+    const { promise, resolve } = createPromise()
+    // save scroll position
+    this.savedScrollPosition = window.pageYOffset
+    // save active element, so we can restore focus when modal is closed
+    this.savedActiveElement = document.activeElement
+
+    if (this.opts.disablePageScrollWhenModalOpen) {
+      document.body.classList.add('uppy-Dashboard-isFixed')
+    }
+
+    if (this.opts.animateOpenClose && this.getPluginState().isClosing) {
+      const handler = () => {
+        this.setPluginState({
+          isHidden: false,
+        })
+        this.el.removeEventListener('animationend', handler, false)
+        resolve()
+      }
+      this.el.addEventListener('animationend', handler, false)
+    } else {
+      this.setPluginState({
+        isHidden: false,
+      })
+      resolve()
+    }
+
+    if (this.opts.browserBackButtonClose) {
+      this.updateBrowserHistory()
+    }
+
+    // handle ESC and TAB keys in modal dialog
+    document.addEventListener('keydown', this.handleKeyDownInModal)
+
+    this.uppy.emit('dashboard:modal-open')
+
+    return promise
+  }
+
+  closeModal = (opts = {}) => {
+    const {
+      // Whether the modal is being closed by the user (`true`) or by other means (e.g. browser back button)
+      manualClose = true,
+    } = opts
+
+    const { isHidden, isClosing } = this.getPluginState()
+    if (isHidden || isClosing) {
+      // short-circuit if animation is ongoing
+      return undefined
+    }
+
+    const { promise, resolve } = createPromise()
+
+    if (this.opts.disablePageScrollWhenModalOpen) {
+      document.body.classList.remove('uppy-Dashboard-isFixed')
+    }
+
+    if (this.opts.animateOpenClose) {
+      this.setPluginState({
+        isClosing: true,
+      })
+      const handler = () => {
+        this.setPluginState({
+          isHidden: true,
+          isClosing: false,
+        })
+
+        this.superFocus.cancel()
+        this.savedActiveElement.focus()
+
+        this.el.removeEventListener('animationend', handler, false)
+        resolve()
+      }
+      this.el.addEventListener('animationend', handler, false)
+    } else {
+      this.setPluginState({
+        isHidden: true,
+      })
+
+      this.superFocus.cancel()
+      this.savedActiveElement.focus()
+
+      resolve()
+    }
+
+    // handle ESC and TAB keys in modal dialog
+    document.removeEventListener('keydown', this.handleKeyDownInModal)
+
+    if (manualClose) {
+      if (this.opts.browserBackButtonClose) {
+        // Make sure that the latest entry in the history state is our modal name
+        // eslint-disable-next-line no-restricted-globals
+        if (history.state?.[this.modalName]) {
+          // Go back in history to clear out the entry we created (ultimately closing the modal)
+          // eslint-disable-next-line no-restricted-globals
+          history.back()
+        }
+      }
+    }
+
+    this.uppy.emit('dashboard:modal-closed')
+
+    return promise
+  }
+
+  isModalOpen = () => {
+    return !this.getPluginState().isHidden || false
+  }
+
+  requestCloseModal = () => {
+    if (this.opts.onRequestCloseModal) {
+      return this.opts.onRequestCloseModal()
+    }
+    return this.closeModal()
+  }
+
+  setDarkModeCapability = (isDarkModeOn) => {
+    const { capabilities } = this.uppy.getState()
+    this.uppy.setState({
+      capabilities: {
+        ...capabilities,
+        darkMode: isDarkModeOn,
+      },
+    })
+  }
+
+  handleSystemDarkModeChange = (event) => {
+    const isDarkModeOnNow = event.matches
+    this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnNow ? 'on' : 'off'}`)
+    this.setDarkModeCapability(isDarkModeOnNow)
+  }
+
+  toggleFileCard = (show, fileID) => {
+    const file = this.uppy.getFile(fileID)
+    if (show) {
+      this.uppy.emit('dashboard:file-edit-start', file)
+    } else {
+      this.uppy.emit('dashboard:file-edit-complete', file)
+    }
+
+    this.setPluginState({
+      fileCardFor: show ? fileID : null,
+      activeOverlayType: show ? 'FileCard' : null,
+    })
+  }
+
+  toggleAddFilesPanel = (show) => {
+    this.setPluginState({
+      showAddFilesPanel: show,
+      activeOverlayType: show ? 'AddFiles' : null,
+    })
+  }
+
+  addFiles = (files) => {
+    const descriptors = files.map((file) => ({
+      source: this.id,
+      name: file.name,
+      type: file.type,
+      data: file,
+      meta: {
+        // path of the file relative to the ancestor directory the user selected.
+        // e.g. 'docs/Old Prague/airbnb.pdf'
+        relativePath: file.relativePath || null,
+      },
+    }))
+
+    try {
+      this.uppy.addFiles(descriptors)
+    } catch (err) {
+      this.uppy.log(err)
+    }
+  }
+
+  // ___Why make insides of Dashboard invisible until first ResizeObserver event is emitted?
+  //    ResizeOberserver doesn't emit the first resize event fast enough, users can see the jump from one .uppy-size-- to
+  //    another (e.g. in Safari)
+  // ___Why not apply visibility property to .uppy-Dashboard-inner?
+  //    Because ideally, acc to specs, ResizeObserver should see invisible elements as of width 0. So even though applying
+  //    invisibility to .uppy-Dashboard-inner works now, it may not work in the future.
+  startListeningToResize = () => {
+    // Watch for Dashboard container (`.uppy-Dashboard-inner`) resize
+    // and update containerWidth/containerHeight in plugin state accordingly.
+    // Emits first event on initialization.
+    this.resizeObserver = new ResizeObserver((entries) => {
+      const uppyDashboardInnerEl = entries[0]
+
+      const { width, height } = uppyDashboardInnerEl.contentRect
+
+      this.uppy.log(`[Dashboard] resized: ${width} / ${height}`, 'debug')
+
+      this.setPluginState({
+        containerWidth: width,
+        containerHeight: height,
+        areInsidesReadyToBeVisible: true,
+      })
+    })
+    this.resizeObserver.observe(this.el.querySelector('.uppy-Dashboard-inner'))
+
+    // If ResizeObserver fails to emit an event telling us what size to use - default to the mobile view
+    this.makeDashboardInsidesVisibleAnywayTimeout = setTimeout(() => {
+      const pluginState = this.getPluginState()
+      const isModalAndClosed = !this.opts.inline && pluginState.isHidden
+      if (
+        // if ResizeObserver hasn't yet fired,
+        !pluginState.areInsidesReadyToBeVisible
+        // and it's not due to the modal being closed
+        && !isModalAndClosed
+      ) {
+        this.uppy.log("[Dashboard] resize event didn't fire on time: defaulted to mobile layout", 'debug')
+
+        this.setPluginState({
+          areInsidesReadyToBeVisible: true,
+        })
+      }
+    }, 1000)
+  }
+
+  stopListeningToResize = () => {
+    this.resizeObserver.disconnect()
+
+    clearTimeout(this.makeDashboardInsidesVisibleAnywayTimeout)
+  }
+
+  // Records whether we have been interacting with uppy right now,
+  // which is then used to determine whether state updates should trigger a refocusing.
+  recordIfFocusedOnUppyRecently = (event) => {
+    if (this.el.contains(event.target)) {
+      this.ifFocusedOnUppyRecently = true
+    } else {
+      this.ifFocusedOnUppyRecently = false
+      // ___Why run this.superFocus.cancel here when it already runs in superFocusOnEachUpdate?
+      //    Because superFocus is debounced, when we move from Uppy to some other element on the page,
+      //    previously run superFocus sometimes hits and moves focus back to Uppy.
+      this.superFocus.cancel()
+    }
+  }
+
+  disableAllFocusableElements = (disable) => {
+    const focusableNodes = toArray(this.el.querySelectorAll(FOCUSABLE_ELEMENTS))
+    if (disable) {
+      focusableNodes.forEach((node) => {
+        // save previous tabindex in a data-attribute, to restore when enabling
+        const currentTabIndex = node.getAttribute('tabindex')
+        if (currentTabIndex) {
+          node.dataset.inertTabindex = currentTabIndex // eslint-disable-line no-param-reassign
+        }
+        node.setAttribute('tabindex', '-1')
+      })
+    } else {
+      focusableNodes.forEach((node) => {
+        if ('inertTabindex' in node.dataset) {
+          node.setAttribute('tabindex', node.dataset.inertTabindex)
+        } else {
+          node.removeAttribute('tabindex')
+        }
+      })
+    }
+    this.dashboardIsDisabled = disable
+  }
+
+  updateBrowserHistory = () => {
+    // Ensure history state does not already contain our modal name to avoid double-pushing
+    // eslint-disable-next-line no-restricted-globals
+    if (!history.state?.[this.modalName]) {
+      // Push to history so that the page is not lost on browser back button press
+      // eslint-disable-next-line no-restricted-globals
+      history.pushState({
+        // eslint-disable-next-line no-restricted-globals
+        ...history.state,
+        [this.modalName]: true,
+      }, '')
+    }
+
+    // Listen for back button presses
+    window.addEventListener('popstate', this.handlePopState, false)
+  }
+
+  handlePopState = (event) => {
+    // Close the modal if the history state no longer contains our modal name
+    if (this.isModalOpen() && (!event.state || !event.state[this.modalName])) {
+      this.closeModal({ manualClose: false })
+    }
+
+    // When the browser back button is pressed and uppy is now the latest entry
+    // in the history but the modal is closed, fix the history by removing the
+    // uppy history entry.
+    // This occurs when another entry is added into the history state while the
+    // modal is open, and then the modal gets manually closed.
+    // Solves PR #575 (https://github.com/transloadit/uppy/pull/575)
+    if (!this.isModalOpen() && event.state?.[this.modalName]) {
+      // eslint-disable-next-line no-restricted-globals
+      history.back()
+    }
+  }
+
+  handleKeyDownInModal = (event) => {
+    // close modal on esc key press
+    if (event.keyCode === ESC_KEY) this.requestCloseModal(event)
+    // trap focus on tab key press
+    if (event.keyCode === TAB_KEY) trapFocus.forModal(event, this.getPluginState().activeOverlayType, this.el)
+  }
+
+  handleClickOutside = () => {
+    if (this.opts.closeModalOnClickOutside) this.requestCloseModal()
+  }
+
+  handlePaste = (event) => {
+    // Let any acquirer plugin (Url/Webcam/etc.) handle pastes to the root
+    this.uppy.iteratePlugins((plugin) => {
+      if (plugin.type === 'acquirer') {
+        // Every Plugin with .type acquirer can define handleRootPaste(event)
+        plugin.handleRootPaste?.(event)
+      }
+    })
+
+    // Add all dropped files
+    const files = toArray(event.clipboardData.files)
+    if (files.length > 0) {
+      this.uppy.log('[Dashboard] Files pasted')
+      this.addFiles(files)
+    }
+  }
+
+  handleInputChange = (event) => {
+    event.preventDefault()
+    const files = toArray(event.target.files)
+    if (files.length > 0) {
+      this.uppy.log('[Dashboard] Files selected through input')
+      this.addFiles(files)
+    }
+  }
+
+  handleDragOver = (event) => {
+    event.preventDefault()
+    event.stopPropagation()
+
+    // Check if some plugin can handle the datatransfer without files —
+    // for instance, the Url plugin can import a url
+    const canSomePluginHandleRootDrop = () => {
+      let somePluginCanHandleRootDrop = true
+      this.uppy.iteratePlugins((plugin) => {
+        if (plugin.canHandleRootDrop?.(event)) {
+          somePluginCanHandleRootDrop = true
+        }
+      })
+      return somePluginCanHandleRootDrop
+    }
+
+    // Check if the "type" of the datatransfer object includes files
+    const doesEventHaveFiles = () => {
+      const { types } = event.dataTransfer
+      return types.some(type => type === 'Files')
+    }
+
+    // Deny drop, if no plugins can handle datatransfer, there are no files,
+    // or when opts.disabled is set, or new uploads are not allowed
+    const somePluginCanHandleRootDrop = canSomePluginHandleRootDrop(event)
+    const hasFiles = doesEventHaveFiles(event)
+    if (
+      (!somePluginCanHandleRootDrop && !hasFiles)
+      || this.opts.disabled
+      // opts.disableLocalFiles should only be taken into account if no plugins
+      // can handle the datatransfer
+      || (this.opts.disableLocalFiles && (hasFiles || !somePluginCanHandleRootDrop))
+      || !this.uppy.getState().allowNewUpload
+    ) {
+      event.dataTransfer.dropEffect = 'none' // eslint-disable-line no-param-reassign
+      clearTimeout(this.removeDragOverClassTimeout)
+      return
+    }
+
+    // Add a small (+) icon on drop
+    // (and prevent browsers from interpreting this as files being _moved_ into the
+    // browser, https://github.com/transloadit/uppy/issues/1978).
+    event.dataTransfer.dropEffect = 'copy' // eslint-disable-line no-param-reassign
+
+    clearTimeout(this.removeDragOverClassTimeout)
+    this.setPluginState({ isDraggingOver: true })
+
+    this.opts.onDragOver?.(event)
+  }
+
+  handleDragLeave = (event) => {
+    event.preventDefault()
+    event.stopPropagation()
+
+    clearTimeout(this.removeDragOverClassTimeout)
+    // Timeout against flickering, this solution is taken from drag-drop library.
+    // Solution with 'pointer-events: none' didn't work across browsers.
+    this.removeDragOverClassTimeout = setTimeout(() => {
+      this.setPluginState({ isDraggingOver: false })
+    }, 50)
+
+    this.opts.onDragLeave?.(event)
+  }
+
+  handleDrop = async (event) => {
+    event.preventDefault()
+    event.stopPropagation()
+
+    clearTimeout(this.removeDragOverClassTimeout)
+
+    this.setPluginState({ isDraggingOver: false })
+
+    // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
+    this.uppy.iteratePlugins((plugin) => {
+      if (plugin.type === 'acquirer') {
+        // Every Plugin with .type acquirer can define handleRootDrop(event)
+        plugin.handleRootDrop?.(event)
+      }
+    })
+
+    // Add all dropped files
+    let executedDropErrorOnce = false
+    const logDropError = (error) => {
+      this.uppy.log(error, 'error')
+
+      // In practice all drop errors are most likely the same,
+      // so let's just show one to avoid overwhelming the user
+      if (!executedDropErrorOnce) {
+        this.uppy.info(error.message, 'error')
+        executedDropErrorOnce = true
+      }
+    }
+
+    // Add all dropped files
+    const files = await getDroppedFiles(event.dataTransfer, { logDropError })
+    if (files.length > 0) {
+      this.uppy.log('[Dashboard] Files dropped')
+      this.addFiles(files)
+    }
+
+    this.opts.onDrop?.(event)
+  }
+
+  handleRequestThumbnail = (file) => {
+    if (!this.opts.waitForThumbnailsBeforeUpload) {
+      this.uppy.emit('thumbnail:request', file)
+    }
+  }
+
+  /**
+   * We cancel thumbnail requests when a file item component unmounts to avoid
+   * clogging up the queue when the user scrolls past many elements.
+   */
+  handleCancelThumbnail = (file) => {
+    if (!this.opts.waitForThumbnailsBeforeUpload) {
+      this.uppy.emit('thumbnail:cancel', file)
+    }
+  }
+
+  handleKeyDownInInline = (event) => {
+    // Trap focus on tab key press.
+    if (event.keyCode === TAB_KEY) trapFocus.forInline(event, this.getPluginState().activeOverlayType, this.el)
+  }
+
+  // ___Why do we listen to the 'paste' event on a document instead of onPaste={props.handlePaste} prop,
+  //    or this.el.addEventListener('paste')?
+  //    Because (at least) Chrome doesn't handle paste if focus is on some button, e.g. 'My Device'.
+  //    => Therefore, the best option is to listen to all 'paste' events, and only react to them when we are focused on our
+  //       particular Uppy instance.
+  // ___Why do we still need onPaste={props.handlePaste} for the DashboardUi?
+  //    Because if we click on the 'Drop files here' caption e.g., `document.activeElement` will be 'body'. Which means our
+  //    standard determination of whether we're pasting into our Uppy instance won't work.
+  //    => Therefore, we need a traditional onPaste={props.handlePaste} handler too.
+  handlePasteOnBody = (event) => {
+    const isFocusInOverlay = this.el.contains(document.activeElement)
+    if (isFocusInOverlay) {
+      this.handlePaste(event)
+    }
+  }
+
+  handleComplete = ({ failed }) => {
+    if (this.opts.closeAfterFinish && failed.length === 0) {
+      // All uploads are done
+      this.requestCloseModal()
+    }
+  }
+
+  handleCancelRestore = () => {
+    this.uppy.emit('restore-canceled')
+  }
+
+  #openFileEditorWhenFilesAdded = (files) => {
+    const firstFile = files[0]
+    if (this.canEditFile(firstFile)) {
+      this.openFileEditor(firstFile)
+    }
+  }
+
+  initEvents = () => {
+    // Modal open button
+    if (this.opts.trigger && !this.opts.inline) {
+      const showModalTrigger = findAllDOMElements(this.opts.trigger)
+      if (showModalTrigger) {
+        showModalTrigger.forEach(trigger => trigger.addEventListener('click', this.openModal))
+      } else {
+        this.uppy.log('Dashboard modal trigger not found. Make sure `trigger` is set in Dashboard options, unless you are planning to call `dashboard.openModal()` method yourself', 'warning')
+      }
+    }
+
+    this.startListeningToResize()
+    document.addEventListener('paste', this.handlePasteOnBody)
+
+    this.uppy.on('plugin-remove', this.removeTarget)
+    this.uppy.on('file-added', this.hideAllPanels)
+    this.uppy.on('dashboard:modal-closed', this.hideAllPanels)
+    this.uppy.on('file-editor:complete', this.hideAllPanels)
+    this.uppy.on('complete', this.handleComplete)
+
+    // ___Why fire on capture?
+    //    Because this.ifFocusedOnUppyRecently needs to change before onUpdate() fires.
+    document.addEventListener('focus', this.recordIfFocusedOnUppyRecently, true)
+    document.addEventListener('click', this.recordIfFocusedOnUppyRecently, true)
+
+    if (this.opts.inline) {
+      this.el.addEventListener('keydown', this.handleKeyDownInInline)
+    }
+
+    if (this.opts.autoOpenFileEditor) {
+      this.uppy.on('files-added', this.#openFileEditorWhenFilesAdded)
+    }
+  }
+
+  removeEvents = () => {
+    const showModalTrigger = findAllDOMElements(this.opts.trigger)
+    if (!this.opts.inline && showModalTrigger) {
+      showModalTrigger.forEach(trigger => trigger.removeEventListener('click', this.openModal))
+    }
+
+    this.stopListeningToResize()
+    document.removeEventListener('paste', this.handlePasteOnBody)
+
+    window.removeEventListener('popstate', this.handlePopState, false)
+    this.uppy.off('plugin-remove', this.removeTarget)
+    this.uppy.off('file-added', this.hideAllPanels)
+    this.uppy.off('dashboard:modal-closed', this.hideAllPanels)
+    this.uppy.off('file-editor:complete', this.hideAllPanels)
+    this.uppy.off('complete', this.handleComplete)
+
+    document.removeEventListener('focus', this.recordIfFocusedOnUppyRecently)
+    document.removeEventListener('click', this.recordIfFocusedOnUppyRecently)
+
+    if (this.opts.inline) {
+      this.el.removeEventListener('keydown', this.handleKeyDownInInline)
+    }
+
+    if (this.opts.autoOpenFileEditor) {
+      this.uppy.off('files-added', this.#openFileEditorWhenFilesAdded)
+    }
+  }
+
+  superFocusOnEachUpdate = () => {
+    const isFocusInUppy = this.el.contains(document.activeElement)
+    // When focus is lost on the page (== focus is on body for most browsers, or focus is null for IE11)
+    const isFocusNowhere = document.activeElement === document.body || document.activeElement === null
+    const isInformerHidden = this.uppy.getState().info.length === 0
+    const isModal = !this.opts.inline
+
+    if (
+      // If update is connected to showing the Informer - let the screen reader calmly read it.
+      isInformerHidden
+      && (
+        // If we are in a modal - always superfocus without concern for other elements
+        // on the page (user is unlikely to want to interact with the rest of the page)
+        isModal
+        // If we are already inside of Uppy, or
+        || isFocusInUppy
+        // If we are not focused on anything BUT we have already, at least once, focused on uppy
+        //   1. We focus when isFocusNowhere, because when the element we were focused
+        //      on disappears (e.g. an overlay), - focus gets lost. If user is typing
+        //      something somewhere else on the page, - focus won't be 'nowhere'.
+        //   2. We only focus when focus is nowhere AND this.ifFocusedOnUppyRecently,
+        //      to avoid focus jumps if we do something else on the page.
+        //   [Practical check] Without '&& this.ifFocusedOnUppyRecently', in Safari, in inline mode,
+        //                     when file is uploading, - navigate via tab to the checkbox,
+        //                     try to press space multiple times. Focus will jump to Uppy.
+        || (isFocusNowhere && this.ifFocusedOnUppyRecently)
+      )
+    ) {
+      this.superFocus(this.el, this.getPluginState().activeOverlayType)
+    } else {
+      this.superFocus.cancel()
+    }
+  }
+
+  afterUpdate = () => {
+    if (this.opts.disabled && !this.dashboardIsDisabled) {
+      this.disableAllFocusableElements(true)
+      return
+    }
+
+    if (!this.opts.disabled && this.dashboardIsDisabled) {
+      this.disableAllFocusableElements(false)
+    }
+
+    this.superFocusOnEachUpdate()
+  }
+
+  saveFileCard = (meta, fileID) => {
+    this.uppy.setFileMeta(fileID, meta)
+    this.toggleFileCard(false, fileID)
+  }
+
+  #attachRenderFunctionToTarget = (target) => {
+    const plugin = this.uppy.getPlugin(target.id)
+    return {
+      ...target,
+      icon: plugin.icon || this.opts.defaultPickerIcon,
+      render: plugin.render,
+    }
+  }
+
+  #isTargetSupported = (target) => {
+    const plugin = this.uppy.getPlugin(target.id)
+    // If the plugin does not provide a `supported` check, assume the plugin works everywhere.
+    if (typeof plugin.isSupported !== 'function') {
+      return true
+    }
+    return plugin.isSupported()
+  }
+
+  #getAcquirers = memoize((targets) => {
+    return targets
+      .filter(target => target.type === 'acquirer' && this.#isTargetSupported(target))
+      .map(this.#attachRenderFunctionToTarget)
+  })
+
+  #getProgressIndicators = memoize((targets) => {
+    return targets
+      .filter(target => target.type === 'progressindicator')
+      .map(this.#attachRenderFunctionToTarget)
+  })
+
+  #getEditors = memoize((targets) => {
+    return targets
+      .filter(target => target.type === 'editor')
+      .map(this.#attachRenderFunctionToTarget)
+  })
+
+  render = (state) => {
+    const pluginState = this.getPluginState()
+    const { files, capabilities, allowNewUpload } = state
+    const {
+      newFiles,
+      uploadStartedFiles,
+      completeFiles,
+      erroredFiles,
+      inProgressFiles,
+      inProgressNotPausedFiles,
+      processingFiles,
+
+      isUploadStarted,
+      isAllComplete,
+      isAllErrored,
+      isAllPaused,
+    } = this.uppy.getObjectOfFilesPerState()
+
+    const acquirers = this.#getAcquirers(pluginState.targets)
+    const progressindicators = this.#getProgressIndicators(pluginState.targets)
+    const editors = this.#getEditors(pluginState.targets)
+
+    let theme
+    if (this.opts.theme === 'auto') {
+      theme = capabilities.darkMode ? 'dark' : 'light'
+    } else {
+      theme = this.opts.theme
+    }
+
+    if (['files', 'folders', 'both'].indexOf(this.opts.fileManagerSelectionType) < 0) {
+      this.opts.fileManagerSelectionType = 'files'
+      // eslint-disable-next-line no-console
+      console.warn(`Unsupported option for "fileManagerSelectionType". Using default of "${this.opts.fileManagerSelectionType}".`)
+    }
+
+    return DashboardUI({
+      state,
+      isHidden: pluginState.isHidden,
+      files,
+      newFiles,
+      uploadStartedFiles,
+      completeFiles,
+      erroredFiles,
+      inProgressFiles,
+      inProgressNotPausedFiles,
+      processingFiles,
+      isUploadStarted,
+      isAllComplete,
+      isAllErrored,
+      isAllPaused,
+      totalFileCount: Object.keys(files).length,
+      totalProgress: state.totalProgress,
+      allowNewUpload,
+      acquirers,
+      theme,
+      disabled: this.opts.disabled,
+      disableLocalFiles: this.opts.disableLocalFiles,
+      direction: this.opts.direction,
+      activePickerPanel: pluginState.activePickerPanel,
+      showFileEditor: pluginState.showFileEditor,
+      saveFileEditor: this.saveFileEditor,
+      disableAllFocusableElements: this.disableAllFocusableElements,
+      animateOpenClose: this.opts.animateOpenClose,
+      isClosing: pluginState.isClosing,
+      progressindicators,
+      editors,
+      autoProceed: this.uppy.opts.autoProceed,
+      id: this.id,
+      closeModal: this.requestCloseModal,
+      handleClickOutside: this.handleClickOutside,
+      handleInputChange: this.handleInputChange,
+      handlePaste: this.handlePaste,
+      inline: this.opts.inline,
+      showPanel: this.showPanel,
+      hideAllPanels: this.hideAllPanels,
+      i18n: this.i18n,
+      i18nArray: this.i18nArray,
+      uppy: this.uppy,
+      note: this.opts.note,
+      recoveredState: state.recoveredState,
+      metaFields: pluginState.metaFields,
+      resumableUploads: capabilities.resumableUploads || false,
+      individualCancellation: capabilities.individualCancellation,
+      isMobileDevice: capabilities.isMobileDevice,
+      fileCardFor: pluginState.fileCardFor,
+      toggleFileCard: this.toggleFileCard,
+      toggleAddFilesPanel: this.toggleAddFilesPanel,
+      showAddFilesPanel: pluginState.showAddFilesPanel,
+      saveFileCard: this.saveFileCard,
+      openFileEditor: this.openFileEditor,
+      canEditFile: this.canEditFile,
+      width: this.opts.width,
+      height: this.opts.height,
+      showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,
+      fileManagerSelectionType: this.opts.fileManagerSelectionType,
+      proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
+      hideCancelButton: this.opts.hideCancelButton,
+      hideRetryButton: this.opts.hideRetryButton,
+      hidePauseResumeButton: this.opts.hidePauseResumeButton,
+      showRemoveButtonAfterComplete: this.opts.showRemoveButtonAfterComplete,
+      containerWidth: pluginState.containerWidth,
+      containerHeight: pluginState.containerHeight,
+      areInsidesReadyToBeVisible: pluginState.areInsidesReadyToBeVisible,
+      isTargetDOMEl: this.isTargetDOMEl,
+      parentElement: this.el,
+      allowedFileTypes: this.uppy.opts.restrictions.allowedFileTypes,
+      maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles,
+      requiredMetaFields: this.uppy.opts.restrictions.requiredMetaFields,
+      showSelectedFiles: this.opts.showSelectedFiles,
+      handleCancelRestore: this.handleCancelRestore,
+      handleRequestThumbnail: this.handleRequestThumbnail,
+      handleCancelThumbnail: this.handleCancelThumbnail,
+      // drag props
+      isDraggingOver: pluginState.isDraggingOver,
+      handleDragOver: this.handleDragOver,
+      handleDragLeave: this.handleDragLeave,
+      handleDrop: this.handleDrop,
+    })
+  }
+
+  discoverProviderPlugins = () => {
+    this.uppy.iteratePlugins((plugin) => {
+      if (plugin && !plugin.target && plugin.opts && plugin.opts.target === this.constructor) {
+        this.addTarget(plugin)
+      }
+    })
+  }
+
+  install = () => {
+    // Set default state for Dashboard
+    this.setPluginState({
+      isHidden: true,
+      fileCardFor: null,
+      activeOverlayType: null,
+      showAddFilesPanel: false,
+      activePickerPanel: false,
+      showFileEditor: false,
+      metaFields: this.opts.metaFields,
+      targets: [],
+      // We'll make them visible once .containerWidth is determined
+      areInsidesReadyToBeVisible: false,
+      isDraggingOver: false,
+    })
+
+    const { inline, closeAfterFinish } = this.opts
+    if (inline && closeAfterFinish) {
+      throw new Error('[Dashboard] `closeAfterFinish: true` cannot be used on an inline Dashboard, because an inline Dashboard cannot be closed at all. Either set `inline: false`, or disable the `closeAfterFinish` option.')
+    }
+
+    const { allowMultipleUploads, allowMultipleUploadBatches } = this.uppy.opts
+    if ((allowMultipleUploads || allowMultipleUploadBatches) && closeAfterFinish) {
+      this.uppy.log('[Dashboard] When using `closeAfterFinish`, we recommended setting the `allowMultipleUploadBatches` option to `false` in the Uppy constructor. See https://uppy.io/docs/uppy/#allowMultipleUploads-true', 'warning')
+    }
+
+    const { target } = this.opts
+
+    if (target) {
+      this.mount(target, this)
+    }
+
+    const plugins = this.opts.plugins || []
+
+    plugins.forEach((pluginID) => {
+      const plugin = this.uppy.getPlugin(pluginID)
+      if (plugin) {
+        plugin.mount(this, plugin)
+      }
+    })
+
+    if (!this.opts.disableStatusBar) {
+      this.uppy.use(StatusBar, {
+        id: `${this.id}:StatusBar`,
+        target: this,
+        hideUploadButton: this.opts.hideUploadButton,
+        hideRetryButton: this.opts.hideRetryButton,
+        hidePauseResumeButton: this.opts.hidePauseResumeButton,
+        hideCancelButton: this.opts.hideCancelButton,
+        showProgressDetails: this.opts.showProgressDetails,
+        hideAfterFinish: this.opts.hideProgressAfterFinish,
+        locale: this.opts.locale,
+        doneButtonHandler: this.opts.doneButtonHandler,
+      })
+    }
+
+    if (!this.opts.disableInformer) {
+      this.uppy.use(Informer, {
+        id: `${this.id}:Informer`,
+        target: this,
+      })
+    }
+
+    if (!this.opts.disableThumbnailGenerator) {
+      this.uppy.use(ThumbnailGenerator, {
+        id: `${this.id}:ThumbnailGenerator`,
+        thumbnailWidth: this.opts.thumbnailWidth,
+        thumbnailHeight: this.opts.thumbnailHeight,
+        thumbnailType: this.opts.thumbnailType,
+        waitForThumbnailsBeforeUpload: this.opts.waitForThumbnailsBeforeUpload,
+        // If we don't block on thumbnails, we can lazily generate them
+        lazy: !this.opts.waitForThumbnailsBeforeUpload,
+      })
+    }
+
+    // Dark Mode / theme
+    this.darkModeMediaQuery = (typeof window !== 'undefined' && window.matchMedia)
+      ? window.matchMedia('(prefers-color-scheme: dark)')
+      : null
+
+    const isDarkModeOnFromTheStart = this.darkModeMediaQuery ? this.darkModeMediaQuery.matches : false
+    this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnFromTheStart ? 'on' : 'off'}`)
+    this.setDarkModeCapability(isDarkModeOnFromTheStart)
+
+    if (this.opts.theme === 'auto') {
+      this.darkModeMediaQuery.addListener(this.handleSystemDarkModeChange)
+    }
+
+    this.discoverProviderPlugins()
+    this.initEvents()
+  }
+
+  uninstall = () => {
+    if (!this.opts.disableInformer) {
+      const informer = this.uppy.getPlugin(`${this.id}:Informer`)
+      // Checking if this plugin exists, in case it was removed by uppy-core
+      // before the Dashboard was.
+      if (informer) this.uppy.removePlugin(informer)
+    }
+
+    if (!this.opts.disableStatusBar) {
+      const statusBar = this.uppy.getPlugin(`${this.id}:StatusBar`)
+      if (statusBar) this.uppy.removePlugin(statusBar)
+    }
+
+    if (!this.opts.disableThumbnailGenerator) {
+      const thumbnail = this.uppy.getPlugin(`${this.id}:ThumbnailGenerator`)
+      if (thumbnail) this.uppy.removePlugin(thumbnail)
+    }
+
+    const plugins = this.opts.plugins || []
+    plugins.forEach((pluginID) => {
+      const plugin = this.uppy.getPlugin(pluginID)
+      if (plugin) plugin.unmount()
+    })
+
+    if (this.opts.theme === 'auto') {
+      this.darkModeMediaQuery.removeListener(this.handleSystemDarkModeChange)
+    }
+
+    this.unmount()
+    this.removeEvents()
+  }
+}

+ 3 - 3
packages/@uppy/dashboard/src/components/AddFiles.js → packages/@uppy/dashboard/src/components/AddFiles.jsx

@@ -1,4 +1,4 @@
-const { h, Component } = require('preact')
+import {  h, Component  } from 'preact'
 
 class AddFiles extends Component {
   triggerFileInputClick = () => {
@@ -18,7 +18,7 @@ class AddFiles extends Component {
     // ___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
+    event.target.value = null // eslint-disable-line no-param-reassign
   }
 
   renderHiddenInput = (isFolder, refCallback) => {
@@ -198,4 +198,4 @@ class AddFiles extends Component {
   }
 }
 
-module.exports = AddFiles
+export default AddFiles

+ 5 - 4
packages/@uppy/dashboard/src/components/AddFilesPanel.js → packages/@uppy/dashboard/src/components/AddFilesPanel.jsx

@@ -1,6 +1,6 @@
-const { h } = require('preact')
-const classNames = require('classnames')
-const AddFiles = require('./AddFiles')
+import { h } from 'preact'
+import classNames from 'classnames'
+import AddFiles from './AddFiles.jsx'
 
 const AddFilesPanel = (props) => {
   return (
@@ -21,9 +21,10 @@ const AddFilesPanel = (props) => {
           {props.i18n('back')}
         </button>
       </div>
+      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
       <AddFiles {...props} />
     </div>
   )
 }
 
-module.exports = AddFilesPanel
+export default AddFilesPanel

+ 19 - 12
packages/@uppy/dashboard/src/components/Dashboard.js → packages/@uppy/dashboard/src/components/Dashboard.jsx

@@ -1,14 +1,14 @@
-const { h } = require('preact')
-const classNames = require('classnames')
-const isDragDropSupported = require('@uppy/utils/lib/isDragDropSupported')
-const FileList = require('./FileList')
-const AddFiles = require('./AddFiles')
-const AddFilesPanel = require('./AddFilesPanel')
-const PickerPanelContent = require('./PickerPanelContent')
-const EditorPanel = require('./EditorPanel')
-const PanelTopBar = require('./PickerPanelTopBar')
-const FileCard = require('./FileCard')
-const Slide = require('./Slide')
+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'
 
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal
@@ -18,7 +18,7 @@ const WIDTH_LG = 700
 const WIDTH_MD = 576
 const HEIGHT_MD = 400
 
-module.exports = function Dashboard (props) {
+export default function Dashboard (props) {
   const noFiles = props.totalFileCount === 0
   const isSizeMD = props.containerWidth > WIDTH_MD
 
@@ -110,6 +110,7 @@ module.exports = function Dashboard (props) {
             {props.i18n('dropHint')}
           </div>
 
+          {/* eslint-disable-next-line react/jsx-props-no-spreading */}
           {showFileList && <PanelTopBar {...props} />}
 
           {numberOfFilesForRecovery && (
@@ -132,26 +133,32 @@ module.exports = function Dashboard (props) {
 
           {showFileList ? (
             <FileList
+              // eslint-disable-next-line react/jsx-props-no-spreading
               {...props}
               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}
           </Slide>
 
           <Slide>
+            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {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}
           </Slide>
 
           <Slide>
+            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {props.showFileEditor ? <EditorPanel key="Editor" {...props} /> : null}
           </Slide>
 

+ 3 - 3
packages/@uppy/dashboard/src/components/EditorPanel.js → packages/@uppy/dashboard/src/components/EditorPanel.jsx

@@ -1,5 +1,5 @@
-const { h } = require('preact')
-const classNames = require('classnames')
+import { h } from 'preact'
+import classNames from 'classnames'
 
 function EditorPanel (props) {
   const file = props.files[props.fileCardFor]
@@ -41,4 +41,4 @@ function EditorPanel (props) {
   )
 }
 
-module.exports = EditorPanel
+export default EditorPanel

+ 7 - 7
packages/@uppy/dashboard/src/components/FileCard/index.js → packages/@uppy/dashboard/src/components/FileCard/index.jsx

@@ -1,9 +1,9 @@
-const { h, Component } = require('preact')
-const classNames = require('classnames')
-const { nanoid } = require('nanoid/non-secure')
-const getFileTypeIcon = require('../../utils/getFileTypeIcon')
-const ignoreEvent = require('../../utils/ignoreEvent.js')
-const FilePreview = require('../FilePreview')
+import {  h, Component  } from 'preact'
+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'
 
 class FileCard extends Component {
   form = document.createElement('form')
@@ -197,4 +197,4 @@ class FileCard extends Component {
   }
 }
 
-module.exports = FileCard
+export default FileCard

+ 3 - 3
packages/@uppy/dashboard/src/components/FileItem/Buttons/index.js → packages/@uppy/dashboard/src/components/FileItem/Buttons/index.jsx

@@ -1,5 +1,5 @@
-const { h } = require('preact')
-const copyToClipboard = require('../../../utils/copyToClipboard')
+import { h } from 'preact'
+import copyToClipboard from '../../../utils/copyToClipboard.js'
 
 function EditButton ({
   file,
@@ -80,7 +80,7 @@ function CopyLinkButton (props) {
   )
 }
 
-module.exports = function Buttons (props) {
+export default function Buttons (props) {
   const {
     uppy,
     file,

+ 5 - 5
packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.js → packages/@uppy/dashboard/src/components/FileItem/FileInfo/index.jsx

@@ -1,7 +1,7 @@
-const { h, Fragment } = require('preact')
-const prettierBytes = require('@transloadit/prettier-bytes')
-const truncateString = require('@uppy/utils/lib/truncateString')
-const MetaErrorMessage = require('../MetaErrorMessage')
+import {  h, Fragment  } from 'preact'
+import prettierBytes from '@transloadit/prettier-bytes'
+import truncateString from '@uppy/utils/lib/truncateString'
+import MetaErrorMessage from '../MetaErrorMessage.jsx'
 
 const renderFileName = (props) => {
   const { author, name } = props.file.meta
@@ -92,7 +92,7 @@ const ErrorButton = ({ file, onClick }) => {
   return null
 }
 
-module.exports = function FileInfo (props) {
+export default function FileInfo (props) {
   const { file } = props
   return (
     <div

+ 5 - 5
packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.js → packages/@uppy/dashboard/src/components/FileItem/FilePreviewAndLink/index.jsx

@@ -1,9 +1,9 @@
-const { h } = require('preact')
-const FilePreview = require('../../FilePreview')
-const MetaErrorMessage = require('../MetaErrorMessage')
-const getFileTypeIcon = require('../../../utils/getFileTypeIcon')
+import { h } from 'preact'
+import FilePreview from '../../FilePreview.jsx'
+import MetaErrorMessage from '../MetaErrorMessage.jsx'
+import getFileTypeIcon from '../../../utils/getFileTypeIcon.jsx'
 
-module.exports = function FilePreviewAndLink (props) {
+export default function FilePreviewAndLink (props) {
   return (
     <div
       className="uppy-Dashboard-Item-previewInnerWrap"

+ 6 - 3
packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.js → packages/@uppy/dashboard/src/components/FileItem/FileProgress/index.jsx

@@ -1,4 +1,4 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
 function onPauseResumeCancelRetry (props) {
   if (props.isUploaded) return
@@ -96,7 +96,7 @@ function ProgressCircle ({ progress }) {
   )
 }
 
-module.exports = function FileProgress (props) {
+export default function FileProgress (props) {
   // Nothing if upload has not started
   if (!props.file.progress.uploadStarted) {
     return null
@@ -117,12 +117,13 @@ module.exports = function FileProgress (props) {
   }
 
   if (props.recoveredState) {
-    return
+    return undefined
   }
 
   // Retry button for error
   if (props.error && !props.hideRetryButton) {
     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">
           <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
@@ -137,6 +138,7 @@ module.exports = function FileProgress (props) {
   // Pause/resume button for resumable uploads
   if (props.resumableUploads && !props.hidePauseResumeButton) {
     return (
+      // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
         <ProgressCircleContainer>
           <ProgressCircle progress={props.file.progress.percentage} />
@@ -158,6 +160,7 @@ module.exports = function FileProgress (props) {
   // Cancel button for non-resumable uploads if individualCancellation is supported (not bundled)
   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} />

+ 2 - 2
packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.js → packages/@uppy/dashboard/src/components/FileItem/MetaErrorMessage.jsx

@@ -1,11 +1,11 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
 const metaFieldIdToName = (metaFieldId, metaFields) => {
   const field = metaFields.filter(f => f.id === metaFieldId)
   return field[0].name
 }
 
-module.exports = function renderMissingMetaFieldsError (props) {
+export default function renderMissingMetaFieldsError (props) {
   const { file, toggleFileCard, i18n, metaFields } = props
   const { missingRequiredMetaFields } = file
   if (!missingRequiredMetaFields?.length) {

+ 8 - 8
packages/@uppy/dashboard/src/components/FileItem/index.js → packages/@uppy/dashboard/src/components/FileItem/index.jsx

@@ -1,12 +1,12 @@
-const { h, Component } = require('preact')
-const classNames = require('classnames')
-const shallowEqual = require('is-shallow-equal')
-const FilePreviewAndLink = require('./FilePreviewAndLink')
-const FileProgress = require('./FileProgress')
-const FileInfo = require('./FileInfo')
-const Buttons = require('./Buttons')
+import {  h, Component  } 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'
 
-module.exports = class FileItem extends Component {
+export default class FileItem extends Component {
   componentDidMount () {
     const { file } = this.props
     if (!file.preview) {

+ 6 - 6
packages/@uppy/dashboard/src/components/FileList.js → packages/@uppy/dashboard/src/components/FileList.jsx

@@ -1,7 +1,7 @@
-const classNames = require('classnames')
-const { h } = require('preact')
-const FileItem = require('./FileItem/index.js')
-const VirtualList = require('./VirtualList')
+import classNames from 'classnames'
+import { h } from 'preact'
+import FileItem from './FileItem/index.jsx'
+import VirtualList from './VirtualList.jsx'
 
 function chunks (list, size) {
   const chunked = []
@@ -18,7 +18,7 @@ function chunks (list, size) {
   return chunked
 }
 
-module.exports = (props) => {
+export default (props) => {
   const noFiles = props.totalFileCount === 0
   const dashboardFilesClass = classNames(
     'uppy-Dashboard-files',
@@ -77,7 +77,7 @@ module.exports = (props) => {
         <FileItem
           key={fileID}
           uppy={props.uppy}
-          {...fileProps}
+          {...fileProps} // eslint-disable-line react/jsx-props-no-spreading
           role="listitem"
           openFileEditor={props.openFileEditor}
           canEditFile={props.canEditFile}

+ 3 - 3
packages/@uppy/dashboard/src/components/FilePreview.js → packages/@uppy/dashboard/src/components/FilePreview.jsx

@@ -1,7 +1,7 @@
-const { h } = require('preact')
-const getFileTypeIcon = require('../utils/getFileTypeIcon')
+import { h } from 'preact'
+import getFileTypeIcon from '../utils/getFileTypeIcon.jsx'
 
-module.exports = function FilePreview (props) {
+export default function FilePreview (props) {
   const { file } = props
 
   if (file.preview) {

+ 0 - 36
packages/@uppy/dashboard/src/components/PickerPanelContent.js

@@ -1,36 +0,0 @@
-const { h } = require('preact')
-const classNames = require('classnames')
-const ignoreEvent = require('../utils/ignoreEvent.js')
-
-function PickerPanelContent (props) {
-  return (
-    <div
-      className={classNames('uppy-DashboardContent-panel', props.className)}
-      role="tabpanel"
-      data-uppy-panelType="PickerPanel"
-      id={`uppy-DashboardContent-panel--${props.activePickerPanel.id}`}
-      onDragOver={ignoreEvent}
-      onDragLeave={ignoreEvent}
-      onDrop={ignoreEvent}
-      onPaste={ignoreEvent}
-    >
-      <div className="uppy-DashboardContent-bar">
-        <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
-          {props.i18n('importFrom', { name: props.activePickerPanel.name })}
-        </div>
-        <button
-          className="uppy-DashboardContent-back"
-          type="button"
-          onClick={props.hideAllPanels}
-        >
-          {props.i18n('cancel')}
-        </button>
-      </div>
-      <div className="uppy-DashboardContent-panelBody">
-        {props.uppy.getPlugin(props.activePickerPanel.id).render(props.state)}
-      </div>
-    </div>
-  )
-}
-
-module.exports = PickerPanelContent

+ 36 - 0
packages/@uppy/dashboard/src/components/PickerPanelContent.jsx

@@ -0,0 +1,36 @@
+import { h } from 'preact'
+import classNames from 'classnames'
+import ignoreEvent from '../utils/ignoreEvent.js'
+
+function PickerPanelContent ({ activePickerPanel, className, hideAllPanels, i18n, state, uppy }) {
+  return (
+    <div
+      className={classNames('uppy-DashboardContent-panel', className)}
+      role="tabpanel"
+      data-uppy-panelType="PickerPanel"
+      id={`uppy-DashboardContent-panel--${activePickerPanel.id}`}
+      onDragOver={ignoreEvent}
+      onDragLeave={ignoreEvent}
+      onDrop={ignoreEvent}
+      onPaste={ignoreEvent}
+    >
+      <div className="uppy-DashboardContent-bar">
+        <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+          {i18n('importFrom', { name: activePickerPanel.name })}
+        </div>
+        <button
+          className="uppy-DashboardContent-back"
+          type="button"
+          onClick={hideAllPanels}
+        >
+          {i18n('cancel')}
+        </button>
+      </div>
+      <div className="uppy-DashboardContent-panelBody">
+        {uppy.getPlugin(activePickerPanel.id).render(state)}
+      </div>
+    </div>
+  )
+}
+
+export default PickerPanelContent

+ 28 - 21
packages/@uppy/dashboard/src/components/PickerPanelTopBar.js → packages/@uppy/dashboard/src/components/PickerPanelTopBar.jsx

@@ -1,4 +1,4 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
 const uploadStates = {
   STATE_ERROR: 'error',
@@ -45,51 +45,58 @@ function getUploadingState (isAllErrored, isAllComplete, isAllPaused, files = {}
   return state
 }
 
-function UploadStatus (props) {
+function UploadStatus ({
+  files, i18n, isAllComplete, isAllErrored, isAllPaused,
+  inProgressNotPausedFiles, newFiles, processingFiles,
+}) {
   const uploadingState = getUploadingState(
-    props.isAllErrored,
-    props.isAllComplete,
-    props.isAllPaused,
-    props.files,
+    isAllErrored,
+    isAllComplete,
+    isAllPaused,
+    files,
   )
 
   switch (uploadingState) {
     case 'uploading':
-      return props.i18n('uploadingXFiles', { smart_count: props.inProgressNotPausedFiles.length })
+      return i18n('uploadingXFiles', { smart_count: inProgressNotPausedFiles.length })
     case 'preprocessing':
     case 'postprocessing':
-      return props.i18n('processingXFiles', { smart_count: props.processingFiles.length })
+      return i18n('processingXFiles', { smart_count: processingFiles.length })
     case 'paused':
-      return props.i18n('uploadPaused')
+      return i18n('uploadPaused')
     case 'waiting':
-      return props.i18n('xFilesSelected', { smart_count: props.newFiles.length })
+      return i18n('xFilesSelected', { smart_count: newFiles.length })
     case 'complete':
-      return props.i18n('uploadComplete')
+      return i18n('uploadComplete')
+    default:
   }
 }
 
 function PanelTopBar (props) {
+  const { i18n, isAllComplete, hideCancelButton, maxNumberOfFiles, toggleAddFilesPanel, uppy } = props
   let { allowNewUpload } = props
-  // TODO maybe this should be done in ../index.js, then just pass that down as `allowNewUpload`
-  if (allowNewUpload && props.maxNumberOfFiles) {
+  // TODO maybe this should be done in ../Dashboard.jsx, then just pass that down as `allowNewUpload`
+  if (allowNewUpload && maxNumberOfFiles) {
+    // eslint-disable-next-line react/destructuring-assignment
     allowNewUpload = props.totalFileCount < props.maxNumberOfFiles
   }
 
   return (
     <div className="uppy-DashboardContent-bar">
-      {!props.isAllComplete && !props.hideCancelButton ? (
+      {!isAllComplete && !hideCancelButton ? (
         <button
           className="uppy-DashboardContent-back"
           type="button"
-          onClick={() => props.uppy.cancelAll()}
+          onClick={() => uppy.cancelAll()}
         >
-          {props.i18n('cancel')}
+          {i18n('cancel')}
         </button>
       ) : (
         <div />
       )}
 
       <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
         <UploadStatus {...props} />
       </div>
 
@@ -97,14 +104,14 @@ function PanelTopBar (props) {
         <button
           className="uppy-DashboardContent-addMore"
           type="button"
-          aria-label={props.i18n('addMoreFiles')}
-          title={props.i18n('addMoreFiles')}
-          onClick={() => props.toggleAddFilesPanel(true)}
+          aria-label={i18n('addMoreFiles')}
+          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">
             <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">{props.i18n('addMore')}</span>
+          <span className="uppy-DashboardContent-addMoreCaption">{i18n('addMore')}</span>
         </button>
       ) : (
         <div />
@@ -113,4 +120,4 @@ function PanelTopBar (props) {
   )
 }
 
-module.exports = PanelTopBar
+export default PanelTopBar

+ 3 - 3
packages/@uppy/dashboard/src/components/Slide.js → packages/@uppy/dashboard/src/components/Slide.jsx

@@ -1,5 +1,5 @@
-const { cloneElement, Component, toChildArray } = require('preact')
-const classNames = require('classnames')
+import {  cloneElement, Component, toChildArray  } from 'preact'
+import classNames from 'classnames'
 
 const transitionName = 'uppy-transition-slideDownUp'
 const duration = 250
@@ -96,4 +96,4 @@ class Slide extends Component {
   }
 }
 
-module.exports = Slide
+export default Slide

+ 3 - 2
packages/@uppy/dashboard/src/components/VirtualList.js → packages/@uppy/dashboard/src/components/VirtualList.jsx

@@ -26,7 +26,7 @@
  * - Tweaked styles for Uppy's Dashboard use case
  */
 
-const { h, Component } = require('preact')
+import {  h, Component  } from 'preact'
 
 const STYLE_INNER = {
   position: 'relative',
@@ -143,6 +143,7 @@ class VirtualList extends Component {
     // The `role="presentation"` attributes ensure that these wrapper elements are not treated as list
     // items by accessibility and outline tools.
     return (
+      // eslint-disable-next-line react/jsx-props-no-spreading
       <div onScroll={this.handleScroll} {...props}>
         <div role="presentation" style={styleInner}>
           <div role="presentation" style={styleContent}>
@@ -154,4 +155,4 @@ class VirtualList extends Component {
   }
 }
 
-module.exports = VirtualList
+export default VirtualList

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

@@ -1,1109 +1 @@
-const { h } = require('preact')
-const { UIPlugin } = require('@uppy/core')
-const StatusBar = require('@uppy/status-bar')
-const Informer = require('@uppy/informer')
-const ThumbnailGenerator = require('@uppy/thumbnail-generator')
-const findAllDOMElements = require('@uppy/utils/lib/findAllDOMElements')
-const toArray = require('@uppy/utils/lib/toArray')
-const getDroppedFiles = require('@uppy/utils/lib/getDroppedFiles')
-const { nanoid } = require('nanoid/non-secure')
-const trapFocus = require('./utils/trapFocus')
-const createSuperFocus = require('./utils/createSuperFocus')
-const memoize = require('memoize-one').default || require('memoize-one')
-const FOCUSABLE_ELEMENTS = require('@uppy/utils/lib/FOCUSABLE_ELEMENTS')
-const DashboardUI = require('./components/Dashboard')
-
-const locale = require('./locale')
-
-const TAB_KEY = 9
-const ESC_KEY = 27
-
-function createPromise () {
-  const o = {}
-  o.promise = new Promise((resolve, reject) => {
-    o.resolve = resolve
-    o.reject = reject
-  })
-  return o
-}
-
-function defaultPickerIcon () {
-  return (
-    <svg aria-hidden="true" focusable="false" width="30" height="30" viewBox="0 0 30 30">
-      <path d="M15 30c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15C6.716 0 0 6.716 0 15c0 8.284 6.716 15 15 15zm4.258-12.676v6.846h-8.426v-6.846H5.204l9.82-12.364 9.82 12.364H19.26z" />
-    </svg>
-  )
-}
-
-/**
- * Dashboard UI with previews, metadata editing, tabs for various services and more
- */
-module.exports = class Dashboard extends UIPlugin {
-  static VERSION = require('../package.json').version
-
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.id = this.opts.id || 'Dashboard'
-    this.title = 'Dashboard'
-    this.type = 'orchestrator'
-    this.modalName = `uppy-Dashboard-${nanoid()}`
-
-    this.defaultLocale = locale
-
-    // set default options
-    const defaultOptions = {
-      target: 'body',
-      metaFields: [],
-      trigger: null,
-      inline: false,
-      width: 750,
-      height: 550,
-      thumbnailWidth: 280,
-      thumbnailType: 'image/jpeg',
-      waitForThumbnailsBeforeUpload: false,
-      defaultPickerIcon,
-      showLinkToFileUploadResult: false,
-      showProgressDetails: false,
-      hideUploadButton: false,
-      hideCancelButton: false,
-      hideRetryButton: false,
-      hidePauseResumeButton: false,
-      hideProgressAfterFinish: false,
-      doneButtonHandler: () => {
-        this.uppy.reset()
-        this.requestCloseModal()
-      },
-      note: null,
-      closeModalOnClickOutside: false,
-      closeAfterFinish: false,
-      disableStatusBar: false,
-      disableInformer: false,
-      disableThumbnailGenerator: false,
-      disablePageScrollWhenModalOpen: true,
-      animateOpenClose: true,
-      fileManagerSelectionType: 'files',
-      proudlyDisplayPoweredByUppy: true,
-      onRequestCloseModal: () => this.closeModal(),
-      showSelectedFiles: true,
-      showRemoveButtonAfterComplete: false,
-      browserBackButtonClose: false,
-      theme: 'light',
-      autoOpenFileEditor: false,
-      disabled: false,
-      disableLocalFiles: false,
-    }
-
-    // merge default options with the ones set by user
-    this.opts = { ...defaultOptions, ...opts }
-
-    this.i18nInit()
-
-    this.superFocus = createSuperFocus()
-    this.ifFocusedOnUppyRecently = false
-
-    // Timeouts
-    this.makeDashboardInsidesVisibleAnywayTimeout = null
-    this.removeDragOverClassTimeout = null
-  }
-
-  removeTarget = (plugin) => {
-    const pluginState = this.getPluginState()
-    // filter out the one we want to remove
-    const newTargets = pluginState.targets.filter(target => target.id !== plugin.id)
-
-    this.setPluginState({
-      targets: newTargets,
-    })
-  }
-
-  addTarget = (plugin) => {
-    const callerPluginId = plugin.id || plugin.constructor.name
-    const callerPluginName = plugin.title || callerPluginId
-    const callerPluginType = plugin.type
-
-    if (callerPluginType !== 'acquirer'
-        && callerPluginType !== 'progressindicator'
-        && callerPluginType !== 'editor') {
-      const msg = 'Dashboard: can only be targeted by plugins of types: acquirer, progressindicator, editor'
-      this.uppy.log(msg, 'error')
-      return
-    }
-
-    const target = {
-      id: callerPluginId,
-      name: callerPluginName,
-      type: callerPluginType,
-    }
-
-    const state = this.getPluginState()
-    const newTargets = state.targets.slice()
-    newTargets.push(target)
-
-    this.setPluginState({
-      targets: newTargets,
-    })
-
-    return this.el
-  }
-
-  hideAllPanels = () => {
-    const state = this.getPluginState()
-    const update = {
-      activePickerPanel: false,
-      showAddFilesPanel: false,
-      activeOverlayType: null,
-      fileCardFor: null,
-      showFileEditor: false,
-    }
-
-    if (state.activePickerPanel === update.activePickerPanel
-        && state.showAddFilesPanel === update.showAddFilesPanel
-        && state.showFileEditor === update.showFileEditor
-        && state.activeOverlayType === update.activeOverlayType) {
-      // avoid doing a state update if nothing changed
-      return
-    }
-
-    this.setPluginState(update)
-  }
-
-  showPanel = (id) => {
-    const { targets } = this.getPluginState()
-
-    const activePickerPanel = targets.filter((target) => {
-      return target.type === 'acquirer' && target.id === id
-    })[0]
-
-    this.setPluginState({
-      activePickerPanel,
-      activeOverlayType: 'PickerPanel',
-    })
-  }
-
-  canEditFile = (file) => {
-    const { targets } = this.getPluginState()
-    const editors = this.#getEditors(targets)
-
-    return editors.some((target) => (
-      this.uppy.getPlugin(target.id).canEditFile(file)
-    ))
-  }
-
-  openFileEditor = (file) => {
-    const { targets } = this.getPluginState()
-    const editors = this.#getEditors(targets)
-
-    this.setPluginState({
-      showFileEditor: true,
-      fileCardFor: file.id || null,
-      activeOverlayType: 'FileEditor',
-    })
-
-    editors.forEach((editor) => {
-      this.uppy.getPlugin(editor.id).selectFile(file)
-    })
-  }
-
-  saveFileEditor = () => {
-    const { targets } = this.getPluginState()
-    const editors = this.#getEditors(targets)
-
-    editors.forEach((editor) => {
-      this.uppy.getPlugin(editor.id).save()
-    })
-
-    this.hideAllPanels()
-  }
-
-  openModal = () => {
-    const { promise, resolve } = createPromise()
-    // save scroll position
-    this.savedScrollPosition = window.pageYOffset
-    // save active element, so we can restore focus when modal is closed
-    this.savedActiveElement = document.activeElement
-
-    if (this.opts.disablePageScrollWhenModalOpen) {
-      document.body.classList.add('uppy-Dashboard-isFixed')
-    }
-
-    if (this.opts.animateOpenClose && this.getPluginState().isClosing) {
-      const handler = () => {
-        this.setPluginState({
-          isHidden: false,
-        })
-        this.el.removeEventListener('animationend', handler, false)
-        resolve()
-      }
-      this.el.addEventListener('animationend', handler, false)
-    } else {
-      this.setPluginState({
-        isHidden: false,
-      })
-      resolve()
-    }
-
-    if (this.opts.browserBackButtonClose) {
-      this.updateBrowserHistory()
-    }
-
-    // handle ESC and TAB keys in modal dialog
-    document.addEventListener('keydown', this.handleKeyDownInModal)
-
-    this.uppy.emit('dashboard:modal-open')
-
-    return promise
-  }
-
-  closeModal = (opts = {}) => {
-    const {
-      // Whether the modal is being closed by the user (`true`) or by other means (e.g. browser back button)
-      manualClose = true,
-    } = opts
-
-    const { isHidden, isClosing } = this.getPluginState()
-    if (isHidden || isClosing) {
-      // short-circuit if animation is ongoing
-      return
-    }
-
-    const { promise, resolve } = createPromise()
-
-    if (this.opts.disablePageScrollWhenModalOpen) {
-      document.body.classList.remove('uppy-Dashboard-isFixed')
-    }
-
-    if (this.opts.animateOpenClose) {
-      this.setPluginState({
-        isClosing: true,
-      })
-      const handler = () => {
-        this.setPluginState({
-          isHidden: true,
-          isClosing: false,
-        })
-
-        this.superFocus.cancel()
-        this.savedActiveElement.focus()
-
-        this.el.removeEventListener('animationend', handler, false)
-        resolve()
-      }
-      this.el.addEventListener('animationend', handler, false)
-    } else {
-      this.setPluginState({
-        isHidden: true,
-      })
-
-      this.superFocus.cancel()
-      this.savedActiveElement.focus()
-
-      resolve()
-    }
-
-    // handle ESC and TAB keys in modal dialog
-    document.removeEventListener('keydown', this.handleKeyDownInModal)
-
-    if (manualClose) {
-      if (this.opts.browserBackButtonClose) {
-        // Make sure that the latest entry in the history state is our modal name
-        // eslint-disable-next-line no-restricted-globals
-        if (history.state?.[this.modalName]) {
-          // Go back in history to clear out the entry we created (ultimately closing the modal)
-          // eslint-disable-next-line no-restricted-globals
-          history.back()
-        }
-      }
-    }
-
-    this.uppy.emit('dashboard:modal-closed')
-
-    return promise
-  }
-
-  isModalOpen = () => {
-    return !this.getPluginState().isHidden || false
-  }
-
-  requestCloseModal = () => {
-    if (this.opts.onRequestCloseModal) {
-      return this.opts.onRequestCloseModal()
-    }
-    return this.closeModal()
-  }
-
-  setDarkModeCapability = (isDarkModeOn) => {
-    const { capabilities } = this.uppy.getState()
-    this.uppy.setState({
-      capabilities: {
-        ...capabilities,
-        darkMode: isDarkModeOn,
-      },
-    })
-  }
-
-  handleSystemDarkModeChange = (event) => {
-    const isDarkModeOnNow = event.matches
-    this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnNow ? 'on' : 'off'}`)
-    this.setDarkModeCapability(isDarkModeOnNow)
-  }
-
-  toggleFileCard = (show, fileID) => {
-    const file = this.uppy.getFile(fileID)
-    if (show) {
-      this.uppy.emit('dashboard:file-edit-start', file)
-    } else {
-      this.uppy.emit('dashboard:file-edit-complete', file)
-    }
-
-    this.setPluginState({
-      fileCardFor: show ? fileID : null,
-      activeOverlayType: show ? 'FileCard' : null,
-    })
-  }
-
-  toggleAddFilesPanel = (show) => {
-    this.setPluginState({
-      showAddFilesPanel: show,
-      activeOverlayType: show ? 'AddFiles' : null,
-    })
-  }
-
-  addFiles = (files) => {
-    const descriptors = files.map((file) => ({
-      source: this.id,
-      name: file.name,
-      type: file.type,
-      data: file,
-      meta: {
-        // path of the file relative to the ancestor directory the user selected.
-        // e.g. 'docs/Old Prague/airbnb.pdf'
-        relativePath: file.relativePath || null,
-      },
-    }))
-
-    try {
-      this.uppy.addFiles(descriptors)
-    } catch (err) {
-      this.uppy.log(err)
-    }
-  }
-
-  // ___Why make insides of Dashboard invisible until first ResizeObserver event is emitted?
-  //    ResizeOberserver doesn't emit the first resize event fast enough, users can see the jump from one .uppy-size-- to
-  //    another (e.g. in Safari)
-  // ___Why not apply visibility property to .uppy-Dashboard-inner?
-  //    Because ideally, acc to specs, ResizeObserver should see invisible elements as of width 0. So even though applying
-  //    invisibility to .uppy-Dashboard-inner works now, it may not work in the future.
-  startListeningToResize = () => {
-    // Watch for Dashboard container (`.uppy-Dashboard-inner`) resize
-    // and update containerWidth/containerHeight in plugin state accordingly.
-    // Emits first event on initialization.
-    this.resizeObserver = new ResizeObserver((entries) => {
-      const uppyDashboardInnerEl = entries[0]
-
-      const { width, height } = uppyDashboardInnerEl.contentRect
-
-      this.uppy.log(`[Dashboard] resized: ${width} / ${height}`, 'debug')
-
-      this.setPluginState({
-        containerWidth: width,
-        containerHeight: height,
-        areInsidesReadyToBeVisible: true,
-      })
-    })
-    this.resizeObserver.observe(this.el.querySelector('.uppy-Dashboard-inner'))
-
-    // If ResizeObserver fails to emit an event telling us what size to use - default to the mobile view
-    this.makeDashboardInsidesVisibleAnywayTimeout = setTimeout(() => {
-      const pluginState = this.getPluginState()
-      const isModalAndClosed = !this.opts.inline && pluginState.isHidden
-      if (
-        // if ResizeObserver hasn't yet fired,
-        !pluginState.areInsidesReadyToBeVisible
-        // and it's not due to the modal being closed
-        && !isModalAndClosed
-      ) {
-        this.uppy.log("[Dashboard] resize event didn't fire on time: defaulted to mobile layout", 'debug')
-
-        this.setPluginState({
-          areInsidesReadyToBeVisible: true,
-        })
-      }
-    }, 1000)
-  }
-
-  stopListeningToResize = () => {
-    this.resizeObserver.disconnect()
-
-    clearTimeout(this.makeDashboardInsidesVisibleAnywayTimeout)
-  }
-
-  // Records whether we have been interacting with uppy right now,
-  // which is then used to determine whether state updates should trigger a refocusing.
-  recordIfFocusedOnUppyRecently = (event) => {
-    if (this.el.contains(event.target)) {
-      this.ifFocusedOnUppyRecently = true
-    } else {
-      this.ifFocusedOnUppyRecently = false
-      // ___Why run this.superFocus.cancel here when it already runs in superFocusOnEachUpdate?
-      //    Because superFocus is debounced, when we move from Uppy to some other element on the page,
-      //    previously run superFocus sometimes hits and moves focus back to Uppy.
-      this.superFocus.cancel()
-    }
-  }
-
-  disableAllFocusableElements = (disable) => {
-    const focusableNodes = toArray(this.el.querySelectorAll(FOCUSABLE_ELEMENTS))
-    if (disable) {
-      focusableNodes.forEach((node) => {
-        // save previous tabindex in a data-attribute, to restore when enabling
-        const currentTabIndex = node.getAttribute('tabindex')
-        if (currentTabIndex) {
-          node.dataset.inertTabindex = currentTabIndex
-        }
-        node.setAttribute('tabindex', '-1')
-      })
-    } else {
-      focusableNodes.forEach((node) => {
-        if ('inertTabindex' in node.dataset) {
-          node.setAttribute('tabindex', node.dataset.inertTabindex)
-        } else {
-          node.removeAttribute('tabindex')
-        }
-      })
-    }
-    this.dashboardIsDisabled = disable
-  }
-
-  updateBrowserHistory = () => {
-    // Ensure history state does not already contain our modal name to avoid double-pushing
-    // eslint-disable-next-line no-restricted-globals
-    if (!history.state?.[this.modalName]) {
-      // Push to history so that the page is not lost on browser back button press
-      // eslint-disable-next-line no-restricted-globals
-      history.pushState({
-        // eslint-disable-next-line no-restricted-globals
-        ...history.state,
-        [this.modalName]: true,
-      }, '')
-    }
-
-    // Listen for back button presses
-    window.addEventListener('popstate', this.handlePopState, false)
-  }
-
-  handlePopState = (event) => {
-    // Close the modal if the history state no longer contains our modal name
-    if (this.isModalOpen() && (!event.state || !event.state[this.modalName])) {
-      this.closeModal({ manualClose: false })
-    }
-
-    // When the browser back button is pressed and uppy is now the latest entry
-    // in the history but the modal is closed, fix the history by removing the
-    // uppy history entry.
-    // This occurs when another entry is added into the history state while the
-    // modal is open, and then the modal gets manually closed.
-    // Solves PR #575 (https://github.com/transloadit/uppy/pull/575)
-    if (!this.isModalOpen() && event.state?.[this.modalName]) {
-      // eslint-disable-next-line no-restricted-globals
-      history.back()
-    }
-  }
-
-  handleKeyDownInModal = (event) => {
-    // close modal on esc key press
-    if (event.keyCode === ESC_KEY) this.requestCloseModal(event)
-    // trap focus on tab key press
-    if (event.keyCode === TAB_KEY) trapFocus.forModal(event, this.getPluginState().activeOverlayType, this.el)
-  }
-
-  handleClickOutside = () => {
-    if (this.opts.closeModalOnClickOutside) this.requestCloseModal()
-  }
-
-  handlePaste = (event) => {
-    // Let any acquirer plugin (Url/Webcam/etc.) handle pastes to the root
-    this.uppy.iteratePlugins((plugin) => {
-      if (plugin.type === 'acquirer') {
-        // Every Plugin with .type acquirer can define handleRootPaste(event)
-        plugin.handleRootPaste?.(event)
-      }
-    })
-
-    // Add all dropped files
-    const files = toArray(event.clipboardData.files)
-    if (files.length > 0) {
-      this.uppy.log('[Dashboard] Files pasted')
-      this.addFiles(files)
-    }
-  }
-
-  handleInputChange = (event) => {
-    event.preventDefault()
-    const files = toArray(event.target.files)
-    if (files.length > 0) {
-      this.uppy.log('[Dashboard] Files selected through input')
-      this.addFiles(files)
-    }
-  }
-
-  handleDragOver = (event) => {
-    event.preventDefault()
-    event.stopPropagation()
-
-    // Check if some plugin can handle the datatransfer without files —
-    // for instance, the Url plugin can import a url
-    const canSomePluginHandleRootDrop = () => {
-      let somePluginCanHandleRootDrop = true
-      this.uppy.iteratePlugins((plugin) => {
-        if (plugin.canHandleRootDrop?.(event)) {
-          somePluginCanHandleRootDrop = true
-        }
-      })
-      return somePluginCanHandleRootDrop
-    }
-
-    // Check if the "type" of the datatransfer object includes files
-    const doesEventHaveFiles = () => {
-      const { types } = event.dataTransfer
-      return types.some(type => type === 'Files')
-    }
-
-    // Deny drop, if no plugins can handle datatransfer, there are no files,
-    // or when opts.disabled is set, or new uploads are not allowed
-    const somePluginCanHandleRootDrop = canSomePluginHandleRootDrop(event)
-    const hasFiles = doesEventHaveFiles(event)
-    if (
-      (!somePluginCanHandleRootDrop && !hasFiles)
-      || this.opts.disabled
-      // opts.disableLocalFiles should only be taken into account if no plugins
-      // can handle the datatransfer
-      || (this.opts.disableLocalFiles && (hasFiles || !somePluginCanHandleRootDrop))
-      || !this.uppy.getState().allowNewUpload
-    ) {
-      event.dataTransfer.dropEffect = 'none'
-      clearTimeout(this.removeDragOverClassTimeout)
-      return
-    }
-
-    // Add a small (+) icon on drop
-    // (and prevent browsers from interpreting this as files being _moved_ into the
-    // browser, https://github.com/transloadit/uppy/issues/1978).
-    event.dataTransfer.dropEffect = 'copy'
-
-    clearTimeout(this.removeDragOverClassTimeout)
-    this.setPluginState({ isDraggingOver: true })
-
-    this.opts.onDragOver?.(event)
-  }
-
-  handleDragLeave = (event) => {
-    event.preventDefault()
-    event.stopPropagation()
-
-    clearTimeout(this.removeDragOverClassTimeout)
-    // Timeout against flickering, this solution is taken from drag-drop library.
-    // Solution with 'pointer-events: none' didn't work across browsers.
-    this.removeDragOverClassTimeout = setTimeout(() => {
-      this.setPluginState({ isDraggingOver: false })
-    }, 50)
-
-    this.opts.onDragLeave?.(event)
-  }
-
-  handleDrop = async (event) => {
-    event.preventDefault()
-    event.stopPropagation()
-
-    clearTimeout(this.removeDragOverClassTimeout)
-
-    this.setPluginState({ isDraggingOver: false })
-
-    // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
-    this.uppy.iteratePlugins((plugin) => {
-      if (plugin.type === 'acquirer') {
-        // Every Plugin with .type acquirer can define handleRootDrop(event)
-        plugin.handleRootDrop?.(event)
-      }
-    })
-
-    // Add all dropped files
-    let executedDropErrorOnce = false
-    const logDropError = (error) => {
-      this.uppy.log(error, 'error')
-
-      // In practice all drop errors are most likely the same,
-      // so let's just show one to avoid overwhelming the user
-      if (!executedDropErrorOnce) {
-        this.uppy.info(error.message, 'error')
-        executedDropErrorOnce = true
-      }
-    }
-
-    // Add all dropped files
-    const files = await getDroppedFiles(event.dataTransfer, { logDropError })
-    if (files.length > 0) {
-      this.uppy.log('[Dashboard] Files dropped')
-      this.addFiles(files)
-    }
-
-    this.opts.onDrop?.(event)
-  }
-
-  handleRequestThumbnail = (file) => {
-    if (!this.opts.waitForThumbnailsBeforeUpload) {
-      this.uppy.emit('thumbnail:request', file)
-    }
-  }
-
-  /**
-   * We cancel thumbnail requests when a file item component unmounts to avoid
-   * clogging up the queue when the user scrolls past many elements.
-   */
-  handleCancelThumbnail = (file) => {
-    if (!this.opts.waitForThumbnailsBeforeUpload) {
-      this.uppy.emit('thumbnail:cancel', file)
-    }
-  }
-
-  handleKeyDownInInline = (event) => {
-    // Trap focus on tab key press.
-    if (event.keyCode === TAB_KEY) trapFocus.forInline(event, this.getPluginState().activeOverlayType, this.el)
-  }
-
-  // ___Why do we listen to the 'paste' event on a document instead of onPaste={props.handlePaste} prop,
-  //    or this.el.addEventListener('paste')?
-  //    Because (at least) Chrome doesn't handle paste if focus is on some button, e.g. 'My Device'.
-  //    => Therefore, the best option is to listen to all 'paste' events, and only react to them when we are focused on our
-  //       particular Uppy instance.
-  // ___Why do we still need onPaste={props.handlePaste} for the DashboardUi?
-  //    Because if we click on the 'Drop files here' caption e.g., `document.activeElement` will be 'body'. Which means our
-  //    standard determination of whether we're pasting into our Uppy instance won't work.
-  //    => Therefore, we need a traditional onPaste={props.handlePaste} handler too.
-  handlePasteOnBody = (event) => {
-    const isFocusInOverlay = this.el.contains(document.activeElement)
-    if (isFocusInOverlay) {
-      this.handlePaste(event)
-    }
-  }
-
-  handleComplete = ({ failed }) => {
-    if (this.opts.closeAfterFinish && failed.length === 0) {
-      // All uploads are done
-      this.requestCloseModal()
-    }
-  }
-
-  handleCancelRestore = () => {
-    this.uppy.emit('restore-canceled')
-  }
-
-  #openFileEditorWhenFilesAdded = (files) => {
-    const firstFile = files[0]
-    if (this.canEditFile(firstFile)) {
-      this.openFileEditor(firstFile)
-    }
-  }
-
-  initEvents = () => {
-    // Modal open button
-    if (this.opts.trigger && !this.opts.inline) {
-      const showModalTrigger = findAllDOMElements(this.opts.trigger)
-      if (showModalTrigger) {
-        showModalTrigger.forEach(trigger => trigger.addEventListener('click', this.openModal))
-      } else {
-        this.uppy.log('Dashboard modal trigger not found. Make sure `trigger` is set in Dashboard options, unless you are planning to call `dashboard.openModal()` method yourself', 'warning')
-      }
-    }
-
-    this.startListeningToResize()
-    document.addEventListener('paste', this.handlePasteOnBody)
-
-    this.uppy.on('plugin-remove', this.removeTarget)
-    this.uppy.on('file-added', this.hideAllPanels)
-    this.uppy.on('dashboard:modal-closed', this.hideAllPanels)
-    this.uppy.on('file-editor:complete', this.hideAllPanels)
-    this.uppy.on('complete', this.handleComplete)
-
-    // ___Why fire on capture?
-    //    Because this.ifFocusedOnUppyRecently needs to change before onUpdate() fires.
-    document.addEventListener('focus', this.recordIfFocusedOnUppyRecently, true)
-    document.addEventListener('click', this.recordIfFocusedOnUppyRecently, true)
-
-    if (this.opts.inline) {
-      this.el.addEventListener('keydown', this.handleKeyDownInInline)
-    }
-
-    if (this.opts.autoOpenFileEditor) {
-      this.uppy.on('files-added', this.#openFileEditorWhenFilesAdded)
-    }
-  }
-
-  removeEvents = () => {
-    const showModalTrigger = findAllDOMElements(this.opts.trigger)
-    if (!this.opts.inline && showModalTrigger) {
-      showModalTrigger.forEach(trigger => trigger.removeEventListener('click', this.openModal))
-    }
-
-    this.stopListeningToResize()
-    document.removeEventListener('paste', this.handlePasteOnBody)
-
-    window.removeEventListener('popstate', this.handlePopState, false)
-    this.uppy.off('plugin-remove', this.removeTarget)
-    this.uppy.off('file-added', this.hideAllPanels)
-    this.uppy.off('dashboard:modal-closed', this.hideAllPanels)
-    this.uppy.off('file-editor:complete', this.hideAllPanels)
-    this.uppy.off('complete', this.handleComplete)
-
-    document.removeEventListener('focus', this.recordIfFocusedOnUppyRecently)
-    document.removeEventListener('click', this.recordIfFocusedOnUppyRecently)
-
-    if (this.opts.inline) {
-      this.el.removeEventListener('keydown', this.handleKeyDownInInline)
-    }
-
-    if (this.opts.autoOpenFileEditor) {
-      this.uppy.off('files-added', this.#openFileEditorWhenFilesAdded)
-    }
-  }
-
-  superFocusOnEachUpdate = () => {
-    const isFocusInUppy = this.el.contains(document.activeElement)
-    // When focus is lost on the page (== focus is on body for most browsers, or focus is null for IE11)
-    const isFocusNowhere = document.activeElement === document.body || document.activeElement === null
-    const isInformerHidden = this.uppy.getState().info.length === 0
-    const isModal = !this.opts.inline
-
-    if (
-      // If update is connected to showing the Informer - let the screen reader calmly read it.
-      isInformerHidden
-      && (
-        // If we are in a modal - always superfocus without concern for other elements
-        // on the page (user is unlikely to want to interact with the rest of the page)
-        isModal
-        // If we are already inside of Uppy, or
-        || isFocusInUppy
-        // If we are not focused on anything BUT we have already, at least once, focused on uppy
-        //   1. We focus when isFocusNowhere, because when the element we were focused
-        //      on disappears (e.g. an overlay), - focus gets lost. If user is typing
-        //      something somewhere else on the page, - focus won't be 'nowhere'.
-        //   2. We only focus when focus is nowhere AND this.ifFocusedOnUppyRecently,
-        //      to avoid focus jumps if we do something else on the page.
-        //   [Practical check] Without '&& this.ifFocusedOnUppyRecently', in Safari, in inline mode,
-        //                     when file is uploading, - navigate via tab to the checkbox,
-        //                     try to press space multiple times. Focus will jump to Uppy.
-        || (isFocusNowhere && this.ifFocusedOnUppyRecently)
-      )
-    ) {
-      this.superFocus(this.el, this.getPluginState().activeOverlayType)
-    } else {
-      this.superFocus.cancel()
-    }
-  }
-
-  afterUpdate = () => {
-    if (this.opts.disabled && !this.dashboardIsDisabled) {
-      this.disableAllFocusableElements(true)
-      return
-    }
-
-    if (!this.opts.disabled && this.dashboardIsDisabled) {
-      this.disableAllFocusableElements(false)
-    }
-
-    this.superFocusOnEachUpdate()
-  }
-
-  saveFileCard = (meta, fileID) => {
-    this.uppy.setFileMeta(fileID, meta)
-    this.toggleFileCard(false, fileID)
-  }
-
-  #attachRenderFunctionToTarget = (target) => {
-    const plugin = this.uppy.getPlugin(target.id)
-    return {
-      ...target,
-      icon: plugin.icon || this.opts.defaultPickerIcon,
-      render: plugin.render,
-    }
-  }
-
-  #isTargetSupported = (target) => {
-    const plugin = this.uppy.getPlugin(target.id)
-    // If the plugin does not provide a `supported` check, assume the plugin works everywhere.
-    if (typeof plugin.isSupported !== 'function') {
-      return true
-    }
-    return plugin.isSupported()
-  }
-
-  #getAcquirers = memoize((targets) => {
-    return targets
-      .filter(target => target.type === 'acquirer' && this.#isTargetSupported(target))
-      .map(this.#attachRenderFunctionToTarget)
-  })
-
-  #getProgressIndicators = memoize((targets) => {
-    return targets
-      .filter(target => target.type === 'progressindicator')
-      .map(this.#attachRenderFunctionToTarget)
-  })
-
-  #getEditors = memoize((targets) => {
-    return targets
-      .filter(target => target.type === 'editor')
-      .map(this.#attachRenderFunctionToTarget)
-  })
-
-  render = (state) => {
-    const pluginState = this.getPluginState()
-    const { files, capabilities, allowNewUpload } = state
-    const {
-      newFiles,
-      uploadStartedFiles,
-      completeFiles,
-      erroredFiles,
-      inProgressFiles,
-      inProgressNotPausedFiles,
-      processingFiles,
-
-      isUploadStarted,
-      isAllComplete,
-      isAllErrored,
-      isAllPaused,
-    } = this.uppy.getObjectOfFilesPerState()
-
-    const acquirers = this.#getAcquirers(pluginState.targets)
-    const progressindicators = this.#getProgressIndicators(pluginState.targets)
-    const editors = this.#getEditors(pluginState.targets)
-
-    let theme
-    if (this.opts.theme === 'auto') {
-      theme = capabilities.darkMode ? 'dark' : 'light'
-    } else {
-      theme = this.opts.theme
-    }
-
-    if (['files', 'folders', 'both'].indexOf(this.opts.fileManagerSelectionType) < 0) {
-      this.opts.fileManagerSelectionType = 'files'
-      // eslint-disable-next-line no-console
-      console.warn(`Unsupported option for "fileManagerSelectionType". Using default of "${this.opts.fileManagerSelectionType}".`)
-    }
-
-    return DashboardUI({
-      state,
-      isHidden: pluginState.isHidden,
-      files,
-      newFiles,
-      uploadStartedFiles,
-      completeFiles,
-      erroredFiles,
-      inProgressFiles,
-      inProgressNotPausedFiles,
-      processingFiles,
-      isUploadStarted,
-      isAllComplete,
-      isAllErrored,
-      isAllPaused,
-      totalFileCount: Object.keys(files).length,
-      totalProgress: state.totalProgress,
-      allowNewUpload,
-      acquirers,
-      theme,
-      disabled: this.opts.disabled,
-      disableLocalFiles: this.opts.disableLocalFiles,
-      direction: this.opts.direction,
-      activePickerPanel: pluginState.activePickerPanel,
-      showFileEditor: pluginState.showFileEditor,
-      saveFileEditor: this.saveFileEditor,
-      disableAllFocusableElements: this.disableAllFocusableElements,
-      animateOpenClose: this.opts.animateOpenClose,
-      isClosing: pluginState.isClosing,
-      progressindicators,
-      editors,
-      autoProceed: this.uppy.opts.autoProceed,
-      id: this.id,
-      closeModal: this.requestCloseModal,
-      handleClickOutside: this.handleClickOutside,
-      handleInputChange: this.handleInputChange,
-      handlePaste: this.handlePaste,
-      inline: this.opts.inline,
-      showPanel: this.showPanel,
-      hideAllPanels: this.hideAllPanels,
-      i18n: this.i18n,
-      i18nArray: this.i18nArray,
-      uppy: this.uppy,
-      note: this.opts.note,
-      recoveredState: state.recoveredState,
-      metaFields: pluginState.metaFields,
-      resumableUploads: capabilities.resumableUploads || false,
-      individualCancellation: capabilities.individualCancellation,
-      isMobileDevice: capabilities.isMobileDevice,
-      fileCardFor: pluginState.fileCardFor,
-      toggleFileCard: this.toggleFileCard,
-      toggleAddFilesPanel: this.toggleAddFilesPanel,
-      showAddFilesPanel: pluginState.showAddFilesPanel,
-      saveFileCard: this.saveFileCard,
-      openFileEditor: this.openFileEditor,
-      canEditFile: this.canEditFile,
-      width: this.opts.width,
-      height: this.opts.height,
-      showLinkToFileUploadResult: this.opts.showLinkToFileUploadResult,
-      fileManagerSelectionType: this.opts.fileManagerSelectionType,
-      proudlyDisplayPoweredByUppy: this.opts.proudlyDisplayPoweredByUppy,
-      hideCancelButton: this.opts.hideCancelButton,
-      hideRetryButton: this.opts.hideRetryButton,
-      hidePauseResumeButton: this.opts.hidePauseResumeButton,
-      showRemoveButtonAfterComplete: this.opts.showRemoveButtonAfterComplete,
-      containerWidth: pluginState.containerWidth,
-      containerHeight: pluginState.containerHeight,
-      areInsidesReadyToBeVisible: pluginState.areInsidesReadyToBeVisible,
-      isTargetDOMEl: this.isTargetDOMEl,
-      parentElement: this.el,
-      allowedFileTypes: this.uppy.opts.restrictions.allowedFileTypes,
-      maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles,
-      requiredMetaFields: this.uppy.opts.restrictions.requiredMetaFields,
-      showSelectedFiles: this.opts.showSelectedFiles,
-      handleCancelRestore: this.handleCancelRestore,
-      handleRequestThumbnail: this.handleRequestThumbnail,
-      handleCancelThumbnail: this.handleCancelThumbnail,
-      // drag props
-      isDraggingOver: pluginState.isDraggingOver,
-      handleDragOver: this.handleDragOver,
-      handleDragLeave: this.handleDragLeave,
-      handleDrop: this.handleDrop,
-    })
-  }
-
-  discoverProviderPlugins = () => {
-    this.uppy.iteratePlugins((plugin) => {
-      if (plugin && !plugin.target && plugin.opts && plugin.opts.target === this.constructor) {
-        this.addTarget(plugin)
-      }
-    })
-  }
-
-  install = () => {
-    // Set default state for Dashboard
-    this.setPluginState({
-      isHidden: true,
-      fileCardFor: null,
-      activeOverlayType: null,
-      showAddFilesPanel: false,
-      activePickerPanel: false,
-      showFileEditor: false,
-      metaFields: this.opts.metaFields,
-      targets: [],
-      // We'll make them visible once .containerWidth is determined
-      areInsidesReadyToBeVisible: false,
-      isDraggingOver: false,
-    })
-
-    const { inline, closeAfterFinish } = this.opts
-    if (inline && closeAfterFinish) {
-      throw new Error('[Dashboard] `closeAfterFinish: true` cannot be used on an inline Dashboard, because an inline Dashboard cannot be closed at all. Either set `inline: false`, or disable the `closeAfterFinish` option.')
-    }
-
-    const { allowMultipleUploads, allowMultipleUploadBatches } = this.uppy.opts
-    if ((allowMultipleUploads || allowMultipleUploadBatches) && closeAfterFinish) {
-      this.uppy.log('[Dashboard] When using `closeAfterFinish`, we recommended setting the `allowMultipleUploadBatches` option to `false` in the Uppy constructor. See https://uppy.io/docs/uppy/#allowMultipleUploads-true', 'warning')
-    }
-
-    const { target } = this.opts
-
-    if (target) {
-      this.mount(target, this)
-    }
-
-    const plugins = this.opts.plugins || []
-
-    plugins.forEach((pluginID) => {
-      const plugin = this.uppy.getPlugin(pluginID)
-      if (plugin) {
-        plugin.mount(this, plugin)
-      }
-    })
-
-    if (!this.opts.disableStatusBar) {
-      this.uppy.use(StatusBar, {
-        id: `${this.id}:StatusBar`,
-        target: this,
-        hideUploadButton: this.opts.hideUploadButton,
-        hideRetryButton: this.opts.hideRetryButton,
-        hidePauseResumeButton: this.opts.hidePauseResumeButton,
-        hideCancelButton: this.opts.hideCancelButton,
-        showProgressDetails: this.opts.showProgressDetails,
-        hideAfterFinish: this.opts.hideProgressAfterFinish,
-        locale: this.opts.locale,
-        doneButtonHandler: this.opts.doneButtonHandler,
-      })
-    }
-
-    if (!this.opts.disableInformer) {
-      this.uppy.use(Informer, {
-        id: `${this.id}:Informer`,
-        target: this,
-      })
-    }
-
-    if (!this.opts.disableThumbnailGenerator) {
-      this.uppy.use(ThumbnailGenerator, {
-        id: `${this.id}:ThumbnailGenerator`,
-        thumbnailWidth: this.opts.thumbnailWidth,
-        thumbnailHeight: this.opts.thumbnailHeight,
-        thumbnailType: this.opts.thumbnailType,
-        waitForThumbnailsBeforeUpload: this.opts.waitForThumbnailsBeforeUpload,
-        // If we don't block on thumbnails, we can lazily generate them
-        lazy: !this.opts.waitForThumbnailsBeforeUpload,
-      })
-    }
-
-    // Dark Mode / theme
-    this.darkModeMediaQuery = (typeof window !== 'undefined' && window.matchMedia)
-      ? window.matchMedia('(prefers-color-scheme: dark)')
-      : null
-
-    const isDarkModeOnFromTheStart = this.darkModeMediaQuery ? this.darkModeMediaQuery.matches : false
-    this.uppy.log(`[Dashboard] Dark mode is ${isDarkModeOnFromTheStart ? 'on' : 'off'}`)
-    this.setDarkModeCapability(isDarkModeOnFromTheStart)
-
-    if (this.opts.theme === 'auto') {
-      this.darkModeMediaQuery.addListener(this.handleSystemDarkModeChange)
-    }
-
-    this.discoverProviderPlugins()
-    this.initEvents()
-  }
-
-  uninstall = () => {
-    if (!this.opts.disableInformer) {
-      const informer = this.uppy.getPlugin(`${this.id}:Informer`)
-      // Checking if this plugin exists, in case it was removed by uppy-core
-      // before the Dashboard was.
-      if (informer) this.uppy.removePlugin(informer)
-    }
-
-    if (!this.opts.disableStatusBar) {
-      const statusBar = this.uppy.getPlugin(`${this.id}:StatusBar`)
-      if (statusBar) this.uppy.removePlugin(statusBar)
-    }
-
-    if (!this.opts.disableThumbnailGenerator) {
-      const thumbnail = this.uppy.getPlugin(`${this.id}:ThumbnailGenerator`)
-      if (thumbnail) this.uppy.removePlugin(thumbnail)
-    }
-
-    const plugins = this.opts.plugins || []
-    plugins.forEach((pluginID) => {
-      const plugin = this.uppy.getPlugin(pluginID)
-      if (plugin) plugin.unmount()
-    })
-
-    if (this.opts.theme === 'auto') {
-      this.darkModeMediaQuery.removeListener(this.handleSystemDarkModeChange)
-    }
-
-    this.unmount()
-    this.removeEvents()
-  }
-}
+export { default } from './Dashboard.jsx'

+ 8 - 5
packages/@uppy/dashboard/src/index.test.js

@@ -1,11 +1,14 @@
-const Core = require('@uppy/core')
-const StatusBarPlugin = require('@uppy/status-bar')
-const GoogleDrivePlugin = require('@uppy/google-drive') // eslint-disable-line
-const DashboardPlugin = require('./index')
+import { describe, it, expect } from '@jest/globals'
+
+import Core from '@uppy/core'
+import StatusBarPlugin from '@uppy/status-bar'
+import GoogleDrivePlugin from '@uppy/google-drive'
+import resizeObserverPolyfill from 'resize-observer-polyfill'
+import DashboardPlugin from '../lib/index.js'
 
 describe('Dashboard', () => {
   beforeAll(() => {
-    globalThis.ResizeObserver = require('resize-observer-polyfill').default || require('resize-observer-polyfill')
+    globalThis.ResizeObserver = resizeObserverPolyfill.default || resizeObserverPolyfill
   })
   afterAll(() => {
     delete globalThis.ResizeObserver

+ 1 - 1
packages/@uppy/dashboard/src/locale.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   strings: {
     // When `inline: false`, used as the screen reader label for the button that closes the modal.
     closeModal: 'Close Modal',

+ 3 - 2
packages/@uppy/dashboard/src/utils/copyToClipboard.js

@@ -8,8 +8,9 @@
  * @param {string} fallbackString
  * @returns {Promise}
  */
-module.exports = function copyToClipboard (textToCopy, fallbackString) {
-  fallbackString = fallbackString || 'Copy the URL below'
+export default function copyToClipboard (textToCopy, fallbackString) {
+  // TODO: make `fallbackString` an optional parameter instead.
+  fallbackString ||= 'Copy the URL below' // eslint-disable-line no-param-reassign
 
   return new Promise((resolve) => {
     const textArea = document.createElement('textarea')

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

@@ -1,4 +1,5 @@
-const copyToClipboard = require('./copyToClipboard')
+import { describe, xit, expect } from '@jest/globals'
+import copyToClipboard from './copyToClipboard.js'
 
 describe('copyToClipboard', () => {
   xit('should copy the specified text to the clipboard', () => {

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

@@ -1,6 +1,6 @@
-const debounce = require('lodash.debounce')
-const FOCUSABLE_ELEMENTS = require('@uppy/utils/lib/FOCUSABLE_ELEMENTS')
-const getActiveOverlayEl = require('./getActiveOverlayEl')
+import debounce from 'lodash.debounce'
+import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS'
+import getActiveOverlayEl from './getActiveOverlayEl.js'
 
 /*
   Focuses on some element in the currently topmost overlay.
@@ -12,7 +12,7 @@ const getActiveOverlayEl = require('./getActiveOverlayEl')
   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.
 */
-module.exports = function createSuperFocus () {
+export default function createSuperFocus () {
   let lastFocusWasOnSuperFocusableEl = false
 
   const superFocus = (dashboardEl, activeOverlayType) => {

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

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

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

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

+ 2 - 2
packages/@uppy/dashboard/src/utils/getFileTypeIcon.js → packages/@uppy/dashboard/src/utils/getFileTypeIcon.jsx

@@ -1,4 +1,4 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
 function iconImage () {
   return (
@@ -63,7 +63,7 @@ function iconText () {
   )
 }
 
-module.exports = function getIconByMime (fileType) {
+export default function getIconByMime (fileType) {
   const defaultChoice = {
     color: '#838999',
     icon: iconFile(),

+ 1 - 1
packages/@uppy/dashboard/src/utils/ignoreEvent.js

@@ -14,4 +14,4 @@ function ignoreEvent (ev) {
   ev.stopPropagation()
 }
 
-module.exports = ignoreEvent
+export default ignoreEvent

+ 16 - 20
packages/@uppy/dashboard/src/utils/trapFocus.js

@@ -1,6 +1,6 @@
-const toArray = require('@uppy/utils/lib/toArray')
-const FOCUSABLE_ELEMENTS = require('@uppy/utils/lib/FOCUSABLE_ELEMENTS')
-const getActiveOverlayEl = require('./getActiveOverlayEl')
+import toArray from '@uppy/utils/lib/toArray'
+import FOCUSABLE_ELEMENTS from '@uppy/utils/lib/FOCUSABLE_ELEMENTS'
+import getActiveOverlayEl from './getActiveOverlayEl.js'
 
 function focusOnFirstNode (event, nodes) {
   const node = nodes[0]
@@ -49,23 +49,19 @@ function trapFocus (event, activeOverlayType, dashboardEl) {
   }
 }
 
-module.exports = {
-  // Traps focus inside of the currently open overlay (e.g. Dashboard, or e.g. Instagram),
-  // never lets focus disappear from the modal.
-  forModal: (event, activeOverlayType, dashboardEl) => {
-    trapFocus(event, activeOverlayType, dashboardEl)
-  },
+// 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 }
 
-  // Traps focus inside of the currently open overlay, unless overlay is null - then let the user tab away.
-  forInline: (event, activeOverlayType, dashboardEl) => {
-    // ___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
+// Traps focus inside of the currently open overlay, unless overlay is null - then let the user tab away.
+export function forInline (event, activeOverlayType, dashboardEl) {
+  // ___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
     // ___When there is some overlay with 'Done' button
-    } else {
-      // Trap the focus inside this overlay!
-      // User can close the overlay (click 'Done') if they want to travel away from Uppy.
-      trapFocus(event, activeOverlayType, dashboardEl)
-    }
-  },
+  } else {
+    // Trap the focus inside this overlay!
+    // User can close the overlay (click 'Done') if they want to travel away from Uppy.
+    trapFocus(event, activeOverlayType, dashboardEl)
+  }
 }

+ 1 - 1
website/src/docs/dashboard.md

@@ -337,7 +337,7 @@ Dashboard ships with the `ThumbnailGenerator` plugin that adds small resized ima
 <!-- eslint-disable no-restricted-globals, no-multiple-empty-lines -->
 
 ```js
-module.exports = {
+export default {
   strings: {
     // When `inline: false`, used as the screen reader label for the button that closes the modal.
     closeModal: 'Close Modal',

+ 1 - 0
yarn.lock

@@ -9822,6 +9822,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@uppy/dashboard@workspace:packages/@uppy/dashboard"
   dependencies:
+    "@jest/globals": ^27.4.2
     "@transloadit/prettier-bytes": 0.0.7
     "@uppy/google-drive": "workspace:^"
     "@uppy/informer": "workspace:^"