Accessibility: Improve focus management in the Media Views.

- keeps focus management only where necessary to avoid focus losses
- removes focus management where a specific user workflow was assumed
- makes the "Attachment Details" navigation buttons really disabled when there are no next or previous attachments
- adds inline comments to clarify all the usages of focus()

Fixes #43169.

Built from https://develop.svn.wordpress.org/trunk@45524


git-svn-id: http://core.svn.wordpress.org/trunk@45335 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Andrea Fercia 2019-06-12 21:03:52 +00:00
parent 9f579ea448
commit 340fd28d82
12 changed files with 171 additions and 103 deletions

View File

@ -682,14 +682,11 @@ border color while dragging a file over the uploader drop area */
content: "\f341";
}
.edit-attachment-frame .edit-media-header .left.disabled,
.edit-attachment-frame .edit-media-header .right.disabled,
.edit-attachment-frame .edit-media-header .left.disabled:hover,
.edit-attachment-frame .edit-media-header .right.disabled:hover {
.edit-attachment-frame .edit-media-header [disabled],
.edit-attachment-frame .edit-media-header [disabled]:hover {
color: #ccc;
background: inherit;
cursor: default;
pointer-events: none;
}
.edit-attachment-frame .media-frame-content,

File diff suppressed because one or more lines are too long

View File

@ -682,14 +682,11 @@ border color while dragging a file over the uploader drop area */
content: "\f345";
}
.edit-attachment-frame .edit-media-header .left.disabled,
.edit-attachment-frame .edit-media-header .right.disabled,
.edit-attachment-frame .edit-media-header .left.disabled:hover,
.edit-attachment-frame .edit-media-header .right.disabled:hover {
.edit-attachment-frame .edit-media-header [disabled],
.edit-attachment-frame .edit-media-header [disabled]:hover {
color: #ccc;
background: inherit;
cursor: default;
pointer-events: none;
}
.edit-attachment-frame .media-frame-content,

File diff suppressed because one or more lines are too long

View File

@ -1001,7 +1001,7 @@ MediaDetails = AttachmentDisplay.extend(/** @lends wp.media.view.MediaDetails.pr
AttachmentDisplay.prototype.render.apply( this, arguments );
setTimeout( _.bind( function() {
this.resetFocus();
this.scrollToTop();
}, this ), 10 );
this.settings = _.defaults( {
@ -1011,7 +1011,7 @@ MediaDetails = AttachmentDisplay.extend(/** @lends wp.media.view.MediaDetails.pr
return this.setMedia();
},
resetFocus: function() {
scrollToTop: function() {
this.$( '.embed-media-settings' ).scrollTop( 0 );
}
},/** @lends wp.media.view.MediaDetails */{

File diff suppressed because one or more lines are too long

View File

@ -718,8 +718,9 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
// Completely destroy the modal DOM element when closing it.
this.modal.on( 'close', _.bind( function() {
$( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */
// Restore the original focus item if possible
// Remove the keydown event.
$( 'body' ).off( 'keydown.media-modal' );
// Move focus back to the original item in the grid if possible.
$( 'li.attachment[data-id="' + this.model.get( 'id' ) +'"]' ).focus();
this.resetRoute();
}, this ) );
@ -800,8 +801,8 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
},
toggleNav: function() {
this.$('.left').toggleClass( 'disabled', ! this.hasPrevious() );
this.$('.right').toggleClass( 'disabled', ! this.hasNext() );
this.$( '.left' ).prop( 'disabled', ! this.hasPrevious() );
this.$( '.right' ).prop( 'disabled', ! this.hasNext() );
},
/**
@ -831,8 +832,10 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
if ( ! this.hasPrevious() ) {
return;
}
this.trigger( 'refresh', this.library.at( this.getCurrentIndex() - 1 ) );
this.$( '.left' ).focus();
// Move focus to the Previous button. When there are no more items, to the Next button.
this.focusNavButton( this.hasPrevious() ? '.left' : '.right' );
},
/**
@ -842,8 +845,21 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
if ( ! this.hasNext() ) {
return;
}
this.trigger( 'refresh', this.library.at( this.getCurrentIndex() + 1 ) );
this.$( '.right' ).focus();
// Move focus to the Next button. When there are no more items, to the Previous button.
this.focusNavButton( this.hasNext() ? '.right' : '.left' );
},
/**
* Set focus to the navigation buttons depending on the browsing direction.
*
* @since 5.3.0
*
* @param {string} which A CSS selector to target the button to focus.
*/
focusNavButton: function( which ) {
$( which ).focus();
},
getCurrentIndex: function() {

File diff suppressed because one or more lines are too long

View File

@ -3655,8 +3655,7 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
frame.close();
}
// Keep focus inside media modal
// after canceling a gallery
// Move focus to the modal after canceling a Gallery.
this.controller.modal.focusManager.focus();
}
},
@ -3682,6 +3681,9 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
} else {
frame.close();
}
// Move focus to the modal after canceling an Audio Playlist.
this.controller.modal.focusManager.focus();
}
},
separateCancel: new wp.media.View({
@ -3706,6 +3708,9 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
} else {
frame.close();
}
// Move focus to the modal after canceling a Video Playlist.
this.controller.modal.focusManager.focus();
}
},
separateCancel: new wp.media.View({
@ -3723,10 +3728,6 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
}).render();
this.content.set( view );
if ( ! wp.media.isTouchDevice ) {
view.url.focus();
}
},
editSelectionContent: function() {
@ -3848,10 +3849,10 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
multiple: true
}) );
this.controller.setState('gallery-edit');
// Jump to Edit Gallery view.
this.controller.setState( 'gallery-edit' );
// Keep focus inside media modal
// after jumping to gallery view
// Move focus to the modal after jumping to Edit Gallery view.
this.controller.modal.focusManager.focus();
}
});
@ -3878,10 +3879,10 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
multiple: true
}) );
this.controller.setState('playlist-edit');
// Jump to Edit Audio Playlist view.
this.controller.setState( 'playlist-edit' );
// Keep focus inside media modal
// after jumping to playlist view
// Move focus to the modal after jumping to Edit Audio Playlist view.
this.controller.modal.focusManager.focus();
}
});
@ -3908,10 +3909,10 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
multiple: true
}) );
this.controller.setState('video-playlist-edit');
// Jump to Edit Video Playlist view.
this.controller.setState( 'video-playlist-edit' );
// Keep focus inside media modal
// after jumping to video playlist view
// Move focus to the modal after jumping to Edit Video Playlist view.
this.controller.modal.focusManager.focus();
}
});
@ -3981,6 +3982,8 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
edit.get('library').add( state.get('selection').models );
state.trigger('reset');
controller.setState('gallery-edit');
// Move focus to the modal when jumping back from Add to Gallery to Edit Gallery view.
this.controller.modal.focusManager.focus();
}
}
}
@ -4038,6 +4041,8 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
edit.get('library').add( state.get('selection').models );
state.trigger('reset');
controller.setState('playlist-edit');
// Move focus to the modal when jumping back from Add to Audio Playlist to Edit Audio Playlist view.
this.controller.modal.focusManager.focus();
}
}
}
@ -4092,6 +4097,8 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
edit.get('library').add( state.get('selection').models );
state.trigger('reset');
controller.setState('video-playlist-edit');
// Move focus to the modal when jumping back from Add to Video Playlist to Edit Video Playlist view.
this.controller.modal.focusManager.focus();
}
}
}
@ -4427,10 +4434,12 @@ Modal = wp.media.View.extend(/** @lends wp.media.view.Modal.prototype */{
// Hide modal and remove restricted media modal tab focus once it's closed
this.$el.hide().undelegate( 'keydown' );
// Put focus back in useful location once modal is closed.
// Move focus back in useful location once modal is closed.
if ( null !== this.clickedOpenerEl ) {
// Move focus back to the element that opened the modal.
this.clickedOpenerEl.focus();
} else {
// Fallback to the admin page main element.
$( '#wpbody-content' )
.attr( 'tabindex', '-1' )
.focus();
@ -4519,11 +4528,20 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
},
/**
* Moves focus to the first visible menu item in the modal.
* Gets all the tabbable elements.
*/
getTabbables: function() {
// Skip the file input added by Plupload.
return this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' );
},
/**
* Moves focus to the modal dialog.
*/
focus: function() {
this.$( '.media-menu-item' ).filter( ':visible' ).first().focus();
this.$( '.media-modal' ).focus();
},
/**
* @param {Object} event
*/
@ -4535,8 +4553,7 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
return;
}
// Skip the file input added by Plupload.
tabbables = this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' );
tabbables = this.getTabbables();
// Keep tab focus within media modal while it's open
if ( tabbables.last()[0] === event.target && ! event.shiftKey ) {
@ -5177,7 +5194,7 @@ UploaderStatus = View.extend(/** @lends wp.media.view.UploaderStatus.prototype *
_.invoke( errors, 'remove' );
}
wp.Uploader.errors.reset();
// Keep focus within the modal after the dismiss button gets removed from the DOM.
// Move focus to the modal after the dismiss button gets removed from the DOM.
this.controller.modal.focusManager.focus();
}
});
@ -5745,8 +5762,7 @@ module.exports = PriorityList;
/* 68 */
/***/ (function(module, exports) {
var $ = jQuery,
MenuItem;
var MenuItem;
/**
* wp.media.view.MenuItem
@ -5784,12 +5800,6 @@ MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{
} else {
this.click();
}
// When selecting a tab along the left side,
// focus should be transferred into the main panel
if ( ! wp.media.isTouchDevice ) {
$('.media-frame-content input').first().focus();
}
},
click: function() {
@ -6759,7 +6769,7 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{
this.collection.on( 'reset', this.render, this );
this.listenTo( this.controller, 'library:selection:add', this.attachmentFocus );
this.controller.on( 'library:selection:add', this.attachmentFocus, this );
// Throttle the scroll handler and bind this.
this.scroll = _.chain( this.scroll ).bind( this ).throttle( this.options.refreshSensitivity ).value();
@ -6807,12 +6817,28 @@ Attachments = View.extend(/** @lends wp.media.view.Attachments.prototype */{
* @returns {void}
*/
attachmentFocus: function() {
this.$( 'li:first' ).focus();
/*
* @todo: when uploading new attachments, this tries to move focus to the
* attachmentz grid. Actually, a progress bar gets initially displayed
* and then updated when uploading completes, so focus is lost.
* Additionally: this view is used for both the attachments list and the
* list of selected attachments in the bottom media toolbar. Thus, when
* uploading attachments, it is called twice and returns two different `this`.
* `this.columns` is truthy within the modal.
*/
if ( this.columns ) {
// Move focus to the grid list within the modal.
this.$el.focus();
}
},
/**
* Restores focus to the selected item in the collection.
*
* Moves focus back to the first selected attachment in the grid. Used when
* tabbing backwards from the attachment details sidebar.
* See media.view.AttachmentsBrowser.
*
* @since 4.0.0
*
* @returns {void}
@ -7584,6 +7610,7 @@ AttachmentsBrowser = View.extend(/** @lends wp.media.view.AttachmentsBrowser.pro
},
editSelection: function( modal ) {
// When editing a selection, move focus to the "Return to library" button.
modal.$( '.media-button-backToLibrary' ).focus();
},
@ -8066,8 +8093,7 @@ Selection = wp.media.View.extend(/** @lends wp.media.view.Selection.prototype */
event.preventDefault();
this.collection.reset();
// Keep focus inside media modal
// after clear link is selected
// Move focus to the modal.
this.controller.modal.focusManager.focus();
}
});
@ -8380,11 +8406,6 @@ AttachmentDisplay = Settings.extend(/** @lends wp.media.view.Settings.Attachment
}
$input.closest( '.setting' ).removeClass( 'hidden' );
// If the input is visible, focus and select its contents.
if ( ! wp.media.isTouchDevice && $input.is(':visible') ) {
$input.focus()[0].select();
}
}
});
@ -8443,6 +8464,7 @@ module.exports = Playlist;
var Attachment = wp.media.view.Attachment,
l10n = wp.media.view.l10n,
$ = jQuery,
Details;
/**
@ -8488,35 +8510,98 @@ Details = Attachment.extend(/** @lends wp.media.view.Attachment.Details.prototyp
Attachment.prototype.initialize.apply( this, arguments );
},
/**
* Gets the focusable elements to move focus to.
*
* @since 5.3.0
*/
getFocusableElements: function() {
var editedAttachment = $( 'li[data-id="' + this.model.id + '"]' );
this.previousAttachment = editedAttachment.prev();
this.nextAttachment = editedAttachment.next();
},
/**
* Moves focus to the previous or next attachment in the grid.
* Fallbacks to the upload button or media frame when there are no attachments.
*
* @since 5.3.0
*/
moveFocus: function() {
if ( this.previousAttachment.length ) {
this.previousAttachment.focus();
return;
}
if ( this.nextAttachment.length ) {
this.nextAttachment.focus();
return;
}
// Fallback: move focus to the "Select Files" button in the media modal.
if ( this.controller.uploader && this.controller.uploader.$browser ) {
this.controller.uploader.$browser.focus();
return;
}
// Last fallback.
this.moveFocusToLastFallback();
},
/**
* Moves focus to the media frame as last fallback.
*
* @since 5.3.0
*/
moveFocusToLastFallback: function() {
// Last fallback: make the frame focusable and move focus to it.
$( '.media-frame' )
.attr( 'tabindex', '-1' )
.focus();
},
/**
* @param {Object} event
*/
deleteAttachment: function( event ) {
event.preventDefault();
this.getFocusableElements();
if ( window.confirm( l10n.warnDelete ) ) {
this.model.destroy();
// Keep focus inside media modal
// after image is deleted
this.controller.modal.focusManager.focus();
this.moveFocus();
}
},
/**
* @param {Object} event
*/
trashAttachment: function( event ) {
var library = this.controller.library;
var library = this.controller.library,
self = this;
event.preventDefault();
this.getFocusableElements();
// When in the Media Library and the Media trash is enabled.
if ( wp.media.view.settings.mediaTrash &&
'edit-metadata' === this.controller.content.mode() ) {
this.model.set( 'status', 'trash' );
this.model.save().done( function() {
library._requery( true );
/*
* @todo: We need to move focus back to the previous, next, or first
* attachment but the library gets re-queried and refreshed. Thus,
* the references to the previous attachments are lost. We need an
* alternate method.
*/
self.moveFocusToLastFallback();
} );
} else {
} else {
this.model.destroy();
this.moveFocus();
}
},
/**
@ -8546,8 +8631,8 @@ Details = Attachment.extend(/** @lends wp.media.view.Attachment.Details.prototyp
}
},
/**
* When reverse tabbing(shift+tab) out of the right details panel, deliver
* the focus to the item in the list that was being edited.
* When reverse tabbing (shift+tab) out of the right details panel,
* move focus to the item that was being edited in the attachments list.
*
* @param {Object} event
*/
@ -8556,11 +8641,6 @@ Details = Attachment.extend(/** @lends wp.media.view.Attachment.Details.prototyp
this.controller.trigger( 'attachment:details:shift-tab', event );
return false;
}
if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
this.controller.trigger( 'attachment:keydown:arrow', event );
return;
}
}
});
@ -8852,24 +8932,8 @@ EmbedUrl = View.extend(/** @lends wp.media.view.EmbedUrl.prototype */{
return this;
},
ready: function() {
if ( ! wp.media.isTouchDevice ) {
this.focus();
}
},
url: function( event ) {
this.model.set( 'url', $.trim( event.target.value ) );
},
/**
* If the input is visible, focus and select its contents.
*/
focus: function() {
var $input = this.$input;
if ( $input.is(':visible') ) {
$input.focus()[0].select();
}
}
});
@ -9095,7 +9159,7 @@ ImageDetails = AttachmentDisplay.extend(/** @lends wp.media.view.ImageDetails.pr
},
postRender: function() {
setTimeout( _.bind( this.resetFocus, this ), 10 );
setTimeout( _.bind( this.scrollToTop, this ), 10 );
this.toggleLinkSettings();
if ( window.getUserSetting( 'advImgDetails' ) === 'show' ) {
this.toggleAdvanced( true );
@ -9103,8 +9167,7 @@ ImageDetails = AttachmentDisplay.extend(/** @lends wp.media.view.ImageDetails.pr
this.trigger( 'post-render' );
},
resetFocus: function() {
this.$( '.link-to-custom' ).blur();
scrollToTop: function() {
this.$( '.embed-media-settings' ).scrollTop( 0 );
},
@ -9436,12 +9499,7 @@ EditImage = View.extend(/** @lends wp.media.view.EditImage.prototype */{
},
loadEditor: function() {
var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
dfd.done( _.bind( this.focus, this ) );
},
focus: function() {
this.$( '.imgedit-submit .button' ).eq( 0 ).focus();
this.editor.open( this.model.get( 'id' ), this.model.get( 'nonces' ).edit, this );
},
back: function() {

File diff suppressed because one or more lines are too long

View File

@ -323,8 +323,8 @@ function wp_print_media_templates() {
<?php // Template for the Attachment Details layout in the media browser. ?>
<script type="text/html" id="tmpl-edit-attachment-frame">
<div class="edit-media-header">
<button class="left dashicons <# if ( ! data.hasPrevious ) { #> disabled <# } #>"><span class="screen-reader-text"><?php _e( 'Previous' ); ?></span></button>
<button class="right dashicons <# if ( ! data.hasNext ) { #> disabled <# } #>"><span class="screen-reader-text"><?php _e( 'Next' ); ?></span></button>
<button class="left dashicons"<# if ( ! data.hasPrevious ) { #> disabled<# } #>><span class="screen-reader-text"><?php _e( 'Edit previous media item' ); ?></span></button>
<button class="right dashicons"<# if ( ! data.hasNext ) { #> disabled<# } #>><span class="screen-reader-text"><?php _e( 'Edit next media item' ); ?></span></button>
<button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text"><?php _e( 'Close dialog' ); ?></span></span></button>
</div>
<div class="media-frame-title"></div>

View File

@ -13,7 +13,7 @@
*
* @global string $wp_version
*/
$wp_version = '5.3-alpha-45522';
$wp_version = '5.3-alpha-45524';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.