Image: Add upload progress indicator with cancel button and a11y announcements#75110
Image: Add upload progress indicator with cancel button and a11y announcements#75110adamsilverstein wants to merge 16 commits intotrunkfrom
Conversation
|
Size Change: +4.13 kB (+0.05%) Total Size: 7.74 MB
ℹ️ View Unchanged
|
f2c4b61 to
450d7ad
Compare
|
Adding needs accessibility feedback - this should get reviewed for accessibility needs before merge. The description is sound and makes sense, but ensuring that it's been tested with a variety of different screen readers is important, to make sure everything is announced as expected. |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Thanks @joedolson - appreciate any feedback/testing here. I can start with some basic manual testing with Voice over and capture a screen session when I get back to working on this. Ideally we could write e2e tests for this to validate the announcements we want happen as expected. Some other things I want to review/test:
|
0ae6c4f to
6755f94
Compare
|
Flaky tests detected in 0efa9a1. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/23818942617
|
Expand test coverage beyond getOperationLabel to include: - UploadingOverlay rendering, accessibility, progress display, batch labels, thumbnail progress, cancel behavior, and attachmentId fallback lookup - useUploadAnnouncer start/completion/error announcements, batch handling, and deduplication
|
Thanks for the work. I have to ask what I expect will be a dumb question, but bear with me, I mean it in good faith and we will find a solution regardless. Do we need the label to be visible? The progressbar implies what's happening, and more easily scales to tiny images, e.g. what happens if you upload a 10x10px image? And similarly, do we need the "Cancel" button? I would expect deleting the image cancels it. |
ciampo
left a comment
There was a problem hiding this comment.
👋 Passing by and leaving a review more from the UI and general API point of view.
My main observation is that this PR basically implements an "inline, non-modal dialog" — which is such a niche edge case, that we don't really have an existing built-in component to use.
At the same time, it's lacking some important accessibility features, such as correct focus management
There was a problem hiding this comment.
Not necessarily blocking (since there are existing private actions/selectors), but why are we introducing private actions/selectors? What's the reason why they can't be public?
| jest.mock( '@wordpress/components', () => { | ||
| const actual = jest.requireActual( '@wordpress/element' ); | ||
| return { | ||
| ProgressBar: ( { value, ...props } ) => | ||
| actual.createElement( 'div', { | ||
| role: 'progressbar', | ||
| 'aria-valuenow': value, | ||
| ...props, | ||
| } ), | ||
| Button: ( { children, __next40pxDefaultSize, variant, ...props } ) => | ||
| actual.createElement( 'button', props, children ), | ||
| }; | ||
| } ); |
There was a problem hiding this comment.
What's the reason for mocking @wordpress/components here? It feels to me like it partially defeats the purpose of the tests, since those components are effectively what end users are going to interact with
| export default function UploadingOverlay( { url, attachmentId, onCancel } ) { | ||
| const overlayRef = useRef(); | ||
|
|
||
| // When the overlay unmounts, return focus to the block wrapper if focus |
There was a problem hiding this comment.
We should handle focus also when mounting, ie. when the overlay is opened, focus should be moved to the overlay. Probably the overlay container is a better choice than the cancel button — which means that the container likely needs to have tabindex="-1" so that it be focused programmatically (and some visual indication of focus, too).
Thinking about it, we should actually consider using useFocusOnMount + useFocusReturn from @wordpress/compose to handle initial and final focus.
Show a single progress overlay at the gallery level when multiple images are uploaded simultaneously, instead of individual overlays on each image. The overlay tracks "Processing image X of Y" based on top-level upload completion count, filtering out child sideload items. Individual images remain dimmed during upload and become fully visible as each completes.
|
Thanks for the review and feedback @ciampo! I will review and address. |
Use a stable notice ID for gallery image upload errors so batch cancellation deduplicates into a single snackbar. Remove silent cancel mode so image blocks properly clean up their state (clear blob URLs, reset attributes) when cancelled.
The useUploadAnnouncer hook already handles screen reader announcements via speak(). The role=status live region would cause excessive announcements on every percentage change.
aria-busy indicates a region is being updated, so it belongs on the container figure rather than the img element itself.
useLayoutEffect runs synchronously before DOM repaint, ensuring the overlay element is still connected when checking active focus. With useEffect the element may already be disconnected and focus silently lost to document.body.
Replace manual useLayoutEffect focus logic with the standard WordPress compose hooks. Focus moves to the first tabbable element (Cancel button) on mount and returns to the previously focused element on unmount.
Provide screen reader context by including the filename in the cancel button's aria-label when available, e.g. "Cancel upload of photo.jpg". Falls back to "Cancel upload" when no filename.
Pressing Escape while the overlay is focused cancels the upload, matching the Cancel button behavior. Event propagation is stopped to prevent side-effects in the block editor. The overlay container uses role=group with an aria-label for accessibility.
Remove the 0.95 alpha transparency to guarantee 4.6:1 contrast ratio for gray-700 text on the overlay, ensuring text legibility.
|
@jasmussen thanks for reviewing - check out the latest screencasts at the top, I made some changes already before seeing your feedback.
by label do you mean the words that show up "uploading image" "generating subsize 2 of 10" etc? so maybe just show the progress bar, without text? I like that better. Then maybe simplify the progress bar to more of a loader bar ( component where the bar moves back and forth). I guess I was trying to provide more information to the user about what was happening during the processing and uploading. Looking at what I built, however it feels too noisy - most users don't care about these details. So i'm fine removing them. Do you think there is value in keeping them somewhere (tooltip?)? For accessibility, I assume we should still announce the progress (uploading image 3 of 10) to make sure screen reader users know what is happening.
A cancel button feels like a natural enhancement to me, especially in the case of a batch - eg. dragging a bunch of images into the editor. say i drop 15 images onto the editor then realize its in the wrong spot or i did the wrong images, this gives me an easy way to cancel the upload before it completes. Why do you question the cancel button, do you find it confusing or obtrusive? I didn't even think about canceling an upload by deleting an image! I see how that works now from a user perspective, especially for a single image. When testing this I noticed it actually doesn't trigger a cancel of client side media processing so although the deleting action removes the image, it still winds up being processed and uploaded in the background. That feels like a bug so I'll work on fixing that. |
…olders When an image upload fails or is cancelled inside a gallery, remove the block entirely rather than clearing its attributes, which would leave an empty gray placeholder box.
Document why the mock is necessary: the real package has a deep dependency chain that fails in this isolated test environment.
Clarify the actual root cause: the @wordpress/data mock doesn't provide combineReducers/createSelector needed by the rich-text store that components imports. Verified by testing without the mock — the dependency chain fails at rich-text store init.
The tests required mocking @wordpress/components due to the @wordpress/data mock breaking the dependency chain, which limits their value for testing real user interactions. Upload overlay behavior is better suited for E2E testing.
Add e2e tests that verify the upload progress overlay appears during image uploads and can be cancelled. Uses page.route() to hold the media REST API request in-flight, giving time to assert overlay visibility. Gallery batch overlay tests are written but skipped because the gallery overlay depends on the upload-media store, which requires client-side media processing (SharedArrayBuffer / cross-origin isolation) not reliably available in the e2e environment. Also fix filename prop fallback on UploadingOverlay when sideloading without a temporaryURL.
Exactly that, yes.
Your instinct and motivation is not wrong, but it is exactly the balance to strike after testing it in the PR. In general I try to look for established patterns in similar/adjacent software, finding precedence. For uploading, I do appreciate progressbars (and we can make them tiny so they fit any size image), and I appreciate verbose information if something fails. But it's rare that I personally find a need for more than that. For me the context is drag 10 images into a part of the page, then maybe start writing something else below, while it's working, so even a snackbar might be noisy. That's also context for a question on this:
Not sure if you mean also visibly, or entirely invisibly, and wherever this lands I'll help support what gets decided as necessary. But if it's noisy/interruptive to me, while I go about my multi-tasking workflow, would it not also get noisy (literally) for a screen reader user?
I recently switched to Proton Drive, from Google Photos. This had me upload tens of thousands of images using the browser. This is obviously a sensitive flow where serious things can go wrong, so I appreciated verbose information and upload status, in a tray that sat in the corner during upload:
It would even detect if I had already uploaded a particular file, skipping duplicates, etc. And there was a "Cancel all" button as well. I doubt people will be uploading that many images to WordPress, but if they do I'd expect them to do it inside the media library itself, and I'd probably suggest a similar tray for status indication as Proton Drive offers, as an enhancement. For a gallery where images are likely to upload quickly, I just don't think you'll have time to click the cancel button on an image that's uploaded before it's too late. A "cancel all" could potentially sit as an action on a snackbar:
But for individual images, it's not clear you'd engage with it a lot. And it's a fair bit of UI overhead to get this right. If possible I'd recommend maybe exploring it as a separate/followup PR. If it continues to feel critical to the experience, we can get it right: we'd want to fint the right button type, perhaps an X icon so it scales to as small an image as possible, and perhaps it appears on button-hover/focus. Or, as noted, cancelling through deleting.
Nice! Note, none of this is strongly felt. I'm happy to veer the course we need to. I'm approaching each of these improvements as individual features, and always questioning whether something earns its click-through. Nice work. |
There was a problem hiding this comment.
Thank you for addressing the feedback! Here are few more notes.
I tested briefly, and when I press the "Cancel" button or press the Escape key, the UI updates, but the upload continues in the background, and the block updates with the new image, despite canceling the process. We should fix it and add tests?
Also, this new UI doesn't seem to apply when more than one image is dragged / uploaded — is that expected? Could it be confusing for the end user?
Finally, the progress bar doesn't seem to take the whole width of the overlay, or at least, is not horizontally centered:
There was a problem hiding this comment.
There seems to be a lot of common code between this file and packages/block-library/src/image/uploading-overlay.js, can we maybe refactor to a common component to avoid duplication?
There was a problem hiding this comment.
good point, I'll try.
| onCancel, | ||
| filename, | ||
| } ) { | ||
| const focusOnMountRef = useFocusOnMount( 'firstElement' ); |
There was a problem hiding this comment.
firstElement currently focuses the "cancel" button, which could cause end users to inadvertently cancel the upload. Focusing the overlayRef feels like a safer option to me.
There was a problem hiding this comment.
Based on the discussion above, I'm going to remove the cancel button and feature entirely, other than deleting the image during an upload. I'll make sure that does actually cancel the upload!
No, thats a recent regression, fixing.
Right! fixing. |
|
a loader bar ( component where the bar moves back and forth).
got it!
I think thats probably the most critical part actually, showing a contextual error when there is a problem. I agree most of the progress details will be noise for most users.
Good point, I'm not exactly sure what should be announced when uploading 10 images. What would you suggest @joedolson - would we just announce uploading start and finish? what is the appropriate way to announce this?
I'm going to remove the cancel button entirely (although the deleting action will still cancel the upload as you pointed out.
I really appreciate your feedback and insights here and generally trust your intuition on the design. |
|
After reviewing the feedback here and removing the cancel button and progress labels, I realized this PR isn't doing much or really helping. I'm going to close as "not planned" and leave the upload spinners for now. The code will stand as prior work should someone want to try adding this later. Perhaps it is more appropriate to add in the media library as @jasmussen mentioned where a user might try to upload hundreds or thousands of images at once. |
I'm going to leave this as is because I realized the user could delete an image then undo that delete action. if we cancel the upload this would lead to a broken image or partial upload. |
Yes, that sounds good to me. For what it's worth, we've started exploring this idea generally in the experimental media modal that now has an uploading state footer that can be expanded to show further details (as of #76228). Here's how that's looking:
Once the client-side media work has progressed a bit further, it'd be good to integrate it into that popover (I'll be able to help there), and this could also form the basis of proposing it (or something like it) in the media library proper. Good explorations here, and I'm sure this'll be helpful to revisit further down the track! |
|
I am exploring high level tracking (Uploading file x of y) in the Post panel in #74362, matching what we show in the media library. |




What?
Closes #74363
Part of client side media.
Replace the plain
<Spinner />shown during image uploads with a rich progress overlay that displays:<ProgressBar>showing upload/processing completionAlso adds screen reader announcements via
wp.a11y.speakfor upload start, completion, and errors — with batch-aware announcements that say "Uploading N images…" once instead of N separate announcements.Why?
The Image block previously showed only a spinner during uploads with no progress feedback, no way to cancel, and no screen reader support. With the addition of client-side media processing (resize, rotate, transcode, thumbnail generation), uploads can take significantly longer and go through multiple stages — making progress feedback essential.
When uploading multiple images at once, each block previously showed an identical generic overlay with no indication of batch context. Now each overlay shows its position in the batch ("Image 3 of 5"), and screen readers hear a single batch announcement instead of N duplicates.
How?
Upload overlay & progress
UploadingOverlaycomponent (uploading-overlay.js): Renders the progress overlay with ProgressBar, operation label, percentage, and cancel button. Retrieves upload state from the@wordpress/upload-mediastore viagetItemByBlobUrl.useUploadAnnouncerhook (use-upload-announcer.js): Announces upload start (polite), completion (polite), and errors (assertive) viawp.a11y.speak.getItemByBlobUrlselector (private-selectors.ts): Private selector in@wordpress/upload-mediathat finds a queue item by its blob URL.editor.scss): Overlay positioned absolutely over the image with semi-transparent background, centered content.Batch progress (multi-image uploads)
batchSize/batchIndexonQueueItem: Each item stores its 1-based position and total count from the originating batch. Computed via a pre-validation pass inaddItems()so only valid files are counted.batchSize > 1.Thumbnail generation progress
thumbnailCountonQueueItem: Set ingenerateThumbnails()to the total number of sideload items created (subsizes + scaled version).getChildItemCountselector: Counts remaining child sideload items for a parent.thumbnailCount - remainingChildren + 1.Accessibility improvements
role="status"on the overlay makes it a live region for automatic screen reader announcementsaria-busyon the image element during upload signals in-progress statearia-label="Upload progress"on the ProgressBarTesting Instructions
Multi-image batch upload
Thumbnail generation progress
Testing Instructions for Keyboard
Accessibility Testing Plan
This section addresses the needs-accessibility-feedback label. The following tests should be performed across multiple screen readers.
Screen Readers to Test
1. Upload Start Announcement
2. Upload Completion Announcement
3. Error Announcement
4. Cancel Button
5. Progress Bar Semantics
6. Operation Label Changes
role="status"live region7. Focus Management
aria-busyis set on the image element during upload and removed after completion8. Multiple Image Uploads
9. Visual Accessibility
$gray-700on white/near-white background) meets WCAG AA (4.5:1 ratio)prefers-reduced-motion: reduce— verify the progress bar does not animate (or uses a reduced animation)Screenshots or screencast
Before - Plain spinner, no progress info
screencast.2026-02-28.16-56-52.mp4
After:
Single
upload.with.progress.mp4
Multiple
multi.upload.mp4