Skip to content

Commit

Permalink
Improve handling of visual feedback showing the dropzone
Browse files Browse the repository at this point in the history
- Only show the dropzone when the user drags into it rather than when entering the document. This will prevent multiple announcements when we add feedback for screenreaders, in case there's multiple `FileUpload`s on the page
- Add a test to check if the user is dragging files before showing dropzone
- Fix disappearance of the dropzone due to many `dragleave` events being triggered as user drags over the different elements inside the wrapper
- Separate the handler of `drop` event as it doesn't need the same complexity as the `dragleave` one before hiding the dropzone.

The component still relies on the native `<input>` receiving the files being dropped, as it ensures a `change` event gets triggered on drop (which we'd have to simulate if setting its `files` properties programmatically).
  • Loading branch information
romaricpascal committed Jan 14, 2025
1 parent d52283f commit 7c5e8e6
Showing 1 changed file with 71 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,78 @@ export class FileUpload extends ConfigurableComponent {
// Bind change event to the underlying input
this.$root.addEventListener('change', this.onChange.bind(this))

// When a file is dropped on the input
this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this))
// Handle drag'n'drop events
this.$wrapper.addEventListener('drop', this.onDrop.bind(this))

// When a file is dragged over the page (or dragged off the page)
document.addEventListener('dragenter', this.onDragEnter.bind(this))
document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this))
this.$wrapper.addEventListener('dragenter', this.onDragEnter.bind(this))
this.$wrapper.addEventListener('dragleave', this.onDragLeave.bind(this))
}

/**
* Decides whether the component may accept a DataTransfer
*
* @internal
* @param {DataTransfer} dataTransfer - The `dataTransfer` payload currently being dragged
* @returns {boolean} - `true` if what's being dragged is accepted, false otherwise
*/
accepts(dataTransfer) {
// Safari sometimes does not provide info about types :'(
// In which case best not to assume anything and try to set the files
const hasNoTypesInfo = dataTransfer.types.length === 0

// When dragging images, there's a mix of mime types + Files
// which we can't assign to the native input
const isDraggingFiles = dataTransfer.types.some((type) => type === 'Files')

return hasNoTypesInfo || isDraggingFiles
}

/**
* Handles the users entering the area where they can drop their files
*
* Reveals the drop zone if the user components accepts what the user
* is dragging
*
* @param {DragEvent} event - The `dragenter` event
*/
onDragEnter(event) {
if (this.accepts(event.dataTransfer)) {

Check failure on line 133 in packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs

View workflow job for this annotation

GitHub Actions / TypeScript compiler (windows-latest)

Argument of type 'DataTransfer | null' is not assignable to parameter of type 'DataTransfer'.

Check failure on line 133 in packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs

View workflow job for this annotation

GitHub Actions / TypeScript compiler (windows-latest)

Argument of type 'DataTransfer | null' is not assignable to parameter of type 'DataTransfer'.
this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone')
}
}

/**
* Hides the drop zone visually when users mouse leave it
*
* `dragleave` events will fire for each element that the user moves
* their move out of. We only want to remove the styling when
* they either leave the wrapper or the window altogether
*
* @param {DragEvent} event - The `dragleave` event
*/
onDragLeave(event) {
// `relatedTarget` is only an `EventTarget` so we need to check if it's :
// - it's a `Node` in which case we'd still be on the page
// - something else in which case user has left the page
const relatedTarget =
event.relatedTarget instanceof Node ? event.relatedTarget : null

const leavesWindow = !relatedTarget
const leavesDropZone = !this.$wrapper.contains(relatedTarget)
if (leavesWindow || leavesDropZone) {
this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone')
}
}

/**
* Hides the dropzone once user has dropped files on the `<input>`
*
* Note: the component relies on the native behaviour to get the files
* being dragged set as value of the `<input>`. This allows a `change`
* event to be automatically fired from the element.
*/
onDrop() {
this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone')
}

/**
Expand Down Expand Up @@ -153,28 +219,6 @@ export class FileUpload extends ConfigurableComponent {
this.$label.click()
}

/**
* When a file is dragged over the container, show a visual indicator that a
* file can be dropped here.
*
* @param {DragEvent} event - the drag event
*/
onDragEnter(event) {
// Check if the thing being dragged is a file (and not text or something
// else), we only want to indicate files.
console.log(event)

this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone')
}

/**
* When a dragged file leaves the container, or the file is dropped,
* remove the visual indicator.
*/
onDragLeaveOrDrop() {
this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone')
}

/**
* Create a mutation observer to check if the input's attributes altered.
*/
Expand Down

0 comments on commit 7c5e8e6

Please sign in to comment.