<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[45572] trunk/src: Accessibility: Make the Media modal an ARIA modal dialog.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/45572">45572</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/45572","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>afercia</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2019-06-27 12:32:28 +0000 (Thu, 27 Jun 2019)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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 <a href="https://core.trac.wordpress.org/ticket/47145">#47145</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcjsmediaviewsfocusmanagerjs">trunk/src/js/media/views/focus-manager.js</a></li>
<li><a href="#trunksrcjsmediaviewsmodaljs">trunk/src/js/media/views/modal.js</a></li>
<li><a href="#trunksrcwpincludescssmediaviewscss">trunk/src/wp-includes/css/media-views.css</a></li>
<li><a href="#trunksrcwpincludesmediatemplatephp">trunk/src/wp-includes/media-template.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcjsmediaviewsfocusmanagerjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/js/media/views/focus-manager.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/js/media/views/focus-manager.js 2019-06-27 12:05:44 UTC (rev 45571)
+++ trunk/src/js/media/views/focus-manager.js   2019-06-27 12:32:28 UTC (rev 45572)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -16,6 +16,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Gets all the tabbable elements.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *
+        * @since 5.3.0
+        *
+        * @returns {object} A jQuery collection of tabbable elements.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        getTabbables: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                // Skip the file input added by Plupload.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -24,6 +28,10 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Moves focus to the modal dialog.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         *
+        * @since 3.5.0
+        *
+        * @returns {void}
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        focus: function() {
</span><span class="cx" style="display: block; padding: 0 10px">                this.$( '.media-modal' ).focus();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -30,7 +38,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">        },
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param {Object} event
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Constrains navigation with the Tab key within the media view element.
+        *
+        * @since 4.0.0
+        *
+        * @param {Object} event A keydown jQuery event.
+        *
+        * @returns {void}
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><span class="cx" style="display: block; padding: 0 10px">        constrainTabbing: function( event ) {
</span><span class="cx" style="display: block; padding: 0 10px">                var tabbables;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -50,8 +64,107 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        tabbables.last().focus();
</span><span class="cx" style="display: block; padding: 0 10px">                        return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ },
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * Hides from assistive technologies all the body children except the
+        * provided element and other elements that should not be hidden.
+        *
+        * The reason why we use `aria-hidden` is that `aria-modal="true"` is buggy
+        * in Safari 11.1 and support is spotty in other browsers. In the future we
+        * should consider to remove this helper function and only use `aria-modal="true"`.
+        *
+        * @since 5.3.0
+        *
+        * @param {object} visibleElement The jQuery object representing the element that should not be hidden.
+        *
+        * @returns {void}
+        */
+       setAriaHiddenOnBodyChildren: function( visibleElement ) {
+               var bodyChildren,
+                       self = this;
+
+               if ( this.isBodyAriaHidden ) {
+                       return;
+               }
+
+               // Get all the body children.
+               bodyChildren = document.body.children;
+
+               // Loop through the body children and hide the ones that should be hidden.
+               _.each( bodyChildren, function( element ) {
+                       // Don't hide the modal element.
+                       if ( element === visibleElement[0] ) {
+                               return;
+                       }
+
+                       // Determine the body children to hide.
+                       if ( self.elementShouldBeHidden( element ) ) {
+                               element.setAttribute( 'aria-hidden', 'true' );
+                               // Store the hidden elements.
+                               self.ariaHiddenElements.push( element );
+                       }
+               } );
+
+               this.isBodyAriaHidden = true;
+       },
+
+       /**
+        * Makes visible again to assistive technologies all body children
+        * previously hidden and stored in this.ariaHiddenElements.
+        *
+        * @since 5.3.0
+        *
+        * @returns {void}
+        */
+       removeAriaHiddenFromBodyChildren: function() {
+               _.each( this.ariaHiddenElements, function( element ) {
+                       element.removeAttribute( 'aria-hidden' );
+               } );
+
+               this.ariaHiddenElements = [];
+               this.isBodyAriaHidden   = false;
+       },
+
+       /**
+        * Determines if the passed element should not be hidden from assistive technologies.
+        *
+        * @since 5.3.0
+        *
+        * @param {object} element The DOM element that should be checked.
+        *
+        * @returns {boolean} Whether the element should not be hidden from assistive technologies.
+        */
+       elementShouldBeHidden: function( element ) {
+               var role = element.getAttribute( 'role' ),
+                       liveRegionsRoles = [ 'alert', 'status', 'log', 'marquee', 'timer' ];
+
+               /*
+                * Don't hide scripts, elements that already have `aria-hidden`, and
+                * ARIA live regions.
+                */
+               return ! (
+                       element.tagName === 'SCRIPT' ||
+                       element.hasAttribute( 'aria-hidden' ) ||
+                       element.hasAttribute( 'aria-live' ) ||
+                       liveRegionsRoles.indexOf( role ) !== -1
+               );
+       },
+
+       /**
+        * Whether the body children are hidden from assistive technologies.
+        *
+        * @since 5.3.0
+        */
+       isBodyAriaHidden: false,
+
+       /**
+        * Stores an array of DOM elements that should be hidden from assistive
+        * technologies, for example when the media modal dialog opens.
+        *
+        * @since 5.3.0
+        */
+       ariaHiddenElements: []
</ins><span class="cx" style="display: block; padding: 0 10px"> });
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> module.exports = FocusManager;
</span></span></pre></div>
<a id="trunksrcjsmediaviewsmodaljs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/js/media/views/modal.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/js/media/views/modal.js 2019-06-27 12:05:44 UTC (rev 45571)
+++ trunk/src/js/media/views/modal.js   2019-06-27 12:32:28 UTC (rev 45572)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -117,6 +117,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // Set initial focus on the content instead of this view element, to avoid page scrolling.
</span><span class="cx" style="display: block; padding: 0 10px">                this.$( '.media-modal' ).focus();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Hide the page content from assistive technologies.
+               this.focusManager.setAriaHiddenOnBodyChildren( $el );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return this.propagate('open');
</span><span class="cx" style="display: block; padding: 0 10px">        },
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -135,6 +138,12 @@
</span><span class="cx" style="display: block; padding: 0 10px">                // Hide modal and remove restricted media modal tab focus once it's closed
</span><span class="cx" style="display: block; padding: 0 10px">                this.$el.hide().undelegate( 'keydown' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * Make visible again to assistive technologies all body children that
+                * have been made hidden when the modal opened.
+                */
+               this.focusManager.removeAriaHiddenFromBodyChildren();
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Move focus back in useful location once modal is closed.
</span><span class="cx" style="display: block; padding: 0 10px">                if ( null !== this.clickedOpenerEl ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        // Move focus back to the element that opened the modal.
</span></span></pre></div>
<a id="trunksrcwpincludescssmediaviewscss"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/css/media-views.css</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/css/media-views.css 2019-06-27 12:05:44 UTC (rev 45571)
+++ trunk/src/wp-includes/css/media-views.css   2019-06-27 12:32:28 UTC (rev 45572)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -542,7 +542,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        right: 0;
</span><span class="cx" style="display: block; padding: 0 10px">        bottom: 0;
</span><span class="cx" style="display: block; padding: 0 10px">        margin: 0;
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        padding: 10px 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ padding: 50px 0 10px;
</ins><span class="cx" style="display: block; padding: 0 10px">         background: #f3f3f3;
</span><span class="cx" style="display: block; padding: 0 10px">        border-right-width: 1px;
</span><span class="cx" style="display: block; padding: 0 10px">        border-right-style: solid;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2530,8 +2530,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /* Landscape specific header override */
</span><span class="cx" style="display: block; padding: 0 10px"> @media screen and (max-height: 400px) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        .media-menu {
-               padding: 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ .media-menu,
+       .media-frame:not(.hide-menu) .media-menu {
+               top: 44px;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        .media-frame-router {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2552,6 +2553,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+@media only screen and (min-width: 901px) and (max-height: 400px) {
+       .media-menu,
+       .media-frame:not(.hide-menu) .media-menu {
+               top: 0;
+               padding-top: 44px;
+       }
+}
+
</ins><span class="cx" style="display: block; padding: 0 10px"> @media only screen and (max-width: 480px) {
</span><span class="cx" style="display: block; padding: 0 10px">        .media-modal-close {
</span><span class="cx" style="display: block; padding: 0 10px">                top: -5px;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2578,6 +2587,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        .media-frame-router,
</span><span class="cx" style="display: block; padding: 0 10px">        .media-frame:not(.hide-menu) .media-menu {
</span><span class="cx" style="display: block; padding: 0 10px">                top: 40px;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                padding-top: 0;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        .media-frame-content {
</span></span></pre></div>
<a id="trunksrcwpincludesmediatemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/media-template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/media-template.php  2019-06-27 12:05:44 UTC (rev 45571)
+++ trunk/src/wp-includes/media-template.php    2019-06-27 12:32:28 UTC (rev 45572)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -177,8 +177,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        <?php // Template for the media frame: used both in the media grid and in the media modal. ?>
</span><span class="cx" style="display: block; padding: 0 10px">        <script type="text/html" id="tmpl-media-frame">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                <div class="media-frame-title" id="media-frame-title"></div>
</ins><span class="cx" style="display: block; padding: 0 10px">                 <div class="media-frame-menu"></div>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                <div class="media-frame-title"></div>
</del><span class="cx" style="display: block; padding: 0 10px">                 <div class="media-frame-router"></div>
</span><span class="cx" style="display: block; padding: 0 10px">                <div class="media-frame-content"></div>
</span><span class="cx" style="display: block; padding: 0 10px">                <div class="media-frame-toolbar"></div>
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -187,11 +187,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        <?php // Template for the media modal. ?>
</span><span class="cx" style="display: block; padding: 0 10px">        <script type="text/html" id="tmpl-media-modal">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                <div tabindex="0" class="<?php echo $class; ?>">
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         <div tabindex="0" class="<?php echo $class; ?>" role="dialog" aria-modal="true" aria-labelledby="media-frame-title">
</ins><span class="cx" style="display: block; padding: 0 10px">                         <# if ( data.hasCloseButton ) { #>
</span><span class="cx" style="display: block; padding: 0 10px">                                <button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text"><?php _e( 'Close dialog' ); ?></span></span></button>
</span><span class="cx" style="display: block; padding: 0 10px">                        <# } #>
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        <div class="media-modal-content"></div>
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 <div class="media-modal-content" role="document"></div>
</ins><span class="cx" style="display: block; padding: 0 10px">                 </div>
</span><span class="cx" style="display: block; padding: 0 10px">                <div class="media-modal-backdrop"></div>
</span><span class="cx" style="display: block; padding: 0 10px">        </script>
</span></span></pre>
</div>
</div>

</body>
</html>