Browse Source

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

Antoine du Hamel 2 years ago
parent
commit
327509a7dc
34 changed files with 1311 additions and 1274 deletions
  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/aws-s3-multipart/src/**/*.js',
         'packages/@uppy/box/src/**/*.js',
         'packages/@uppy/box/src/**/*.js',
         'packages/@uppy/compressor/src/**/*.js',
         'packages/@uppy/compressor/src/**/*.js',
+        'packages/@uppy/dashboard/src/**/*.js',
         'packages/@uppy/drag-drop/src/**/*.js',
         'packages/@uppy/drag-drop/src/**/*.js',
         'packages/@uppy/drop-target/src/**/*.js',
         'packages/@uppy/drop-target/src/**/*.js',
         'packages/@uppy/dropbox/src/**/*.js',
         'packages/@uppy/dropbox/src/**/*.js',
@@ -269,6 +270,14 @@ module.exports = {
         'import/extensions': ['error', 'ignorePackages'],
         'import/extensions': ['error', 'ignorePackages'],
       },
       },
     },
     },
+    {
+      files: [
+        'packages/@uppy/dashboard/src/components/**/*.jsx',
+      ],
+      rules: {
+        'react/destructuring-assignment': 'off',
+      },
+    },
     {
     {
       files: [
       files: [
         // Those need looser rules, and cannot be made part of the stricter rules above.
         // 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",
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "style": "dist/style.min.css",
   "types": "types/index.d.ts",
   "types": "types/index.d.ts",
+  "type": "module",
   "keywords": [
   "keywords": [
     "file uploader",
     "file uploader",
     "uppy",
     "uppy",
@@ -36,6 +37,7 @@
     "preact": "^10.5.13"
     "preact": "^10.5.13"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@jest/globals": "^27.4.2",
     "@uppy/google-drive": "workspace:^",
     "@uppy/google-drive": "workspace:^",
     "@uppy/status-bar": "workspace:^",
     "@uppy/status-bar": "workspace:^",
     "resize-observer-polyfill": "^1.5.0"
     "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 {
 class AddFiles extends Component {
   triggerFileInputClick = () => {
   triggerFileInputClick = () => {
@@ -18,7 +18,7 @@ class AddFiles extends Component {
     // ___Why not use value="" on <input/> instead?
     // ___Why not use value="" on <input/> instead?
     //    Because if we use that method of clearing the input,
     //    Because if we use that method of clearing the input,
     //    Chrome will not trigger change if we drop the same file twice (Issue #768).
     //    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) => {
   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) => {
 const AddFilesPanel = (props) => {
   return (
   return (
@@ -21,9 +21,10 @@ const AddFilesPanel = (props) => {
           {props.i18n('back')}
           {props.i18n('back')}
         </button>
         </button>
       </div>
       </div>
+      {/* eslint-disable-next-line react/jsx-props-no-spreading */}
       <AddFiles {...props} />
       <AddFiles {...props} />
     </div>
     </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
 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal
 // https://github.com/ghosh/micromodal
@@ -18,7 +18,7 @@ const WIDTH_LG = 700
 const WIDTH_MD = 576
 const WIDTH_MD = 576
 const HEIGHT_MD = 400
 const HEIGHT_MD = 400
 
 
-module.exports = function Dashboard (props) {
+export default function Dashboard (props) {
   const noFiles = props.totalFileCount === 0
   const noFiles = props.totalFileCount === 0
   const isSizeMD = props.containerWidth > WIDTH_MD
   const isSizeMD = props.containerWidth > WIDTH_MD
 
 
@@ -110,6 +110,7 @@ module.exports = function Dashboard (props) {
             {props.i18n('dropHint')}
             {props.i18n('dropHint')}
           </div>
           </div>
 
 
+          {/* eslint-disable-next-line react/jsx-props-no-spreading */}
           {showFileList && <PanelTopBar {...props} />}
           {showFileList && <PanelTopBar {...props} />}
 
 
           {numberOfFilesForRecovery && (
           {numberOfFilesForRecovery && (
@@ -132,26 +133,32 @@ module.exports = function Dashboard (props) {
 
 
           {showFileList ? (
           {showFileList ? (
             <FileList
             <FileList
+              // eslint-disable-next-line react/jsx-props-no-spreading
               {...props}
               {...props}
               itemsPerRow={itemsPerRow}
               itemsPerRow={itemsPerRow}
             />
             />
           ) : (
           ) : (
+            // eslint-disable-next-line react/jsx-props-no-spreading
             <AddFiles {...props} isSizeMD={isSizeMD} />
             <AddFiles {...props} isSizeMD={isSizeMD} />
           )}
           )}
 
 
           <Slide>
           <Slide>
+            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {props.showAddFilesPanel ? <AddFilesPanel key="AddFiles" {...props} isSizeMD={isSizeMD} /> : null}
             {props.showAddFilesPanel ? <AddFilesPanel key="AddFiles" {...props} isSizeMD={isSizeMD} /> : null}
           </Slide>
           </Slide>
 
 
           <Slide>
           <Slide>
+            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {props.fileCardFor ? <FileCard key="FileCard" {...props} /> : null}
             {props.fileCardFor ? <FileCard key="FileCard" {...props} /> : null}
           </Slide>
           </Slide>
 
 
           <Slide>
           <Slide>
+            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {props.activePickerPanel ? <PickerPanelContent key="Picker" {...props} /> : null}
             {props.activePickerPanel ? <PickerPanelContent key="Picker" {...props} /> : null}
           </Slide>
           </Slide>
 
 
           <Slide>
           <Slide>
+            {/* eslint-disable-next-line react/jsx-props-no-spreading */}
             {props.showFileEditor ? <EditorPanel key="Editor" {...props} /> : null}
             {props.showFileEditor ? <EditorPanel key="Editor" {...props} /> : null}
           </Slide>
           </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) {
 function EditorPanel (props) {
   const file = props.files[props.fileCardFor]
   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 {
 class FileCard extends Component {
   form = document.createElement('form')
   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 ({
 function EditButton ({
   file,
   file,
@@ -80,7 +80,7 @@ function CopyLinkButton (props) {
   )
   )
 }
 }
 
 
-module.exports = function Buttons (props) {
+export default function Buttons (props) {
   const {
   const {
     uppy,
     uppy,
     file,
     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 renderFileName = (props) => {
   const { author, name } = props.file.meta
   const { author, name } = props.file.meta
@@ -92,7 +92,7 @@ const ErrorButton = ({ file, onClick }) => {
   return null
   return null
 }
 }
 
 
-module.exports = function FileInfo (props) {
+export default function FileInfo (props) {
   const { file } = props
   const { file } = props
   return (
   return (
     <div
     <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 (
   return (
     <div
     <div
       className="uppy-Dashboard-Item-previewInnerWrap"
       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) {
 function onPauseResumeCancelRetry (props) {
   if (props.isUploaded) return
   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
   // Nothing if upload has not started
   if (!props.file.progress.uploadStarted) {
   if (!props.file.progress.uploadStarted) {
     return null
     return null
@@ -117,12 +117,13 @@ module.exports = function FileProgress (props) {
   }
   }
 
 
   if (props.recoveredState) {
   if (props.recoveredState) {
-    return
+    return undefined
   }
   }
 
 
   // Retry button for error
   // Retry button for error
   if (props.error && !props.hideRetryButton) {
   if (props.error && !props.hideRetryButton) {
     return (
     return (
+      // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
       <ProgressIndicatorButton {...props}>
         <svg aria-hidden="true" focusable="false" className="uppy-c-icon uppy-Dashboard-Item-progressIcon--retry" width="28" height="31" viewBox="0 0 16 19">
         <svg aria-hidden="true" focusable="false" className="uppy-c-icon uppy-Dashboard-Item-progressIcon--retry" width="28" height="31" viewBox="0 0 16 19">
           <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
           <path d="M16 11a8 8 0 1 1-8-8v2a6 6 0 1 0 6 6h2z" />
@@ -137,6 +138,7 @@ module.exports = function FileProgress (props) {
   // Pause/resume button for resumable uploads
   // Pause/resume button for resumable uploads
   if (props.resumableUploads && !props.hidePauseResumeButton) {
   if (props.resumableUploads && !props.hidePauseResumeButton) {
     return (
     return (
+      // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
       <ProgressIndicatorButton {...props}>
         <ProgressCircleContainer>
         <ProgressCircleContainer>
           <ProgressCircle progress={props.file.progress.percentage} />
           <ProgressCircle progress={props.file.progress.percentage} />
@@ -158,6 +160,7 @@ module.exports = function FileProgress (props) {
   // Cancel button for non-resumable uploads if individualCancellation is supported (not bundled)
   // Cancel button for non-resumable uploads if individualCancellation is supported (not bundled)
   if (!props.resumableUploads && props.individualCancellation && !props.hideCancelButton) {
   if (!props.resumableUploads && props.individualCancellation && !props.hideCancelButton) {
     return (
     return (
+      // eslint-disable-next-line react/jsx-props-no-spreading
       <ProgressIndicatorButton {...props}>
       <ProgressIndicatorButton {...props}>
         <ProgressCircleContainer>
         <ProgressCircleContainer>
           <ProgressCircle progress={props.file.progress.percentage} />
           <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 metaFieldIdToName = (metaFieldId, metaFields) => {
   const field = metaFields.filter(f => f.id === metaFieldId)
   const field = metaFields.filter(f => f.id === metaFieldId)
   return field[0].name
   return field[0].name
 }
 }
 
 
-module.exports = function renderMissingMetaFieldsError (props) {
+export default function renderMissingMetaFieldsError (props) {
   const { file, toggleFileCard, i18n, metaFields } = props
   const { file, toggleFileCard, i18n, metaFields } = props
   const { missingRequiredMetaFields } = file
   const { missingRequiredMetaFields } = file
   if (!missingRequiredMetaFields?.length) {
   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 () {
   componentDidMount () {
     const { file } = this.props
     const { file } = this.props
     if (!file.preview) {
     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) {
 function chunks (list, size) {
   const chunked = []
   const chunked = []
@@ -18,7 +18,7 @@ function chunks (list, size) {
   return chunked
   return chunked
 }
 }
 
 
-module.exports = (props) => {
+export default (props) => {
   const noFiles = props.totalFileCount === 0
   const noFiles = props.totalFileCount === 0
   const dashboardFilesClass = classNames(
   const dashboardFilesClass = classNames(
     'uppy-Dashboard-files',
     'uppy-Dashboard-files',
@@ -77,7 +77,7 @@ module.exports = (props) => {
         <FileItem
         <FileItem
           key={fileID}
           key={fileID}
           uppy={props.uppy}
           uppy={props.uppy}
-          {...fileProps}
+          {...fileProps} // eslint-disable-line react/jsx-props-no-spreading
           role="listitem"
           role="listitem"
           openFileEditor={props.openFileEditor}
           openFileEditor={props.openFileEditor}
           canEditFile={props.canEditFile}
           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
   const { file } = props
 
 
   if (file.preview) {
   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 = {
 const uploadStates = {
   STATE_ERROR: 'error',
   STATE_ERROR: 'error',
@@ -45,51 +45,58 @@ function getUploadingState (isAllErrored, isAllComplete, isAllPaused, files = {}
   return state
   return state
 }
 }
 
 
-function UploadStatus (props) {
+function UploadStatus ({
+  files, i18n, isAllComplete, isAllErrored, isAllPaused,
+  inProgressNotPausedFiles, newFiles, processingFiles,
+}) {
   const uploadingState = getUploadingState(
   const uploadingState = getUploadingState(
-    props.isAllErrored,
-    props.isAllComplete,
-    props.isAllPaused,
-    props.files,
+    isAllErrored,
+    isAllComplete,
+    isAllPaused,
+    files,
   )
   )
 
 
   switch (uploadingState) {
   switch (uploadingState) {
     case 'uploading':
     case 'uploading':
-      return props.i18n('uploadingXFiles', { smart_count: props.inProgressNotPausedFiles.length })
+      return i18n('uploadingXFiles', { smart_count: inProgressNotPausedFiles.length })
     case 'preprocessing':
     case 'preprocessing':
     case 'postprocessing':
     case 'postprocessing':
-      return props.i18n('processingXFiles', { smart_count: props.processingFiles.length })
+      return i18n('processingXFiles', { smart_count: processingFiles.length })
     case 'paused':
     case 'paused':
-      return props.i18n('uploadPaused')
+      return i18n('uploadPaused')
     case 'waiting':
     case 'waiting':
-      return props.i18n('xFilesSelected', { smart_count: props.newFiles.length })
+      return i18n('xFilesSelected', { smart_count: newFiles.length })
     case 'complete':
     case 'complete':
-      return props.i18n('uploadComplete')
+      return i18n('uploadComplete')
+    default:
   }
   }
 }
 }
 
 
 function PanelTopBar (props) {
 function PanelTopBar (props) {
+  const { i18n, isAllComplete, hideCancelButton, maxNumberOfFiles, toggleAddFilesPanel, uppy } = props
   let { allowNewUpload } = 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
     allowNewUpload = props.totalFileCount < props.maxNumberOfFiles
   }
   }
 
 
   return (
   return (
     <div className="uppy-DashboardContent-bar">
     <div className="uppy-DashboardContent-bar">
-      {!props.isAllComplete && !props.hideCancelButton ? (
+      {!isAllComplete && !hideCancelButton ? (
         <button
         <button
           className="uppy-DashboardContent-back"
           className="uppy-DashboardContent-back"
           type="button"
           type="button"
-          onClick={() => props.uppy.cancelAll()}
+          onClick={() => uppy.cancelAll()}
         >
         >
-          {props.i18n('cancel')}
+          {i18n('cancel')}
         </button>
         </button>
       ) : (
       ) : (
         <div />
         <div />
       )}
       )}
 
 
       <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
       <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
+        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
         <UploadStatus {...props} />
         <UploadStatus {...props} />
       </div>
       </div>
 
 
@@ -97,14 +104,14 @@ function PanelTopBar (props) {
         <button
         <button
           className="uppy-DashboardContent-addMore"
           className="uppy-DashboardContent-addMore"
           type="button"
           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">
           <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="15" height="15" viewBox="0 0 15 15">
             <path d="M8 6.5h6a.5.5 0 0 1 .5.5v.5a.5.5 0 0 1-.5.5H8v6a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V8h-6a.5.5 0 0 1-.5-.5V7a.5.5 0 0 1 .5-.5h6v-6A.5.5 0 0 1 7 0h.5a.5.5 0 0 1 .5.5v6z" />
             <path d="M8 6.5h6a.5.5 0 0 1 .5.5v.5a.5.5 0 0 1-.5.5H8v6a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V8h-6a.5.5 0 0 1-.5-.5V7a.5.5 0 0 1 .5-.5h6v-6A.5.5 0 0 1 7 0h.5a.5.5 0 0 1 .5.5v6z" />
           </svg>
           </svg>
-          <span className="uppy-DashboardContent-addMoreCaption">{props.i18n('addMore')}</span>
+          <span className="uppy-DashboardContent-addMoreCaption">{i18n('addMore')}</span>
         </button>
         </button>
       ) : (
       ) : (
         <div />
         <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 transitionName = 'uppy-transition-slideDownUp'
 const duration = 250
 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
  * - Tweaked styles for Uppy's Dashboard use case
  */
  */
 
 
-const { h, Component } = require('preact')
+import {  h, Component  } from 'preact'
 
 
 const STYLE_INNER = {
 const STYLE_INNER = {
   position: 'relative',
   position: 'relative',
@@ -143,6 +143,7 @@ class VirtualList extends Component {
     // The `role="presentation"` attributes ensure that these wrapper elements are not treated as list
     // The `role="presentation"` attributes ensure that these wrapper elements are not treated as list
     // items by accessibility and outline tools.
     // items by accessibility and outline tools.
     return (
     return (
+      // eslint-disable-next-line react/jsx-props-no-spreading
       <div onScroll={this.handleScroll} {...props}>
       <div onScroll={this.handleScroll} {...props}>
         <div role="presentation" style={styleInner}>
         <div role="presentation" style={styleInner}>
           <div role="presentation" style={styleContent}>
           <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', () => {
 describe('Dashboard', () => {
   beforeAll(() => {
   beforeAll(() => {
-    globalThis.ResizeObserver = require('resize-observer-polyfill').default || require('resize-observer-polyfill')
+    globalThis.ResizeObserver = resizeObserverPolyfill.default || resizeObserverPolyfill
   })
   })
   afterAll(() => {
   afterAll(() => {
     delete globalThis.ResizeObserver
     delete globalThis.ResizeObserver

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

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

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

@@ -8,8 +8,9 @@
  * @param {string} fallbackString
  * @param {string} fallbackString
  * @returns {Promise}
  * @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) => {
   return new Promise((resolve) => {
     const textArea = document.createElement('textarea')
     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', () => {
 describe('copyToClipboard', () => {
   xit('should copy the specified text to the clipboard', () => {
   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.
   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
   2. If there are no [data-uppy-super-focusable] elements yet (or ever) - focuses
      on the first focusable element, but switches focus if superfocusable elements appear on next render.
      on the first focusable element, but switches focus if superfocusable elements appear on next render.
 */
 */
-module.exports = function createSuperFocus () {
+export default function createSuperFocus () {
   let lastFocusWasOnSuperFocusableEl = false
   let lastFocusWasOnSuperFocusableEl = false
 
 
   const superFocus = (dashboardEl, activeOverlayType) => {
   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', () => {
 describe('createSuperFocus', () => {
   // superFocus.cancel() is used in dashboard
   // 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
  * @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) {
   if (activeOverlayType) {
     const overlayEl = dashboardEl.querySelector(`[data-uppy-paneltype="${activeOverlayType}"]`)
     const overlayEl = dashboardEl.querySelector(`[data-uppy-paneltype="${activeOverlayType}"]`)
     // if an overlay is already mounted
     // 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 () {
 function iconImage () {
   return (
   return (
@@ -63,7 +63,7 @@ function iconText () {
   )
   )
 }
 }
 
 
-module.exports = function getIconByMime (fileType) {
+export default function getIconByMime (fileType) {
   const defaultChoice = {
   const defaultChoice = {
     color: '#838999',
     color: '#838999',
     icon: iconFile(),
     icon: iconFile(),

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

@@ -14,4 +14,4 @@ function ignoreEvent (ev) {
   ev.stopPropagation()
   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) {
 function focusOnFirstNode (event, nodes) {
   const node = nodes[0]
   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
     // ___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 -->
 <!-- eslint-disable no-restricted-globals, no-multiple-empty-lines -->
 
 
 ```js
 ```js
-module.exports = {
+export default {
   strings: {
   strings: {
     // When `inline: false`, used as the screen reader label for the button that closes the modal.
     // When `inline: false`, used as the screen reader label for the button that closes the modal.
     closeModal: 'Close Modal',
     closeModal: 'Close Modal',

+ 1 - 0
yarn.lock

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