Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6244184
Accessibility: Require Alt key for arrow navigation in theme and medi…
Sukhendu2002 Apr 13, 2026
dbf5b3e
Merge branch 'trunk' into fix/63760-arrow-key-screen-reader-modal-nav…
Sukhendu2002 Apr 15, 2026
2e73448
fix(a11y): announce modal navigation changes
Sukhendu2002 Jul 7, 2026
1ad59dc
Merge branch 'trunk' into fix/63760-arrow-key-screen-reader-modal-nav…
Sukhendu2002 Jul 7, 2026
0330c1f
Merge branch 'trunk' into fix/63760-arrow-key-screen-reader-modal-nav…
joedolson Jul 21, 2026
d80d6d6
Merge remote-tracking branch 'upstream/trunk' into pr/11560
joedolson Jul 24, 2026
d852e3f
Switch to originalEvent
joedolson Jul 24, 2026
35dd5bc
Update shortcut documentation
joedolson Jul 24, 2026
7ed5d7a
Add support for alt key navigation in customizer in
joedolson Jul 24, 2026
0a0e4cd
Merge branch 'trunk' into fix/63760-arrow-key-screen-reader-modal-nav…
joedolson Jul 26, 2026
f0af403
Remove event repetition gate to restore previous behavior
joedolson Jul 26, 2026
6a80e02
Merge & simplify announce strings
joedolson Jul 26, 2026
1fa386b
Add help text for theme viewer to communicate shortcuts
joedolson Jul 26, 2026
26f8003
debounce announcements
joedolson Jul 26, 2026
e74c7f4
Fix debounce announcement implementation and add docblocks.
afercia Jul 27, 2026
9092055
Cancel pending media announcement when closing the dialog and clean up.
afercia Jul 28, 2026
cdfc1c6
Cancel pending theme announcement when closing the dialog and clean up.
afercia Jul 28, 2026
40041e5
Simplify replacement for wp.a11y.speak.
afercia Jul 28, 2026
f345b8e
Debounce the announcement in the Customizer Themes Section.
afercia Jul 28, 2026
930aaab
Prevent default on Customizer arrow keys navigation.
afercia Jul 28, 2026
b4a70c5
Update QUnit tests.
afercia Jul 28, 2026
7fb34d0
Improve help text and wrap key names within code tags.
afercia Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/js/_enqueues/wp/customize/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,7 @@
filtersHeight: 0,
headerContainer: null,
updateCountDebounced: null,
announceThemeDebounced: null,

/**
* wp.customize.ThemesSection
Expand All @@ -1724,6 +1725,13 @@
section.$body = $( document.body );
api.Section.prototype.initialize.call( section, id, options );
section.updateCountDebounced = _.debounce( section.updateCount, 500 );
section.announceThemeDebounced = _.debounce( function( name ) {
if ( ! name ) {
return;
}

wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', name ) );
}, 500 );
},

/**
Expand Down Expand Up @@ -1777,13 +1785,20 @@
return;
}

// Require the alt key for arrow events.
if ( 27 !== event.keyCode && ! event.altKey ) {
return;
}

Comment thread
joedolson marked this conversation as resolved.
// Pressing the right arrow key fires a theme:next event.
if ( 39 === event.keyCode ) {
event.preventDefault(); // Prevent browser from triggering history shortcuts.
section.nextTheme();
}

// Pressing the left arrow key fires a theme:previous event.
if ( 37 === event.keyCode ) {
event.preventDefault(); // Prevent browser from triggering history shortcuts.
section.previousTheme();
}

Expand Down Expand Up @@ -2602,7 +2617,8 @@
section.$body.addClass( 'modal-open' );
section.containFocus( section.overlay );
section.updateLimits();
wp.a11y.speak( api.settings.l10n.announceThemeDetails.replace( '%s', theme.name ) );

section.announceThemeDebounced( theme.name );
if ( callback ) {
callback();
}
Expand All @@ -2620,6 +2636,8 @@
section.$body.removeClass( 'modal-open' );
section.overlay.fadeOut( 'fast' );
api.control( section.params.action + '_theme_' + section.currentTheme ).container.find( '.theme' ).focus();
// Cancel any pending navigation announcement.
section.announceThemeDebounced.cancel();
},

/**
Expand Down
68 changes: 52 additions & 16 deletions src/js/_enqueues/wp/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ themes = wp.themes = wp.themes || {};
themes.data = _wpThemeSettings;
l10n = themes.data.l10n;

/**
* Announces to screen readers the theme shown after previous/next navigation.
*
* @since 7.1.0
*
* @param {Object} model The theme model.
* @return {void}
*/
themes.announceThemeDebounced = _.debounce( function( model ) {
var name;

if ( ! model ) {
return;
}

name = model.get( 'name' ) || model.get( 'id' );

if ( ! name ) {
return;
}

wp.a11y.speak( l10n.themeViewed.replace( '%s', name ) );
}, 500 );

// Shortcut for isInstall check.
themes.isInstall = !! themes.data.settings.isInstall;

Expand Down Expand Up @@ -549,6 +573,7 @@ themes.view.Theme = wp.Backbone.View.extend({
preview.render();
this.setNavButtonsState();
$( '.next-theme' ).trigger( 'focus' );
themes.announceThemeDebounced( self.current );
})
.listenTo( preview, 'theme:previous', function() {

Expand Down Expand Up @@ -579,6 +604,7 @@ themes.view.Theme = wp.Backbone.View.extend({
preview.render();
this.setNavButtonsState();
$( '.previous-theme' ).trigger( 'focus' );
themes.announceThemeDebounced( self.current );
});

this.listenTo( preview, 'preview:close', function() {
Expand Down Expand Up @@ -769,6 +795,9 @@ themes.view.Details = wp.Backbone.View.extend({
}
});
}

// Cancel any pending navigation announcement.
themes.announceThemeDebounced.cancel();
},

// Handles .disabled classes for next/previous buttons.
Expand Down Expand Up @@ -909,7 +938,7 @@ themes.view.Preview = themes.view.Details.extend({
'click .devices button': 'previewDevice',
'click .previous-theme': 'previousTheme',
'click .next-theme': 'nextTheme',
'keyup': 'keyEvent',
'keydown': 'keyEvent',
'click .theme-install': 'installTheme'
},

Expand Down Expand Up @@ -967,6 +996,9 @@ themes.view.Preview = themes.view.Details.extend({
this.trigger( 'preview:close' );
this.undelegateEvents();
this.unbind();

// Cancel any pending navigation announcement.
themes.announceThemeDebounced.cancel();
return false;
},

Expand Down Expand Up @@ -1012,18 +1044,20 @@ themes.view.Preview = themes.view.Details.extend({
this.close();
}

// Return if Ctrl + Shift or Shift key pressed
if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
// Arrow key navigation requires Alt key to avoid interfering with screen reader navigation.
if ( ! event.altKey ) {
return;
}

// The right arrow key, next theme.
if ( event.keyCode === 39 ) {
_.once( this.nextTheme() );
event.preventDefault();
this.nextTheme();
}

// The left arrow key, previous theme.
if ( event.keyCode === 37 ) {
event.preventDefault();
this.previousTheme();
}
},
Expand Down Expand Up @@ -1111,7 +1145,7 @@ themes.view.Themes = wp.Backbone.View.extend({
} );

// Bind keyboard events.
$( 'body' ).on( 'keyup', function( event ) {
$( 'body' ).on( 'keydown.wp-themes', function( event ) {
if ( ! self.overlay ) {
return;
}
Expand All @@ -1121,25 +1155,27 @@ themes.view.Themes = wp.Backbone.View.extend({
return;
}

// Return if Ctrl + Shift or Shift key pressed
if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
// Pressing the escape key fires a theme:collapse event.
if ( event.keyCode === 27 ) {
self.overlay.collapse( event );
}

// Arrow key navigation requires Alt key to avoid interfering with screen reader navigation.
if ( ! event.altKey ) {
return;
}

// Pressing the right arrow key fires a theme:next event.
// Pressing Alt + right arrow key fires a theme:next event.
if ( event.keyCode === 39 ) {
event.preventDefault();
self.overlay.nextTheme();
}

// Pressing the left arrow key fires a theme:previous event.
// Pressing Alt + left arrow key fires a theme:previous event.
if ( event.keyCode === 37 ) {
event.preventDefault();
self.overlay.previousTheme();
}

// Pressing the escape key fires a theme:collapse event.
if ( event.keyCode === 27 ) {
self.overlay.collapse( event );
}
});
},

Expand Down Expand Up @@ -1322,7 +1358,7 @@ themes.view.Themes = wp.Backbone.View.extend({

// Trigger a route update for the current model.
self.theme.trigger( 'theme:expand', nextModel.cid );

themes.announceThemeDebounced( nextModel );
}
},

Expand All @@ -1349,7 +1385,7 @@ themes.view.Themes = wp.Backbone.View.extend({

// Trigger a route update for the current model.
self.theme.trigger( 'theme:expand', previousModel.cid );

themes.announceThemeDebounced( previousModel );
}
},

Expand Down
56 changes: 47 additions & 9 deletions src/js/media/views/frame/edit-attachments.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var Frame = wp.media.view.Frame,
MediaFrame = wp.media.view.MediaFrame,
l10n = wp.media.view.l10n,

$ = jQuery,
EditAttachments;
Expand Down Expand Up @@ -33,6 +34,30 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
'click .right': 'nextMediaItem'
},

/**
* Announces to screen readers the attachment shown after previous/next navigation.
*
* @since 7.1.0
*
* @param {Object} model The attachment model.
* @return {void}
*/
announceMediaItemDebounced: _.debounce( function( model ) {
var title;

if ( ! model ) {
return;
}

title = model.get( 'title' ) || model.get( 'filename' ) || model.get( 'id' );

if ( ! title ) {
return;
}

wp.a11y.speak( l10n.mediaItemViewed.replace( '%s', title ) );
}, 500 ),

initialize: function() {
Frame.prototype.initialize.apply( this, arguments );

Expand Down Expand Up @@ -96,6 +121,8 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
// Move focus back to the original item in the grid if possible.
$( 'li.attachment[data-id="' + this.model.get( 'id' ) +'"]' ).trigger( 'focus' );
this.resetRoute();
// Cancel any pending navigation announcement.
this.announceMediaItemDebounced.cancel();
}, this ) );

// Set this frame as the modal's content.
Expand Down Expand Up @@ -202,26 +229,34 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
* Click handler to switch to the previous media item.
*/
previousMediaItem: function() {
var model;

if ( ! this.hasPrevious() ) {
return;
}

this.trigger( 'refresh', this.library.at( this.getCurrentIndex() - 1 ) );
model = this.library.at( this.getCurrentIndex() - 1 );
this.trigger( 'refresh', model );
// Move focus to the Previous button. When there are no more items, to the Next button.
this.focusNavButton( this.hasPrevious() ? '.left' : '.right' );
this.announceMediaItemDebounced( model );
},

/**
* Click handler to switch to the next media item.
*/
nextMediaItem: function() {
var model;

if ( ! this.hasNext() ) {
return;
}

this.trigger( 'refresh', this.library.at( this.getCurrentIndex() + 1 ) );
model = this.library.at( this.getCurrentIndex() + 1 );
this.trigger( 'refresh', model );
// Move focus to the Next button. When there are no more items, to the Previous button.
this.focusNavButton( this.hasNext() ? '.right' : '.left' );
this.announceMediaItemDebounced( model );
},

/**
Expand All @@ -247,25 +282,28 @@ EditAttachments = MediaFrame.extend(/** @lends wp.media.view.MediaFrame.EditAtta
return ( this.getCurrentIndex() - 1 ) > -1;
},
/**
* Respond to the keyboard events: right arrow, left arrow, except when
* focus is in a textarea or input field.
* Respond to the keyboard events: Alt + right arrow, Alt + left arrow,
* except when focus is in a form field. Requires the Alt modifier key to
* avoid interfering with screen reader navigation.
*/
keyEvent: function( event ) {
if ( ( 'INPUT' === event.target.nodeName || 'TEXTAREA' === event.target.nodeName ) && ! event.target.disabled ) {
if ( ( 'INPUT' === event.target.nodeName || 'TEXTAREA' === event.target.nodeName || 'SELECT' === event.target.nodeName ) && ! event.target.disabled ) {
return;
}

// Return if Ctrl + Shift or Shift key pressed
if ( event.shiftKey || ( event.ctrlKey && event.shiftKey ) ) {
// Arrow key navigation requires Alt key to avoid interfering with screen reader navigation.
if ( ! event.altKey ) {
return;
}

// The right arrow key.
// Alt + right arrow key.
if ( 39 === event.keyCode ) {
event.preventDefault();
this.nextMediaItem();
}
// The left arrow key.
// Alt + left arrow key.
if ( 37 === event.keyCode ) {
event.preventDefault();
this.previousMediaItem();
}
},
Expand Down
2 changes: 2 additions & 0 deletions src/wp-admin/theme-install.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
/* translators: %d: Number of themes. */
'themesFound' => __( 'Number of Themes found: %d' ),
'noThemesFound' => __( 'No themes found. Try a different search.' ),
/* translators: %s: Theme name. */
'themeViewed' => __( 'Theme details: %s' ),
'collapseSidebar' => __( 'Collapse Sidebar' ),
'expandSidebar' => __( 'Expand Sidebar' ),
/* translators: Hidden accessibility text. */
Expand Down
9 changes: 6 additions & 3 deletions src/wp-admin/themes.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@
if ( current_user_can( 'switch_themes' ) ) {
$help_overview = '<p>' . __( 'This screen is used for managing your installed themes. Aside from the default theme(s) included with your WordPress installation, themes are designed and developed by third parties.' ) . '</p>' .
'<p>' . __( 'From this screen you can:' ) . '</p>' .
'<ul><li>' . __( 'Hover or tap to see Activate and Live Preview buttons' ) . '</li>' .
'<li>' . __( 'Click on the theme to see the theme name, version, author, description, tags, and the Delete link' ) . '</li>' .
'<li>' . __( 'Click Customize for the active theme or Live Preview for any other theme to see a live preview' ) . '</li></ul>' .
'<ul><li>' . __( 'Hover or tap to see Activate and Live Preview buttons.' ) . '</li>' .
'<li>' . __( 'Click Customize for the active theme or Live Preview for any other theme to see a live preview.' ) . '</li>' .
'<li>' . __( 'Click on a theme to open the Theme Details dialog and see the theme name, version, author, description, tags, and the Delete link.' ) . '</li>' .
'<li>' . __( 'Use the buttons at the top of the dialog, or <code>alt</code>/<code>option</code> plus the left or right arrow keys on your keyboard, to navigate between themes quickly.' ) . '</li></ul>' .
'<p>' . __( 'The active theme is displayed highlighted as the first theme.' ) . '</p>' .
'<p>' . __( 'The search for installed themes will search for terms in their name, description, author, or tag.' ) . ' <span id="live-search-desc">' . __( 'The search results will be updated as you type.' ) . '</span></p>';

Expand Down Expand Up @@ -236,6 +237,8 @@
/* translators: %d: Number of themes. */
'themesFound' => __( 'Number of Themes found: %d' ),
'noThemesFound' => __( 'No themes found. Try a different search.' ),
/* translators: %s: Theme name. */
'themeViewed' => __( 'Theme details: %s' ),
),
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/wp-admin/upload.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function () {
'title' => __( 'Attachment Details' ),
'content' =>
'<p>' . __( 'Clicking an item will display an Attachment Details dialog, which allows you to preview media and make quick edits. Any changes you make to the attachment details will be automatically saved.' ) . '</p>' .
'<p>' . __( 'Use the arrow buttons at the top of the dialog, or the left and right arrow keys on your keyboard, to navigate between media items quickly.' ) . '</p>' .
'<p>' . __( 'Use the buttons at the top of the dialog, or <code>alt</code>/<code>option</code> plus the left or right arrow keys on your keyboard, to navigate between media items quickly.' ) . '</p>' .
'<p>' . __( 'You can also delete individual items and access the extended edit screen from the details dialog.' ) . '</p>',
)
);
Expand Down
2 changes: 1 addition & 1 deletion src/wp-includes/class-wp-customize-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4959,7 +4959,7 @@ public function customize_pane_settings() {
/* translators: %d: Number of themes being displayed, which cannot currently consider singular vs. plural forms. */
'announceThemeCount' => __( 'Displaying %d themes' ),
/* translators: %s: Theme name. */
'announceThemeDetails' => __( 'Showing details for theme: %s' ),
'announceThemeDetails' => __( 'Theme details: %s' ),
),
);

Expand Down
Loading
Loading