[dropzone] Add Dropzone component#4907
Open
mbrookes wants to merge 23 commits into
Open
Conversation
commit: |
Bundle size
PerformanceTotal duration: 1,151.31 ms -70.44 ms(-5.8%) | Renders: 50 (+0) | Paint: 1,764.35 ms -94.37 ms(-5.1%)
10 tests within noise — details Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove stale dropzone-input.json (leftover from Input → HiddenInput rename) - Clarify onOpen JSDoc: only fires when no HiddenInput is present - CSS modules demo: replace hardcoded hex colors with design tokens, wrap hover in @media (hover: hover), fix :focus → :focus-visible, remove redundant .input class (HiddenInput handles its own visibility) - Tailwind demo: fix focus: → focus-visible: variants - public-types: fix Dropzone.Props/State → Dropzone.Root.Props/State - Update generated types.md for onOpen description change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add './dropzone' to packages/react/package.json exports so @base-ui/react/dropzone resolves correctly - Fix non-breaking space in 'Base UI' brand name in dropzone/page.mdx and components/page.mdx (vale MuiBrandName rule) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use 'Base\u00a0UI' as a standalone keyword to match the convention of all other components, so docs:validate generates the correct non-breaking space in the component index (vale MuiBrandName rule). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CSS modules demo: replace undefined --color-gray-* vars with direct oklch() values; set explicit color on container to prevent currentColor from inheriting red from the docs demo sentinel - Tailwind demo: replace gray-* with neutral-* (the project's defined color tokens) and blue-600/50/100 with available blue-500/blue-500/10 blue-500/15 tokens; add text-neutral-900 to the container for correct currentColor baseline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sions CSS module <p> elements inherit UA default margins and line-heights because all: revert-layer removes Tailwind preflight. Add explicit margin: 0 and line-height to match Tailwind's preflight behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use 'Whether the component should ignore user interaction.' for disabled - Use 'Event handler called when...' for event handler props - Add docs link and 'Renders a <X> element.' to component JSDoc - Simplify children description to match Dialog/Collapsible patterns - Regenerate types.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dragging state is driven purely by browser drag events — there's no meaningful external trigger to set it programmatically. onDraggingChange is sufficient for observing the state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Matt <github@nospam.33m.co>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Matt <github@nospam.33m.co>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Matt <github@nospam.33m.co>
Member
Author
|
Converted to draft until the Copilot review fixes are complete |
- Restore HTMLInputElement.prototype.click spy in try/finally to prevent
leaking into subsequent tests (vi.resetAllMocks does not restore spies)
- Guard handleDragEnter to short-circuit when already dragging, preventing
repeated onDraggingChange(true) calls and screen reader announcements
as the cursor moves over descendant elements
- Guard handleKeyDown to bail out when event.target !== event.currentTarget,
matching the existing click handler behavior for nested interactive elements
- Move [role=status] live region back inside the dropzone element so
dropzone.querySelector('[role=status]') resolves correctly in tests
- Add test: onDraggingChange called only once for repeated dragenter
- Add test: keyboard does not open picker when nested button has focus
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The [role=status] live region was inside role=button. Since
position:absolute does not remove elements from the accessibility tree,
its text content ('Ready to drop files', 'Dropped N files', etc.) was
being included in the button's computed accessible name, causing the
control's name to change dynamically during drag/drop.
Move it to a sibling element (rendered via React.Fragment) so it
announces state changes without affecting the dropzone's accessible name.
Update accessibility tests to use screen.getByRole('status') rather than
dropzone.querySelector('[role=status]') to reflect the new structure.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Matt <github@nospam.33m.co>
File-drag guard:
- Add hasFiles() helper that checks event.dataTransfer.types.includes('Files')
- Guard handleDragEnter, handleDragOver, and handleDrop with hasFiles() so
dragging text, links or DOM elements over the dropzone doesn't enter the
dragging state, set dropEffect='copy', or fire onFilesDrop
file-drag context
Interactive-element selector:
- Import TYPEABLE_SELECTOR from floating-ui-react/utils/constants
- Replace the hand-rolled click handler selector with the same one used by
isInteractiveElement() — adds [tabindex]:not([tabindex=-1]), a[href]
specificity, and [contenteditable] to the existing set
Tests:
- Add createFileDragTransfer() helper for dragenter/dragover event mocking
(security: types includes 'Files' but files list is inaccessible)
- Update createDataTransfer() to explicitly set types (JSDOM does not
populate types automatically when items are added)
- Add test: non-file drags are ignored (no state change, no announcement)
- Add test: element with tabindex=0 does not open picker on click
- Update 'announces when no files are dropped' to reflect new behavior
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace <div role="status"> with <output> — semantic element with implicit role="status" and aria-live="polite" - Add role="menuitem" to the tabIndex test div so keyboard users have expected behavior on focus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Testing Library's fireEvent creates a new DataTransfer() internally and copies only own properties from the provided object. Browser DataTransfer stores its data in internal slots (not own properties), so the types and files values were lost in Chromium. Fix: always shadow the relevant properties with Object.defineProperty so they are preserved when Testing Library copies them. Also mark the 'announces when a file drop yields no files' edge case test as JSDOM-only since the types=['Files']+empty files scenario is impossible in real browsers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These files don't exist for any other component and are not referenced by the docs pipeline. The API reference is powered by types.md, which is auto-generated from types.ts by `pnpm docs:validate`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Base UI Dropzone
Background
This component was originally developed as a personal project, but seemed like a useful addition to Base UI, so submitting here for consideration. Colm had input on the API, and I've reviewed the code, but it was developed with agent assistance, therefore all errors and omissions are mine.
Overview
Dropzone is an unstyled drop target primitive that provides both drag-and-drop and click-to-select file picking in a single composable component. It is intentionally scoped to the drop/selection surface only — it does not manage file state, upload logic, or progress — making it composable with whatever file handling the consuming application needs.
API:
Components
Dropzone.Root— the interactive drop target<div role="button">. Handles drag events, click-to-open, and keyboard activation.Dropzone.HiddenInput— a visually hidden<input type="file">that powers the native file picker and enables form participation when a name prop is provided.Key features:
onDraggingChange)data-disabledattributeisDraggingfor inline conditional renderingsuppressHydrationWarningon the hidden input to avoid SSR mismatchesDocumentation
react/components/dropzone/page.mdx/react/components/dropzone/demos/hero/Comparison with other implementations
Most existing dropzone libraries (react-dropzone, react-files, etc.) are monolithic hooks or components that bundle file validation, preview, upload queuing, and state management together. Dropzone follows the Base UI philosophy of doing one thing: providing the interactive drop target surface. Consumers own file handling.
Notable differences:
File[]arraydata-dragging,data-disabled) rather thanclassNameinjectionArchitecture
Follows standard Base UI compound component conventions:
useRenderElementfor the render prop / className / style / state-attribute contractuseStableCallbackfor all event handlers to avoid stale closures and unnecessary re-rendersDropzoneRootContext) to connectHiddenInputtoRootwithout prop drilling —HiddenInputregisters its ref into Root so Root can imperatively call.click()on itcontainsfrom thefloating-ui-reactutils for correct drag-leave detection across child elements (avoids false drag-end when cursor moves over children)stateAttributesMapping+DropzoneRootDataAttributesenum, consistent with the rest of the libraryvisuallyHidden/visuallyHiddenInpututilities for the hidden inputDeploy preview
https://deploy-preview-4907--base-ui.netlify.app/react/components/dropzone