Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
9650fb9
Build: Merge the latest trunk into the list table branch.
mattmiklic Oct 22, 2026
be81042
List Tables: Wrap preformatted text and refine overflow cues.
mattmiklic Oct 22, 2026
e2441b1
List Tables: Refine overflow callbacks and type documentation.
mattmiklic Oct 22, 2026
a28e1bd
List Tables: Preserve inline icons and sort alignment.
mattmiklic Oct 22, 2026
3f5f9a2
List Tables: Restore white fades at scrolling edges.
mattmiklic Oct 23, 2026
4fba9c5
List Tables: Simplify overflow handling and match row fades.
mattmiklic Oct 26, 2026
648eed6
List Tables: Pin row fades and constrain comment Quick Edit.
mattmiklic Oct 26, 2026
af9d8e3
List Tables: Render sticky fades in WebKit.
mattmiklic Oct 27, 2026
4e982e0
List Tables: Merge the latest trunk changes.
mattmiklic Oct 27, 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 @@ -301,7 +301,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 @@ -664,8 +664,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
112 changes: 112 additions & 0 deletions src/js/_enqueues/admin/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,118 @@ $( function( $ ) {

}( jQuery, window ));

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

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

/**
* 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' );

/**
* Updates the overflow indicators and the wrapper's place in the Tab order.
*
* @since 7.2.0
*
* @return {void}
*/
function update() {
Comment thread
mattmiklic marked this conversation as resolved.
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,

/**
* Releases overflow tracking and restores the wrapper's original Tab order.
*
* @since 7.2.0
*
* @return {void}
*/
cleanup: () => {
observer.disconnect();
wrapper.removeEventListener( 'scroll', update );
wrapper.classList.remove( 'has-scroll-overflow-start', 'has-scroll-overflow-end' );
if ( tabIndex === null ) {
wrapper.removeAttribute( 'tabindex' );
} else {
wrapper.setAttribute( 'tabindex', tabIndex );
}
},
} );
}

/**
* Initializes added tables and releases observers for removed tables.
*
* @since 7.2.0
*
* @return {void}
*/
function refreshTables() {
tables.forEach( ( 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( ( table ) => {
if ( ! tables.has( table ) ) {
observeTableOverflow( table );
}
} );
}

refreshTables();

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

/**
* Freeze animated plugin icons when reduced motion is enabled.
*
Expand Down
214 changes: 214 additions & 0 deletions src/wp-admin/css/list-tables.css
Original file line number Diff line number Diff line change
Expand Up @@ -1789,9 +1789,223 @@ div.action-links,
width: 80px;
}

.wp-list-table-scroll:focus-visible {
outline: 2px solid var(--wp-admin-theme-color);
outline-offset: 0;
}

/* =Media Queries
-------------------------------------------------------------- */

@media screen and (min-width: 783px) {

/* Keep the table within a horizontally scrollable viewport. */
.wp-list-table-scroll {
display: grid;
container-type: inline-size;
clear: both;
overflow-x: auto;
Comment thread
ciampo marked this conversation as resolved.
overflow-y: hidden;
overscroll-behavior-x: none;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
}

/* Wrap preformatted text, including inline preformatting, while preserving whitespace. */
.wp-list-table-scroll > .wp-list-table pre {
white-space: pre-wrap !important;
overflow-wrap: anywhere;
}

/* Keep inline editing forms within the visible table container. */
.wp-list-table-scroll .inline-edit-wrapper,
.wp-list-table-scroll #replyrow.inline-edit-row fieldset.comment-reply {
position: sticky;
inset-inline-start: 0;
z-index: 2;
box-sizing: border-box;
width: 100cqi;
}

/* Account for the comment editing cell's 2px padding on each side. */
.wp-list-table-scroll #replyrow.inline-edit-row fieldset.comment-reply {
inset-inline-start: 2px;
float: none;
width: calc(100cqi - 4px);
}

.wp-list-table-scroll > .widefat {
grid-area: 1 / 1;
border: 0;
box-shadow: none;
}

/* Keep sort indicators beside their labels when columns shrink. */
.wp-list-table-scroll > .wp-list-table th.sortable a,
.wp-list-table-scroll > .wp-list-table th.sorted a {
display: flex;
align-items: center;
}

/* Keep the sort icons' layout boxes from extending past the last column. */
.wp-list-table-scroll .sorting-indicators {
overflow: clip;
}

/* Keep sorting links' focus indicators inside the scrolling viewport. */
.wp-list-table-scroll th.sortable a:focus,
.wp-list-table-scroll th.sorted a:focus {
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9);
outline-offset: -2px;
}

.wp-list-table-scroll > .fixed {
table-layout: auto;
}

/* Preserve the checkbox column's spacing under automatic table layout. */
.wp-list-table-scroll > .fixed .check-column {
min-width: 2.2em;
}

:where(.wp-list-table-scroll) > .fixed .column-author,
:where(.wp-list-table-scroll) > .fixed .column-categories,
:where(.wp-list-table-scroll) > .fixed .column-tags,
:where(.wp-list-table-scroll) > .fixed .column-date,
:where(.wp-list-table-scroll) > .fixed .column-response,
:where(.wp-list-table-scroll) > .fixed .column-role,
:where(.wp-list-table-scroll) > .fixed .column-slug,
:where(.wp-list-table-scroll) > .fixed .column-locations {
width: auto;
}

/* Long words, URLs, and filenames must not set the table's intrinsic width. */
.wp-list-table-scroll > .fixed .column-primary,
.wp-list-table-scroll > .fixed .column-primary *,
.wp-list-table-scroll > .fixed .column-author,
.wp-list-table-scroll > .fixed .column-author *,
.wp-list-table-scroll > .fixed .column-categories,
.wp-list-table-scroll > .fixed .column-categories *,
.wp-list-table-scroll > .fixed .column-tags,
.wp-list-table-scroll > .fixed .column-tags *,
.wp-list-table-scroll > .fixed .column-response,
.wp-list-table-scroll > .fixed .column-response *,
.wp-list-table-scroll > .fixed .column-role,
.wp-list-table-scroll > .fixed .column-role *,
.wp-list-table-scroll > .fixed .column-slug,
.wp-list-table-scroll > .fixed .column-slug *,
.wp-list-table-scroll > .fixed .column-locations,
.wp-list-table-scroll > .fixed .column-locations * {
overflow-wrap: anywhere;
}

.wp-list-table-scroll > .fixed th.column-author,
.wp-list-table-scroll > .fixed th.column-categories,
.wp-list-table-scroll > .fixed th.column-tags,
.wp-list-table-scroll > .fixed th.column-response,
.wp-list-table-scroll > .fixed th.column-role,
.wp-list-table-scroll > .fixed th.column-slug,
.wp-list-table-scroll > .fixed th.column-locations {
min-width: 10em;
}

.wp-list-table-scroll > .fixed th.column-comments {
min-width: 5.5em;
}

.wp-list-table-scroll > .fixed th.column-date {
min-width: 12em;
}

.wp-list-table-scroll > .fixed th.column-primary {
min-width: 20em;
}

/* Match the edge fades to Core's row backgrounds. */
.wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr {
--list-table-row-background: #fff;
position: relative;
}

.wp-list-table-scroll > .striped > tbody > :nth-child(odd),
.wp-list-table-scroll > .wp-list-table > tbody > .alternate,
.wp-list-table-scroll > .wp-list-table > tbody > .undo {
--list-table-row-background: #f6f7f7;
}

.wp-list-table-scroll > .plugins > tbody > tr {
--list-table-row-background: #fff;
}

.wp-list-table-scroll > .plugins > tbody > .active {
--list-table-row-background: color-mix(in srgb, var(--wp-admin-theme-color) 8%, #fff);
}

.wp-list-table-scroll > .plugins > tbody > .paused {
--list-table-row-background: #f6f7f7;
}

.wp-list-table-scroll > .wp-list-table > tbody > :is(.unapproved, .site-spammed, .site-mature) {
--list-table-row-background: #fcf9e8;
}

.wp-list-table-scroll > .wp-list-table > tbody > :is(.site-deleted, .site-archived) {
--list-table-row-background: #fcf0f1;
}

/* Core applies the 8% keyboard-selection tint to both the row and its cells. */
.wp-list-table-scroll > .wp-list-table > tbody > .vim-current {
--list-table-row-background: color-mix(in srgb, var(--wp-admin-theme-color) 15.36%, #fff);
}

/* Zero-width sticky cells paint the fades inward without adding column width. */
.wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr::before,
.wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr::after {
content: "";
display: table-cell;
position: sticky;
z-index: 1;
width: 0;
padding: 0;

/* WebKit needs a nonzero, nontransparent border to paint the sticky cell's border image. */
border: 0 solid var(--list-table-row-background);
border-top-width: 1px;
pointer-events: none;
opacity: 0;
}

.wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr::before {
inset-inline-start: 0;

/* rtl:ignore */
border-image: linear-gradient(to right, var(--list-table-row-background), transparent) 0 100% / 0 32px 0 0 / 0 32px 0 0 stretch;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new edge fades don't render in WebKit 26.5, in either LTR or RTL, even when the overflow classes are set. Could we adjust the zero-width border-image approach or keep a working fallback?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ciampo Thanks for catching this! I found a CSS workaround that I've implemented, and verified it's working in Safari now.

}

.wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr::after {
inset-inline-end: 0;

/* rtl:ignore */
border-image: linear-gradient(to left, var(--list-table-row-background), transparent) 0 0 0 100% / 0 0 0 32px / 0 0 0 32px stretch;
}

.wp-list-table-scroll.has-scroll-overflow-start > .wp-list-table > :is(thead, tbody, tfoot) > tr::before,
.wp-list-table-scroll.has-scroll-overflow-end > .wp-list-table > :is(thead, tbody, tfoot) > tr::after {
opacity: 1;
}

/* Keep the same table grid in editing rows, without covering their forms. */
.wp-list-table-scroll > .wp-list-table > tbody > :is(.inline-edit-row, .inline-editor, #replyrow)::before,
.wp-list-table-scroll > .wp-list-table > tbody > :is(.inline-edit-row, .inline-editor, #replyrow)::after {
visibility: hidden;
}

.rtl .wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr::before,
.rtl .wp-list-table-scroll > .wp-list-table > :is(thead, tbody, tfoot) > tr::after {
transform: scaleX(-1);
}
}

@media screen and (max-width: 1100px) and (min-width: 782px), (max-width: 480px) {
.plugin-card .action-links {
position: static;
Expand Down
Loading
Loading