Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
73b207e
List Tables: Allow horizontal scrolling on desktop.
mattmiklic Sep 23, 2026
26dee73
List Tables: Keep Comments sort arrows beside the icon.
mattmiklic Sep 23, 2026
b996dd5
List Tables: Indicate horizontal overflow with a shadow.
mattmiklic Sep 23, 2026
1436c6f
List Tables: Use JavaScript for scroll shadows.
mattmiklic Sep 23, 2026
8d3511f
List Tables: Indicate overflow at both horizontal edges.
mattmiklic Sep 23, 2026
4385e19
List Tables: Use the admin RTL class for scroll shadows.
mattmiklic Sep 23, 2026
a1cf6e4
List Tables: Add scroll shadow regression tests.
mattmiklic Sep 23, 2026
bc1703f
List Tables: Add accessible scroll controls.
mattmiklic Sep 24, 2026
4009efc
List Tables: Replace scroll controls with edge fades.
mattmiklic Sep 25, 2026
19a7caa
List Tables: Preserve keyboard focus at mobile widths.
mattmiklic Sep 25, 2026
3b07994
List Tables: Keep sorting focus rings visible.
mattmiklic Sep 30, 2026
6fce642
List Tables: Skip tab stops for non-scrolling wrappers.
mattmiklic Sep 30, 2026
b342025
List Tables: Align overflow code with coding standards.
mattmiklic Sep 30, 2026
b48e146
List Tables: Merge the latest trunk changes.
mattmiklic Sep 30, 2026
05e34dc
List Tables: Refine overflow fades and focus outline.
mattmiklic Sep 30, 2026
d75ecfc
List Tables: Keep inline editing within the scroll viewport.
mattmiklic Oct 20, 2026
5eb211c
List Tables: Preserve checkbox column spacing.
mattmiklic Oct 20, 2026
b974418
Merge branch 'trunk' into improve/list-table-overflow
ciampo Oct 20, 2026
9fb1d8d
Merge branch 'trunk' into improve/list-table-overflow
westonruter Oct 20, 2026
2f074e0
Add missing types for properties on list table subclass
westonruter Oct 20, 2026
64bd1ef
List Tables: Refine markup and overflow initialization.
mattmiklic Oct 21, 2026
f91fac9
List Tables: Contain oversized content in comment cells.
mattmiklic Oct 21, 2026
00ff490
List Tables: Scroll oversized content within cells.
mattmiklic Oct 21, 2026
39d045b
List Tables: Allow scrolling content columns to shrink.
mattmiklic Oct 21, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ wp-tests-config.php
/tests/phpunit/data/plugins/wordpress-importer
/tests/phpunit/data/.trac-ticket-cache*
/tests/qunit/compiled.html
/tests/qunit/common-compiled.html
/tests/performance/**/*.test.results.json
/src/.wp-tests-version
/node_modules
Expand Down
8 changes: 5 additions & 3 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ module.exports = function(grunt) {
cwd: WORKING_DIR,
src: []
},
qunit: ['tests/qunit/compiled.html'],
qunit: [ 'tests/qunit/compiled.html', 'tests/qunit/common-compiled.html' ],

// This is only meant to run within a numbered branch after branching has occurred.
workflows: {
Expand Down Expand Up @@ -663,8 +663,10 @@ module.exports = function(grunt) {
files: {}
},
qunit: {
src: 'tests/qunit/index.html',
dest: 'tests/qunit/compiled.html',
files: {
'tests/qunit/compiled.html': 'tests/qunit/index.html',
'tests/qunit/common-compiled.html': 'tests/qunit/common.html',
},
options: {
processContent: function( src ) {
return src.replace( /(\".+?\/)build(\/.+?)(?:.min)?(.js\")/g , function( match, $1, $2, $3 ) {
Expand Down
177 changes: 177 additions & 0 deletions src/js/_enqueues/admin/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,183 @@ $( function( $ ) {

}( jQuery, window ));

/**
* Manages overflow indicators and keyboard access for list tables.
*
* @since 7.2.0
*/
jQuery( function() {
const content = document.getElementById( 'wpbody-content' );
if (
! content ||
! content.querySelector( '.wp-list-table-scroll' )
) {
return;
}

/** @type {Map<HTMLTableElement, { wrapper: HTMLElement, update: function(): void, cleanup: function(): void }>} */
const tables = new Map();
Comment thread
westonruter marked this conversation as resolved.

/**
* Restores an element's original Tab order after overflow tracking ends.
*
* @since 7.2.0
*
* @param {HTMLElement} element The previously observed element.
* @param {?string} tabIndex Its original tabindex attribute.
*
* @return {void}
*/
function restoreTabIndex( element, tabIndex ) {
if ( tabIndex === null ) {
element.removeAttribute( 'tabindex' );
} else {
element.setAttribute( 'tabindex', tabIndex );
}
}

/**
* Tracks a table's scroll position and dimensions.
*
* @since 7.2.0
*
* @param {HTMLTableElement} table The table inside a scroll wrapper.
*
* @return {void}
*/
function observeTableOverflow( table ) {
const wrapper = table.parentElement;
const tabIndex = wrapper.getAttribute( 'tabindex' );
const contentAreas = new Set();

/**
* Updates the overflow indicators and the wrapper's place in the Tab order.
*
* @since 7.2.0
*
* @return {void}
*/
function update() {
contentAreas.forEach( function( area ) {
if ( area.closest( '.wp-list-table' ) !== table || ! table.contains( area ) ) {
observer.unobserve( area );
area.replaceWith( ...area.childNodes );
contentAreas.delete( area );
}
} );

table.querySelectorAll( ':scope > tbody > tr > td, :scope > tbody > tr > th' ).forEach( function( cell ) {
if ( cell.colSpan > 1 || cell.closest( '.inline-edit-row, .inline-editor, #replyrow' ) ) {
return;
}
Array.from( cell.children ).forEach( function( block ) {
if (
! block.matches( 'p, pre, div, blockquote, ul, ol, dl, figure, table' ) ||
block.matches( '.row-actions, .hidden, .comment-author, .wp-list-table-content-scroll, [hidden]' ) ||
window.getComputedStyle( block ).display === 'none'
) {
return;
}
const area = document.createElement( 'div' );
area.className = 'wp-list-table-content-scroll';
block.before( area );
area.append( block );
contentAreas.add( area );
observer.observe( area );
} );
} );

contentAreas.forEach( function( area ) {
const areaStyle = window.getComputedStyle( area );
const scrollable = area.scrollWidth > area.clientWidth &&
( areaStyle.overflowX === 'auto' || areaStyle.overflowX === 'scroll' );
area.tabIndex = scrollable ? 0 : -1;
if ( scrollable ) {
area.setAttribute( 'role', 'region' );
area.setAttribute( 'aria-label', wp.i18n.__( 'Scrollable content' ) );
} else {
area.removeAttribute( 'role' );
area.removeAttribute( 'aria-label' );
}
} );

const style = window.getComputedStyle( wrapper );
const isRTL = style.direction === 'rtl';
const scrollLeft = isRTL ? -wrapper.scrollLeft : wrapper.scrollLeft;
const maxScroll = Math.max( 0, wrapper.scrollWidth - wrapper.clientWidth );
const position = Math.min( maxScroll, Math.max( 0, scrollLeft ) );

// Skip non-scrolling wrappers without moving focus if the wrapper is already focused.
wrapper.tabIndex = maxScroll > 0 &&
( style.overflowX === 'auto' || style.overflowX === 'scroll' ) ? 0 : -1;

// Allow for fractional scroll positions at either end of the table.
wrapper.classList.toggle( 'has-scroll-overflow-start', position > 1 );
wrapper.classList.toggle( 'has-scroll-overflow-end', maxScroll - position > 1 );
}

const observer = new window.ResizeObserver( update );
observer.observe( wrapper );
observer.observe( table );
wrapper.addEventListener( 'scroll', update, { passive: true } );
update();

tables.set( table, {
wrapper: wrapper,
update: update,

/**
* Releases overflow tracking and restores the wrapper's original Tab order.
*
* @since 7.2.0
*
* @return {void}
*/
cleanup: function() {
observer.disconnect();
wrapper.removeEventListener( 'scroll', update );
wrapper.classList.remove( 'has-scroll-overflow-start', 'has-scroll-overflow-end' );
restoreTabIndex( wrapper, tabIndex );
contentAreas.forEach( function( area ) {
area.replaceWith( ...area.childNodes );
} );
contentAreas.clear();
},
} );
}

/**
* Initializes added tables and releases observers for removed tables.
*
* @since 7.2.0
*
* @return {void}
*/
function refreshTables() {
tables.forEach( function( state, table ) {
if ( ! content.contains( table ) || table.parentElement !== state.wrapper ) {
state.cleanup();
tables.delete( table );
}
} );

content.querySelectorAll( '.wp-list-table-scroll > .wp-list-table' )
.forEach( function( table ) {
if ( ! tables.has( table ) ) {
observeTableOverflow( table );
} else {
tables.get( table ).update();
}
} );
}

refreshTables();

// Plugins live search replaces the table and its wrapper.
const observer = new window.MutationObserver( refreshTables );
observer.observe( content, { childList: true, characterData: true, subtree: true } );
} );

/**
* Freeze animated plugin icons when reduced motion is enabled.
*
Expand Down
8 changes: 4 additions & 4 deletions src/js/_enqueues/admin/edit-comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,8 +806,8 @@ window.commentReply = {
});

// Add events.
$('#the-comment-list .column-comment > p').on( 'dblclick', function(){
commentReply.toggle($(this).parent());
$('#the-comment-list .column-comment > p, #the-comment-list .column-comment > .wp-list-table-content-scroll > p').on( 'dblclick', function(){
commentReply.toggle($(this).closest('.column-comment'));
});

$('#doaction, #post-query-submit').on( 'click', function(){
Expand All @@ -833,8 +833,8 @@ window.commentReply = {
*/
addEvents : function(r) {
r.each(function() {
$(this).find('.column-comment > p').on( 'dblclick', function(){
commentReply.toggle($(this).parent());
$(this).find('.column-comment > p, .column-comment > .wp-list-table-content-scroll > p').on( 'dblclick', function(){
commentReply.toggle($(this).closest('.column-comment'));
});
});
},
Expand Down
Loading
Loading