Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/js/media/views/attachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ Attachment = View.extend(/** @lends wp.media.view.Attachment.prototype */{
className: 'attachment',
template: wp.template('attachment'),

attributes: function() {
/**
* Compute the accessible name for an attachment tile.
*
* Factored out of attributes() so render() can refresh the label when a
* model finishes loading after the view was created. See #65852.
*
* @since 7.2.0
*
* @return {string} Accessible name for the attachment.
*/
getAriaLabel: function() {
var ariaLabel = this.model.get( 'title' );

if ( ! ariaLabel ) {
Expand All @@ -28,10 +38,14 @@ Attachment = View.extend(/** @lends wp.media.view.Attachment.prototype */{
}
}

return ariaLabel;
},

attributes: function() {
return {
'tabIndex': 0,
'role': 'checkbox',
'aria-label': ariaLabel,
'aria-label': this.getAriaLabel(),
'aria-checked': false,
'data-id': this.model.get( 'id' )
};
Expand Down Expand Up @@ -161,6 +175,17 @@ Attachment = View.extend(/** @lends wp.media.view.Attachment.prototype */{

this.views.render();

/*
* Backbone only evaluates attributes() when the element is created.
* When a tile is built for an id-only model that is still fetching,
* the aria-label is computed from empty data and never refreshes.
* Re-apply it here once the model has data. Skip subclasses that
* reset attributes (e.g. Attachment.Details). See #65852, #47458.
*/
if ( undefined !== _.result( this, 'attributes' )['aria-label'] ) {
this.$el.attr( 'aria-label', this.getAriaLabel() );
}

return this;
},

Expand Down
2 changes: 2 additions & 0 deletions tests/qunit/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<script>
window._wpMediaViewsL10n = {"mediaFrameDefaultTitle":"Media","url":"URL","addMedia":"Add Media","search":"Search","select":"Select","cancel":"Cancel","update":"Update","replace":"Replace","remove":"Remove","back":"Back","selected":"%d selected","dragInfo":"Drag and drop to reorder media files.","uploadFilesTitle":"Upload Files","uploadImagesTitle":"Upload Images","mediaLibraryTitle":"Media Library","insertMediaTitle":"Add Media","createNewGallery":"Create a new gallery","createNewPlaylist":"Create a new playlist","createNewVideoPlaylist":"Create a new video playlist","returnToLibrary":"\u2190 Return to library","allMediaItems":"All media items","allDates":"All dates","noItemsFound":"No items found.","insertIntoPost":"Insert into post","unattached":"Unattached","mine":"Mine","trash":"Trash","uploadedToThisPost":"Uploaded to this post","warnDelete":"You are about to permanently delete this item from your site.\nThis action cannot be undone.\n 'Cancel' to stop, 'OK' to delete.","warnBulkDelete":"You are about to permanently delete these items from your site.\nThis action cannot be undone.\n 'Cancel' to stop, 'OK' to delete.","warnBulkTrash":"You are about to trash these items.\n 'Cancel' to stop, 'OK' to delete.","bulkSelect":"Bulk Select","trashSelected":"Move to Trash","restoreSelected":"Restore from Trash","deletePermanently":"Delete Permanently","apply":"Apply","filterByDate":"Filter by date","filterByType":"Filter by type","searchMediaLabel":"Search Media","searchMediaPlaceholder":"Search media items...","mediaFound":"Number of media items found: %d","mediaFoundHasMoreResults":"Number of media items displayed: %d. Scroll the page for more results.","noMedia":"No media items found.","noMediaTryNewSearch":"No media items found. Try a different search.","attachmentDetails":"Attachment Details","insertFromUrlTitle":"Insert from URL","setFeaturedImageTitle":"Featured image","setFeaturedImage":"Set featured image","createGalleryTitle":"Create Gallery","editGalleryTitle":"Edit Gallery","cancelGalleryTitle":"\u2190 Cancel Gallery","insertGallery":"Insert gallery","updateGallery":"Update gallery","addToGallery":"Add to gallery","addToGalleryTitle":"Add to Gallery","reverseOrder":"Reverse order","imageDetailsTitle":"Image Details","imageReplaceTitle":"Replace Image","imageDetailsCancel":"Cancel Edit","editImage":"Edit Image","chooseImage":"Choose Image","selectAndCrop":"Select and Crop","skipCropping":"Skip Cropping","cropImage":"Crop Image","cropYourImage":"Crop your image","cropping":"Cropping\u2026","suggestedDimensions":"Suggested image dimensions: %1$s by %2$s pixels.","cropError":"There has been an error cropping your image.","audioDetailsTitle":"Audio Details","audioReplaceTitle":"Replace Audio","audioAddSourceTitle":"Add Audio Source","audioDetailsCancel":"Cancel Edit","videoDetailsTitle":"Video Details","videoReplaceTitle":"Replace Video","videoAddSourceTitle":"Add Video Source","videoDetailsCancel":"Cancel Edit","videoSelectPosterImageTitle":"Select Poster Image","videoAddTrackTitle":"Add Subtitles","playlistDragInfo":"Drag and drop to reorder tracks.","createPlaylistTitle":"Create Audio Playlist","editPlaylistTitle":"Edit Audio Playlist","cancelPlaylistTitle":"\u2190 Cancel Audio Playlist","insertPlaylist":"Insert audio playlist","updatePlaylist":"Update audio playlist","addToPlaylist":"Add to audio playlist","addToPlaylistTitle":"Add to Audio Playlist","videoPlaylistDragInfo":"Drag and drop to reorder videos.","createVideoPlaylistTitle":"Create Video Playlist","editVideoPlaylistTitle":"Edit Video Playlist","cancelVideoPlaylistTitle":"\u2190 Cancel Video Playlist","insertVideoPlaylist":"Insert video playlist","updateVideoPlaylist":"Update video playlist","addToVideoPlaylist":"Add to video playlist","addToVideoPlaylistTitle":"Add to Video Playlist","filterAttachments":"Filter Media","attachmentsList":"Media list","settings":{"tabs":[],"tabUrl":"http:\/\/build.wordpress-develop.test\/wp-admin\/media-upload.php?chromeless=1","mimeTypes":{"image":"Images","audio":"Audio","video":"Video","application\/msword,application\/vnd.openxmlformats-officedocument.wordprocessingml.document,application\/vnd.ms-word.document.macroEnabled.12,application\/vnd.ms-word.template.macroEnabled.12,application\/vnd.oasis.opendocument.text,application\/vnd.apple.pages,application\/pdf,application\/vnd.ms-xpsdocument,application\/oxps,application\/rtf,application\/wordperfect,application\/octet-stream":"Documents","application\/vnd.apple.numbers,application\/vnd.oasis.opendocument.spreadsheet,application\/vnd.ms-excel,application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application\/vnd.ms-excel.sheet.macroEnabled.12,application\/vnd.ms-excel.sheet.binary.macroEnabled.12":"Spreadsheets","application\/x-gzip,application\/rar,application\/x-tar,application\/zip,application\/x-7z-compressed":"Archives"},"captions":true,"nonce":{"sendToEditor":"078fccea44"},"post":{"id":7734,"nonce":"b81d45d05c","featuredImageId":-1},"defaultProps":{"link":"","align":"","size":""},"attachmentCounts":{"audio":1,"video":1},"oEmbedProxyUrl":"http:\/\/build.wordpress-develop.test\/wp-json\/oembed\/1.0\/proxy","embedExts":["mp3","ogg","flac","m4a","wav","mp4","m4v","webm","ogv","flv"],"embedMimes":{"mp3":"audio\/mpeg","ogg":"audio\/ogg","flac":"audio\/flac","m4a":"audio\/mpeg","wav":"audio\/wav","mp4":"video\/mp4","m4v":"video\/mp4","webm":"video\/webm","ogv":"video\/ogg","flv":"video\/x-flv"},"contentWidth":580,"months":[{"year":"2019","month":"10","text":"October 2019"},{"year":"2019","month":"9","text":"September 2019"},{"year":"2019","month":"7","text":"July 2019"},{"year":"2019","month":"6","text":"June 2019"},{"year":"2014","month":"1","text":"January 2014"},{"year":"2013","month":"12","text":"December 2013"},{"year":"2013","month":"9","text":"September 2013"},{"year":"2013","month":"4","text":"April 2013"},{"year":"2013","month":"3","text":"March 2013"},{"year":"2012","month":"7","text":"July 2012"},{"year":"2012","month":"6","text":"June 2012"},{"year":"2011","month":"7","text":"July 2011"},{"year":"2011","month":"1","text":"January 2011"}],"mediaTrash":0}};
</script>
<script src="../../build/wp-includes/js/clipboard.js"></script>
<script src="../../build/wp-includes/js/media-views.js"></script>
<script src="../../build/wp-includes/js/media-editor.js"></script>
<script src="../../build/wp-includes/js/media-audiovideo.js"></script>
Expand Down Expand Up @@ -200,6 +201,7 @@
<script src="wp-admin/js/widgets/test-media-gallery-widget.js"></script>
<script src="wp-admin/js/widgets/test-media-video-widget.js"></script>
<script src="wp-includes/js/media/test-models.js"></script>
<script src="wp-includes/js/media/test-attachment-view.js"></script>

<!-- Customizer templates for sections -->
<script type="text/html" id="tmpl-customize-section-default">
Expand Down
107 changes: 107 additions & 0 deletions tests/qunit/wp-includes/js/media/test-attachment-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* globals wp, jQuery */
/* jshint qunit: true */
/* eslint-env qunit */

( function() {
'use strict';

var $template;

QUnit.module( 'wp.media.view.Attachment', {
beforeEach: function() {
$template = jQuery( '<script type="text/html" id="tmpl-attachment"><div class="attachment-preview"></div></script>' )
.appendTo( 'body' );
},
afterEach: function() {
$template.remove();
}
} );

/**
* Creates an attachment view for a new model, with a minimal controller.
*
* @since 7.2.0
*
* @param {Object} attributes Attributes for the attachment model.
* @param {Function} [ViewClass] View class to instantiate. Defaults to wp.media.view.Attachment.
* @param {wp.media.model.Selection} [selection] Selection the view should reflect.
* @return {wp.media.view.Attachment} The created view.
*/
function createView( attributes, ViewClass, selection ) {
var controller = {
state: function() {
return { get: function() {} };
},
isModeActive: function() {
return false;
},
trigger: function() {},
states: new wp.media.model.Attachments()
},
model = new wp.media.model.Attachment( attributes );

ViewClass = ViewClass || wp.media.view.Attachment;

return new ViewClass( { controller: controller, model: model, selection: selection } );
}

/*
* Trac #65852: Backbone evaluates attributes() only when the element is
* created, so a view built for an id-only model kept a stale aria-label
* after the model received its data.
*/
QUnit.test( 'aria-label is refreshed when the model title loads (Trac #65852)', function( assert ) {
var view = createView( { id: 123 } );

assert.strictEqual( view.el.getAttribute( 'aria-label' ), '(no title)', 'Initial label is the no-title fallback.' );

view.model.set( { title: 'Sunset' } );

assert.strictEqual( view.el.getAttribute( 'aria-label' ), 'Sunset', 'Label is updated to the loaded title.' );
} );

QUnit.test( 'aria-label stays "uploading…" until the upload completes (Trac #65852)', function( assert ) {
var view = createView( { uploading: true, filename: 'sunset.jpg', percent: 0 } );

assert.strictEqual( view.el.getAttribute( 'aria-label' ), 'uploading…', 'Initial label is the uploading label.' );

view.model.set( { percent: 50 } );

assert.strictEqual( view.el.getAttribute( 'aria-label' ), 'uploading…', 'Label is unchanged while progress is reported.' );

view.model.set( { uploading: false, title: 'Sunset' } );

assert.strictEqual( view.el.getAttribute( 'aria-label' ), 'Sunset', 'Label is updated once the upload completes.' );
} );

QUnit.test( 'untitled attachments keep the no-title label (Trac #65852)', function( assert ) {
var view = createView( { id: 123, title: '' } );

view.model.set( { caption: 'A caption' } );

assert.strictEqual( view.el.getAttribute( 'aria-label' ), '(no title)', 'Label remains the no-title fallback.' );
} );

QUnit.test( 'aria-checked is not reset by re-rendering (Trac #65852)', function( assert ) {
var selection = new wp.media.model.Selection(),
view = createView( { id: 123 }, null, selection );

selection.add( view.model );
view.render();

assert.strictEqual( view.el.getAttribute( 'aria-checked' ), 'true', 'The selected tile is checked.' );

view.model.set( { title: 'Sunset' } );

assert.strictEqual( view.el.getAttribute( 'aria-checked' ), 'true', 'Selection state is preserved after the label refresh.' );
} );

QUnit.test( 'Attachment.Details does not receive an aria-label (Trac #65852)', function( assert ) {
var view = createView( { id: 123 }, wp.media.view.Attachment.Details );

view.model.set( { title: 'Sunset' } );

assert.notOk( view.el.hasAttribute( 'aria-label' ), 'No aria-label is added.' );
assert.notOk( view.el.hasAttribute( 'role' ), 'No role is added.' );
} );
} )();
Loading