Make WordPress Core

Changeset 64241


Ignore:
Timestamp:
10/08/2026 11:33:48 AM (9 days ago)
Author:
afercia
Message:

Administration: Improve focus management and screen reader announcements when navigating the media and theme browsers.

In the various media and theme browsers, the announcements for screen readers were sometimes too noisy because focus was set too often and unnecessarily on some elements. To improve the announcements and move focus only when really necessary, all the media and theme browsers are now standardized to the following pattern:

  • Makes sure all the media and theme browsers have a dialog role.
  • Keeps the navigation buttons focusable by using aria-disabled instead of disabled.
  • Ensures that screen reader announcements are cleaner by keeping focus on the activated control rather than moving it to another element.
  • Makes sure initial focus is set to the dialog element when it is opened.
  • Makes sure to return focus to the opener control when the dialog is closed.
  • Makes sure the opener control is a focusable element.
  • Adjusts styles accordingly to take the aria-disabled attribute into account.
  • Simplifies the buttons accessible name to: Previous / Next / Close.
  • Makes sure the ARIA live regions are not hidden when they inherit visibility: hidden.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/13816

Props sukhendu2002, khokansardar, afercia.
Fixes #65746.

Location:
trunk
Files:
12 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/wp/customize/controls.js

    r63600 r64241  
    25982598                updateLimits: function () {
    25992599                        if ( ! this.getNextTheme() ) {
    2600                                 this.overlay.find( '.right' ).addClass( 'disabled' );
     2600                                this.overlay.find( '.right' )
     2601                                        .addClass( 'disabled' )
     2602                                        .attr( 'aria-disabled', true );
    26012603                        }
    26022604                        if ( ! this.getPreviousTheme() ) {
    2603                                 this.overlay.find( '.left' ).addClass( 'disabled' );
     2605                                this.overlay.find( '.left' )
     2606                                        .addClass( 'disabled' )
     2607                                        .attr( 'aria-disabled', true );
    26042608                        }
    26052609                },
  • trunk/src/js/_enqueues/wp/theme.js

    r63766 r64241  
    481481                }
    482482
    483                 // Set focused theme to current element.
     483                // Store the theme that had focus when expand is called.
    484484                themes.focusedTheme = this.$el;
    485485
    … …  
    521521                event = event || window.event;
    522522
    523                 // Set focus to current theme.
     523                // Store the theme that had focus when preview is called.
    524524                themes.focusedTheme = this.$el;
    525525
    … …  
    621621                        previousThemeButton
    622622                                .addClass( 'disabled' )
    623                                 .prop( 'disabled', true );
    624 
    625                         nextThemeButton.trigger( 'focus' );
     623                                .attr( 'aria-disabled', true )
     624                                .trigger( 'focus' );
    626625                }
    627626
    … …  
    630629                        nextThemeButton
    631630                                .addClass( 'disabled' )
    632                                 .prop( 'disabled', true );
    633 
    634                         previousThemeButton.trigger( 'focus' );
     631                                .attr( 'aria-disabled', true )
     632                                .trigger( 'focus' );
    635633                }
    636634        },
    … …  
    726724        // Set initial focus and constrain tabbing within the theme browser modal.
    727725        containFocus: function( $el ) {
    728 
    729                 // Set initial focus on the primary action control.
    730                 _.delay( function() {
    731                         $( '.theme-overlay' ).trigger( 'focus' );
    732                 }, 100 );
     726                // Set initial focus on the theme overlay modal dialog, only on first render.
     727                var $themeOverlay = $( '.theme-overlay' );
     728                if ( $themeOverlay.hasClass( 'theme-overlay-initial' ) ) {
     729                        _.delay( function() {
     730                                $themeOverlay.trigger( 'focus' ).removeClass( 'theme-overlay-initial' );
     731                        }, 100 );
     732                }
     733
    733734
    734735                // Constrain tabbing within the modal.
    … …  
    786787                                document.body.scrollTop = scroll;
    787788
    788                                 // Return focus to the theme div.
     789                                // Return focus to the opener theme 'more-details' button.
    789790                                if ( themes.focusedTheme ) {
    790                                         themes.focusedTheme.find('.more-details').trigger( 'focus' );
     791                                        themes.focusedTheme.find( '.more-details' ).trigger( 'focus' );
    791792                                }
     793
     794                                // Add back the `theme-overlay-initial` class for the theme overlay.
     795                                $( '.theme-overlay' ).addClass( 'theme-overlay-initial' );
    792796                        });
    793797                }
    … …  
    804808                        this.$el.find( '.left' )
    805809                                .addClass( 'disabled' )
    806                                 .prop( 'disabled', true );
     810                                .attr( 'aria-disabled', true );
    807811                }
    808812                if ( this.model.cid === this.model.collection.at( this.model.collection.length - 1 ).cid ) {
    809813                        this.$el.find( '.right' )
    810814                                .addClass( 'disabled' )
    811                                 .prop( 'disabled', true );
     815                                .attr( 'aria-disabled', true );
    812816                }
    813817        },
    … …  
    961965                this.$el.fadeIn( 200, function() {
    962966                        $body.addClass( 'theme-installer-active full-overlay-active' );
     967                        // Set initial focus on the theme installer overlay modal dialog.
     968                        self.$el.trigger( 'focus' );
    963969                });
    964970
    … …  
    977983                        $( 'body' ).removeClass( 'theme-installer-active full-overlay-active' );
    978984
    979                         // Return focus to the theme div.
     985                        // Return focus to the opener theme 'more-details' button.
    980986                        if ( themes.focusedTheme ) {
    981                                 themes.focusedTheme.find('.more-details').trigger( 'focus' );
     987                                themes.focusedTheme.find( '.more-details' ).trigger( 'focus' );
    982988                        }
    983989                }).removeClass( 'iframe-ready' );
    … …  
    13551361                        // Trigger a route update for the current model.
    13561362                        self.theme.trigger( 'theme:expand', nextModel.cid );
     1363                        // Set focus again to the 'Next' navigation button after the view fully re-rendeers.
     1364                        $( '.theme-header .right' ).trigger( 'focus' );
    13571365                        themes.announceThemeDebounced( nextModel );
    13581366                }
    … …  
    13901398                        // Trigger a route update for the current model.
    13911399                        self.theme.trigger( 'theme:expand', previousModel.cid );
     1400                        // Set focus again to the 'Previous' navigation button after the view fully re-rendeers.
     1401                        $( '.theme-header .left' ).trigger( 'focus' );
    13921402                        themes.announceThemeDebounced( previousModel );
    13931403                }
  • trunk/src/js/media/views/frame/edit-attachments.js

    r63409 r64241  
    202202
    203203        toggleNav: function() {
    204                 this.$( '.left' ).prop( 'disabled', ! this.hasPrevious() );
    205                 this.$( '.right' ).prop( 'disabled', ! this.hasNext() );
     204                this.$( '.left' ).attr( 'aria-disabled', ! this.hasPrevious() );
     205                this.$( '.right' ).attr( 'aria-disabled', ! this.hasNext() );
    206206        },
    207207
    … …  
    239239                model = this.library.at( this.getCurrentIndex() - 1 );
    240240                this.trigger( 'refresh', model );
    241                 // Move focus to the Previous button. When there are no more items, to the Next button.
    242                 this.focusNavButton( this.hasPrevious() ? '.left' : '.right' );
    243241                this.announceMediaItemDebounced( model );
    244242        },
    … …  
    256254                model = this.library.at( this.getCurrentIndex() + 1 );
    257255                this.trigger( 'refresh', model );
    258                 // Move focus to the Next button. When there are no more items, to the Previous button.
    259                 this.focusNavButton( this.hasNext() ? '.right' : '.left' );
    260256                this.announceMediaItemDebounced( model );
    261         },
    262 
    263         /**
    264          * Set focus to the navigation buttons depending on the browsing direction.
    265          *
    266          * @since 5.3.0
    267          *
    268          * @param {string} which A CSS selector to target the button to focus.
    269          */
    270         focusNavButton: function( which ) {
    271                 $( which ).trigger( 'focus' );
    272257        },
    273258
  • trunk/src/wp-admin/css/colors/_admin.scss

    r63996 r64241  
    774774        &.wp-customizer .theme-overlay .theme-header .close:focus,
    775775        &.wp-customizer .theme-overlay .theme-header .close:hover,
    776         &.wp-customizer .theme-overlay .theme-header .right:focus,
    777         &.wp-customizer .theme-overlay .theme-header .right:hover,
    778         &.wp-customizer .theme-overlay .theme-header .left:focus,
    779         &.wp-customizer .theme-overlay .theme-header .left:hover {
     776        &.wp-customizer .theme-overlay .theme-header .right:focus:not(.disabled),
     777        &.wp-customizer .theme-overlay .theme-header .right:hover:not(.disabled),
     778        &.wp-customizer .theme-overlay .theme-header .left:focus:not(.disabled),
     779        &.wp-customizer .theme-overlay .theme-header .left:hover:not(.disabled) {
    780780                border-bottom-color: variables.$button-color;
    781781                color: variables.$link;
  • trunk/src/wp-admin/css/customize-controls.css

    r64229 r64241  
    24562456.wp-customizer .theme-overlay .theme-header .close:focus,
    24572457.wp-customizer .theme-overlay .theme-header .close:hover,
    2458 .wp-customizer .theme-overlay .theme-header .right:focus,
    2459 .wp-customizer .theme-overlay .theme-header .right:hover,
    2460 .wp-customizer .theme-overlay .theme-header .left:focus,
    2461 .wp-customizer .theme-overlay .theme-header .left:hover {
     2458.wp-customizer .theme-overlay .theme-header .right:focus:not(.disabled),
     2459.wp-customizer .theme-overlay .theme-header .right:hover:not(.disabled),
     2460.wp-customizer .theme-overlay .theme-header .left:focus:not(.disabled),
     2461.wp-customizer .theme-overlay .theme-header .left:hover:not(.disabled) {
    24622462        background: #fff;
    24632463}
  • trunk/src/wp-admin/css/media.css

    r63987 r64241  
    726726.edit-attachment-frame .edit-media-header .left:focus,
    727727.edit-attachment-frame .edit-media-header .right:focus {
    728         /* Only visible in Windows High Contrast mode */
    729         outline: 2px solid transparent;
    730         outline-offset: -2px;
     728        /* Always visible also in Windows High Contrast mode */
     729        outline: 1px solid #c3c4c7;;
     730        outline-offset: -1px;
    731731}
    732732
    … …  
    746746}
    747747
    748 .edit-attachment-frame .edit-media-header [disabled],
    749 .edit-attachment-frame .edit-media-header [disabled]:hover {
     748.edit-attachment-frame .edit-media-header [aria-disabled="true"],
     749.edit-attachment-frame .edit-media-header [aria-disabled="true"]:hover,
     750.edit-attachment-frame .edit-media-header [aria-disabled="true"]:focus {
    750751        color: #c3c4c7;
    751752        background: inherit;
  • trunk/src/wp-admin/css/themes.css

    r63998 r64241  
    14071407}
    14081408
     1409/* Make sure the ARIA live regions are visible, as they inherit the hidden visibility from the body. */
     1410.a11y-speak-intro-text,
     1411.a11y-speak-region {
     1412        visibility: visible;
     1413}
     1414
    14091415.wp-full-overlay {
    14101416        background: #1d2327;
    … …  
    15621568}
    15631569
     1570.theme-install-overlay .close-full-overlay:focus,
     1571.theme-install-overlay .previous-theme:focus,
     1572.theme-install-overlay .next-theme:focus {
     1573        /* Always visible also in Windows High Contrast mode */
     1574        outline: 1px solid #c3c4c7;;
     1575        outline-offset: -1px;
     1576}
     1577
    15641578.theme-install-overlay .close-full-overlay::before {
    15651579        font: 400 22px/1 dashicons;
    … …  
    15981612        background: #f0f0f1;
    15991613        cursor: default;
    1600         pointer-events: none;
    16011614}
    16021615
  • trunk/src/wp-admin/includes/theme.php

    r63494 r64241  
    829829                                        <?php
    830830                                        /* translators: Hidden accessibility text. */
    831                                         _e( 'Show previous theme' );
     831                                        _ex( 'Previous', 'theme' );
    832832                                        ?>
    833833                                </span></button>
    … …  
    835835                                        <?php
    836836                                        /* translators: Hidden accessibility text. */
    837                                         _e( 'Show next theme' );
     837                                        _ex( 'Next', 'theme' );
    838838                                        ?>
    839839                                </span></button>
  • trunk/src/wp-admin/theme-install.php

    r62878 r64241  
    282282        </h2>
    283283        <div class="theme-browser content-filterable"></div>
    284         <div class="theme-install-overlay wp-full-overlay expanded"></div>
     284        <div class="theme-install-overlay wp-full-overlay expanded" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Browse Themes' ); ?>"></div>
    285285
    286286        <p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
    … …  
    391391        <# } #>
    392392
     393        <# if ( data.compatible_wp && data.compatible_php ) { #>
     394        <button type="button" class="more-details"><?php _ex( 'Details &amp; Preview', 'theme' ); ?></button>
     395        <# } else { #>
    393396        <span class="more-details"><?php _ex( 'Details &amp; Preview', 'theme' ); ?></span>
     397        <# } #>
    394398        <div class="theme-author">
    395399                <?php
    … …  
    474478                                <?php
    475479                                /* translators: Hidden accessibility text. */
    476                                 _e( 'Previous theme' );
     480                                _ex( 'Previous', 'theme' );
    477481                                ?>
    478482                        </span></button>
    … …  
    480484                                <?php
    481485                                /* translators: Hidden accessibility text. */
    482                                 _e( 'Next theme' );
     486                                _ex( 'Next', 'theme' );
    483487                                ?>
    484488                        </span></button>
  • trunk/src/wp-admin/themes.php

    r62878 r64241  
    665665        </div>
    666666</div>
    667 <div class="theme-overlay" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
     667<div class="theme-overlay theme-overlay-initial" tabindex="0" role="dialog" aria-label="<?php esc_attr_e( 'Theme Details' ); ?>"></div>
    668668
    669669<p class="no-themes"><?php _e( 'No themes found. Try a different search.' ); ?></p>
    … …  
    10581058                                <?php
    10591059                                /* translators: Hidden accessibility text. */
    1060                                 _e( 'Show previous theme' );
     1060                                _ex( 'Previous', 'theme' );
    10611061                                ?>
    10621062                        </span></button>
    … …  
    10641064                                <?php
    10651065                                /* translators: Hidden accessibility text. */
    1066                                 _e( 'Show next theme' );
     1066                                _ex( 'Next', 'theme' );
    10671067                                ?>
    10681068                        </span></button>
    … …  
    10701070                                <?php
    10711071                                /* translators: Hidden accessibility text. */
    1072                                 _e( 'Close details dialog' );
     1072                                _e( 'Close' );
    10731073                                ?>
    10741074                        </span></button>
  • trunk/src/wp-includes/media-template.php

    r63612 r64241  
    207207                                        <?php
    208208                                        /* translators: Hidden accessibility text. */
    209                                         _e( 'Close dialog' );
     209                                        _e( 'Close' );
    210210                                        ?>
    211211                                </span></button>
    … …  
    370370        <script type="text/html" id="tmpl-edit-attachment-frame">
    371371                <div class="edit-media-header">
    372                         <button class="left dashicons"<# if ( ! data.hasPrevious ) { #> disabled<# } #>><span class="screen-reader-text"><?php /* translators: Hidden accessibility text. */ _e( 'Edit previous media item' ); ?></span></button>
    373                         <button class="right dashicons"<# if ( ! data.hasNext ) { #> disabled<# } #>><span class="screen-reader-text"><?php /* translators: Hidden accessibility text. */ _e( 'Edit next media item' ); ?></span></button>
    374                         <button type="button" class="media-modal-close"><span class="media-modal-icon" aria-hidden="true"></span><span class="screen-reader-text"><?php _e( 'Close dialog' ); ?></span></button>
     372                        <button class="left dashicons"<# if ( ! data.hasPrevious ) { #> aria-disabled='true'<# } #>><span class="screen-reader-text"><?php /* translators: Hidden accessibility text. */ _ex( 'Previous', 'media item' ); ?></span></button>
     373                        <button class="right dashicons"<# if ( ! data.hasNext ) { #> aria-disabled='true'<# } #>><span class="screen-reader-text"><?php /* translators: Hidden accessibility text. */ _ex( 'Next', 'media item' ); ?></span></button>
     374                        <button type="button" class="media-modal-close"><span class="media-modal-icon" aria-hidden="true"></span><span class="screen-reader-text"><?php _e( 'Close' ); ?></span></button>
    375375                </div>
    376376                <div class="media-frame-title"></div>
  • trunk/tests/qunit/index.html

    r63336 r64241  
    10021002        <script type="text/html" id="tmpl-media-modal">
    10031003                <div class="media-modal wp-core-ui">
    1004                         <button type="button" class="media-modal-close"><span class="media-modal-icon" aria-hidden="true"></span><span class="screen-reader-text">Close media panel</span></button>
     1004                        <button type="button" class="media-modal-close"><span class="media-modal-icon" aria-hidden="true"></span><span class="screen-reader-text">Close</span></button>
    10051005                        <div class="media-modal-content"></div>
    10061006                </div>
    … …  
    10821082        <script type="text/html" id="tmpl-edit-attachment-frame">
    10831083                <div class="edit-media-header">
    1084                         <button class="left dashicons <# if ( ! data.hasPrevious ) { #> disabled <# } #>"><span class="screen-reader-text">Edit previous media item</span></button>
    1085                         <button class="right dashicons <# if ( ! data.hasNext ) { #> disabled <# } #>"><span class="screen-reader-text">Edit next media item</span></button>
     1084                        <button class="left dashicons <# if ( ! data.hasPrevious ) { #> disabled <# } #>"><span class="screen-reader-text">Previous</span></button>
     1085                        <button class="right dashicons <# if ( ! data.hasNext ) { #> disabled <# } #>"><span class="screen-reader-text">Next</span></button>
    10861086                </div>
    10871087                <div class="media-frame-title"></div>
Note: See TracChangeset for help on using the changeset viewer.