Make WordPress Core


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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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                }
Note: See TracChangeset for help on using the changeset viewer.