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() {