diff --git a/src/js/media/views/focus-manager.js b/src/js/media/views/focus-manager.js
index 2a7aef5bb741..257118417e68 100644
--- a/src/js/media/views/focus-manager.js
+++ b/src/js/media/views/focus-manager.js
@@ -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.
+ *
+ * @since 3.5.0
+ *
+ * @returns {void}
*/
focus: function() {
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 ) {
var tabbables;
@@ -42,8 +52,107 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
tabbables.last().focus();
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;
diff --git a/src/js/media/views/modal.js b/src/js/media/views/modal.js
index aabf2b48d4be..a17b7f063aa1 100644
--- a/src/js/media/views/modal.js
+++ b/src/js/media/views/modal.js
@@ -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.
this.$( '.media-modal' ).focus();
+ // Hide the page content from assistive technologies.
+ this.focusManager.setAriaHiddenOnBodyChildren( $el );
+
return this.propagate('open');
},
@@ -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
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.
if ( null !== this.clickedOpenerEl ) {
this.clickedOpenerEl.focus();
diff --git a/src/wp-includes/css/media-views.css b/src/wp-includes/css/media-views.css
index ab447702d478..1890b59401a5 100644
--- a/src/wp-includes/css/media-views.css
+++ b/src/wp-includes/css/media-views.css
@@ -539,7 +539,7 @@
right: 0;
bottom: 0;
margin: 0;
- padding: 10px 0;
+ padding: 50px 0 10px;
background: #f3f3f3;
border-right-width: 1px;
border-right-style: solid;
@@ -2561,8 +2561,9 @@
/* Landscape specific header override */
@media screen and (max-height: 400px) {
- .media-menu {
- padding: 0;
+ .media-menu,
+ .media-frame:not(.hide-menu) .media-menu {
+ top: 44px;
}
.media-frame-router {
@@ -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-modal-close {
top: -5px;
@@ -2609,6 +2618,7 @@
.media-frame-router,
.media-frame:not(.hide-menu) .media-menu {
top: 40px;
+ padding-top: 0;
}
.media-frame-content {
diff --git a/src/wp-includes/media-template.php b/src/wp-includes/media-template.php
index d35d6fcd9fd3..d67ef153bed9 100644
--- a/src/wp-includes/media-template.php
+++ b/src/wp-includes/media-template.php
@@ -185,8 +185,8 @@ function wp_print_media_templates() {