Accessibility: Media: Use the ARIA tabs pattern for the media modal menus.

The ARIA tabs pattern improves interaction for keyboard and assistive technologies users.
It gives the menu items proper roles, and `aria-selected` allows users of assistive technologies to know which tab is currently selected.

Props audrasjb, afercia, joedolson, karmatosed, melchoyce.
See #47149.

Built from https://develop.svn.wordpress.org/trunk@46363


git-svn-id: http://core.svn.wordpress.org/trunk@46162 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Andrea Fercia 2019-09-30 19:37:58 +00:00
parent e534fdf5aa
commit 63e199abc7
8 changed files with 558 additions and 246 deletions

View File

@ -580,23 +580,29 @@
user-select: none; user-select: none;
} }
.media-menu > a { .media-menu .media-menu-item {
display: block; display: block;
box-sizing: border-box;
width: 100%;
position: relative; position: relative;
padding: 8px 20px; border: 0;
margin: 0; margin: 0;
line-height: 1.28571428; padding: 8px 20px;
font-size: 14px; font-size: 14px;
line-height: 1.28571428;
background: transparent;
color: #0073aa; color: #0073aa;
text-align: right;
text-decoration: none; text-decoration: none;
cursor: pointer;
} }
.media-menu > a:hover { .media-menu .media-menu-item:hover {
color: #0073aa;
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
} }
.media-menu > a:active { .media-menu .media-menu-item:active {
color: #0073aa;
outline: none; outline: none;
} }
@ -606,6 +612,15 @@
font-weight: 600; font-weight: 600;
} }
.media-menu .media-menu-item:focus {
box-shadow:
0 0 0 1px #5b9dd9,
0 0 2px 1px rgba(30, 140, 190, 0.8);
color: #124964;
/* Only visible in Windows High Contrast mode */
outline: 1px solid transparent;
}
.media-menu .separator { .media-menu .separator {
height: 0; height: 0;
margin: 12px 20px; margin: 12px 20px;
@ -621,42 +636,48 @@
padding: 0 6px; padding: 0 6px;
margin: 0; margin: 0;
clear: both; clear: both;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
} }
.media-router a { .media-router .media-menu-item {
transition: none;
}
.media-router > a {
position: relative; position: relative;
float: right; float: right;
padding: 8px 10px 9px; border: 0;
margin: 0; margin: 0;
padding: 8px 10px 9px;
height: 18px; height: 18px;
line-height: 1.28571428; line-height: 1.28571428;
font-size: 14px; font-size: 14px;
text-decoration: none; text-decoration: none;
background: transparent;
cursor: pointer;
transition: none;
} }
.media-router > a:last-child { .media-router .media-menu-item:last-child {
border-left: 0; border-left: 0;
} }
.media-router > a:active { .media-router .media-menu-item:hover,
outline: none; .media-router .media-menu-item:active {
color: #0073aa;
} }
.media-router .active, .media-router .active,
.media-router .active:hover { .media-router .active:hover {
color: #32373c; color: #23282d;
}
.media-router .media-menu-item:focus {
box-shadow:
0 0 0 1px #5b9dd9,
0 0 2px 1px rgba(30, 140, 190, 0.8);
color: #124964;
/* Only visible in Windows High Contrast mode */
outline: 1px solid transparent;
} }
.media-router .active, .media-router .active,
.media-router > a.active:last-child { .media-router .media-menu-item.active:last-child {
margin: -1px -1px 0; margin: -1px -1px 0;
background: #fff; background: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -752,15 +773,6 @@
display: none; display: none;
} }
.media-frame.hide-router .media-frame-title {
border-bottom: 1px solid #ddd;
box-shadow: 0 4px 4px -4px rgba(0, 0, 0, 0.1);
}
.media-frame-title .dashicons {
display: none;
}
.media-frame-title h1 { .media-frame-title h1 {
padding: 0 16px; padding: 0 16px;
font-size: 22px; font-size: 22px;
@ -768,6 +780,10 @@
margin: 0; margin: 0;
} }
.wp-core-ui .button.media-frame-menu-toggle {
display: none;
}
.media-frame-title .suggested-dimensions { .media-frame-title .suggested-dimensions {
font-size: 14px; font-size: 14px;
float: left; float: left;
@ -2251,27 +2267,60 @@
* Responsive layout * Responsive layout
*/ */
@media only screen and (max-width: 900px) { @media only screen and (max-width: 900px) {
.media-modal .media-frame-title {
height: 40px;
}
.media-modal .media-frame-title h1 {
line-height: 2.22222222;
font-size: 18px;
}
.media-modal-close {
width: 42px;
height: 42px;
}
/* Drop-down menu */ /* Drop-down menu */
.media-frame:not(.hide-menu) .media-frame-title, .media-frame .media-frame-title {
position: static;
padding: 0 44px;
text-align: center;
}
.media-frame:not(.hide-menu) .media-frame-router, .media-frame:not(.hide-menu) .media-frame-router,
.media-frame:not(.hide-menu) .media-frame-content, .media-frame:not(.hide-menu) .media-frame-content,
.media-frame:not(.hide-menu) .media-frame-toolbar { .media-frame:not(.hide-menu) .media-frame-toolbar {
right: 0; right: 0;
} }
.media-frame:not(.hide-menu) .media-frame-router {
/* 40 title + (40 - 6) menu toggle button + 6 spacing */
top: 80px;
}
.media-frame:not(.hide-menu) .media-frame-content {
/* 80 + room for the tabs */
top: 114px;
}
.media-frame.hide-router .media-frame-content {
top: 80px;
}
.media-frame:not(.hide-menu) .media-frame-menu { .media-frame:not(.hide-menu) .media-frame-menu {
position: static; position: static;
width: 0; width: 0;
} }
.media-frame:not(.hide-menu) .media-menu { .media-frame:not(.hide-menu) .media-menu {
display: none;
width: auto; width: auto;
max-width: 80%; max-width: 80%;
overflow: auto; overflow: auto;
z-index: 2000; z-index: 2000;
top: 50px; top: 75px;
right: -300px; right: 0;
left: auto; left: auto;
bottom: auto; bottom: auto;
padding: 5px 0; padding: 5px 0;
@ -2279,7 +2328,7 @@
} }
.media-frame:not(.hide-menu) .media-menu.visible { .media-frame:not(.hide-menu) .media-menu.visible {
right: 0; display: block;
} }
.media-frame:not(.hide-menu) .media-menu > a { .media-frame:not(.hide-menu) .media-menu > a {
@ -2287,29 +2336,32 @@
font-size: 16px; font-size: 16px;
} }
.media-frame:not(.hide-menu) .media-menu > a.active {
display: none;
}
.media-frame:not(.hide-menu) .media-menu .separator { .media-frame:not(.hide-menu) .media-menu .separator {
margin: 5px 10px; margin: 5px 10px;
} }
.media-frame:not(.hide-menu) .media-frame-title { .wp-core-ui .media-frame:not(.hide-menu) .button.media-frame-menu-toggle {
right: 0; display: inline-flex;
align-items: center;
vertical-align: top;
min-height: 40px;
margin: -6px 6px 0;
padding: 0 12px 0 2px;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
background: transparent;
} }
.media-frame:not(.hide-menu) .media-frame-title .dashicons { .wp-core-ui .button.media-frame-menu-toggle:hover,
display: inline-block; .wp-core-ui .button.media-frame-menu-toggle:active {
line-height: 2.5; background: transparent;
transform: none;
} }
.media-frame:not(.hide-menu) .media-frame-title h1 { .wp-core-ui .button.media-frame-menu-toggle:focus {
color: #0073aa; /* Only visible in Windows High Contrast mode */
line-height: 3; outline: 1px solid transparent;
font-size: 18px;
float: right;
cursor: pointer;
} }
/* End drop-down menu */ /* End drop-down menu */
@ -2561,31 +2613,6 @@
} }
} }
/* Landscape specific header override */
@media screen and (max-height: 400px) {
.media-menu,
.media-frame:not(.hide-menu) .media-menu {
top: 44px;
}
.media-frame-router {
top: 44px;
}
.media-frame-content {
top: 78px;
}
.attachments-browser .attachments {
top: 40px;
}
/* Prevent unnecessary scrolling on title input */
.embed-link-settings {
overflow: visible;
}
}
@media only screen and (min-width: 901px) and (max-height: 400px) { @media only screen and (min-width: 901px) and (max-height: 400px) {
.media-menu, .media-menu,
.media-frame:not(.hide-menu) .media-menu { .media-frame:not(.hide-menu) .media-menu {
@ -2595,38 +2622,10 @@
} }
@media only screen and (max-width: 480px) { @media only screen and (max-width: 480px) {
.media-modal-close {
top: -5px;
}
.media-modal .media-frame-title {
height: 40px;
}
.wp-core-ui.wp-customizer .media-button { .wp-core-ui.wp-customizer .media-button {
margin-top: 13px; margin-top: 13px;
} }
.media-modal .media-frame-title h1,
.media-frame:not(.hide-menu) .media-frame-title h1 {
font-size: 18px;
line-height: 2.22222222;
}
.media-frame:not(.hide-menu) .media-frame-title .dashicons {
line-height: 2;
}
.media-frame-router,
.media-frame:not(.hide-menu) .media-menu {
top: 40px;
padding-top: 0;
}
.media-frame-content {
top: 74px;
}
.media-frame.hide-router .media-frame-content { .media-frame.hide-router .media-frame-content {
top: 40px; top: 40px;
} }

File diff suppressed because one or more lines are too long

View File

@ -580,23 +580,29 @@
user-select: none; user-select: none;
} }
.media-menu > a { .media-menu .media-menu-item {
display: block; display: block;
box-sizing: border-box;
width: 100%;
position: relative; position: relative;
padding: 8px 20px; border: 0;
margin: 0; margin: 0;
line-height: 1.28571428; padding: 8px 20px;
font-size: 14px; font-size: 14px;
line-height: 1.28571428;
background: transparent;
color: #0073aa; color: #0073aa;
text-align: left;
text-decoration: none; text-decoration: none;
cursor: pointer;
} }
.media-menu > a:hover { .media-menu .media-menu-item:hover {
color: #0073aa;
background: rgba(0, 0, 0, 0.04); background: rgba(0, 0, 0, 0.04);
} }
.media-menu > a:active { .media-menu .media-menu-item:active {
color: #0073aa;
outline: none; outline: none;
} }
@ -606,6 +612,15 @@
font-weight: 600; font-weight: 600;
} }
.media-menu .media-menu-item:focus {
box-shadow:
0 0 0 1px #5b9dd9,
0 0 2px 1px rgba(30, 140, 190, 0.8);
color: #124964;
/* Only visible in Windows High Contrast mode */
outline: 1px solid transparent;
}
.media-menu .separator { .media-menu .separator {
height: 0; height: 0;
margin: 12px 20px; margin: 12px 20px;
@ -621,42 +636,48 @@
padding: 0 6px; padding: 0 6px;
margin: 0; margin: 0;
clear: both; clear: both;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
} }
.media-router a { .media-router .media-menu-item {
transition: none;
}
.media-router > a {
position: relative; position: relative;
float: left; float: left;
padding: 8px 10px 9px; border: 0;
margin: 0; margin: 0;
padding: 8px 10px 9px;
height: 18px; height: 18px;
line-height: 1.28571428; line-height: 1.28571428;
font-size: 14px; font-size: 14px;
text-decoration: none; text-decoration: none;
background: transparent;
cursor: pointer;
transition: none;
} }
.media-router > a:last-child { .media-router .media-menu-item:last-child {
border-right: 0; border-right: 0;
} }
.media-router > a:active { .media-router .media-menu-item:hover,
outline: none; .media-router .media-menu-item:active {
color: #0073aa;
} }
.media-router .active, .media-router .active,
.media-router .active:hover { .media-router .active:hover {
color: #32373c; color: #23282d;
}
.media-router .media-menu-item:focus {
box-shadow:
0 0 0 1px #5b9dd9,
0 0 2px 1px rgba(30, 140, 190, 0.8);
color: #124964;
/* Only visible in Windows High Contrast mode */
outline: 1px solid transparent;
} }
.media-router .active, .media-router .active,
.media-router > a.active:last-child { .media-router .media-menu-item.active:last-child {
margin: -1px -1px 0; margin: -1px -1px 0;
background: #fff; background: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
@ -752,15 +773,6 @@
display: none; display: none;
} }
.media-frame.hide-router .media-frame-title {
border-bottom: 1px solid #ddd;
box-shadow: 0 4px 4px -4px rgba(0, 0, 0, 0.1);
}
.media-frame-title .dashicons {
display: none;
}
.media-frame-title h1 { .media-frame-title h1 {
padding: 0 16px; padding: 0 16px;
font-size: 22px; font-size: 22px;
@ -768,6 +780,10 @@
margin: 0; margin: 0;
} }
.wp-core-ui .button.media-frame-menu-toggle {
display: none;
}
.media-frame-title .suggested-dimensions { .media-frame-title .suggested-dimensions {
font-size: 14px; font-size: 14px;
float: right; float: right;
@ -2251,27 +2267,60 @@
* Responsive layout * Responsive layout
*/ */
@media only screen and (max-width: 900px) { @media only screen and (max-width: 900px) {
.media-modal .media-frame-title {
height: 40px;
}
.media-modal .media-frame-title h1 {
line-height: 2.22222222;
font-size: 18px;
}
.media-modal-close {
width: 42px;
height: 42px;
}
/* Drop-down menu */ /* Drop-down menu */
.media-frame:not(.hide-menu) .media-frame-title, .media-frame .media-frame-title {
position: static;
padding: 0 44px;
text-align: center;
}
.media-frame:not(.hide-menu) .media-frame-router, .media-frame:not(.hide-menu) .media-frame-router,
.media-frame:not(.hide-menu) .media-frame-content, .media-frame:not(.hide-menu) .media-frame-content,
.media-frame:not(.hide-menu) .media-frame-toolbar { .media-frame:not(.hide-menu) .media-frame-toolbar {
left: 0; left: 0;
} }
.media-frame:not(.hide-menu) .media-frame-router {
/* 40 title + (40 - 6) menu toggle button + 6 spacing */
top: 80px;
}
.media-frame:not(.hide-menu) .media-frame-content {
/* 80 + room for the tabs */
top: 114px;
}
.media-frame.hide-router .media-frame-content {
top: 80px;
}
.media-frame:not(.hide-menu) .media-frame-menu { .media-frame:not(.hide-menu) .media-frame-menu {
position: static; position: static;
width: 0; width: 0;
} }
.media-frame:not(.hide-menu) .media-menu { .media-frame:not(.hide-menu) .media-menu {
display: none;
width: auto; width: auto;
max-width: 80%; max-width: 80%;
overflow: auto; overflow: auto;
z-index: 2000; z-index: 2000;
top: 50px; top: 75px;
left: -300px; left: 0;
right: auto; right: auto;
bottom: auto; bottom: auto;
padding: 5px 0; padding: 5px 0;
@ -2279,7 +2328,7 @@
} }
.media-frame:not(.hide-menu) .media-menu.visible { .media-frame:not(.hide-menu) .media-menu.visible {
left: 0; display: block;
} }
.media-frame:not(.hide-menu) .media-menu > a { .media-frame:not(.hide-menu) .media-menu > a {
@ -2287,29 +2336,32 @@
font-size: 16px; font-size: 16px;
} }
.media-frame:not(.hide-menu) .media-menu > a.active {
display: none;
}
.media-frame:not(.hide-menu) .media-menu .separator { .media-frame:not(.hide-menu) .media-menu .separator {
margin: 5px 10px; margin: 5px 10px;
} }
.media-frame:not(.hide-menu) .media-frame-title { .wp-core-ui .media-frame:not(.hide-menu) .button.media-frame-menu-toggle {
left: 0; display: inline-flex;
align-items: center;
vertical-align: top;
min-height: 40px;
margin: -6px 6px 0;
padding: 0 2px 0 12px;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
background: transparent;
} }
.media-frame:not(.hide-menu) .media-frame-title .dashicons { .wp-core-ui .button.media-frame-menu-toggle:hover,
display: inline-block; .wp-core-ui .button.media-frame-menu-toggle:active {
line-height: 2.5; background: transparent;
transform: none;
} }
.media-frame:not(.hide-menu) .media-frame-title h1 { .wp-core-ui .button.media-frame-menu-toggle:focus {
color: #0073aa; /* Only visible in Windows High Contrast mode */
line-height: 3; outline: 1px solid transparent;
font-size: 18px;
float: left;
cursor: pointer;
} }
/* End drop-down menu */ /* End drop-down menu */
@ -2561,31 +2613,6 @@
} }
} }
/* Landscape specific header override */
@media screen and (max-height: 400px) {
.media-menu,
.media-frame:not(.hide-menu) .media-menu {
top: 44px;
}
.media-frame-router {
top: 44px;
}
.media-frame-content {
top: 78px;
}
.attachments-browser .attachments {
top: 40px;
}
/* Prevent unnecessary scrolling on title input */
.embed-link-settings {
overflow: visible;
}
}
@media only screen and (min-width: 901px) and (max-height: 400px) { @media only screen and (min-width: 901px) and (max-height: 400px) {
.media-menu, .media-menu,
.media-frame:not(.hide-menu) .media-menu { .media-frame:not(.hide-menu) .media-menu {
@ -2595,38 +2622,10 @@
} }
@media only screen and (max-width: 480px) { @media only screen and (max-width: 480px) {
.media-modal-close {
top: -5px;
}
.media-modal .media-frame-title {
height: 40px;
}
.wp-core-ui.wp-customizer .media-button { .wp-core-ui.wp-customizer .media-button {
margin-top: 13px; margin-top: 13px;
} }
.media-modal .media-frame-title h1,
.media-frame:not(.hide-menu) .media-frame-title h1 {
font-size: 18px;
line-height: 2.22222222;
}
.media-frame:not(.hide-menu) .media-frame-title .dashicons {
line-height: 2;
}
.media-frame-router,
.media-frame:not(.hide-menu) .media-menu {
top: 40px;
padding-top: 0;
}
.media-frame-content {
top: 74px;
}
.media-frame.hide-router .media-frame-content { .media-frame.hide-router .media-frame-content {
top: 40px; top: 40px;
} }

File diff suppressed because one or more lines are too long

View File

@ -2924,7 +2924,7 @@ MediaFrame = Frame.extend(/** @lends wp.media.view.MediaFrame.prototype */{
regions: ['menu','title','content','toolbar','router'], regions: ['menu','title','content','toolbar','router'],
events: { events: {
'click div.media-frame-title h1': 'toggleMenu' 'click .media-frame-menu-toggle': 'toggleMenu'
}, },
/** /**
@ -2976,13 +2976,78 @@ MediaFrame = Frame.extend(/** @lends wp.media.view.MediaFrame.prototype */{
this.on( 'title:create:default', this.createTitle, this ); this.on( 'title:create:default', this.createTitle, this );
this.title.mode('default'); this.title.mode('default');
this.on( 'title:render', function( view ) {
view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );
});
// Bind default menu. // Bind default menu.
this.on( 'menu:create:default', this.createMenu, this ); this.on( 'menu:create:default', this.createMenu, this );
// Set the menu ARIA tab panel attributes when the modal opens.
this.on( 'open', this.setMenuTabPanelAriaAttributes, this );
// Set the router ARIA tab panel attributes when the modal opens.
this.on( 'open', this.setRouterTabPanelAriaAttributes, this );
// Update the menu ARIA tab panel attributes when the content updates.
this.on( 'content:render', this.setMenuTabPanelAriaAttributes, this );
// Update the router ARIA tab panel attributes when the content updates.
this.on( 'content:render', this.setRouterTabPanelAriaAttributes, this );
}, },
/**
* Sets the attributes to be used on the menu ARIA tab panel.
*
* @since 5.3.0
*
* @returns {void}
*/
setMenuTabPanelAriaAttributes: function() {
var stateId = this.state().get( 'id' ),
tabPanelEl = this.$el.find( '.media-frame-tab-panel' ),
ariaLabelledby;
tabPanelEl.removeAttr( 'role aria-labelledby tabindex' );
if ( this.menuView && this.menuView.isVisible ) {
ariaLabelledby = 'menu-item-' + stateId;
// Set the tab panel attributes only if the tabs are visible.
tabPanelEl
.attr( {
role: 'tabpanel',
'aria-labelledby': ariaLabelledby,
tabIndex: '0'
} );
}
},
/**
* Sets the attributes to be used on the router ARIA tab panel.
*
* @since 5.3.0
*
* @returns {void}
*/
setRouterTabPanelAriaAttributes: function() {
var tabPanelEl = this.$el.find( '.media-frame-content' ),
ariaLabelledby;
tabPanelEl.removeAttr( 'role aria-labelledby tabindex' );
// On the Embed view the router menu is hidden.
if ( 'embed' === this.content._mode ) {
return;
}
// Set the tab panel attributes only if the tabs are visible.
if ( this.routerView && this.routerView.isVisible && this.content._mode ) {
ariaLabelledby = 'menu-item-' + this.content._mode;
tabPanelEl
.attr( {
role: 'tabpanel',
'aria-labelledby': ariaLabelledby,
tabIndex: '0'
} );
}
},
/** /**
* @returns {wp.media.view.MediaFrame} Returns itself to allow chaining * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
*/ */
@ -3012,12 +3077,22 @@ MediaFrame = Frame.extend(/** @lends wp.media.view.MediaFrame.prototype */{
*/ */
createMenu: function( menu ) { createMenu: function( menu ) {
menu.view = new wp.media.view.Menu({ menu.view = new wp.media.view.Menu({
controller: this controller: this,
attributes: {
role: 'tablist',
'aria-orientation': 'vertical'
}
}); });
this.menuView = menu.view;
}, },
toggleMenu: function() { toggleMenu: function( event ) {
this.$el.find( '.media-menu' ).toggleClass( 'visible' ); var menu = this.$el.find( '.media-menu' );
menu.toggleClass( 'visible' );
$( event.target ).attr( 'aria-expanded', menu.hasClass( 'visible' ) );
}, },
/** /**
@ -3035,8 +3110,15 @@ MediaFrame = Frame.extend(/** @lends wp.media.view.MediaFrame.prototype */{
*/ */
createRouter: function( router ) { createRouter: function( router ) {
router.view = new wp.media.view.Router({ router.view = new wp.media.view.Router({
controller: this controller: this,
attributes: {
role: 'tablist',
'aria-orientation': 'horizontal'
}
}); });
this.routerView = router.view;
}, },
/** /**
* @param {Object} options * @param {Object} options
@ -3620,8 +3702,11 @@ Post = Select.extend(/** @lends wp.media.view.MediaFrame.Post.prototype */{
mainMenu: function( view ) { mainMenu: function( view ) {
view.set({ view.set({
'library-separator': new wp.media.View({ 'library-separator': new wp.media.View({
className: 'separator', className: 'separator',
priority: 100 priority: 100,
attributes: {
role: 'presentation'
}
}) })
}); });
}, },
@ -4520,6 +4605,8 @@ module.exports = Modal;
/* 56 */ /* 56 */
/***/ (function(module, exports) { /***/ (function(module, exports) {
var $ = jQuery;
/** /**
* wp.media.view.FocusManager * wp.media.view.FocusManager
* *
@ -4533,7 +4620,40 @@ module.exports = Modal;
var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.prototype */{ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.prototype */{
events: { events: {
'keydown': 'constrainTabbing' 'keydown': 'focusManagementMode'
},
/**
* Initializes the Focus Manager.
*
* @param {object} options The Focus Manager options.
*
* @since 5.3.0
*
* @return {void}
*/
initialize: function( options ) {
this.mode = options.mode || 'constrainTabbing';
this.tabsAutomaticActivation = options.tabsAutomaticActivation || false;
},
/**
* Determines which focus management mode to use.
*
* @since 5.3.0
*
* @param {object} event jQuery event object.
*
* @returns {void}
*/
focusManagementMode: function( event ) {
if ( this.mode === 'constrainTabbing' ) {
this.constrainTabbing( event );
}
if ( this.mode === 'tabsNavigation' ) {
this.tabsNavigation( event );
}
}, },
/** /**
@ -4589,8 +4709,10 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
}, },
/** /**
* Hides from assistive technologies all the body children except the * Hides from assistive technologies all the body children.
* provided element and other elements that should not be hidden. *
* Sets an `aria-hidden="true"` attribute on 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 * 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. Also, `aria-modal="true"` * in Safari 11.1 and support is spotty in other browsers. Also, `aria-modal="true"`
@ -4633,7 +4755,9 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
}, },
/** /**
* Makes visible again to assistive technologies all body children * Unhides from assistive technologies all the body children.
*
* Makes visible again to assistive technologies all the body children
* previously hidden and stored in this.ariaHiddenElements. * previously hidden and stored in this.ariaHiddenElements.
* *
* @since 5.2.3 * @since 5.2.3
@ -4687,7 +4811,158 @@ var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.pr
* *
* @since 5.2.3 * @since 5.2.3
*/ */
ariaHiddenElements: [] ariaHiddenElements: [],
/**
* Holds the jQuery collection of ARIA tabs.
*
* @since 5.3.0
*/
tabs: $(),
/**
* Sets up tabs in an ARIA tabbed interface.
*
* @since 5.3.0
*
* @param {object} event jQuery event object.
*
* @returns {void}
*/
setupAriaTabs: function() {
this.tabs = this.$( '[role="tab"]' );
// Set up initial attributes.
this.tabs.attr( {
'aria-selected': 'false',
tabIndex: '-1'
} );
// Set up attributes on the initially active tab.
this.tabs.filter( '.active' )
.removeAttr( 'tabindex' )
.attr( 'aria-selected', 'true' );
},
/**
* Enables arrows navigation within the ARIA tabbed interface.
*
* @since 5.3.0
*
* @param {object} event jQuery event object.
*
* @returns {void}
*/
tabsNavigation: function( event ) {
var orientation = 'horizontal',
keys = [ 32, 35, 36, 37, 38, 39, 40 ];
// Return if not Spacebar, End, Home, or Arrow keys.
if ( keys.indexOf( event.which ) === -1 ) {
return;
}
// Determine navigation direction.
if ( this.$el.attr( 'aria-orientation' ) === 'vertical' ) {
orientation = 'vertical';
}
// Make Up and Down arrow keys do nothing with horizontal tabs.
if ( orientation === 'horizontal' && [ 38, 40 ].indexOf( event.which ) !== -1 ) {
return;
}
// Make Left and Right arrow keys do nothing with vertical tabs.
if ( orientation === 'vertical' && [ 37, 39 ].indexOf( event.which ) !== -1 ) {
return;
}
this.switchTabs( event, this.tabs );
},
/**
* Switches tabs in the ARIA tabbed interface.
*
* @since 5.3.0
*
* @param {object} event jQuery event object.
*
* @returns {void}
*/
switchTabs: function( event ) {
var key = event.which,
index = this.tabs.index( $( event.target ) ),
newIndex;
switch ( key ) {
// Space bar: Activate current targeted tab.
case 32: {
this.activateTab( this.tabs[ index ] );
break;
}
// End key: Activate last tab.
case 35: {
event.preventDefault();
this.activateTab( this.tabs[ this.tabs.length - 1 ] );
break;
}
// Home key: Activate first tab.
case 36: {
event.preventDefault();
this.activateTab( this.tabs[ 0 ] );
break;
}
// Left and up keys: Activate previous tab.
case 37:
case 38: {
event.preventDefault();
newIndex = ( index - 1 ) < 0 ? this.tabs.length - 1 : index - 1;
this.activateTab( this.tabs[ newIndex ] );
break;
}
// Right and down keys: Activate next tab.
case 39:
case 40: {
event.preventDefault();
newIndex = ( index + 1 ) === this.tabs.length ? 0 : index + 1;
this.activateTab( this.tabs[ newIndex ] );
break;
}
}
},
/**
* Sets a single tab to be focusable and semantically selected.
*
* @since 5.3.0
*
* @param {object} tab The tab DOM element.
*
* @returns {void}
*/
activateTab: function( tab ) {
if ( ! tab ) {
return;
}
// The tab is a DOM element: no need for jQuery methods.
tab.focus();
// Handle automatic activation.
if ( this.tabsAutomaticActivation ) {
tab.removeAttribute( 'tabindex' );
tab.setAttribute( 'aria-selected', 'true' );
tab.click();
return;
}
// Handle manual activation.
$( tab ).on( 'click', function() {
tab.removeAttribute( 'tabindex' );
tab.setAttribute( 'aria-selected', 'true' );
} );
}
}); });
module.exports = FocusManager; module.exports = FocusManager;
@ -5903,25 +6178,23 @@ var MenuItem;
* @augments Backbone.View * @augments Backbone.View
*/ */
MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{ MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{
tagName: 'a', tagName: 'button',
className: 'media-menu-item', className: 'media-menu-item',
attributes: { attributes: {
href: '#' type: 'button',
role: 'tab'
}, },
events: { events: {
'click': '_click' 'click': '_click'
}, },
/**
* @param {Object} event
*/
_click: function( event ) {
var clickOverride = this.options.click;
if ( event ) { /**
event.preventDefault(); * Allows to override the click event.
} */
_click: function() {
var clickOverride = this.options.click;
if ( clickOverride ) { if ( clickOverride ) {
clickOverride.call( this ); clickOverride.call( this );
@ -5935,14 +6208,17 @@ MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{
if ( state ) { if ( state ) {
this.controller.setState( state ); this.controller.setState( state );
// Toggle the menu visibility in the responsive view.
this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
} }
}, },
/** /**
* @returns {wp.media.view.MenuItem} returns itself to allow chaining * @returns {wp.media.view.MenuItem} returns itself to allow chaining
*/ */
render: function() { render: function() {
var options = this.options; var options = this.options,
menuProperty = options.state || options.contentMode;
if ( options.text ) { if ( options.text ) {
this.$el.text( options.text ); this.$el.text( options.text );
@ -5950,6 +6226,9 @@ MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{
this.$el.html( options.html ); this.$el.html( options.html );
} }
// Set the menu item ID based on the frame state associated to the menu item.
this.$el.attr( 'id', 'menu-item-' + menuProperty );
return this; return this;
} }
}); });
@ -5983,15 +6262,30 @@ Menu = PriorityList.extend(/** @lends wp.media.view.Menu.prototype */{
ItemView: MenuItem, ItemView: MenuItem,
region: 'menu', region: 'menu',
/* TODO: alternatively hide on any click anywhere attributes: {
events: { role: 'tablist',
'click': 'click' 'aria-orientation': 'horizontal'
}, },
click: function() { initialize: function() {
this.$el.removeClass( 'visible' ); this._views = {};
this.set( _.extend( {}, this._views, this.options.views ), { silent: true });
delete this.options.views;
if ( ! this.options.silent ) {
this.render();
}
// Initialize the Focus Manager.
this.focusManager = new wp.media.view.FocusManager( {
el: this.el,
mode: 'tabsNavigation'
} );
// The menu is always rendered and can be visible or hidden on some frames.
this.isVisible = true;
}, },
*/
/** /**
* @param {Object} options * @param {Object} options
@ -6010,6 +6304,9 @@ Menu = PriorityList.extend(/** @lends wp.media.view.Menu.prototype */{
*/ */
PriorityList.prototype.ready.apply( this, arguments ); PriorityList.prototype.ready.apply( this, arguments );
this.visibility(); this.visibility();
// Set up aria tabs initial attributes.
this.focusManager.setupAriaTabs();
}, },
set: function() { set: function() {
@ -6035,6 +6332,9 @@ Menu = PriorityList.extend(/** @lends wp.media.view.Menu.prototype */{
hide = ! views || views.length < 2; hide = ! views || views.length < 2;
if ( this === view ) { if ( this === view ) {
// Flag this menu as hidden or visible.
this.isVisible = ! hide;
// Set or remove a CSS class to hide the menu.
this.controller.$el.toggleClass( 'hide-' + region, hide ); this.controller.$el.toggleClass( 'hide-' + region, hide );
} }
}, },
@ -6050,6 +6350,9 @@ Menu = PriorityList.extend(/** @lends wp.media.view.Menu.prototype */{
this.deselect(); this.deselect();
view.$el.addClass('active'); view.$el.addClass('active');
// Set up again the aria tabs initial attributes after the menu updates.
this.focusManager.setupAriaTabs();
}, },
deselect: function() { deselect: function() {
@ -6136,6 +6439,11 @@ Router = Menu.extend(/** @lends wp.media.view.Router.prototype */{
ItemView: wp.media.view.RouterItem, ItemView: wp.media.view.RouterItem,
region: 'router', region: 'router',
attributes: {
role: 'tablist',
'aria-orientation': 'horizontal'
},
initialize: function() { initialize: function() {
this.controller.on( 'content:render', this.update, this ); this.controller.on( 'content:render', this.update, this );
// Call 'initialize' directly on the parent class. // Call 'initialize' directly on the parent class.

File diff suppressed because one or more lines are too long

View File

@ -178,9 +178,15 @@ function wp_print_media_templates() {
<?php // Template for the media frame: used both in the media grid and in the media modal. ?> <?php // Template for the media frame: used both in the media grid and in the media modal. ?>
<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-title" id="media-frame-title"></div>
<button type="button" class="button button-link media-frame-menu-toggle" aria-expanded="false">
<?php _e( 'Media Types' ); ?>
<span class="dashicons dashicons-arrow-down" aria-hidden="true"></span>
</button>
<div class="media-frame-menu"></div> <div class="media-frame-menu"></div>
<div class="media-frame-router"></div> <div class="media-frame-tab-panel">
<div class="media-frame-content"></div> <div class="media-frame-router"></div>
<div class="media-frame-content"></div>
</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>

View File

@ -13,7 +13,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '5.3-beta1-46362'; $wp_version = '5.3-beta1-46363';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.