Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -660,8 +660,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 @@ -2235,6 +2235,118 @@ $( 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' ) ||
! window.ResizeObserver
) {
return;
}

const tables = new Map();

/**
* 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() {
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,

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

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
138 changes: 138 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,147 @@ 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) {

/* Share the table's scrollable area so the fades stick to the viewport. */
.wp-list-table-scroll {
display: grid;
clear: both;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: none;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
}

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

/* 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;
}

: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;
}

.wp-list-table-scroll > .widefat,
.wp-list-table-scroll::before,
.wp-list-table-scroll::after {
grid-area: 1 / 1;
}

.wp-list-table-scroll::before,
.wp-list-table-scroll::after {
content: "";
position: sticky;
z-index: 1;
width: 32px;
pointer-events: none;
opacity: 0;
}

.wp-list-table-scroll::before {
inset-inline-start: 0;
justify-self: start;

/* rtl:ignore */
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}

.wp-list-table-scroll::after {
inset-inline-end: 0;
justify-self: end;

/* rtl:ignore */
background: linear-gradient(to left, #fff, rgba(255, 255, 255, 0));
}

.wp-list-table-scroll.has-scroll-overflow-start::before,
.wp-list-table-scroll.has-scroll-overflow-end::after {
opacity: 1;
}

.rtl .wp-list-table-scroll::before,
.rtl .wp-list-table-scroll::after {
transform: scaleX(-1);
}
}

@media screen and (min-width: 783px) and (prefers-reduced-motion: no-preference) {

.wp-list-table-scroll::before,
.wp-list-table-scroll::after {
transition: opacity 100ms cubic-bezier(0.15, 0, 0.15, 1);
}
}

@media screen and (max-width: 1100px) and (min-width: 782px), (max-width: 480px) {
.plugin-card .action-links {
position: static;
Expand Down
80 changes: 42 additions & 38 deletions src/wp-admin/includes/class-wp-comments-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ protected function get_default_primary_column_name() {
* Overrides the parent display() method to render extra comments.
*
* @since 3.1.0
* @since 7.2.0 Added a scrollable wrapper around the table.
*/
public function display() {
wp_nonce_field( 'fetch-list-' . get_class( $this ), '_ajax_fetch_list_nonce' );
Expand All @@ -610,50 +611,53 @@ public function display() {
}

$this->screen->render_screen_reader_content( 'heading_list' );
$table_label = $this->screen->get_screen_reader_text( 'heading_list' );

?>
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
<?php
if ( ! isset( $_GET['orderby'] ) ) {
// In the initial view, Comments are ordered by comment's date but there's no column for that.
echo '<caption class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
__( 'Ordered by Comment Date, descending.' ) .
'</caption>';
} else {
$this->print_table_description();
}
?>
<thead>
<tr>
<?php $this->print_column_headers(); ?>
</tr>
</thead>
<div class="wp-list-table-scroll" role="region" aria-label="<?php echo esc_attr( $table_label ? $table_label : __( 'Items list' ) ); ?>" tabindex="0">
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
<?php
if ( ! isset( $_GET['orderby'] ) ) {
// In the initial view, Comments are ordered by comment's date but there's no column for that.
echo '<caption class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
__( 'Ordered by Comment Date, descending.' ) .
'</caption>';
} else {
$this->print_table_description();
}
?>
<thead>
<tr>
<?php $this->print_column_headers(); ?>
</tr>
</thead>

<tbody id="the-comment-list" data-wp-lists="list:comment">
<?php $this->display_rows_or_placeholder(); ?>
</tbody>
<tbody id="the-comment-list" data-wp-lists="list:comment">
<?php $this->display_rows_or_placeholder(); ?>
</tbody>

<tbody id="the-extra-comment-list" data-wp-lists="list:comment" style="display: none;">
<?php
/*
* Back up the items to restore after printing the extra items markup.
* The extra items may be empty, which will prevent the table nav from displaying later.
*/
$items = $this->items;
$this->items = $this->extra_items;
$this->display_rows_or_placeholder();
$this->items = $items;
?>
</tbody>
<tbody id="the-extra-comment-list" data-wp-lists="list:comment" style="display: none;">
<?php
/*
* Back up the items to restore after printing the extra items markup.
* The extra items may be empty, which will prevent the table nav from displaying later.
*/
$items = $this->items;
$this->items = $this->extra_items;
$this->display_rows_or_placeholder();
$this->items = $items;
?>
</tbody>

<tfoot>
<tr>
<?php $this->print_column_headers( false ); ?>
</tr>
</tfoot>
<tfoot>
<tr>
<?php $this->print_column_headers( false ); ?>
</tr>
</tfoot>

</table>
</table>
</div>
<?php

$this->display_tablenav( 'bottom' );
Expand Down
Loading
Loading