﻿id	summary	reporter	owner	description	type	status	priority	milestone	component	version	severity	resolution	keywords	cc	focuses
65852	"Media grid tile label (aria-label) is stale: shows ""uploading…"" / ""(no title)"" for titled attachments whose models load after the view is created"	archon810		"It's been a while since I reported a core WP bug, but I have to because it's a pretty annoying one. Of course, Cursor and Fable 5 assisted me in root-causing, but I assure you I'm a real human (Artem Russakovskii) reporting a real bug.

Since WordPress 7.0, the Media Library grid shows each attachment tile's `aria-label` as a visible label overlay (`.wp-core-ui .attachment:not(:has(.filename))::after { content: attr(aria-label); }`, added in [62104] for #64883).

However, `wp.media.view.Attachment` computes that attribute '''only once''', when the view's element is created - Backbone only evaluates `attributes()` during `_ensureElement()`:

{{{
attributes: function() {
	...
	return {
		'tabIndex':     0,
		'role':         'checkbox',
		'aria-label':   ariaLabel,
		'aria-checked': false,
		'data-id':      this.model.get( 'id' )
	};
},
}}}

When a tile view is created for an attachment model that has not finished loading yet, the label is computed from an empty model and falls back to ""uploading…"" (or ""(no title)"" once [62892] / #65438 ships in 7.1). When the model's data arrives, the view re-renders - but `render()` only replaces the element's inner HTML, so the stale `aria-label` (and therefore the visible label '''and''' the accessible name) is never corrected. The tile stays labeled ""uploading…"" forever, even though the attachment has a title.

This is not an edge case. Core itself creates id-only models with a fetch in flight:

* `wp.media.controller.FeaturedImage.updateSelection()` does `attachment = Attachment.get( id ); attachment.fetch();` and puts that model into the selection, which is synced into the library. The tile for the current featured image is therefore created before the fetch resolves whenever the AJAX response takes longer than the modal render - i.e. on any real site with normal latency. (Even when the `query-attachments` response later includes the same attachment, its data merges into the existing model instance, which only re-renders the tile's inner HTML - the element attribute stays stale.)
* Gutenberg's `MediaUpload` (`@wordpress/media-utils`) uses the same `wp.media.attachment( id )` + `fetch()` pattern in `onOpen` for current values (featured image, galleries), so the block editor is affected the same way.
* Any plugin following the documented `wp.media.attachment( id ).fetch()` pattern to preload selections hits this too.

Symptom from a user's perspective: open a post that already has a featured image, click the featured image to replace it - the currently-featured tile in the grid is labeled ""uploading…"" even though nothing is uploading and the image has a title (the Attachment Details sidebar shows the correct title). Switching to another screen and back (e.g. ""Upload files"" → ""Media Library"", or Edit Gallery → Add to Gallery → back) recreates the views and the labels fix themselves, confirming it's a stale attribute rather than missing data.

Note this is distinct from #65438 (milestoned for 7.1): that fix ([62892]) only changes ''which fallback string'' `attributes()` picks for a titleless model. It does not address the staleness. '''Verified''': applying the exact [62892] change to the 7.0.2/7.0.3 reproduction environment below (patched built `media-views.js` / `media-views.min.js`, cache-busted) changes the stuck label from ""uploading…"" to ""(no title)"" - a permanently wrong label on an attachment that '''has''' a title (see attached screenshot. The stale value never updates to the real title in either case.

Screen reader impact: the stale accessible name predates 7.0 (the attribute was always computed once), but 7.0 made it visible to everyone.

== Steps to reproduce (stock WordPress) ==

1. Use a site with normal (non-localhost) latency, so the `get-attachment` AJAX response arrives after the media modal has rendered.
2. Create a post and set as featured image an image that has a title.
3. Reload the post edit screen and click the featured image thumbnail to open the ""Featured image"" modal (classic editor metabox, or Replace in the block editor).
4. Look at the selected tile in the Media Library grid.

* Expected: the tile label shows the attachment title.
* Actual: the tile label shows ""uploading…"" (""(no title)"" after [62892]) and never updates. Switching to ""Upload files"" and back to ""Media Library"" fixes it.

== One-click reproduction in WordPress Playground ==

[https://playground.wordpress.net/#%7B%22landingPage%22%3A%22%2Fwp-admin%2Fedit.php%22%2C%22preferredVersions%22%3A%7B%22wp%22%3A%22latest%22%2C%22php%22%3A%228.3%22%7D%2C%22steps%22%3A%5B%7B%22step%22%3A%22login%22%2C%22username%22%3A%22admin%22%7D%2C%7B%22step%22%3A%22installPlugin%22%2C%22pluginData%22%3A%7B%22resource%22%3A%22wordpress.org%2Fplugins%22%2C%22slug%22%3A%22classic-editor%22%7D%7D%2C%7B%22step%22%3A%22writeFile%22%2C%22path%22%3A%22%2Fwordpress%2Fwp-content%2Fmu-plugins%2Fdelay-get-attachment.php%22%2C%22data%22%3A%22%3C%3Fphp%5Cn%2F%2F%201%29%20Simulate%20real-world%20server%2Fnetwork%20latency%20for%20the%20single-attachment%5Cn%2F%2F%20fetch%20%28wp.media.model.Attachment.fetch%28%29%29%20so%20the%20race%20is%20deterministic.%5Cn%2F%2F%20On%20production%20sites%20this%20ordering%20happens%20naturally.%5Cnadd_action%28%27admin_init%27%2C%20function%20%28%29%20%7B%5Cn%20%20%20%20if%20%28wp_doing_ajax%28%29%20%26%26%20isset%28%24_REQUEST%5B%27action%27%5D%29%20%26%26%20%27get-attachment%27%20%3D%3D%3D%20%24_REQUEST%5B%27action%27%5D%29%20%7B%5Cn%20%20%20%20%20%20%20%20usleep%282000000%29%3B%5Cn%20%20%20%20%7D%5Cn%7D%29%3B%5Cn%2F%2F%202%29%20Make%20the%20media%20modal%20open%20on%20the%20Media%20Library%20tab%20%28instead%20of%5Cn%2F%2F%20Upload%20files%29%20so%20the%20bug%20is%20visible%20without%20extra%20clicks.%5Cnadd_action%28%27admin_enqueue_scripts%27%2C%20function%20%28%29%20%7B%5Cn%20%20%20%20wp_add_inline_script%28%27utils%27%2C%20%5C%22if%20%28window.setUserSetting%29%20%7B%20setUserSetting%28%27libraryContent%27%2C%20%27browse%27%29%3B%20%7D%5C%22%29%3B%5Cn%7D%29%3B%5Cn%22%7D%2C%7B%22step%22%3A%22runPHP%22%2C%22code%22%3A%22%3C%3Fphp%20require%20%27%2Fwordpress%2Fwp-load.php%27%3B%20require_once%20ABSPATH%20.%20%27wp-admin%2Fincludes%2Fimage.php%27%3B%20%24source%20%3D%20ABSPATH%20.%20%27wp-includes%2Fimages%2Fw-logo-blue.png%27%3B%20%24upload_dir%20%3D%20wp_upload_dir%28%29%3B%20%24file%20%3D%20%24upload_dir%5B%27path%27%5D%20.%20%27%2Ftitled-repro-image.png%27%3B%20copy%28%24source%2C%20%24file%29%3B%20%24featured_id%20%3D%20wp_insert_attachment%28array%28%27post_title%27%20%3D%3E%20%27My%20titled%20image%27%2C%20%27post_mime_type%27%20%3D%3E%20%27image%2Fpng%27%2C%20%27post_status%27%20%3D%3E%20%27inherit%27%2C%20%27post_date%27%20%3D%3E%20%272020-01-01%2000%3A00%3A00%27%29%2C%20%24file%29%3B%20wp_update_attachment_metadata%28%24featured_id%2C%20wp_generate_attachment_metadata%28%24featured_id%2C%20%24file%29%29%3B%20for%20%28%24i%20%3D%201%3B%20%24i%20%3C%3D%205%3B%20%24i%2B%2B%29%20%7B%20%24filler%20%3D%20%24upload_dir%5B%27path%27%5D%20.%20%27%2Ffiller-%27%20.%20%24i%20.%20%27.png%27%3B%20copy%28%24source%2C%20%24filler%29%3B%20%24filler_id%20%3D%20wp_insert_attachment%28array%28%27post_title%27%20%3D%3E%20%27Filler%20image%20%27%20.%20%24i%2C%20%27post_mime_type%27%20%3D%3E%20%27image%2Fpng%27%2C%20%27post_status%27%20%3D%3E%20%27inherit%27%29%2C%20%24filler%29%3B%20wp_update_attachment_metadata%28%24filler_id%2C%20wp_generate_attachment_metadata%28%24filler_id%2C%20%24filler%29%29%3B%20%7D%20%24post_id%20%3D%20wp_insert_post%28array%28%27post_title%27%20%3D%3E%20%27Featured%20image%20label%20repro%27%2C%20%27post_status%27%20%3D%3E%20%27publish%27%2C%20%27post_content%27%20%3D%3E%20%27Open%20this%20post%20and%20click%20the%20featured%20image%20thumbnail%20in%20the%20sidebar.%27%29%29%3B%20set_post_thumbnail%28%24post_id%2C%20%24featured_id%29%3B%20update_user_meta%281%2C%20%27wp_user-settings%27%2C%20%27libraryContent%3Dbrowse%27%29%3B%22%7D%5D%7D Open the one-click reproduction in WordPress Playground] (self-contained link; the entire setup below is encoded in the URL).

The blueprint creates a titled image (""My titled image"", dated 2020), 5 newer ""Filler image N"" attachments (for contrast: their labels render correctly next to the broken one), and a post ""Featured image label repro"" with the 2020 image as its featured image. Because everything in Playground is local and near-instant, the race that occurs naturally on real sites is made deterministic by an mu-plugin that adds a 2-second delay to the `get-attachment` admin-ajax response (the request issued by `wp.media.model.Attachment.fetch()`); a second hook makes the media modal default to the Media Library tab so no extra clicks are needed.

To reproduce: open the Playground link, go to Posts → ""Featured image label repro"" → click the featured image thumbnail in the Featured image metabox. The selected tile shows ""uploading…"" and keeps it indefinitely, while every other tile is labeled correctly and the Attachment Details sidebar shows the real title. Switch ""Upload files"" → ""Media Library"" to watch it self-heal.

Verified on WordPress 7.0.2/7.0.3 / PHP 8.3 in Playground (labels sampled at 0.7 s, 2.7 s, and 5.7 s after opening the modal - the stale label persists while `wp.media.attachment( id ).get( 'title' )` returns the correct title). Re-verified with the #65438 fix ([62892]) applied on top: same staleness, label stuck at ""(no title)"" instead.

== Suggested fix ==

Re-apply the accessible name during `render()` so it catches up once the model has data, e.g. in `src/js/media/views/attachment.js` factor the label computation out of `attributes()`:

{{{
getAriaLabel: function() {
	var ariaLabel = this.model.get( 'title' );

	if ( ! ariaLabel ) {
		if ( this.model.get( 'uploading' ) ) {
			ariaLabel = wp.i18n.__( 'uploading…' );
		} else {
			ariaLabel = wp.i18n.__( '(no title)' );
		}
	}

	return ariaLabel;
},
}}}

and in `render()` after the template is applied:

{{{
// `attributes()` only runs when the view element is created, which can be
// before the model has been fetched. Keep the accessible name in sync.
if ( this.$el.attr( 'aria-label' ) !== undefined ) {
	this.$el.attr( 'aria-label', this.getAriaLabel() );
}
}}}

The `undefined` guard skips subclasses that deliberately reset the inherited attributes (`wp.media.view.Attachment.Details`, see #47458), and leaves `aria-checked` alone so selection state set by `updateSelect()` is not clobbered. Views re-render on every model change (`rerenderOnModelChange` defaults to true), so no new listeners are needed; genuine uploads keep showing ""uploading…"" until the upload finishes and the server response populates the model.
"	defect (bug)	new	normal	Awaiting Review	Media	7.0.3	normal		needs-testing has-test-info has-screenshots		ui, accessibility, javascript
