Ticket #47149: 47149.5.diff
File 47149.5.diff, 19.8 KB (added by , 6 years ago) |
---|
-
src/js/media/views/focus-manager.js
1 var $ = jQuery; 2 1 3 /** 2 4 * wp.media.view.FocusManager 3 5 * … … 11 13 var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.prototype */{ 12 14 13 15 events: { 14 'keydown': ' constrainTabbing'16 'keydown': 'focusManagementMode' 15 17 }, 16 18 17 19 /** 20 * Initializes the Focus Manager. 21 * 22 * @param {object} options The Focus Manager options. 23 * 24 * @since 5.3.0 25 * 26 * @return {void} 27 */ 28 initialize: function( options ) { 29 this.mode = options.mode || 'constrainTabbing'; 30 this.tabsAutomaticActivation = options.tabsAutomaticActivation || false; 31 }, 32 33 /** 34 * Determines which focus management mode to use. 35 * 36 * @since 5.3.0 37 * 38 * @param {object} event jQuery event object. 39 * 40 * @returns {void} 41 */ 42 focusManagementMode: function( event ) { 43 if ( this.mode === 'constrainTabbing' ) { 44 this.constrainTabbing( event ); 45 } 46 47 if ( this.mode === 'tabsNavigation' ) { 48 this.tabsNavigation( event ); 49 } 50 }, 51 52 /** 18 53 * Gets all the tabbable elements. 19 54 * 20 55 * @since 5.3.0 … … 67 102 }, 68 103 69 104 /** 70 * Hides from assistive technologies all the body children except the 71 * provided element and other elements that should not be hidden. 105 * Hides from assistive technologies all the body children. 72 106 * 107 * Sets an `aria-hidden="true"` attribute on all the body children except 108 * the provided element and other elements that should not be hidden. 109 * 73 110 * The reason why we use `aria-hidden` is that `aria-modal="true"` is buggy 74 111 * in Safari 11.1 and support is spotty in other browsers. In the future we 75 112 * should consider to remove this helper function and only use `aria-modal="true"`. … … 110 147 }, 111 148 112 149 /** 113 * Makes visible again to assistive technologies all body children 150 * Unhides from assistive technologies all the body children. 151 * 152 * Makes visible again to assistive technologies all the body children 114 153 * previously hidden and stored in this.ariaHiddenElements. 115 154 * 116 155 * @since 5.2.3 … … 164 203 * 165 204 * @since 5.2.3 166 205 */ 167 ariaHiddenElements: [] 206 ariaHiddenElements: [], 207 208 /** 209 * Holds the jQuery collection of ARIA tabs. 210 * 211 * @since 5.3.0 212 */ 213 tabs: $(), 214 215 /** 216 * Sets up tabs in an ARIA tabbed interface. 217 * 218 * @since 5.3.0 219 * 220 * @param {object} event jQuery event object. 221 * 222 * @returns {void} 223 */ 224 setupAriaTabs: function() { 225 this.tabs = this.$( '[role="tab"]' ); 226 227 // Set up initial attributes. 228 this.tabs.attr( { 229 'aria-selected': 'false', 230 tabIndex: '-1' 231 } ); 232 233 // Set up attributes on the initially active tab. 234 this.tabs.filter( '.active' ) 235 .removeAttr( 'tabindex' ) 236 .attr( 'aria-selected', 'true' ); 237 }, 238 239 /** 240 * Enables arrows navigation within the ARIA tabbed interface. 241 * 242 * @since 5.3.0 243 * 244 * @param {object} event jQuery event object. 245 * 246 * @returns {void} 247 */ 248 tabsNavigation: function( event ) { 249 var orientation = 'horizontal', 250 keys = [ 32, 35, 36, 37, 38, 39, 40 ]; 251 252 // Return if not Spacebar, End, Home, or Arrow keys. 253 if ( keys.indexOf( event.which ) === -1 ) { 254 return; 255 } 256 257 // Determine navigation direction. 258 if ( this.$el.attr( 'aria-orientation' ) === 'vertical' ) { 259 orientation = 'vertical'; 260 } 261 262 // Make Up and Down arrow keys do nothing with horizontal tabs. 263 if ( orientation === 'horizontal' && [ 38, 40 ].indexOf( event.which ) !== -1 ) { 264 return; 265 } 266 267 // Make Left and Right arrow keys do nothing with vertical tabs. 268 if ( orientation === 'vertical' && [ 37, 39 ].indexOf( event.which ) !== -1 ) { 269 return; 270 } 271 272 this.switchTabs( event, this.tabs ); 273 }, 274 275 /** 276 * Switches tabs in the ARIA tabbed interface. 277 * 278 * @since 5.3.0 279 * 280 * @param {object} event jQuery event object. 281 * 282 * @returns {void} 283 */ 284 switchTabs: function( event ) { 285 var key = event.which, 286 index = this.tabs.index( $( event.target ) ), 287 newIndex; 288 289 switch ( key ) { 290 // Space bar: Activate current targeted tab. 291 case 32: { 292 this.activateTab( this.tabs[ index ] ); 293 break; 294 } 295 // End key: Activate last tab. 296 case 35: { 297 event.preventDefault(); 298 this.activateTab( this.tabs[ this.tabs.length - 1 ] ); 299 break; 300 } 301 // Home key: Activate first tab. 302 case 36: { 303 event.preventDefault(); 304 this.activateTab( this.tabs[ 0 ] ); 305 break; 306 } 307 // Left and up keys: Activate previous tab. 308 case 37: 309 case 38: { 310 event.preventDefault(); 311 newIndex = ( index - 1 ) < 0 ? this.tabs.length - 1 : index - 1; 312 this.activateTab( this.tabs[ newIndex ] ); 313 break; 314 } 315 // Right and down keys: Activate next tab. 316 case 39: 317 case 40: { 318 event.preventDefault(); 319 newIndex = ( index + 1 ) === this.tabs.length ? 0 : index + 1; 320 this.activateTab( this.tabs[ newIndex ] ); 321 break; 322 } 323 } 324 }, 325 326 /** 327 * Sets a single tab to be focusable and semantically selected. 328 * 329 * @since 5.3.0 330 * 331 * @param {object} tab The tab DOM element. 332 * 333 * @returns {void} 334 */ 335 activateTab: function( tab ) { 336 if ( ! tab ) { 337 return; 338 } 339 340 // The tab is a DOM element: no need for jQuery methods. 341 tab.focus(); 342 343 // Handle automatic activation. 344 if ( this.tabsAutomaticActivation ) { 345 tab.removeAttribute( 'tabindex' ); 346 tab.setAttribute( 'aria-selected', 'true' ); 347 tab.click(); 348 349 return; 350 } 351 352 // Handle manual activation. 353 $( tab ).on( 'click', function() { 354 tab.removeAttribute( 'tabindex' ); 355 tab.setAttribute( 'aria-selected', 'true' ); 356 } ); 357 } 168 358 }); 169 359 170 360 module.exports = FocusManager; -
src/js/media/views/frame/post.js
257 257 mainMenu: function( view ) { 258 258 view.set({ 259 259 'library-separator': new wp.media.View({ 260 className: 'separator', 261 priority: 100 260 className: 'separator', 261 priority: 100, 262 attributes: { 263 role: 'presentation' 264 } 262 265 }) 263 266 }); 264 267 }, -
src/js/media/views/media-frame.js
23 23 regions: ['menu','title','content','toolbar','router'], 24 24 25 25 events: { 26 'click div.media-frame-title h1': 'toggleMenu'26 'click .media-frame-menu-toggle': 'toggleMenu' 27 27 }, 28 28 29 29 /** … … 75 75 this.on( 'title:create:default', this.createTitle, this ); 76 76 this.title.mode('default'); 77 77 78 this.on( 'title:render', function( view ) {79 view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );80 });81 82 78 // Bind default menu. 83 79 this.on( 'menu:create:default', this.createMenu, this ); 80 81 // Set the menu ARIA tab panel attributes when the modal opens. 82 this.on( 'open', this.setMenuTabPanelAriaAttributes, this ); 83 // Set the router ARIA tab panel attributes when the modal opens. 84 this.on( 'open', this.setRouterTabPanelAriaAttributes, this ); 85 86 // Update the menu ARIA tab panel attributes when the content updates. 87 this.on( 'content:render', this.setMenuTabPanelAriaAttributes, this ); 88 // Update the router ARIA tab panel attributes when the content updates. 89 this.on( 'content:render', this.setRouterTabPanelAriaAttributes, this ); 84 90 }, 91 85 92 /** 93 * Sets the attributes to be used on the menu ARIA tab panel. 94 * 95 * @since 5.3.0 96 * 97 * @returns {void} 98 */ 99 setMenuTabPanelAriaAttributes: function() { 100 var stateId = this.state().get( 'id' ), 101 tabPanelEl = this.$el.find( '.media-frame-tab-panel' ), 102 ariaLabelledby; 103 104 tabPanelEl.removeAttr( 'role aria-labelledby tabindex' ); 105 106 if ( this.menuView && this.menuView.isVisible ) { 107 ariaLabelledby = 'menu-item-' + stateId; 108 109 // Set the tab panel attributes only if the tabs are visible. 110 tabPanelEl 111 .attr( { 112 role: 'tabpanel', 113 'aria-labelledby': ariaLabelledby, 114 tabIndex: '0' 115 } ); 116 } 117 }, 118 119 /** 120 * Sets the attributes to be used on the router ARIA tab panel. 121 * 122 * @since 5.3.0 123 * 124 * @returns {void} 125 */ 126 setRouterTabPanelAriaAttributes: function() { 127 var tabPanelEl = this.$el.find( '.media-frame-content' ), 128 ariaLabelledby; 129 130 tabPanelEl.removeAttr( 'role aria-labelledby tabindex' ); 131 132 // On the Embed view the router menu is hidden. 133 if ( 'embed' === this.content._mode ) { 134 return; 135 } 136 137 // Set the tab panel attributes only if the tabs are visible. 138 if ( this.routerView && this.routerView.isVisible && this.content._mode ) { 139 ariaLabelledby = 'menu-item-' + this.content._mode; 140 141 tabPanelEl 142 .attr( { 143 role: 'tabpanel', 144 'aria-labelledby': ariaLabelledby, 145 tabIndex: '0' 146 } ); 147 } 148 }, 149 150 /** 86 151 * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining 87 152 */ 88 153 render: function() { … … 111 176 */ 112 177 createMenu: function( menu ) { 113 178 menu.view = new wp.media.view.Menu({ 114 controller: this 179 controller: this, 180 181 attributes: { 182 role: 'tablist', 183 'aria-orientation': 'vertical' 184 } 115 185 }); 186 187 this.menuView = menu.view; 116 188 }, 117 189 118 toggleMenu: function() { 119 this.$el.find( '.media-menu' ).toggleClass( 'visible' ); 190 toggleMenu: function( event ) { 191 var menu = this.$el.find( '.media-menu' ); 192 193 menu.toggleClass( 'visible' ); 194 $( event.target ).attr( 'aria-expanded', menu.hasClass( 'visible' ) ); 120 195 }, 121 196 122 197 /** … … 134 209 */ 135 210 createRouter: function( router ) { 136 211 router.view = new wp.media.view.Router({ 137 controller: this 212 controller: this, 213 214 attributes: { 215 role: 'tablist', 216 'aria-orientation': 'horizontal' 217 } 138 218 }); 219 220 this.routerView = router.view; 139 221 }, 140 222 /** 141 223 * @param {Object} options -
src/js/media/views/menu-item.js
11 11 * @augments Backbone.View 12 12 */ 13 13 MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{ 14 tagName: ' a',14 tagName: 'button', 15 15 className: 'media-menu-item', 16 16 17 17 attributes: { 18 href: '#' 18 type: 'button', 19 role: 'tab' 19 20 }, 20 21 21 22 events: { 22 23 'click': '_click' 23 24 }, 25 24 26 /** 25 * @param {Object} event27 * Allows to override the click event. 26 28 */ 27 _click: function( event) {29 _click: function() { 28 30 var clickOverride = this.options.click; 29 31 30 if ( event ) {31 event.preventDefault();32 }33 34 32 if ( clickOverride ) { 35 33 clickOverride.call( this ); 36 34 } else { … … 43 41 44 42 if ( state ) { 45 43 this.controller.setState( state ); 44 // Toggle the menu visibility in the responsive view. 46 45 this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below 47 46 } 48 47 }, 48 49 49 /** 50 50 * @returns {wp.media.view.MenuItem} returns itself to allow chaining 51 51 */ 52 52 render: function() { 53 var options = this.options; 53 var options = this.options, 54 menuProperty = options.state || options.contentMode; 54 55 55 56 if ( options.text ) { 56 57 this.$el.text( options.text ); … … 58 59 this.$el.html( options.html ); 59 60 } 60 61 62 // Set the menu item ID based on the frame state associated to the menu item. 63 this.$el.attr( 'id', 'menu-item-' + menuProperty ); 64 61 65 return this; 62 66 } 63 67 }); -
src/js/media/views/menu.js
20 20 ItemView: MenuItem, 21 21 region: 'menu', 22 22 23 /* TODO: alternatively hide on any click anywhere24 events: {25 ' click': 'click'23 attributes: { 24 role: 'tablist', 25 'aria-orientation': 'horizontal' 26 26 }, 27 27 28 click: function() { 29 this.$el.removeClass( 'visible' ); 28 initialize: function() { 29 this._views = {}; 30 31 this.set( _.extend( {}, this._views, this.options.views ), { silent: true }); 32 delete this.options.views; 33 34 if ( ! this.options.silent ) { 35 this.render(); 36 } 37 38 // Initialize the Focus Manager. 39 this.focusManager = new wp.media.view.FocusManager( { 40 el: this.el, 41 mode: 'tabsNavigation' 42 } ); 43 44 // The menu is always rendered and can be visible or hidden on some frames. 45 this.isVisible = true; 30 46 }, 31 */32 47 33 48 /** 34 49 * @param {Object} options … … 47 62 */ 48 63 PriorityList.prototype.ready.apply( this, arguments ); 49 64 this.visibility(); 65 66 // Set up aria tabs initial attributes. 67 this.focusManager.setupAriaTabs(); 50 68 }, 51 69 52 70 set: function() { … … 72 90 hide = ! views || views.length < 2; 73 91 74 92 if ( this === view ) { 93 // Flag this menu as hidden or visible. 94 this.isVisible = ! hide; 95 // Set or remove a CSS class to hide the menu. 75 96 this.controller.$el.toggleClass( 'hide-' + region, hide ); 76 97 } 77 98 }, … … 87 108 88 109 this.deselect(); 89 110 view.$el.addClass('active'); 111 112 // Set up again the aria tabs initial attributes after the menu updates. 113 this.focusManager.setupAriaTabs(); 90 114 }, 91 115 92 116 deselect: function() { -
src/js/media/views/router.js
20 20 ItemView: wp.media.view.RouterItem, 21 21 region: 'router', 22 22 23 attributes: { 24 role: 'tablist', 25 'aria-orientation': 'horizontal' 26 }, 27 23 28 initialize: function() { 24 29 this.controller.on( 'content:render', this.update, this ); 25 30 // Call 'initialize' directly on the parent class. -
src/wp-includes/css/media-views.css
553 553 user-select: none; 554 554 } 555 555 556 .media-menu > a{556 .media-menu .media-menu-item { 557 557 display: block; 558 box-sizing: border-box; 559 width: 100%; 558 560 position: relative; 561 border: 0; 562 margin: 0; 559 563 padding: 8px 20px; 560 margin: 0;564 font-size: 14px; 561 565 line-height: 1.28571428; 562 font-size: 14px;566 background: transparent; 563 567 color: #0073aa; 568 text-align: left; 564 569 text-decoration: none; 570 cursor: pointer; 565 571 } 566 572 567 .media-menu > a:hover { 568 color: #0073aa; 573 .media-menu .media-menu-item:hover { 569 574 background: rgba(0, 0, 0, 0.04); 570 575 } 571 576 572 .media-menu > a:active { 577 .media-menu .media-menu-item:active { 578 color: #0073aa; 573 579 outline: none; 574 580 } 575 581 … … 579 585 font-weight: 600; 580 586 } 581 587 588 .media-menu .media-menu-item:focus { 589 box-shadow: 590 0 0 0 1px #5b9dd9, 591 0 0 2px 1px rgba(30, 140, 190, 0.8); 592 color: #124964; 593 /* Only visible in Windows High Contrast mode */ 594 outline: 1px solid transparent; 595 } 596 582 597 .media-menu .separator { 583 598 height: 0; 584 599 margin: 12px 20px; … … 594 609 padding: 0 6px; 595 610 margin: 0; 596 611 clear: both; 597 -webkit-user-select: none;598 -moz-user-select: none;599 -ms-user-select: none;600 user-select: none;601 612 } 602 613 603 .media-router a { 604 transition: none; 605 } 606 607 .media-router > a { 614 .media-router .media-menu-item { 608 615 position: relative; 609 616 float: left; 617 border: 0; 618 margin: 0; 610 619 padding: 8px 10px 9px; 611 margin: 0;612 620 height: 18px; 613 621 line-height: 1.28571428; 614 622 font-size: 14px; 615 623 text-decoration: none; 624 background: transparent; 625 cursor: pointer; 626 transition: none; 616 627 } 617 628 618 .media-router > a:last-child {629 .media-router .media-menu-item:last-child { 619 630 border-right: 0; 620 631 } 621 632 622 .media-router > a:active { 623 outline: none; 633 .media-router .media-menu-item:hover, 634 .media-router .media-menu-item:active { 635 color: #0073aa; 624 636 } 625 637 626 638 .media-router .active, 627 639 .media-router .active:hover { 628 color: # 32373c;640 color: #23282d; 629 641 } 630 642 643 .media-router .media-menu-item:focus { 644 box-shadow: 645 0 0 0 1px #5b9dd9, 646 0 0 2px 1px rgba(30, 140, 190, 0.8); 647 color: #124964; 648 /* Only visible in Windows High Contrast mode */ 649 outline: 1px solid transparent; 650 } 651 631 652 .media-router .active, 632 .media-router > a.active:last-child {653 .media-router .media-menu-item.active:last-child { 633 654 margin: -1px -1px 0; 634 655 background: #fff; 635 656 border: 1px solid #ddd; … … 730 751 box-shadow: 0 4px 4px -4px rgba(0, 0, 0, 0.1); 731 752 } 732 753 733 .media-frame-title .dashicons {734 display: none;735 }736 737 754 .media-frame-title h1 { 738 755 padding: 0 16px; 739 756 font-size: 22px; … … 741 758 margin: 0; 742 759 } 743 760 761 .wp-core-ui .button.media-frame-menu-toggle { 762 display: none; 763 } 764 744 765 .media-frame-title .suggested-dimensions { 745 766 font-size: 14px; 746 767 float: right; … … 2220 2241 @media only screen and (max-width: 900px) { 2221 2242 2222 2243 /* Drop-down menu */ 2223 .media-frame:not(.hide-menu) .media-frame-title, 2244 .media-frame:not(.hide-menu) .media-frame-title { 2245 position: static; 2246 display: inline-block; 2247 } 2248 2224 2249 .media-frame:not(.hide-menu) .media-frame-router, 2225 2250 .media-frame:not(.hide-menu) .media-frame-content, 2226 2251 .media-frame:not(.hide-menu) .media-frame-toolbar { … … 2233 2258 } 2234 2259 2235 2260 .media-frame:not(.hide-menu) .media-menu { 2261 display: none; 2236 2262 width: auto; 2237 2263 max-width: 80%; 2238 2264 overflow: auto; 2239 2265 z-index: 2000; 2240 2266 top: 50px; 2241 left: -300px;2267 left: 0; 2242 2268 right: auto; 2243 2269 bottom: auto; 2244 2270 padding: 5px 0; … … 2246 2272 } 2247 2273 2248 2274 .media-frame:not(.hide-menu) .media-menu.visible { 2249 left: 0;2275 display: block; 2250 2276 } 2251 2277 2252 2278 .media-frame:not(.hide-menu) .media-menu > a { … … 2254 2280 font-size: 16px; 2255 2281 } 2256 2282 2257 .media-frame:not(.hide-menu) .media-menu > a.active {2258 display: none;2259 }2260 2261 2283 .media-frame:not(.hide-menu) .media-menu .separator { 2262 2284 margin: 5px 10px; 2263 2285 } 2264 2286 2265 .media-frame:not(.hide-menu) .media-frame-title { 2266 left: 0; 2287 .media-frame:not(.hide-menu) .media-frame-title h1 { 2288 line-height: 3; 2289 font-size: 18px; 2290 padding-left: 0; 2267 2291 } 2268 2292 2269 .media-frame:not(.hide-menu) .media-frame-title .dashicons { 2270 display: inline-block; 2271 line-height: 2.5; 2293 .wp-core-ui .media-frame:not(.hide-menu) .button.media-frame-menu-toggle { 2294 display: inline-flex; 2295 align-items: center; 2296 vertical-align: top; 2297 min-height: 38px; 2298 margin: 6px; 2299 padding: 0 2px 0 12px; 2300 font-size: 1rem; 2301 text-decoration: none; 2272 2302 } 2273 2303 2274 .media-frame:not(.hide-menu) .media-frame-title h1 { 2275 color: #0073aa; 2276 line-height: 3; 2277 font-size: 18px; 2278 float: left; 2279 cursor: pointer; 2304 .wp-core-ui .button.media-frame-menu-toggle:active { 2305 background: transparent; 2306 box-shadow: none; 2307 transform: none; 2280 2308 } 2281 2309 /* End drop-down menu */ 2282 2310 … … 2570 2598 height: 40px; 2571 2599 } 2572 2600 2601 .wp-core-ui .media-frame:not(.hide-menu) .button.media-frame-menu-toggle { 2602 min-height: 28px; 2603 } 2604 2573 2605 .wp-core-ui.wp-customizer .media-button { 2574 2606 margin-top: 13px; 2575 2607 } … … 2580 2612 line-height: 2.22222222; 2581 2613 } 2582 2614 2583 .media-frame:not(.hide-menu) .media-frame-title .dashicons {2584 line-height: 2;2585 }2586 2587 2615 .media-frame-router, 2588 2616 .media-frame:not(.hide-menu) .media-menu { 2589 2617 top: 40px; -
src/wp-includes/media-template.php
177 177 178 178 <?php // Template for the media frame: used both in the media grid and in the media modal. ?> 179 179 <script type="text/html" id="tmpl-media-frame"> 180 <button type="button" class="button button-link media-frame-menu-toggle" aria-expanded="false"> 181 <?php _e( 'Menu' ); ?> 182 <span class="dashicons dashicons-arrow-down" aria-hidden="true"></span> 183 </button> 180 184 <div class="media-frame-title" id="media-frame-title"></div> 181 185 <div class="media-frame-menu"></div> 182 <div class="media-frame-router"></div> 183 <div class="media-frame-content"></div> 186 <div class="media-frame-tab-panel"> 187 <div class="media-frame-router"></div> 188 <div class="media-frame-content"></div> 189 </div> 184 190 <div class="media-frame-toolbar"></div> 185 191 <div class="media-frame-uploader"></div> 186 192 </script>