Make WordPress Core

Changeset 57708

Timestamp:
02/26/2024 12:50:22 AM (5 months ago)
Author:
joedolson
Message:

Toolbar: Accessibility: Keyboard navigation for screen readers.

Change the admin toolbar to have role="menu" and support opening for screen readers. Remove screen reader only log out link and collapse duplicate profile links into one link. This is an imperfect solution to a complex problem in the adminbar, but the lack of screen reader access to submenus is a major accessibility problem, and this fix provides access, even if the mechanism is imperfect.

Screen reader log out added in [21452].

Props abletec, Cheffheid, sabernhardt, alexstine, joedolson, afercia, sparklingrobots, danieltj, swissspidy, netweb, dionysous.
Fixes #34668, #43633.

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/lib/admin-bar.js

    r48650 r57708  
    3232        topMenuItems = adminBar.querySelectorAll( 'li.menupop' );
    3333        allMenuItems = adminBar.querySelectorAll( '.ab-item' );
    34         adminBarLogout = document.getElementById( 'wp-admin-bar-logout' );
     34        adminBarLogout = document.' );
    3535        adminBarSearchForm = document.getElementById( 'adminbarsearch' );
    3636        shortlink = document.getElementById( 'wp-admin-bar-get-shortlink' );
     
    150150        var wrapper;
    151151
    152         if ( event.which !== 13 ) {
     152        // Follow link if pressing Ctrl and/or Shift with Enter (opening in a new tab or window).
     153        if ( event.which !== 13 || event.ctrlKey || event.shiftKey ) {
    153154            return;
    154155        }
     
    337338            element.className += className;
    338339        }
     340
     341
     342
     343
     344
    339345    }
    340346
     
    366372
    367373            element.className = classes.replace( /^[\s]+|[\s]+$/g, '' );
     374
     375
     376
     377
     378
    368379        }
    369380    }
  • trunk/src/wp-includes/admin-bar.php

    r57600 r57708  
    140140            '</span>',
    141141        'href'  => $about_url,
     142
     143
     144
    142145    );
    143146
     
    283286            'href'   => $profile_url,
    284287            'meta'   => array(
    285                 'class' => $class,
     288                'class'      => $class,
     289                'menu_title' => sprintf( __( 'Howdy, %s' ), $current_user->display_name ),
    286290            ),
    287291        )
     
    326330    }
    327331
     332
     333
    328334    $wp_admin_bar->add_node(
    329335        array(
     
    332338            'title'  => $user_info,
    333339            'href'   => $profile_url,
    334             'meta'   => array(
    335                 'tabindex' => -1,
    336             ),
    337         )
    338     );
    339 
    340     if ( false !== $profile_url ) {
    341         $wp_admin_bar->add_node(
    342             array(
    343                 'parent' => 'user-actions',
    344                 'id'     => 'edit-profile',
    345                 'title'  => __( 'Edit Profile' ),
    346                 'href'   => $profile_url,
    347             )
    348         );
    349     }
     340        )
     341    );
    350342
    351343    $wp_admin_bar->add_node(
     
    398390            'title' => $title,
    399391            'href'  => ( is_admin() || ! current_user_can( 'read' ) ) ? home_url( '/' ) : admin_url(),
     392
     393
     394
    400395        )
    401396    );
     
    995990            'title' => $title,
    996991            'href'  => admin_url( current( array_keys( $actions ) ) ),
     992
     993
     994
    997995        )
    998996    );
  • trunk/src/wp-includes/class-wp-admin-bar.php

    r56177 r57708  
    108108     * @since 3.1.0
    109109     * @since 4.5.0 Added the ability to pass 'lang' and 'dir' meta data.
     110
    110111     *
    111112     * @param array $args {
     
    118119     *     @type bool   $group  Optional. Whether or not the node is a group. Default false.
    119120     *     @type array  $meta   Meta data including the following keys: 'html', 'class', 'rel', 'lang', 'dir',
    120      *                          'onclick', 'target', 'title', 'tabindex'. Default empty.
     121     *                          'onclick', 'target', 'title', 'tabindex'. Default empty.
    121122     * }
    122123     */
     
    479480                ?>
    480481            </div>
    481             <?php if ( is_user_logged_in() ) : ?>
    482             <a class="screen-reader-shortcut" href="<?php echo esc_url( wp_logout_url() ); ?>"><?php _e( 'Log Out' ); ?></a>
    483             <?php endif; ?>
    484482        </div>
    485483
     
    506504    /**
    507505     * @since 3.3.0
     506
    508507     *
    509508     * @param object $node
    510      */
    511     final protected function _render_group( $node ) {
     509     * @param string|bool $menu_title The accessible name of this aria menu or false if not provided.
     510     */
     511    final protected function _render_group( $node, $menu_title = false ) {
    512512        if ( 'container' === $node->type ) {
    513513            $this->_render_container( $node );
     
    524524        }
    525525
    526         echo "<ul id='" . esc_attr( 'wp-admin-bar-' . $node->id ) . "'$class>";
     526        if ( empty( $menu_title ) ) {
     527            echo "<ul role='menu' id='" . esc_attr( 'wp-admin-bar-' . $node->id ) . "'$class>";
     528        } else {
     529            echo "<ul role='menu' aria-label='" . esc_attr( $menu_title ) . "' id='" . esc_attr( 'wp-admin-bar-' . $node->id ) . "'$class>";
     530        }
    527531        foreach ( $node->children as $item ) {
    528532            $this->_render_item( $item );
     
    547551
    548552        // Allow only numeric values, then casted to integers, and allow a tabindex value of `0` for a11y.
    549         $tabindex        = ( isset( $node->meta['tabindex'] ) && is_numeric( $node->meta['tabindex'] ) ) ? (int) $node->meta['tabindex'] : '';
    550         $aria_attributes = ( '' !== $tabindex ) ? ' tabindex="' . $tabindex . '"' : '';
     553        $tabindex         = ( isset( $node->meta['tabindex'] ) && is_numeric( $node->meta['tabindex'] ) ) ? (int) $node->meta['tabindex'] : '';
     554        $aria_attributes  = ( '' !== $tabindex ) ? ' tabindex="' . $tabindex . '"' : '';
     555        $aria_attributes .= ' role="menuitem"';
    551556
    552557        $menuclass = '';
     
    555560        if ( $is_parent ) {
    556561            $menuclass        = 'menupop ';
    557             $aria_attributes .= ' aria-haspopup="true"';
     562            $aria_attributes .= ' aria-e"';
    558563        }
    559564
     
    604609            echo '<div class="ab-sub-wrapper">';
    605610            foreach ( $node->children as $group ) {
    606                 $this->_render_group( $group );
     611                if ( empty( $node->meta['menu_title'] ) ) {
     612                    $this->_render_group( $group, false );
     613                } else {
     614                    $this->_render_group( $group, $node->meta['menu_title'] );
     615                }
    607616            }
    608617            echo '</div>';
  • trunk/src/wp-includes/css/admin-bar.css

    r56956 r57708  
    446446    height: auto;
    447447    background: none;
     448
     449
     450
     451
     452
    448453}
    449454
  • trunk/tests/phpunit/tests/adminbar.php

    r56227 r57708  
    9898        $node_my_account   = $wp_admin_bar->get_node( 'my-account' );
    9999        $node_user_info    = $wp_admin_bar->get_node( 'user-info' );
    100         $node_edit_profile = $wp_admin_bar->get_node( 'edit-profile' );
    101100
    102101        // Site menu points to the home page instead of the admin URL.
     
    106105        $this->assertFalse( $node_my_account->href );
    107106        $this->assertFalse( $node_user_info->href );
    108         $this->assertNull( $node_edit_profile );
    109107    }
    110108
     
    117115
    118116        wp_set_current_user( self::$editor_id );
     117
     118
     119
     120
     121
     122
     123
     124
     125
     126
     127
     128
     129
     130
     131
     132
     133
     134
     135
     136
     137
     138
     139
     140
     141
     142
     143
     144
     145
     146
     147
     148
     149
     150
     151
     152
     153
     154
    119155
    120156        $wp_admin_bar = $this->get_standard_admin_bar();
     
    123159        $node_my_account   = $wp_admin_bar->get_node( 'my-account' );
    124160        $node_user_info    = $wp_admin_bar->get_node( 'user-info' );
    125         $node_edit_profile = $wp_admin_bar->get_node( 'edit-profile' );
    126 
    127         // Site menu points to the admin URL.
    128         $this->assertSame( admin_url( '/' ), $node_site_name->href );
    129 
    130         $profile_url = admin_url( 'profile.php' );
    131 
    132         // Profile URLs point to profile.php.
    133         $this->assertSame( $profile_url, $node_my_account->href );
    134         $this->assertSame( $profile_url, $node_user_info->href );
    135         $this->assertSame( $profile_url, $node_edit_profile->href );
     161
     162        // Get primary blog.
     163        $primary = get_active_blog_for_user( self::$editor_id );
     164        $this->assertIsObject( $primary );
     165
     166        // No Site menu as the user isn't a member of this blog.
     167        $this->assertNull( $node_site_name );
     168
     169        $primary_profile_url = get_admin_url( $primary->blog_id, 'profile.php' );
     170
     171        // Ensure the user's primary blog is not the same as the main site.
     172        $this->assertNotEquals( $primary_profile_url, admin_url( 'profile.php' ) );
     173
     174        // Profile URLs should go to the user's primary blog.
     175        $this->assertSame( $primary_profile_url, $node_my_account->href );
     176        $this->assertSame( $primary_profile_url, $node_user_info->href );
     177
     178        restore_current_blog();
    136179    }
    137180
     
    141184     * @group ms-required
    142185     */
    143     public function test_admin_bar_contains_correct_links_for_users_with_no_role_on_blog() {
     186    public function test_admin_bar_contains_correct_links_for_users_with_no_role_on_network() {
     187        $this->assertTrue( user_can( self::$admin_id, 'read' ) );
     188        $this->assertFalse( user_can( self::$no_role_id, 'read' ) );
     189
    144190        $blog_id = self::factory()->blog->create(
    145191            array(
     
    148194        );
    149195
    150         $this->assertTrue( user_can( self::$admin_id, 'read' ) );
    151         $this->assertTrue( user_can( self::$editor_id, 'read' ) );
    152 
    153196        $this->assertTrue( is_user_member_of_blog( self::$admin_id, $blog_id ) );
    154         $this->assertFalse( is_user_member_of_blog( self::$editor_id, $blog_id ) );
    155 
    156         wp_set_current_user( self::$editor_id );
     197        $this->assertFalse( is_user_member_of_blog( self::$no_role_id, $blog_id ) );
     198        $this->assertTrue( is_user_member_of_blog( self::$no_role_id, get_current_blog_id() ) );
     199
     200        // Remove `$nobody` from the current blog, so they're not a member of any blog.
     201        $removed = remove_user_from_blog( self::$no_role_id, get_current_blog_id() );
     202
     203        $this->assertTrue( $removed );
     204        $this->assertFalse( is_user_member_of_blog( self::$no_role_id, get_current_blog_id() ) );
     205
     206        wp_set_current_user( self::$no_role_id );
    157207
    158208        switch_to_blog( $blog_id );
     
    163213        $node_my_account   = $wp_admin_bar->get_node( 'my-account' );
    164214        $node_user_info    = $wp_admin_bar->get_node( 'user-info' );
    165         $node_edit_profile = $wp_admin_bar->get_node( 'edit-profile' );
    166 
    167         // Get primary blog.
    168         $primary = get_active_blog_for_user( self::$editor_id );
    169         $this->assertIsObject( $primary );
    170 
    171         // No Site menu as the user isn't a member of this blog.
    172         $this->assertNull( $node_site_name );
    173 
    174         $primary_profile_url = get_admin_url( $primary->blog_id, 'profile.php' );
    175 
    176         // Ensure the user's primary blog is not the same as the main site.
    177         $this->assertNotEquals( $primary_profile_url, admin_url( 'profile.php' ) );
    178 
    179         // Profile URLs should go to the user's primary blog.
    180         $this->assertSame( $primary_profile_url, $node_my_account->href );
    181         $this->assertSame( $primary_profile_url, $node_user_info->href );
    182         $this->assertSame( $primary_profile_url, $node_edit_profile->href );
    183 
    184         restore_current_blog();
    185     }
    186 
    187     /**
    188      * @ticket 25162
    189      * @group multisite
    190      * @group ms-required
    191      */
    192     public function test_admin_bar_contains_correct_links_for_users_with_no_role_on_network() {
    193         $this->assertTrue( user_can( self::$admin_id, 'read' ) );
    194         $this->assertFalse( user_can( self::$no_role_id, 'read' ) );
    195 
    196         $blog_id = self::factory()->blog->create(
    197             array(
    198                 'user_id' => self::$admin_id,
    199             )
    200         );
    201 
    202         $this->assertTrue( is_user_member_of_blog( self::$admin_id, $blog_id ) );
    203         $this->assertFalse( is_user_member_of_blog( self::$no_role_id, $blog_id ) );
    204         $this->assertTrue( is_user_member_of_blog( self::$no_role_id, get_current_blog_id() ) );
    205 
    206         // Remove `$nobody` from the current blog, so they're not a member of any blog.
    207         $removed = remove_user_from_blog( self::$no_role_id, get_current_blog_id() );
    208 
    209         $this->assertTrue( $removed );
    210         $this->assertFalse( is_user_member_of_blog( self::$no_role_id, get_current_blog_id() ) );
    211 
    212         wp_set_current_user( self::$no_role_id );
    213 
    214         switch_to_blog( $blog_id );
    215 
    216         $wp_admin_bar = $this->get_standard_admin_bar();
    217 
    218         $node_site_name    = $wp_admin_bar->get_node( 'site-name' );
    219         $node_my_account   = $wp_admin_bar->get_node( 'my-account' );
    220         $node_user_info    = $wp_admin_bar->get_node( 'user-info' );
    221         $node_edit_profile = $wp_admin_bar->get_node( 'edit-profile' );
    222215
    223216        // Get primary blog.
     
    235228        $this->assertSame( $user_profile_url, $node_my_account->href );
    236229        $this->assertSame( $user_profile_url, $node_user_info->href );
    237         $this->assertSame( $user_profile_url, $node_edit_profile->href );
    238230
    239231        restore_current_blog();
     
    285277                    'id' => 'test-node',
    286278                ),
    287                 '<div class="ab-item ab-empty-item">',
     279                '<div class="ab-item ab-empty-item">',
    288280            ),
    289281            array(
     
    293285                    'meta' => array( 'tabindex' => '' ),
    294286                ),
    295                 '<div class="ab-item ab-empty-item">',
     287                '<div class="ab-item ab-empty-item">',
    296288            ),
    297289            array(
     
    301293                    'meta' => array( 'tabindex' => '1' ),
    302294                ),
    303                 '<div class="ab-item ab-empty-item" tabindex="1">',
     295                '<div class="ab-item ab-empty-item" tabindex="1">',
    304296            ),
    305297            array(
     
    309301                    'meta' => array( 'tabindex' => '-1' ),
    310302                ),
    311                 '<div class="ab-item ab-empty-item" tabindex="-1">',
     303                '<div class="ab-item ab-empty-item" tabindex="-1">',
    312304            ),
    313305            array(
     
    317309                    'meta' => array( 'tabindex' => '0' ),
    318310                ),
    319                 '<div class="ab-item ab-empty-item" tabindex="0">',
     311                '<div class="ab-item ab-empty-item" tabindex="0">',
    320312            ),
    321313            array(
     
    325317                    'meta' => array( 'tabindex' => 0 ),
    326318                ),
    327                 '<div class="ab-item ab-empty-item" tabindex="0">',
     319                '<div class="ab-item ab-empty-item" tabindex="0">',
    328320            ),
    329321            array(
     
    333325                    'meta' => array( 'tabindex' => 2 ),
    334326                ),
    335                 '<div class="ab-item ab-empty-item" tabindex="2">',
     327                '<div class="ab-item ab-empty-item" tabindex="2">',
    336328            ),
    337329            array(
     
    341333                    'meta' => array( 'tabindex' => false ),
    342334                ),
    343                 '<div class="ab-item ab-empty-item">',
     335                '<div class="ab-item ab-empty-item">',
    344336            ),
    345337        );
Note: See TracChangeset for help on using the changeset viewer.