-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add useImageEditor #6122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add useImageEditor #6122
Conversation
|
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a headless image editor hook (useImageEditor) that allows framework users to build custom image editor UIs while leveraging the ImageEditor plugin's functionality. The implementation refactors the existing ImageEditor plugin to separate UI concerns from core editing logic.
Key changes:
- Created a framework-agnostic image editor controller in
@uppy/componentswith getter functions for image props, button props, and slider props - Refactored the ImageEditor plugin to expose editing methods (
rotateBy,flipHorizontal,zoom,setAspectRatio, etc.) and lifecycle methods (start,stop,initCropper) that can be used by the headless hook - Implemented framework-specific hooks for React, Vue, and Svelte with proper event handler transformations
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
packages/@uppy/image-editor/src/ImageEditor.tsx |
Refactored to expose editing methods and lifecycle functions; added state management for angle, flip, aspect ratio, and cropper readiness |
packages/@uppy/image-editor/src/Editor.tsx |
Simplified to a presentational component that delegates all logic to the parent ImageEditor plugin |
packages/@uppy/image-editor/src/index.ts |
Added exports for AspectRatio type and utility functions used by the headless hook |
packages/@uppy/components/src/hooks/image-editor.ts |
New headless controller that provides props getters and manages plugin interaction |
packages/@uppy/components/src/image-editor.css |
Symlink to cropper.scss for distributing required CSS to consumers |
packages/@uppy/components/package.json |
Added postcss dependencies and build configuration to process the cropper CSS |
packages/@uppy/react/src/useImageEditor.ts |
React hook implementation using useSyncExternalStore and useMemo |
packages/@uppy/vue/src/useImageEditor.ts |
Vue hook implementation returning a ShallowRef |
packages/@uppy/svelte/src/lib/useImageEditor.svelte.ts |
Svelte hook with event handler name transformations (onClick → onclick) |
packages/@uppy/locales/src/en_US.ts |
Removed unused chooseFiles string; added failedToAddFiles string that's used in GooglePicker |
examples/react/src/ImageEditor.tsx |
New example showcasing the React hook usage with custom UI |
examples/vue/src/ImageEditor.vue |
New example showcasing the Vue hook usage with custom UI |
examples/sveltekit/src/components/ImageEditor.svelte |
New example showcasing the Svelte hook usage with custom UI |
yarn.lock, packages/@uppy/**/turbo.json, packages/@uppy/**/package.json |
Build configuration updates for CSS processing and distribution |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
273559f to
ed33106
Compare
| browseFolders: 'browse folders', | ||
| cancel: 'Cancel', | ||
| cancelUpload: 'Cancel upload', | ||
| chooseFiles: 'Choose files', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems something was missing because I only ran yarn build 🤷♂️
| enterUrlToImport: 'Enter URL to import a file', | ||
| error: 'Error', | ||
| exceedsSize: '%{file} exceeds maximum allowed size of %{size}', | ||
| failedToAddFiles: 'Failed to add files', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and this?
| ) | ||
|
|
||
| useEffect(() => { | ||
| controller.start() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| controller.start() |
as copilot mentioned above, if the file object changes, this would lead to createImageEditorController to be called again, but without first unmounting the previous one (because useEffect doesn't depend on file). maybe start should be called internally in createImageEditorController immediately, and if there is an existing "edit" running inside createImageEditorController it should be automatically stop'ed (internally)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that's correct and start/stop should happen in the frameworks, not the abstraction. We do start/stop in the same effect so they both always run when (un)mounting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's big PR so it took me some time to go through and understand, apart from mikael's comments ( which I agree with ) I have couple of questions and few bug reports
| this.cropper.setAspectRatio( | ||
| this.opts.cropperOptions.initialAspectRatio || 0, | ||
| ) | ||
| this.resetEditorState() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here resetEditorState() sets cropperReady: false despite this.cropper
still being active, which is probably causing the buttons to disable. it should preserve
cropperReady state when cropper instance is exists.
|
|
||
| const { angle, angleGranular, isFlippedHorizontally } = | ||
| this.getPluginState() | ||
| const base90 = angle - angleGranular |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tell me if this is also a bug, if I rotate an image using the slider (let’s say +7 degrees) and then rotate it by +90 degrees the final angle should be +97 degrees right? But during testing I noticed it completely ignores the previous rotation and only rotates the image by 90 degrees.
|
BUG : when I open the image editor for an image but don't actually make any edits and just save it why does it still increases the size of the image ? |
| // Props getters | ||
| const getImageProps = (): ImageProps<ImageEventType> => ({ | ||
| id: imgElementId, | ||
| src: plugin.getObjectUrl() ?? '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is causing the warning "An empty string ("") was passed to the src attribute" whenever you'll open the editor , because the inital render happens before the controller.start() creates the objectUrl , using undefined as a fallback will omit the src attribute
| src: plugin.getObjectUrl() ?? '', | |
| src: plugin.getObjectUrl() ?? undefined, |
|
|
||
| type ImageProps<EventType extends Event = Event> = { | ||
| id: string | ||
| src: string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related to https://github.com/transloadit/uppy/pull/6122/changes#r2701350885
| src: string | |
| src: string | undefined |

@uppy/componentsEditor(Preact) andImageEditor(Uppy plugin) to put all methods in the Uppy plugin class so they can be reused by the headless abstractioncropper.scss(just regular CSS) from@uppy/image-editorinto@uppy/components, updatebuild:cssin all framework packages to copy that file too into their owndist/output.CleanShot.2026-01-07.at.16.10.18.mp4