From 690a50c1bffe631631068fa3653bde5a91e1d6e6 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Thu, 21 May 2026 00:18:55 +0530 Subject: [PATCH] Admin Menu: Fix multiple active menu highlights on mobile. Use :focus-visible for admin menu focus indicators so touch taps do not leave a stale active appearance on previously selected items. Clear the responsive selected state when another top-level menu is opened or when navigating via a submenu link. Limit admin menu view transitions to the current menu item to avoid overlapping active styles during navigation. Fixes #65280. --- src/js/_enqueues/admin/common.js | 28 ++++++++++++++++--- src/wp-admin/css/admin-menu.css | 21 ++++++++++---- src/wp-admin/css/view-transitions.css | 3 +- .../wpEnqueueViewTransitionsAdminCss.php | 1 + 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index 2a7daba0d2dc4..1424642c4640a 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -1827,16 +1827,34 @@ $( function() { // Add menu events. $adminmenu.on( 'click.wp-responsive', 'li.wp-has-submenu > a', function( event ) { + var $link, $menuItem, state; + if ( ! $adminmenu.data('wp-responsive') ) { return; } - let state = ( 'false' === $( this ).attr( 'aria-expanded' ) ) ? 'true' : 'false'; - $( this ).parent( 'li' ).toggleClass( 'selected' ); - $( this ).attr( 'aria-expanded', state ); - $( this ).trigger( 'focus' ); + + $link = $( this ); + $menuItem = $link.parent( 'li' ); + state = ( 'false' === $link.attr( 'aria-expanded' ) ) ? 'true' : 'false'; + + $adminmenu.find( 'li.selected' ).not( $menuItem ).removeClass( 'selected' ) + .children( 'a' ).attr( 'aria-expanded', 'false' ); + + $menuItem.toggleClass( 'selected' ); + $link.attr( 'aria-expanded', state ); + $link.trigger( 'focus' ); event.preventDefault(); }); + $adminmenu.on( 'click.wp-responsive', '.wp-submenu a', function() { + if ( ! $adminmenu.data( 'wp-responsive' ) ) { + return; + } + + $adminmenu.find( 'li.selected' ).removeClass( 'selected' ) + .children( 'a' ).attr( 'aria-expanded', 'false' ); + }); + self.trigger(); $document.on( 'wp-window-resized.wp-responsive', this.trigger.bind( this ) ); @@ -1886,6 +1904,8 @@ $( function() { } $adminmenu.data( 'wp-responsive', 1 ); + $adminmenu.find( 'li.selected' ).not( '.wp-has-current-submenu' ).removeClass( 'selected' ) + .children( 'a' ).attr( 'aria-expanded', 'false' ); this.disableSortables(); }, diff --git a/src/wp-admin/css/admin-menu.css b/src/wp-admin/css/admin-menu.css index c4b32ac4b9e87..3e539f182ae25 100644 --- a/src/wp-admin/css/admin-menu.css +++ b/src/wp-admin/css/admin-menu.css @@ -101,20 +101,24 @@ } #adminmenu a:hover, -#adminmenu li.menu-top > a:focus, +#adminmenu li.menu-top > a:focus-visible, #adminmenu .wp-submenu a:hover, -#adminmenu .wp-submenu a:focus { +#adminmenu .wp-submenu a:focus-visible { color: #72aee6; } #adminmenu a:hover, -#adminmenu a:focus, +#adminmenu a:focus-visible, .folded #adminmenu .wp-submenu-head:hover { box-shadow: inset 4px 0 0 0 currentColor; transition: box-shadow .1s linear; border-radius: 0; } +#adminmenu li.wp-not-current-submenu > a.menu-top:focus:not(:focus-visible) { + box-shadow: none; +} + #adminmenu li.menu-top { border: none; min-height: 34px; @@ -167,15 +171,20 @@ /* ensure that wp-submenu's box shadow doesn't appear on top of the focused menu item's background. */ #adminmenu li.menu-top:hover, #adminmenu li.opensub > a.menu-top, -#adminmenu li > a.menu-top:focus { +#adminmenu li > a.menu-top:focus-visible { position: relative; background-color: #1d2327; color: #72aee6; } +#adminmenu li.wp-not-current-submenu > a.menu-top:focus:not(:focus-visible) { + background-color: transparent; + color: #f0f0f1; +} + .folded #adminmenu li.menu-top:hover, .folded #adminmenu li.opensub > a.menu-top, -.folded #adminmenu li > a.menu-top:focus { +.folded #adminmenu li > a.menu-top:focus-visible { z-index: 10000; } @@ -641,7 +650,7 @@ li#wp-admin-bar-menu-toggle { .auto-fold #adminmenu li.menu-top:hover, .auto-fold #adminmenu li.opensub > a.menu-top, - .auto-fold #adminmenu li > a.menu-top:focus { + .auto-fold #adminmenu li > a.menu-top:focus-visible { z-index: 10000; } diff --git a/src/wp-admin/css/view-transitions.css b/src/wp-admin/css/view-transitions.css index 538a90b502f9e..d67055ba12336 100644 --- a/src/wp-admin/css/view-transitions.css +++ b/src/wp-admin/css/view-transitions.css @@ -3,7 +3,8 @@ navigation: auto; } - #adminmenu > .menu-top { + #adminmenu > .menu-top.wp-has-current-submenu, + #adminmenu > .menu-top.current { view-transition-name: attr(id type(), none); } } diff --git a/tests/phpunit/tests/view-transitions/wpEnqueueViewTransitionsAdminCss.php b/tests/phpunit/tests/view-transitions/wpEnqueueViewTransitionsAdminCss.php index 0e977e69606f9..e02926ce9b29e 100644 --- a/tests/phpunit/tests/view-transitions/wpEnqueueViewTransitionsAdminCss.php +++ b/tests/phpunit/tests/view-transitions/wpEnqueueViewTransitionsAdminCss.php @@ -49,6 +49,7 @@ public function test_inline_css_included() { $this->assertIsArray( $after_data, 'Expected `after` data to be an array.' ); $css = wp_get_view_transitions_admin_css(); $this->assertStringContainsString( '@view-transition', $css ); + $this->assertStringContainsString( '.menu-top.wp-has-current-submenu', $css ); $this->assertContains( $css, $after_data ); }