Skip to content

Commit

Permalink
Accessibility: Make the Media modal an ARIA modal dialog.
Browse files Browse the repository at this point in the history
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

Props afercia.
Merges [45572] to the 5.2 branch.
Fixes #47145.

git-svn-id: https://develop.svn.wordpress.org/branches/5.2@45866 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
SergeyBiryukov committed Aug 20, 2019
1 parent e337c89 commit 74a4b51
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 8 deletions.
113 changes: 111 additions & 2 deletions src/js/media/views/focus-manager.js
Expand Up @@ -16,12 +16,22 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr


/** /**
* Moves focus to the first visible menu item in the modal. * Moves focus to the first visible menu item in the modal.
*
* @since 3.5.0
*
* @returns {void}
*/ */
focus: function() { focus: function() {
this.$( '.media-menu-item' ).filter( ':visible' ).first().focus(); this.$( '.media-menu-item' ).filter( ':visible' ).first().focus();
}, },
/** /**
* @param {Object} event * Constrains navigation with the Tab key within the media view element.
*
* @since 4.0.0
*
* @param {Object} event A keydown jQuery event.
*
* @returns {void}
*/ */
constrainTabbing: function( event ) { constrainTabbing: function( event ) {
var tabbables; var tabbables;
Expand All @@ -42,8 +52,107 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
tabbables.last().focus(); tabbables.last().focus();
return false; return false;
} }
} },

/**
* 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: []
}); });


module.exports = FocusManager; module.exports = FocusManager;
9 changes: 9 additions & 0 deletions src/js/media/views/modal.js
Expand Up @@ -117,6 +117,9 @@ Modal = wp.media.View.extend(/** @lends wp.media.view.Modal.prototype */{
// Set initial focus on the content instead of this view element, to avoid page scrolling. // Set initial focus on the content instead of this view element, to avoid page scrolling.
this.$( '.media-modal' ).focus(); this.$( '.media-modal' ).focus();


// Hide the page content from assistive technologies.
this.focusManager.setAriaHiddenOnBodyChildren( $el );

return this.propagate('open'); return this.propagate('open');
}, },


Expand All @@ -135,6 +138,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 // Hide modal and remove restricted media modal tab focus once it's closed
this.$el.hide().undelegate( 'keydown' ); this.$el.hide().undelegate( 'keydown' );


/*
* Make visible again to assistive technologies all body children that
* have been made hidden when the modal opened.
*/
this.focusManager.removeAriaHiddenFromBodyChildren();

// Put focus back in useful location once modal is closed. // Put focus back in useful location once modal is closed.
if ( null !== this.clickedOpenerEl ) { if ( null !== this.clickedOpenerEl ) {
this.clickedOpenerEl.focus(); this.clickedOpenerEl.focus();
Expand Down
16 changes: 13 additions & 3 deletions src/wp-includes/css/media-views.css
Expand Up @@ -539,7 +539,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
margin: 0; margin: 0;
padding: 10px 0; padding: 50px 0 10px;
background: #f3f3f3; background: #f3f3f3;
border-right-width: 1px; border-right-width: 1px;
border-right-style: solid; border-right-style: solid;
Expand Down Expand Up @@ -2561,8 +2561,9 @@


/* Landscape specific header override */ /* Landscape specific header override */
@media screen and (max-height: 400px) { @media screen and (max-height: 400px) {
.media-menu { .media-menu,
padding: 0; .media-frame:not(.hide-menu) .media-menu {
top: 44px;
} }


.media-frame-router { .media-frame-router {
Expand All @@ -2583,6 +2584,14 @@
} }
} }


@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;
}
}

@media only screen and (max-width: 480px) { @media only screen and (max-width: 480px) {
.media-modal-close { .media-modal-close {
top: -5px; top: -5px;
Expand All @@ -2609,6 +2618,7 @@
.media-frame-router, .media-frame-router,
.media-frame:not(.hide-menu) .media-menu { .media-frame:not(.hide-menu) .media-menu {
top: 40px; top: 40px;
padding-top: 0;
} }


.media-frame-content { .media-frame-content {
Expand Down
6 changes: 3 additions & 3 deletions src/wp-includes/media-template.php
Expand Up @@ -185,20 +185,20 @@ function wp_print_media_templates() {
</style> </style>
<![endif]--> <![endif]-->
<script type="text/html" id="tmpl-media-frame"> <script type="text/html" id="tmpl-media-frame">
<div class="media-frame-title" id="media-frame-title"></div>
<div class="media-frame-menu"></div> <div class="media-frame-menu"></div>
<div class="media-frame-title"></div>
<div class="media-frame-router"></div> <div class="media-frame-router"></div>
<div class="media-frame-content"></div> <div class="media-frame-content"></div>
<div class="media-frame-toolbar"></div> <div class="media-frame-toolbar"></div>
<div class="media-frame-uploader"></div> <div class="media-frame-uploader"></div>
</script> </script>


<script type="text/html" id="tmpl-media-modal"> <script type="text/html" id="tmpl-media-modal">
<div tabindex="0" class="<?php echo $class; ?>"> <div tabindex="0" class="<?php echo $class; ?>" role="dialog" aria-modal="true" aria-labelledby="media-frame-title">
<# if ( data.hasCloseButton ) { #> <# if ( data.hasCloseButton ) { #>
<button type="button" class="media-modal-close"><span class="media-modal-icon"><span class="screen-reader-text"><?php _e( 'Close dialog' ); ?></span></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 class="media-modal-content"></div> <div class="media-modal-content" role="document"></div>
</div> </div>
<div class="media-modal-backdrop"></div> <div class="media-modal-backdrop"></div>
</script> </script>
Expand Down

0 comments on commit 74a4b51

Please sign in to comment.