From 6551660d5550472bd36ef3beb50b58926b788448 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 12 Jun 2019 21:02:03 +0000 Subject: [PATCH] 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. git-svn-id: https://develop.svn.wordpress.org/trunk@45524 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/media/views/attachment/details.js | 83 ++++++++++++++++--- src/js/media/views/attachments.js | 20 ++++- src/js/media/views/attachments/browser.js | 1 + src/js/media/views/edit-image.js | 7 +- src/js/media/views/embed/url.js | 16 ---- src/js/media/views/focus-manager.js | 16 +++- src/js/media/views/frame/edit-attachments.js | 28 +++++-- src/js/media/views/frame/post.js | 37 +++++---- src/js/media/views/image-details.js | 5 +- src/js/media/views/media-details.js | 4 +- src/js/media/views/menu-item.js | 9 +- src/js/media/views/modal.js | 4 +- src/js/media/views/selection.js | 3 +- .../views/settings/attachment-display.js | 5 -- src/js/media/views/uploader/status.js | 2 +- src/wp-admin/css/media.css | 7 +- src/wp-includes/media-template.php | 4 +- 17 files changed, 161 insertions(+), 90 deletions(-) diff --git a/src/js/media/views/attachment/details.js b/src/js/media/views/attachment/details.js index fe12487548..977abc0e69 100644 --- a/src/js/media/views/attachment/details.js +++ b/src/js/media/views/attachment/details.js @@ -1,5 +1,6 @@ var Attachment = wp.media.view.Attachment, l10n = wp.media.view.l10n, + $ = jQuery, Details; /** @@ -45,35 +46,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(); } }, /** @@ -103,8 +167,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 */ @@ -113,11 +177,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; - } } }); diff --git a/src/js/media/views/attachments.js b/src/js/media/views/attachments.js index 90a5e116fd..a3abca59a0 100644 --- a/src/js/media/views/attachments.js +++ b/src/js/media/views/attachments.js @@ -82,7 +82,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(); @@ -130,12 +130,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} diff --git a/src/js/media/views/attachments/browser.js b/src/js/media/views/attachments/browser.js index d054093f27..ecc480c53a 100644 --- a/src/js/media/views/attachments/browser.js +++ b/src/js/media/views/attachments/browser.js @@ -86,6 +86,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(); }, diff --git a/src/js/media/views/edit-image.js b/src/js/media/views/edit-image.js index bc8fec7dfb..8409689291 100644 --- a/src/js/media/views/edit-image.js +++ b/src/js/media/views/edit-image.js @@ -26,12 +26,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() { diff --git a/src/js/media/views/embed/url.js b/src/js/media/views/embed/url.js index d27b93caa3..4585e2ba89 100644 --- a/src/js/media/views/embed/url.js +++ b/src/js/media/views/embed/url.js @@ -56,24 +56,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(); - } } }); diff --git a/src/js/media/views/focus-manager.js b/src/js/media/views/focus-manager.js index 2a7aef5bb7..2741bcb33f 100644 --- a/src/js/media/views/focus-manager.js +++ b/src/js/media/views/focus-manager.js @@ -15,11 +15,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 */ @@ -31,8 +40,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 ) { diff --git a/src/js/media/views/frame/edit-attachments.js b/src/js/media/views/frame/edit-attachments.js index 0b5d150cd9..249329f396 100644 --- a/src/js/media/views/frame/edit-attachments.js +++ b/src/js/media/views/frame/edit-attachments.js @@ -91,8 +91,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 ) ); @@ -173,8 +174,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() ); }, /** @@ -204,8 +205,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' ); }, /** @@ -215,8 +218,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() { diff --git a/src/js/media/views/frame/post.js b/src/js/media/views/frame/post.js index 8c8906e26b..b8eea2343c 100644 --- a/src/js/media/views/frame/post.js +++ b/src/js/media/views/frame/post.js @@ -290,8 +290,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(); } }, @@ -317,6 +316,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({ @@ -341,6 +343,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({ @@ -358,10 +363,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() { @@ -483,10 +484,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(); } }); @@ -513,10 +514,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(); } }); @@ -543,10 +544,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(); } }); @@ -616,6 +617,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(); } } } @@ -673,6 +676,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(); } } } @@ -727,6 +732,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(); } } } diff --git a/src/js/media/views/image-details.js b/src/js/media/views/image-details.js index def9ef7bba..cbd62e306d 100644 --- a/src/js/media/views/image-details.js +++ b/src/js/media/views/image-details.js @@ -71,7 +71,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 ); @@ -79,8 +79,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 ); }, diff --git a/src/js/media/views/media-details.js b/src/js/media/views/media-details.js index a418ec65bf..4b9a7c1a0d 100644 --- a/src/js/media/views/media-details.js +++ b/src/js/media/views/media-details.js @@ -129,7 +129,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( { @@ -139,7 +139,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 */{ diff --git a/src/js/media/views/menu-item.js b/src/js/media/views/menu-item.js index 375e86f182..7f13f9cb48 100644 --- a/src/js/media/views/menu-item.js +++ b/src/js/media/views/menu-item.js @@ -1,5 +1,4 @@ -var $ = jQuery, - MenuItem; +var MenuItem; /** * wp.media.view.MenuItem @@ -37,12 +36,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() { diff --git a/src/js/media/views/modal.js b/src/js/media/views/modal.js index aabf2b48d4..11278f904a 100644 --- a/src/js/media/views/modal.js +++ b/src/js/media/views/modal.js @@ -135,10 +135,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(); diff --git a/src/js/media/views/selection.js b/src/js/media/views/selection.js index 0d411bd332..e71965e23c 100644 --- a/src/js/media/views/selection.js +++ b/src/js/media/views/selection.js @@ -74,8 +74,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(); } }); diff --git a/src/js/media/views/settings/attachment-display.js b/src/js/media/views/settings/attachment-display.js index 0d8eadfb92..31bf4f6f0d 100644 --- a/src/js/media/views/settings/attachment-display.js +++ b/src/js/media/views/settings/attachment-display.js @@ -83,11 +83,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(); - } } }); diff --git a/src/js/media/views/uploader/status.js b/src/js/media/views/uploader/status.js index 6dfc70c8fe..7dbc0923ee 100644 --- a/src/js/media/views/uploader/status.js +++ b/src/js/media/views/uploader/status.js @@ -124,7 +124,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(); } }); diff --git a/src/wp-admin/css/media.css b/src/wp-admin/css/media.css index 23796aacc2..5c7242224d 100644 --- a/src/wp-admin/css/media.css +++ b/src/wp-admin/css/media.css @@ -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, diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php index 427ff53b2c..8ebab13d54 100644 --- a/src/wp-includes/media-template.php +++ b/src/wp-includes/media-template.php @@ -323,8 +323,8 @@ function wp_print_media_templates() {