WordPress.org

Make WordPress Core

Changeset 45524


Ignore:
Timestamp:
06/12/2019 09:02:03 PM (3 months ago)
Author:
afercia
Message:

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.

Location:
trunk/src
Files:
17 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/media/views/attachment/details.js

    r45506 r45524  
    11var Attachment = wp.media.view.Attachment,
    22    l10n = wp.media.view.l10n,
     3    $ = jQuery,
    34    Details;
    45
     
    4748
    4849    /**
     50     * Gets the focusable elements to move focus to.
     51     *
     52     * @since 5.3.0
     53     */
     54    getFocusableElements: function() {
     55        var editedAttachment = $( 'li[data-id="' + this.model.id + '"]' );
     56
     57        this.previousAttachment = editedAttachment.prev();
     58        this.nextAttachment = editedAttachment.next();
     59    },
     60
     61    /**
     62     * Moves focus to the previous or next attachment in the grid.
     63     * Fallbacks to the upload button or media frame when there are no attachments.
     64     *
     65     * @since 5.3.0
     66     */
     67    moveFocus: function() {
     68        if ( this.previousAttachment.length ) {
     69            this.previousAttachment.focus();
     70            return;
     71        }
     72
     73        if ( this.nextAttachment.length ) {
     74            this.nextAttachment.focus();
     75            return;
     76        }
     77
     78        // Fallback: move focus to the "Select Files" button in the media modal.
     79        if ( this.controller.uploader && this.controller.uploader.$browser ) {
     80            this.controller.uploader.$browser.focus();
     81            return;
     82        }
     83
     84        // Last fallback.
     85        this.moveFocusToLastFallback();
     86    },
     87
     88    /**
     89     * Moves focus to the media frame as last fallback.
     90     *
     91     * @since 5.3.0
     92     */
     93    moveFocusToLastFallback: function() {
     94        // Last fallback: make the frame focusable and move focus to it.
     95        $( '.media-frame' )
     96            .attr( 'tabindex', '-1' )
     97            .focus();
     98    },
     99
     100    /**
    49101     * @param {Object} event
    50102     */
     
    52104        event.preventDefault();
    53105
     106        this.getFocusableElements();
     107
    54108        if ( window.confirm( l10n.warnDelete ) ) {
    55109            this.model.destroy();
    56             // Keep focus inside media modal
    57             // after image is deleted
    58             this.controller.modal.focusManager.focus();
     110            this.moveFocus();
    59111        }
    60112    },
     
    63115     */
    64116    trashAttachment: function( event ) {
    65         var library = this.controller.library;
     117        var library = this.controller.library,
     118            self = this;
    66119        event.preventDefault();
    67120
     121        this.getFocusableElements();
     122
     123        // When in the Media Library and the Media trash is enabled.
    68124        if ( wp.media.view.settings.mediaTrash &&
    69125            'edit-metadata' === this.controller.content.mode() ) {
     
    72128            this.model.save().done( function() {
    73129                library._requery( true );
     130                /*
     131                 * @todo: We need to move focus back to the previous, next, or first
     132                 * attachment but the library gets re-queried and refreshed. Thus,
     133                 * the references to the previous attachments are lost. We need an
     134                 * alternate method.
     135                 */
     136                self.moveFocusToLastFallback();
    74137            } );
    75         }  else {
     138        } else {
    76139            this.model.destroy();
     140            this.moveFocus();
    77141        }
    78142    },
     
    104168    },
    105169    /**
    106      * When reverse tabbing(shift+tab) out of the right details panel, deliver
    107      * the focus to the item in the list that was being edited.
     170     * When reverse tabbing (shift+tab) out of the right details panel,
     171     * move focus to the item that was being edited in the attachments list.
    108172     *
    109173     * @param {Object} event
     
    114178            return false;
    115179        }
    116 
    117         if ( 37 === event.keyCode || 38 === event.keyCode || 39 === event.keyCode || 40 === event.keyCode ) {
    118             this.controller.trigger( 'attachment:keydown:arrow', event );
    119             return;
    120         }
    121180    }
    122181});
  • trunk/src/js/media/views/attachments.js

    r43309 r45524  
    8383        this.collection.on( 'reset', this.render, this );
    8484
    85         this.listenTo( this.controller, 'library:selection:add',    this.attachmentFocus );
     85        this.controller.on( 'library:selection:add', this.attachmentFocus, this );
    8686
    8787        // Throttle the scroll handler and bind this.
     
    131131     */
    132132    attachmentFocus: function() {
    133         this.$( 'li:first' ).focus();
     133        /*
     134         * @todo: when uploading new attachments, this tries to move focus to the
     135         * attachmentz grid. Actually, a progress bar gets initially displayed
     136         * and then updated when uploading completes, so focus is lost.
     137         * Additionally: this view is used for both the attachments list and the
     138         * list of selected attachments in the bottom media toolbar. Thus, when
     139         * uploading attachments, it is called twice and returns two different `this`.
     140         * `this.columns` is truthy within the modal.
     141         */
     142        if ( this.columns ) {
     143            // Move focus to the grid list within the modal.
     144            this.$el.focus();
     145        }
    134146    },
    135147
    136148    /**
    137149     * Restores focus to the selected item in the collection.
     150     *
     151     * Moves focus back to the first selected attachment in the grid. Used when
     152     * tabbing backwards from the attachment details sidebar.
     153     * See media.view.AttachmentsBrowser.
    138154     *
    139155     * @since 4.0.0
  • trunk/src/js/media/views/attachments/browser.js

    r45147 r45524  
    8787
    8888    editSelection: function( modal ) {
     89        // When editing a selection, move focus to the "Return to library" button.
    8990        modal.$( '.media-button-backToLibrary' ).focus();
    9091    },
  • trunk/src/js/media/views/edit-image.js

    r43309 r45524  
    2727
    2828    loadEditor: function() {
    29         var dfd = this.editor.open( this.model.get('id'), this.model.get('nonces').edit, this );
    30         dfd.done( _.bind( this.focus, this ) );
    31     },
    32 
    33     focus: function() {
    34         this.$( '.imgedit-submit .button' ).eq( 0 ).focus();
     29        this.editor.open( this.model.get( 'id' ), this.model.get( 'nonces' ).edit, this );
    3530    },
    3631
  • trunk/src/js/media/views/embed/url.js

    r45499 r45524  
    5757    },
    5858
    59     ready: function() {
    60         if ( ! wp.media.isTouchDevice ) {
    61             this.focus();
    62         }
    63     },
    64 
    6559    url: function( event ) {
    6660        this.model.set( 'url', $.trim( event.target.value ) );
    67     },
    68 
    69     /**
    70      * If the input is visible, focus and select its contents.
    71      */
    72     focus: function() {
    73         var $input = this.$input;
    74         if ( $input.is(':visible') ) {
    75             $input.focus()[0].select();
    76         }
    7761    }
    7862});
  • trunk/src/js/media/views/focus-manager.js

    r45376 r45524  
    1616
    1717    /**
    18      * Moves focus to the first visible menu item in the modal.
     18     * Gets all the tabbable elements.
     19     */
     20    getTabbables: function() {
     21        // Skip the file input added by Plupload.
     22        return this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' );
     23    },
     24
     25    /**
     26     * Moves focus to the modal dialog.
    1927     */
    2028    focus: function() {
    21         this.$( '.media-menu-item' ).filter( ':visible' ).first().focus();
     29        this.$( '.media-modal' ).focus();
    2230    },
     31
    2332    /**
    2433     * @param {Object} event
     
    3241        }
    3342
    34         // Skip the file input added by Plupload.
    35         tabbables = this.$( ':tabbable' ).not( '.moxie-shim input[type="file"]' );
     43        tabbables = this.getTabbables();
    3644
    3745        // Keep tab focus within media modal while it's open
  • trunk/src/js/media/views/frame/edit-attachments.js

    r45506 r45524  
    9292            // Completely destroy the modal DOM element when closing it.
    9393            this.modal.on( 'close', _.bind( function() {
    94                 $( 'body' ).off( 'keydown.media-modal' ); /* remove the keydown event */
    95                 // Restore the original focus item if possible
     94                // Remove the keydown event.
     95                $( 'body' ).off( 'keydown.media-modal' );
     96                // Move focus back to the original item in the grid if possible.
    9697                $( 'li.attachment[data-id="' + this.model.get( 'id' ) +'"]' ).focus();
    9798                this.resetRoute();
     
    174175
    175176    toggleNav: function() {
    176         this.$('.left').toggleClass( 'disabled', ! this.hasPrevious() );
    177         this.$('.right').toggleClass( 'disabled', ! this.hasNext() );
     177        this.$( '.left' ).prop( 'disabled', ! this.hasPrevious() );
     178        this.$( '.right' ).prop( 'disabled', ! this.hasNext() );
    178179    },
    179180
     
    205206            return;
    206207        }
     208
    207209        this.trigger( 'refresh', this.library.at( this.getCurrentIndex() - 1 ) );
    208         this.$( '.left' ).focus();
     210        // Move focus to the Previous button. When there are no more items, to the Next button.
     211        this.focusNavButton( this.hasPrevious() ? '.left' : '.right' );
    209212    },
    210213
     
    216219            return;
    217220        }
     221
    218222        this.trigger( 'refresh', this.library.at( this.getCurrentIndex() + 1 ) );
    219         this.$( '.right' ).focus();
     223        // Move focus to the Next button. When there are no more items, to the Previous button.
     224        this.focusNavButton( this.hasNext() ? '.right' : '.left' );
     225    },
     226
     227    /**
     228     * Set focus to the navigation buttons depending on the browsing direction.
     229     *
     230     * @since 5.3.0
     231     *
     232     * @param {string} which A CSS selector to target the button to focus.
     233     */
     234    focusNavButton: function( which ) {
     235        $( which ).focus();
    220236    },
    221237
  • trunk/src/js/media/views/frame/post.js

    r43309 r45524  
    291291                    }
    292292
    293                     // Keep focus inside media modal
    294                     // after canceling a gallery
     293                    // Move focus to the modal after canceling a Gallery.
    295294                    this.controller.modal.focusManager.focus();
    296295                }
     
    318317                        frame.close();
    319318                    }
     319
     320                    // Move focus to the modal after canceling an Audio Playlist.
     321                    this.controller.modal.focusManager.focus();
    320322                }
    321323            },
     
    342344                        frame.close();
    343345                    }
     346
     347                    // Move focus to the modal after canceling a Video Playlist.
     348                    this.controller.modal.focusManager.focus();
    344349                }
    345350            },
     
    359364
    360365        this.content.set( view );
    361 
    362         if ( ! wp.media.isTouchDevice ) {
    363             view.url.focus();
    364         }
    365366    },
    366367
     
    484485                }) );
    485486
    486                 this.controller.setState('gallery-edit');
    487 
    488                 // Keep focus inside media modal
    489                 // after jumping to gallery view
     487                // Jump to Edit Gallery view.
     488                this.controller.setState( 'gallery-edit' );
     489
     490                // Move focus to the modal after jumping to Edit Gallery view.
    490491                this.controller.modal.focusManager.focus();
    491492            }
     
    514515                }) );
    515516
    516                 this.controller.setState('playlist-edit');
    517 
    518                 // Keep focus inside media modal
    519                 // after jumping to playlist view
     517                // Jump to Edit Audio Playlist view.
     518                this.controller.setState( 'playlist-edit' );
     519
     520                // Move focus to the modal after jumping to Edit Audio Playlist view.
    520521                this.controller.modal.focusManager.focus();
    521522            }
     
    544545                }) );
    545546
    546                 this.controller.setState('video-playlist-edit');
    547 
    548                 // Keep focus inside media modal
    549                 // after jumping to video playlist view
     547                // Jump to Edit Video Playlist view.
     548                this.controller.setState( 'video-playlist-edit' );
     549
     550                // Move focus to the modal after jumping to Edit Video Playlist view.
    550551                this.controller.modal.focusManager.focus();
    551552            }
     
    617618                        state.trigger('reset');
    618619                        controller.setState('gallery-edit');
     620                        // Move focus to the modal when jumping back from Add to Gallery to Edit Gallery view.
     621                        this.controller.modal.focusManager.focus();
    619622                    }
    620623                }
     
    674677                        state.trigger('reset');
    675678                        controller.setState('playlist-edit');
     679                        // Move focus to the modal when jumping back from Add to Audio Playlist to Edit Audio Playlist view.
     680                        this.controller.modal.focusManager.focus();
    676681                    }
    677682                }
     
    728733                        state.trigger('reset');
    729734                        controller.setState('video-playlist-edit');
     735                        // Move focus to the modal when jumping back from Add to Video Playlist to Edit Video Playlist view.
     736                        this.controller.modal.focusManager.focus();
    730737                    }
    731738                }
  • trunk/src/js/media/views/image-details.js

    r43309 r45524  
    7272
    7373    postRender: function() {
    74         setTimeout( _.bind( this.resetFocus, this ), 10 );
     74        setTimeout( _.bind( this.scrollToTop, this ), 10 );
    7575        this.toggleLinkSettings();
    7676        if ( window.getUserSetting( 'advImgDetails' ) === 'show' ) {
     
    8080    },
    8181
    82     resetFocus: function() {
    83         this.$( '.link-to-custom' ).blur();
     82    scrollToTop: function() {
    8483        this.$( '.embed-media-settings' ).scrollTop( 0 );
    8584    },
  • trunk/src/js/media/views/media-details.js

    r43309 r45524  
    130130
    131131        setTimeout( _.bind( function() {
    132             this.resetFocus();
     132            this.scrollToTop();
    133133        }, this ), 10 );
    134134
     
    140140    },
    141141
    142     resetFocus: function() {
     142    scrollToTop: function() {
    143143        this.$( '.embed-media-settings' ).scrollTop( 0 );
    144144    }
  • trunk/src/js/media/views/menu-item.js

    r43309 r45524  
    1 var $ = jQuery,
    2     MenuItem;
     1var MenuItem;
    32
    43/**
     
    3837            this.click();
    3938        }
    40 
    41         // When selecting a tab along the left side,
    42         // focus should be transferred into the main panel
    43         if ( ! wp.media.isTouchDevice ) {
    44             $('.media-frame-content input').first().focus();
    45         }
    4639    },
    4740
  • trunk/src/js/media/views/modal.js

    r45506 r45524  
    136136        this.$el.hide().undelegate( 'keydown' );
    137137
    138         // Put focus back in useful location once modal is closed.
     138        // Move focus back in useful location once modal is closed.
    139139        if ( null !== this.clickedOpenerEl ) {
     140            // Move focus back to the element that opened the modal.
    140141            this.clickedOpenerEl.focus();
    141142        } else {
     143            // Fallback to the admin page main element.
    142144            $( '#wpbody-content' )
    143145                .attr( 'tabindex', '-1' )
  • trunk/src/js/media/views/selection.js

    r43309 r45524  
    7575        this.collection.reset();
    7676
    77         // Keep focus inside media modal
    78         // after clear link is selected
     77        // Move focus to the modal.
    7978        this.controller.modal.focusManager.focus();
    8079    }
  • trunk/src/js/media/views/settings/attachment-display.js

    r45499 r45524  
    8484
    8585        $input.closest( '.setting' ).removeClass( 'hidden' );
    86 
    87         // If the input is visible, focus and select its contents.
    88         if ( ! wp.media.isTouchDevice && $input.is(':visible') ) {
    89             $input.focus()[0].select();
    90         }
    9186    }
    9287});
  • trunk/src/js/media/views/uploader/status.js

    r45376 r45524  
    125125        }
    126126        wp.Uploader.errors.reset();
    127         // Keep focus within the modal after the dismiss button gets removed from the DOM.
     127        // Move focus to the modal after the dismiss button gets removed from the DOM.
    128128        this.controller.modal.focusManager.focus();
    129129    }
  • trunk/src/wp-admin/css/media.css

    r45499 r45524  
    683683}
    684684
    685 .edit-attachment-frame .edit-media-header .left.disabled,
    686 .edit-attachment-frame .edit-media-header .right.disabled,
    687 .edit-attachment-frame .edit-media-header .left.disabled:hover,
    688 .edit-attachment-frame .edit-media-header .right.disabled:hover {
     685.edit-attachment-frame .edit-media-header [disabled],
     686.edit-attachment-frame .edit-media-header [disabled]:hover {
    689687    color: #ccc;
    690688    background: inherit;
    691689    cursor: default;
    692     pointer-events: none;
    693690}
    694691
  • trunk/src/wp-includes/media-template.php

    r45506 r45524  
    324324    <script type="text/html" id="tmpl-edit-attachment-frame">
    325325        <div class="edit-media-header">
    326             <button class="left dashicons <# if ( ! data.hasPrevious ) { #> disabled <# } #>"><span class="screen-reader-text"><?php _e( 'Previous' ); ?></span></button>
    327             <button class="right dashicons <# if ( ! data.hasNext ) { #> disabled <# } #>"><span class="screen-reader-text"><?php _e( 'Next' ); ?></span></button>
     326            <button class="left dashicons"<# if ( ! data.hasPrevious ) { #> disabled<# } #>><span class="screen-reader-text"><?php _e( 'Edit previous media item' ); ?></span></button>
     327            <button class="right dashicons"<# if ( ! data.hasNext ) { #> disabled<# } #>><span class="screen-reader-text"><?php _e( 'Edit next media item' ); ?></span></button>
    328328            <button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text"><?php _e( 'Close dialog' ); ?></span></span></button>
    329329        </div>
Note: See TracChangeset for help on using the changeset viewer.