WordPress.org

Make WordPress Core

Ticket #47149: 47149.5.diff

File 47149.5.diff, 19.8 KB (added by afercia, 2 months ago)
  • src/js/media/views/focus-manager.js

     
     1var $ = jQuery;
     2
    13/**
    24 * wp.media.view.FocusManager
    35 *
     
    1113var FocusManager = wp.media.View.extend(/** @lends wp.media.view.FocusManager.prototype */{
    1214
    1315        events: {
    14                 'keydown': 'constrainTabbing'
     16                'keydown': 'focusManagementMode'
    1517        },
    1618
    1719        /**
     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        /**
    1853         * Gets all the tabbable elements.
    1954         *
    2055         * @since 5.3.0
     
    67102        },
    68103
    69104        /**
    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.
    72106         *
     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         *
    73110         * The reason why we use `aria-hidden` is that `aria-modal="true"` is buggy
    74111         * in Safari 11.1 and support is spotty in other browsers. In the future we
    75112         * should consider to remove this helper function and only use `aria-modal="true"`.
     
    110147        },
    111148
    112149        /**
    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
    114153         * previously hidden and stored in this.ariaHiddenElements.
    115154         *
    116155         * @since 5.2.3
     
    164203         *
    165204         * @since 5.2.3
    166205         */
    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        }
    168358});
    169359
    170360module.exports = FocusManager;
  • src/js/media/views/frame/post.js

     
    257257        mainMenu: function( view ) {
    258258                view.set({
    259259                        'library-separator': new wp.media.View({
    260                                 className: 'separator',
    261                                 priority: 100
     260                                className:  'separator',
     261                                priority:   100,
     262                                attributes: {
     263                                        role: 'presentation'
     264                                }
    262265                        })
    263266                });
    264267        },
  • src/js/media/views/media-frame.js

     
    2323        regions:   ['menu','title','content','toolbar','router'],
    2424
    2525        events: {
    26                 'click div.media-frame-title h1': 'toggleMenu'
     26                'click .media-frame-menu-toggle': 'toggleMenu'
    2727        },
    2828
    2929        /**
     
    7575                this.on( 'title:create:default', this.createTitle, this );
    7676                this.title.mode('default');
    7777
    78                 this.on( 'title:render', function( view ) {
    79                         view.$el.append( '<span class="dashicons dashicons-arrow-down"></span>' );
    80                 });
    81 
    8278                // Bind default menu.
    8379                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 );
    8490        },
     91
    8592        /**
     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        /**
    86151         * @returns {wp.media.view.MediaFrame} Returns itself to allow chaining
    87152         */
    88153        render: function() {
     
    111176         */
    112177        createMenu: function( menu ) {
    113178                menu.view = new wp.media.view.Menu({
    114                         controller: this
     179                        controller: this,
     180
     181                        attributes: {
     182                                role:               'tablist',
     183                                'aria-orientation': 'vertical'
     184                        }
    115185                });
     186
     187                this.menuView = menu.view;
    116188        },
    117189
    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' ) );
    120195        },
    121196
    122197        /**
     
    134209         */
    135210        createRouter: function( router ) {
    136211                router.view = new wp.media.view.Router({
    137                         controller: this
     212                        controller: this,
     213
     214                        attributes: {
     215                                role:               'tablist',
     216                                'aria-orientation': 'horizontal'
     217                        }
    138218                });
     219
     220                this.routerView = router.view;
    139221        },
    140222        /**
    141223         * @param {Object} options
  • src/js/media/views/menu-item.js

     
    1111 * @augments Backbone.View
    1212 */
    1313MenuItem = wp.media.View.extend(/** @lends wp.media.view.MenuItem.prototype */{
    14         tagName:   'a',
     14        tagName:   'button',
    1515        className: 'media-menu-item',
    1616
    1717        attributes: {
    18                 href: '#'
     18                type: 'button',
     19                role: 'tab'
    1920        },
    2021
    2122        events: {
    2223                'click': '_click'
    2324        },
     25
    2426        /**
    25          * @param {Object} event
     27         * Allows to override the click event.
    2628         */
    27         _click: function( event ) {
     29        _click: function() {
    2830                var clickOverride = this.options.click;
    2931
    30                 if ( event ) {
    31                         event.preventDefault();
    32                 }
    33 
    3432                if ( clickOverride ) {
    3533                        clickOverride.call( this );
    3634                } else {
     
    4341
    4442                if ( state ) {
    4543                        this.controller.setState( state );
     44                        // Toggle the menu visibility in the responsive view.
    4645                        this.views.parent.$el.removeClass( 'visible' ); // TODO: or hide on any click, see below
    4746                }
    4847        },
     48
    4949        /**
    5050         * @returns {wp.media.view.MenuItem} returns itself to allow chaining
    5151         */
    5252        render: function() {
    53                 var options = this.options;
     53                var options = this.options,
     54                        menuProperty = options.state || options.contentMode;
    5455
    5556                if ( options.text ) {
    5657                        this.$el.text( options.text );
     
    5859                        this.$el.html( options.html );
    5960                }
    6061
     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
    6165                return this;
    6266        }
    6367});
  • src/js/media/views/menu.js

     
    2020        ItemView:  MenuItem,
    2121        region:    'menu',
    2222
    23         /* TODO: alternatively hide on any click anywhere
    24         events: {
    25                 'click': 'click'
     23        attributes: {
     24                role:               'tablist',
     25                'aria-orientation': 'horizontal'
    2626        },
    2727
    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;
    3046        },
    31         */
    3247
    3348        /**
    3449         * @param {Object} options
     
    4762                 */
    4863                PriorityList.prototype.ready.apply( this, arguments );
    4964                this.visibility();
     65
     66                // Set up aria tabs initial attributes.
     67                this.focusManager.setupAriaTabs();
    5068        },
    5169
    5270        set: function() {
     
    7290                        hide = ! views || views.length < 2;
    7391
    7492                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.
    7596                        this.controller.$el.toggleClass( 'hide-' + region, hide );
    7697                }
    7798        },
     
    87108
    88109                this.deselect();
    89110                view.$el.addClass('active');
     111
     112                // Set up again the aria tabs initial attributes after the menu updates.
     113                this.focusManager.setupAriaTabs();
    90114        },
    91115
    92116        deselect: function() {
  • src/js/media/views/router.js

     
    2020        ItemView:  wp.media.view.RouterItem,
    2121        region:    'router',
    2222
     23        attributes: {
     24                role:               'tablist',
     25                'aria-orientation': 'horizontal'
     26        },
     27
    2328        initialize: function() {
    2429                this.controller.on( 'content:render', this.update, this );
    2530                // Call 'initialize' directly on the parent class.
  • src/wp-includes/css/media-views.css

     
    553553        user-select: none;
    554554}
    555555
    556 .media-menu > a {
     556.media-menu .media-menu-item {
    557557        display: block;
     558        box-sizing: border-box;
     559        width: 100%;
    558560        position: relative;
     561        border: 0;
     562        margin: 0;
    559563        padding: 8px 20px;
    560         margin: 0;
     564        font-size: 14px;
    561565        line-height: 1.28571428;
    562         font-size: 14px;
     566        background: transparent;
    563567        color: #0073aa;
     568        text-align: left;
    564569        text-decoration: none;
     570        cursor: pointer;
    565571}
    566572
    567 .media-menu > a:hover {
    568         color: #0073aa;
     573.media-menu .media-menu-item:hover {
    569574        background: rgba(0, 0, 0, 0.04);
    570575}
    571576
    572 .media-menu > a:active {
     577.media-menu .media-menu-item:active {
     578        color: #0073aa;
    573579        outline: none;
    574580}
    575581
     
    579585        font-weight: 600;
    580586}
    581587
     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
    582597.media-menu .separator {
    583598        height: 0;
    584599        margin: 12px 20px;
     
    594609        padding: 0 6px;
    595610        margin: 0;
    596611        clear: both;
    597         -webkit-user-select: none;
    598         -moz-user-select: none;
    599         -ms-user-select: none;
    600         user-select: none;
    601612}
    602613
    603 .media-router a {
    604         transition: none;
    605 }
    606 
    607 .media-router > a {
     614.media-router .media-menu-item {
    608615        position: relative;
    609616        float: left;
     617        border: 0;
     618        margin: 0;
    610619        padding: 8px 10px 9px;
    611         margin: 0;
    612620        height: 18px;
    613621        line-height: 1.28571428;
    614622        font-size: 14px;
    615623        text-decoration: none;
     624        background: transparent;
     625        cursor: pointer;
     626        transition: none;
    616627}
    617628
    618 .media-router > a:last-child {
     629.media-router .media-menu-item:last-child {
    619630        border-right: 0;
    620631}
    621632
    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;
    624636}
    625637
    626638.media-router .active,
    627639.media-router .active:hover {
    628         color: #32373c;
     640        color: #23282d;
    629641}
    630642
     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
    631652.media-router .active,
    632 .media-router > a.active:last-child {
     653.media-router .media-menu-item.active:last-child {
    633654        margin: -1px -1px 0;
    634655        background: #fff;
    635656        border: 1px solid #ddd;
     
    730751        box-shadow: 0 4px 4px -4px rgba(0, 0, 0, 0.1);
    731752}
    732753
    733 .media-frame-title .dashicons {
    734         display: none;
    735 }
    736 
    737754.media-frame-title h1 {
    738755        padding: 0 16px;
    739756        font-size: 22px;
     
    741758        margin: 0;
    742759}
    743760
     761.wp-core-ui .button.media-frame-menu-toggle {
     762        display: none;
     763}
     764
    744765.media-frame-title .suggested-dimensions {
    745766        font-size: 14px;
    746767        float: right;
     
    22202241@media only screen and (max-width: 900px) {
    22212242
    22222243        /* 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
    22242249        .media-frame:not(.hide-menu) .media-frame-router,
    22252250        .media-frame:not(.hide-menu) .media-frame-content,
    22262251        .media-frame:not(.hide-menu) .media-frame-toolbar {
     
    22332258        }
    22342259
    22352260        .media-frame:not(.hide-menu) .media-menu {
     2261                display: none;
    22362262                width: auto;
    22372263                max-width: 80%;
    22382264                overflow: auto;
    22392265                z-index: 2000;
    22402266                top: 50px;
    2241                 left: -300px;
     2267                left: 0;
    22422268                right: auto;
    22432269                bottom: auto;
    22442270                padding: 5px 0;
     
    22462272        }
    22472273
    22482274        .media-frame:not(.hide-menu) .media-menu.visible {
    2249                 left: 0;
     2275                display: block;
    22502276        }
    22512277
    22522278        .media-frame:not(.hide-menu) .media-menu > a {
     
    22542280                font-size: 16px;
    22552281        }
    22562282
    2257         .media-frame:not(.hide-menu) .media-menu > a.active {
    2258                 display: none;
    2259         }
    2260 
    22612283        .media-frame:not(.hide-menu) .media-menu .separator {
    22622284                margin: 5px 10px;
    22632285        }
    22642286
    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;
    22672291        }
    22682292
    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;
    22722302        }
    22732303
    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;
    22802308        }
    22812309        /* End drop-down menu */
    22822310
     
    25702598                height: 40px;
    25712599        }
    25722600
     2601        .wp-core-ui .media-frame:not(.hide-menu) .button.media-frame-menu-toggle {
     2602                min-height: 28px;
     2603        }
     2604
    25732605        .wp-core-ui.wp-customizer .media-button {
    25742606                margin-top: 13px;
    25752607        }
     
    25802612                line-height: 2.22222222;
    25812613        }
    25822614
    2583         .media-frame:not(.hide-menu) .media-frame-title .dashicons {
    2584                 line-height: 2;
    2585         }
    2586 
    25872615        .media-frame-router,
    25882616        .media-frame:not(.hide-menu) .media-menu {
    25892617                top: 40px;
  • src/wp-includes/media-template.php

     
    177177
    178178        <?php // Template for the media frame: used both in the media grid and in the media modal. ?>
    179179        <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>
    180184                <div class="media-frame-title" id="media-frame-title"></div>
    181185                <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>
    184190                <div class="media-frame-toolbar"></div>
    185191                <div class="media-frame-uploader"></div>
    186192        </script>