@uppy/dashboard: fix form appending for shadow dom (#6031)

Fixes #6028

I've also added `private/dev/dashboard_shadow.html` to more easily test
ShadowDOM-specific bugs.

I looked into writing a test case under
`packages/@uppy/dashboard/src/index.browser.test.ts` but couldn't get it
to work locally on Windows.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Ensure FileCard’s hidden form is appended/cleaned up in the correct
root (Document body or ShadowRoot) using a ref-derived root node, and
add a patch changeset.
> 
> - **@uppy/dashboard – FileCard**:
> - Append hidden `form` to the correct root using `getRootNode()`
(handles Light DOM/iframes via `Document.body`, and Shadow DOM via
`ShadowRoot`).
> - Add `domRef` to root element to detect rendering context; attach
`ref` and update effect accordingly.
>   - Clean up by removing the `form` from its actual `parentNode`.
> - **Release**:
>   - Add changeset for patch: `Fix form appending for shadow dom`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
edf81e871c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Murderlon <merlijn@soverin.net>
This commit is contained in:
Austin Jackson 2025-11-03 04:16:33 -06:00 committed by GitHub
parent b1e33bcef7
commit 5e166a101d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
"@uppy/dashboard": patch
---
Fix form appending for shadow dom

View file

@ -1,6 +1,6 @@
import classNames from 'classnames'
import { nanoid } from 'nanoid/non-secure'
import { useCallback, useEffect, useState } from 'preact/hooks'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import getFileTypeIcon from '../../utils/getFileTypeIcon.js'
import ignoreEvent from '../../utils/ignoreEvent.js'
import FilePreview from '../FilePreview.js'
@ -66,14 +66,37 @@ export default function FileCard(props: $TSFixMe) {
return formEl
})
// We need to know where Uppy is being rendered
const domRef = useRef<HTMLDivElement>(null)
useEffect(() => {
document.body.appendChild(form)
/**
* Use the "rootNode" of whereever Uppy is rendered, falling back
* to `window.document` if domRef isn't initialized for some reason
*/
const rootNode = domRef.current?.getRootNode() ?? (document as Node)
/**
* This is the case for the Light DOM and <iframes>.
* In these scenarios, we don't want to append a child to an
* <html> element, but to the <body>
*/
if (rootNode instanceof Document) {
rootNode.body.appendChild(form)
}
// This is the case for the Shadow DOM
else if (rootNode instanceof ShadowRoot) {
rootNode.appendChild(form)
}
// Everything else (realistically there isn't)
else {
rootNode.appendChild(form)
}
form.addEventListener('submit', handleSave)
return () => {
form.removeEventListener('submit', handleSave)
// check if form is still in the DOM before removing
if (form.parentNode) {
document.body.removeChild(form)
form.parentNode.removeChild(form)
}
}
}, [form, handleSave])
@ -87,6 +110,7 @@ export default function FileCard(props: $TSFixMe) {
onDragLeave={ignoreEvent}
onDrop={ignoreEvent}
onPaste={ignoreEvent}
ref={domRef}
>
<div className="uppy-DashboardContent-bar">
<div