Make WordPress Core

Changeset 45572


Ignore:
Timestamp:
06/27/2019 12:32:28 PM (7 years ago)
Author:
afercia
Message:

Accessibility: Make the Media modal an ARIA modal dialog.

For a number of years, the Media modal missed an explicit ARIA role and the required attributes for modal dialogs.

This was confusing for assistive technology users, since they may not realize they're inside a dialog, and that consequently the keyboard interactions may be different from the rest of the page. Lack of an explicit label for the dialog was confusing as well, since assistive technology users didn't have an immediate sense of what the dialog is for.

This change makes the Media modal meet the ARIA Authoring Practices recommendations, helping users better understand the purpose and interactions with the modal. Also, it makes sure to hide the rest of the page content from assistive technologies, until support for aria-modal="true" improves.

Additionally:

  • moves the modal H1 heading to the beginning of the modal content
  • changes the modal left menu position to make visual and DOM order match
  • improves the wp.media.view.FocusManager documentation

Fixes #47145.

Location:
trunk/src
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/media/views/focus-manager.js

    r45524 r45572  
    1717        /**
    1818         * Gets all the tabbable elements.
     19         *
     20         * @since 5.3.0
     21         *
     22         * @returns {object} A jQuery collection of tabbable elements.
    1923         */
    2024        getTabbables: function() {
     
    2529        /**
    2630         * Moves focus to the modal dialog.
     31         *
     32         * @since 3.5.0
     33         *
     34         * @returns {void}
    2735         */
    2836        focus: function() {
     
    3139
    3240        /**
    33          * @param {Object} event
     41         * Constrains navigation with the Tab key within the media view element.
     42         *
     43         * @since 4.0.0
     44         *
     45         * @param {Object} event A keydown jQuery event.
     46         *
     47         * @returns {void}
    3448         */
    3549        constrainTabbing: function( event ) {
     
    5165                        return false;
    5266                }
    53         }
     67        },
    5468
     69        /**
     70         * Hides from assistive technologies all the body children except the
     71         * provided element and other elements that should not be hidden.
     72         *
     73         * The reason why we use `aria-hidden` is that `aria-modal="true"` is buggy
     74         * in Safari 11.1 and support is spotty in other browsers. In the future we
     75         * should consider to remove this helper function and only use `aria-modal="true"`.
     76         *
     77         * @since 5.3.0
     78         *
     79         * @param {object} visibleElement The jQuery object representing the element that should not be hidden.
     80         *
     81         * @returns {void}
     82         */
     83        setAriaHiddenOnBodyChildren: function( visibleElement ) {
     84                var bodyChildren,
     85                        self = this;
     86
     87                if ( this.isBodyAriaHidden ) {
     88                        return;
     89                }
     90
     91                // Get all the body children.
     92                bodyChildren = document.body.children;
     93
     94                // Loop through the body children and hide the ones that should be hidden.
     95                _.each( bodyChildren, function( element ) {
     96                        // Don't hide the modal element.
     97                        if ( element === visibleElement[0] ) {
     98                                return;
     99                        }
     100
     101                        // Determine the body children to hide.
     102                        if ( self.elementShouldBeHidden( element ) ) {
     103                                element.setAttribute( 'aria-hidden', 'true' );
     104                                // Store the hidden elements.
     105                                self.ariaHiddenElements.push( element );
     106                        }
     107                } );
     108
     109                this.isBodyAriaHidden = true;
     110        },
     111
     112        /**
     113         * Makes visible again to assistive technologies all body children
     114         * previously hidden and stored in this.ariaHiddenElements.
     115         *
     116         * @since 5.3.0
     117         *
     118         * @returns {void}
     119         */
     120        removeAriaHiddenFromBodyChildren: function() {
     121                _.each( this.ariaHiddenElements, function( element ) {
     122                        element.removeAttribute( 'aria-hidden' );
     123                } );
     124
     125                this.ariaHiddenElements = [];
     126                this.isBodyAriaHidden   = false;
     127        },
     128
     129        /**
     130         * Determines if the passed element should not be hidden from assistive technologies.
     131         *
     132         * @since 5.3.0
     133         *
     134         * @param {object} element The DOM element that should be checked.
     135         *
     136         * @returns {boolean} Whether the element should not be hidden from assistive technologies.
     137         */
     138        elementShouldBeHidden: function( element ) {
     139                var role = element.getAttribute( 'role' ),
     140                        liveRegionsRoles = [ 'alert', 'status', 'log', 'marquee', 'timer' ];
     141
     142                /*
     143                 * Don't hide scripts, elements that already have `aria-hidden`, and
     144                 * ARIA live regions.
     145                 */
     146                return ! (
     147                        element.tagName === 'SCRIPT' ||
     148                        element.hasAttribute( 'aria-hidden' ) ||
     149                        element.hasAttribute( 'aria-live' ) ||
     150                        liveRegionsRoles.indexOf( role ) !== -1
     151                );
     152        },
     153
     154        /**
     155         * Whether the body children are hidden from assistive technologies.
     156         *
     157         * @since 5.3.0
     158         */
     159        isBodyAriaHidden: false,
     160
     161        /**
     162         * Stores an array of DOM elements that should be hidden from assistive
     163         * technologies, for example when the media modal dialog opens.
     164         *
     165         * @since 5.3.0
     166         */
     167        ariaHiddenElements: []
    55168});
    56169
  • trunk/src/js/media/views/modal.js

    r45524 r45572  
    118118                this.$( '.media-modal' ).focus();
    119119
     120                // Hide the page content from assistive technologies.
     121                this.focusManager.setAriaHiddenOnBodyChildren( $el );
     122
    120123                return this.propagate('open');
    121124        },
     
    135138                // Hide modal and remove restricted media modal tab focus once it's closed
    136139                this.$el.hide().undelegate( 'keydown' );
     140
     141                /*
     142                 * Make visible again to assistive technologies all body children that
     143                 * have been made hidden when the modal opened.
     144                 */
     145                this.focusManager.removeAriaHiddenFromBodyChildren();
    137146
    138147                // Move focus back in useful location once modal is closed.
  • trunk/src/wp-includes/css/media-views.css

    r45526 r45572  
    543543        bottom: 0;
    544544        margin: 0;
    545         padding: 10px 0;
     545        padding: 50px 0 10px;
    546546        background: #f3f3f3;
    547547        border-right-width: 1px;
     
    25312531/* Landscape specific header override */
    25322532@media screen and (max-height: 400px) {
    2533         .media-menu {
    2534                 padding: 0;
     2533        .media-menu,
     2534        .media-frame:not(.hide-menu) .media-menu {
     2535                top: 44px;
    25352536        }
    25362537
     
    25502551        .embed-link-settings {
    25512552                overflow: visible;
     2553        }
     2554}
     2555
     2556@media only screen and (min-width: 901px) and (max-height: 400px) {
     2557        .media-menu,
     2558        .media-frame:not(.hide-menu) .media-menu {
     2559                top: 0;
     2560                padding-top: 44px;
    25522561        }
    25532562}
     
    25792588        .media-frame:not(.hide-menu) .media-menu {
    25802589                top: 40px;
     2590                padding-top: 0;
    25812591        }
    25822592
  • trunk/src/wp-includes/media-template.php

    r45524 r45572  
    178178        <?php // Template for the media frame: used both in the media grid and in the media modal. ?>
    179179        <script type="text/html" id="tmpl-media-frame">
     180                <div class="media-frame-title" id="media-frame-title"></div>
    180181                <div class="media-frame-menu"></div>
    181                 <div class="media-frame-title"></div>
    182182                <div class="media-frame-router"></div>
    183183                <div class="media-frame-content"></div>
     
    188188        <?php // Template for the media modal. ?>
    189189        <script type="text/html" id="tmpl-media-modal">
    190                 <div tabindex="0" class="<?php echo $class; ?>">
     190                <div tabindex="0" class="<?php echo $class; ?>" role="dialog" aria-modal="true" aria-labelledby="media-frame-title">
    191191                        <# if ( data.hasCloseButton ) { #>
    192192                                <button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text"><?php _e( 'Close dialog' ); ?></span></span></button>
    193193                        <# } #>
    194                         <div class="media-modal-content"></div>
     194                        <div class="media-modal-content" role="document"></div>
    195195                </div>
    196196                <div class="media-modal-backdrop"></div>
Note: See TracChangeset for help on using the changeset viewer.