--- a/wp/wp-admin/includes/class-wp-list-table.php Thu Sep 29 08:06:27 2022 +0200
+++ b/wp/wp-admin/includes/class-wp-list-table.php Fri Sep 05 18:40:08 2025 +0200
@@ -11,8 +11,8 @@
* Base class for displaying a list of items in an ajaxified HTML table.
*
* @since 3.1.0
- * @access private
*/
+#[AllowDynamicProperties]
class WP_List_Table {
/**
@@ -173,9 +173,10 @@
}
/**
- * Make private properties readable for backward compatibility.
+ * Makes private properties readable for backward compatibility.
*
* @since 4.0.0
+ * @since 6.4.0 Getting a dynamic property is deprecated.
*
* @param string $name Property to get.
* @return mixed Property.
@@ -184,27 +185,44 @@
if ( in_array( $name, $this->compat_fields, true ) ) {
return $this->$name;
}
+
+ wp_trigger_error(
+ __METHOD__,
+ "The property `{$name}` is not declared. Getting a dynamic property is " .
+ 'deprecated since version 6.4.0! Instead, declare the property on the class.',
+ E_USER_DEPRECATED
+ );
+ return null;
}
/**
- * Make private properties settable for backward compatibility.
+ * Makes private properties settable for backward compatibility.
*
* @since 4.0.0
+ * @since 6.4.0 Setting a dynamic property is deprecated.
*
* @param string $name Property to check if set.
* @param mixed $value Property value.
- * @return mixed Newly-set property.
*/
public function __set( $name, $value ) {
if ( in_array( $name, $this->compat_fields, true ) ) {
- return $this->$name = $value;
+ $this->$name = $value;
+ return;
}
+
+ wp_trigger_error(
+ __METHOD__,
+ "The property `{$name}` is not declared. Setting a dynamic property is " .
+ 'deprecated since version 6.4.0! Instead, declare the property on the class.',
+ E_USER_DEPRECATED
+ );
}
/**
- * Make private properties checkable for backward compatibility.
+ * Makes private properties checkable for backward compatibility.
*
* @since 4.0.0
+ * @since 6.4.0 Checking a dynamic property is deprecated.
*
* @param string $name Property to check if set.
* @return bool Whether the property is a back-compat property and it is set.
@@ -214,24 +232,39 @@
return isset( $this->$name );
}
+ wp_trigger_error(
+ __METHOD__,
+ "The property `{$name}` is not declared. Checking `isset()` on a dynamic property " .
+ 'is deprecated since version 6.4.0! Instead, declare the property on the class.',
+ E_USER_DEPRECATED
+ );
return false;
}
/**
- * Make private properties un-settable for backward compatibility.
+ * Makes private properties un-settable for backward compatibility.
*
* @since 4.0.0
+ * @since 6.4.0 Unsetting a dynamic property is deprecated.
*
* @param string $name Property to unset.
*/
public function __unset( $name ) {
if ( in_array( $name, $this->compat_fields, true ) ) {
unset( $this->$name );
+ return;
}
+
+ wp_trigger_error(
+ __METHOD__,
+ "A property `{$name}` is not declared. Unsetting a dynamic property is " .
+ 'deprecated since version 6.4.0! Instead, declare the property on the class.',
+ E_USER_DEPRECATED
+ );
}
/**
- * Make private/protected methods readable for backward compatibility.
+ * Makes private/protected methods readable for backward compatibility.
*
* @since 4.0.0
*
@@ -269,7 +302,7 @@
}
/**
- * An internal method that sets all the necessary pagination arguments
+ * Sets all the necessary pagination arguments.
*
* @since 3.1.0
*
@@ -286,7 +319,7 @@
);
if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
- $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
+ $args['total_pages'] = (int) ceil( $args['total_items'] / $args['per_page'] );
}
// Redirect if page number is invalid and headers are not already sent.
@@ -320,7 +353,7 @@
}
/**
- * Whether the table has items to display or not
+ * Determines whether the table has items to display or not
*
* @since 3.1.0
*
@@ -355,7 +388,13 @@
$input_id = $input_id . '-search-input';
if ( ! empty( $_REQUEST['orderby'] ) ) {
- echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
+ if ( is_array( $_REQUEST['orderby'] ) ) {
+ foreach ( $_REQUEST['orderby'] as $key => $value ) {
+ echo '<input type="hidden" name="orderby[' . esc_attr( $key ) . ']" value="' . esc_attr( $value ) . '" />';
+ }
+ } else {
+ echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
+ }
}
if ( ! empty( $_REQUEST['order'] ) ) {
echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
@@ -376,6 +415,79 @@
}
/**
+ * Generates views links.
+ *
+ * @since 6.1.0
+ *
+ * @param array $link_data {
+ * An array of link data.
+ *
+ * @type string $url The link URL.
+ * @type string $label The link label.
+ * @type bool $current Optional. Whether this is the currently selected view.
+ * }
+ * @return string[] An array of link markup. Keys match the `$link_data` input array.
+ */
+ protected function get_views_links( $link_data = array() ) {
+ if ( ! is_array( $link_data ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ /* translators: %s: The $link_data argument. */
+ __( 'The %s argument must be an array.' ),
+ '<code>$link_data</code>'
+ ),
+ '6.1.0'
+ );
+
+ return array( '' );
+ }
+
+ $views_links = array();
+
+ foreach ( $link_data as $view => $link ) {
+ if ( empty( $link['url'] ) || ! is_string( $link['url'] ) || '' === trim( $link['url'] ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ /* translators: %1$s: The argument name. %2$s: The view name. */
+ __( 'The %1$s argument must be a non-empty string for %2$s.' ),
+ '<code>url</code>',
+ '<code>' . esc_html( $view ) . '</code>'
+ ),
+ '6.1.0'
+ );
+
+ continue;
+ }
+
+ if ( empty( $link['label'] ) || ! is_string( $link['label'] ) || '' === trim( $link['label'] ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ /* translators: %1$s: The argument name. %2$s: The view name. */
+ __( 'The %1$s argument must be a non-empty string for %2$s.' ),
+ '<code>label</code>',
+ '<code>' . esc_html( $view ) . '</code>'
+ ),
+ '6.1.0'
+ );
+
+ continue;
+ }
+
+ $views_links[ $view ] = sprintf(
+ '<a href="%s"%s>%s</a>',
+ esc_url( $link['url'] ),
+ isset( $link['current'] ) && true === $link['current'] ? ' class="current" aria-current="page"' : '',
+ $link['label']
+ );
+ }
+
+ return $views_links;
+ }
+
+ /**
* Gets the list of views available on this table.
*
* The format is an associative array:
@@ -458,7 +570,7 @@
*
* @since 3.1.0
*
- * @param string $which The location of the bulk actions: 'top' or 'bottom'.
+ * @param string $which The location of the bulk actions: Either 'top' or 'bottom'.
* This is designated as optional for backward compatibility.
*/
protected function bulk_actions( $which = '' ) {
@@ -487,7 +599,10 @@
return;
}
- echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>';
+ echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' .
+ /* translators: Hidden accessibility text. */
+ __( 'Select bulk action' ) .
+ '</label>';
echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
echo '<option value="-1">' . __( 'Bulk actions' ) . "</option>\n";
@@ -526,7 +641,7 @@
return false;
}
- if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) {
+ if ( isset( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) {
return $_REQUEST['action'];
}
@@ -555,23 +670,26 @@
$always_visible = true;
}
- $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
+ $output = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
$i = 0;
foreach ( $actions as $action => $link ) {
++$i;
- $sep = ( $i < $action_count ) ? ' | ' : '';
+ $separator = ( $i < $action_count ) ? ' | ' : '';
- $out .= "<span class='$action'>$link$sep</span>";
+ $output .= "<span class='$action'>{$link}{$separator}</span>";
}
- $out .= '</div>';
+ $output .= '</div>';
- $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
+ $output .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' .
+ /* translators: Hidden accessibility text. */
+ __( 'Show more details' ) .
+ '</span></button>';
- return $out;
+ return $output;
}
/**
@@ -600,7 +718,7 @@
}
/**
- * Filters to short-circuit performing the months dropdown query.
+ * Filters whether to short-circuit performing the months dropdown query.
*
* @since 5.7.0
*
@@ -619,13 +737,11 @@
$months = $wpdb->get_results(
$wpdb->prepare(
- "
- SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
- FROM $wpdb->posts
- WHERE post_type = %s
- $extra_checks
- ORDER BY post_date DESC
- ",
+ "SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
+ FROM $wpdb->posts
+ WHERE post_type = %s
+ $extra_checks
+ ORDER BY post_date DESC",
$post_type
)
);
@@ -643,7 +759,7 @@
$month_count = count( $months );
- if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) {
+ if ( ! $month_count || ( 1 === $month_count && 0 === (int) $months[0]->month ) ) {
return;
}
@@ -654,7 +770,7 @@
<option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
<?php
foreach ( $months as $arc_row ) {
- if ( 0 == $arc_row->year ) {
+ if ( 0 === (int) $arc_row->year ) {
continue;
}
@@ -696,7 +812,9 @@
}
printf(
- "<a href='%s' class='%s' id='view-switch-$mode'$aria_current><span class='screen-reader-text'>%s</span></a>\n",
+ "<a href='%s' class='%s' id='view-switch-$mode'$aria_current>" .
+ "<span class='screen-reader-text'>%s</span>" .
+ "</a>\n",
esc_url( remove_query_arg( 'attachment-filter', add_query_arg( 'mode', $mode ) ) ),
implode( ' ', $classes ),
$title
@@ -716,6 +834,17 @@
* @param int $pending_comments Number of pending comments.
*/
protected function comments_bubble( $post_id, $pending_comments ) {
+ $post_object = get_post( $post_id );
+ $edit_post_cap = $post_object ? 'edit_post' : 'edit_posts';
+
+ if ( ! current_user_can( $edit_post_cap, $post_id )
+ && ( post_password_required( $post_id )
+ || ! current_user_can( 'read_post', $post_id ) )
+ ) {
+ // The user has no access to the post and thus cannot see the comments.
+ return false;
+ }
+
$approved_comments = get_comments_number();
$approved_comments_number = number_format_i18n( $approved_comments );
@@ -742,20 +871,27 @@
if ( ! $approved_comments && ! $pending_comments ) {
// No comments at all.
printf(
- '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
+ '<span aria-hidden="true">—</span>' .
+ '<span class="screen-reader-text">%s</span>',
__( 'No comments' )
);
} elseif ( $approved_comments && 'trash' === get_post_status( $post_id ) ) {
// Don't link the comment bubble for a trashed post.
printf(
- '<span class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
+ '<span class="post-com-count post-com-count-approved">' .
+ '<span class="comment-count-approved" aria-hidden="true">%s</span>' .
+ '<span class="screen-reader-text">%s</span>' .
+ '</span>',
$approved_comments_number,
$pending_comments ? $approved_phrase : $approved_only_phrase
);
} elseif ( $approved_comments ) {
// Link the comment bubble to approved comments.
printf(
- '<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
+ '<a href="%s" class="post-com-count post-com-count-approved">' .
+ '<span class="comment-count-approved" aria-hidden="true">%s</span>' .
+ '<span class="screen-reader-text">%s</span>' .
+ '</a>',
esc_url(
add_query_arg(
array(
@@ -771,15 +907,25 @@
} else {
// Don't link the comment bubble when there are no approved comments.
printf(
- '<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
+ '<span class="post-com-count post-com-count-no-comments">' .
+ '<span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span>' .
+ '<span class="screen-reader-text">%s</span>' .
+ '</span>',
$approved_comments_number,
- $pending_comments ? __( 'No approved comments' ) : __( 'No comments' )
+ $pending_comments ?
+ /* translators: Hidden accessibility text. */
+ __( 'No approved comments' ) :
+ /* translators: Hidden accessibility text. */
+ __( 'No comments' )
);
}
if ( $pending_comments ) {
printf(
- '<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
+ '<a href="%s" class="post-com-count post-com-count-pending">' .
+ '<span class="comment-count-pending" aria-hidden="true">%s</span>' .
+ '<span class="screen-reader-text">%s</span>' .
+ '</a>',
esc_url(
add_query_arg(
array(
@@ -794,9 +940,16 @@
);
} else {
printf(
- '<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
+ '<span class="post-com-count post-com-count-pending post-com-count-no-pending">' .
+ '<span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span>' .
+ '<span class="screen-reader-text">%s</span>' .
+ '</span>',
$pending_comments_number,
- $approved_comments ? __( 'No pending comments' ) : __( 'No comments' )
+ $approved_comments ?
+ /* translators: Hidden accessibility text. */
+ __( 'No pending comments' ) :
+ /* translators: Hidden accessibility text. */
+ __( 'No comments' )
);
}
}
@@ -842,10 +995,10 @@
* - `edit_comments_per_page`
* - `sites_network_per_page`
* - `site_themes_network_per_page`
- * - `themes_network_per_page'`
+ * - `themes_network_per_page`
* - `users_network_per_page`
* - `edit_post_per_page`
- * - `edit_page_per_page'`
+ * - `edit_page_per_page`
* - `edit_{$post_type}_per_page`
* - `edit_post_tag_per_page`
* - `edit_category_per_page`
@@ -865,7 +1018,7 @@
*
* @since 3.1.0
*
- * @param string $which
+ * @param string $which The location of the pagination: Either 'top' or 'bottom'.
*/
protected function pagination( $which ) {
if ( empty( $this->_pagination_args ) ) {
@@ -906,11 +1059,11 @@
$disable_prev = false;
$disable_next = false;
- if ( 1 == $current ) {
+ if ( 1 === $current ) {
$disable_first = true;
$disable_prev = true;
}
- if ( $total_pages == $current ) {
+ if ( $total_pages === $current ) {
$disable_last = true;
$disable_next = true;
}
@@ -919,8 +1072,12 @@
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">«</span>';
} else {
$page_links[] = sprintf(
- "<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+ "<a class='first-page button' href='%s'>" .
+ "<span class='screen-reader-text'>%s</span>" .
+ "<span aria-hidden='true'>%s</span>" .
+ '</a>',
esc_url( remove_query_arg( 'paged', $current_url ) ),
+ /* translators: Hidden accessibility text. */
__( 'First page' ),
'«'
);
@@ -930,8 +1087,12 @@
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span>';
} else {
$page_links[] = sprintf(
- "<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+ "<a class='prev-page button' href='%s'>" .
+ "<span class='screen-reader-text'>%s</span>" .
+ "<span aria-hidden='true'>%s</span>" .
+ '</a>',
esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
+ /* translators: Hidden accessibility text. */
__( 'Previous page' ),
'‹'
);
@@ -939,17 +1100,29 @@
if ( 'bottom' === $which ) {
$html_current_page = $current;
- $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
+ $total_pages_before = sprintf(
+ '<span class="screen-reader-text">%s</span>' .
+ '<span id="table-paging" class="paging-input">' .
+ '<span class="tablenav-paging-text">',
+ /* translators: Hidden accessibility text. */
+ __( 'Current Page' )
+ );
} else {
$html_current_page = sprintf(
- "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
- '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',
+ '<label for="current-page-selector" class="screen-reader-text">%s</label>' .
+ "<input class='current-page' id='current-page-selector' type='text'
+ name='paged' value='%s' size='%d' aria-describedby='table-paging' />" .
+ "<span class='tablenav-paging-text'>",
+ /* translators: Hidden accessibility text. */
+ __( 'Current Page' ),
$current,
strlen( $total_pages )
);
}
+
$html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
- $page_links[] = $total_pages_before . sprintf(
+
+ $page_links[] = $total_pages_before . sprintf(
/* translators: 1: Current page, 2: Total pages. */
_x( '%1$s of %2$s', 'paging' ),
$html_current_page,
@@ -960,8 +1133,12 @@
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span>';
} else {
$page_links[] = sprintf(
- "<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+ "<a class='next-page button' href='%s'>" .
+ "<span class='screen-reader-text'>%s</span>" .
+ "<span aria-hidden='true'>%s</span>" .
+ '</a>',
esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
+ /* translators: Hidden accessibility text. */
__( 'Next page' ),
'›'
);
@@ -971,8 +1148,12 @@
$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">»</span>';
} else {
$page_links[] = sprintf(
- "<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+ "<a class='last-page button' href='%s'>" .
+ "<span class='screen-reader-text'>%s</span>" .
+ "<span aria-hidden='true'>%s</span>" .
+ '</a>',
esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
+ /* translators: Hidden accessibility text. */
__( 'Last page' ),
'»'
);
@@ -1014,10 +1195,17 @@
*
* The format is:
* - `'internal-name' => 'orderby'`
+ * - `'internal-name' => array( 'orderby', bool, 'abbr', 'orderby-text', 'initially-sorted-column-order' )` -
* - `'internal-name' => array( 'orderby', 'asc' )` - The second element sets the initial sorting order.
* - `'internal-name' => array( 'orderby', true )` - The second element makes the initial order descending.
*
+ * In the second format, passing true as second parameter will make the initial
+ * sorting order be descending. Following parameters add a short column name to
+ * be used as 'abbr' attribute, a translatable string for the current sorting,
+ * and the initial order for the initial sorted column, 'asc' or 'desc' (default: false).
+ *
* @since 3.1.0
+ * @since 6.3.0 Added 'abbr', 'orderby-text' and 'initially-sorted-column-order'.
*
* @return array
*/
@@ -1040,8 +1228,10 @@
return $column;
}
- // We need a primary defined so responsive views show something,
- // so let's fall back to the first non-checkbox column.
+ /*
+ * We need a primary defined so responsive views show something,
+ * so let's fall back to the first non-checkbox column.
+ */
foreach ( $columns as $col => $column_name ) {
if ( 'cb' === $col ) {
continue;
@@ -1055,6 +1245,8 @@
}
/**
+ * Gets the name of the primary column.
+ *
* Public wrapper for WP_List_Table::get_default_primary_column_name().
*
* @since 4.4.0
@@ -1076,8 +1268,10 @@
$columns = get_column_headers( $this->screen );
$default = $this->get_default_primary_column_name();
- // If the primary column doesn't exist,
- // fall back to the first non-checkbox column.
+ /*
+ * If the primary column doesn't exist,
+ * fall back to the first non-checkbox column.
+ */
if ( ! isset( $columns[ $default ] ) ) {
$default = self::get_default_primary_column_name();
}
@@ -1108,7 +1302,10 @@
*/
protected function get_column_info() {
// $_column_headers is already set / cached.
- if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
+ if (
+ isset( $this->_column_headers ) &&
+ is_array( $this->_column_headers )
+ ) {
/*
* Backward compatibility for `$_column_headers` format prior to WordPress 4.3.
*
@@ -1116,12 +1313,18 @@
* column headers property. This ensures the primary column name is included
* in plugins setting the property directly in the three item format.
*/
+ if ( 4 === count( $this->_column_headers ) ) {
+ return $this->_column_headers;
+ }
+
$column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
foreach ( $this->_column_headers as $key => $value ) {
$column_headers[ $key ] = $value;
}
- return $column_headers;
+ $this->_column_headers = $column_headers;
+
+ return $this->_column_headers;
}
$columns = get_column_headers( $this->screen );
@@ -1147,9 +1350,22 @@
}
$data = (array) $data;
+ // Descending initial sorting.
if ( ! isset( $data[1] ) ) {
$data[1] = false;
}
+ // Current sorting translatable string.
+ if ( ! isset( $data[2] ) ) {
+ $data[2] = '';
+ }
+ // Initial view sorted column and asc/desc order, default: false.
+ if ( ! isset( $data[3] ) ) {
+ $data[3] = false;
+ }
+ // Initial order for the initial sorted column, default: false.
+ if ( ! isset( $data[4] ) ) {
+ $data[4] = false;
+ }
$sortable[ $id ] = $data;
}
@@ -1186,27 +1402,39 @@
$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
$current_url = remove_query_arg( 'paged', $current_url );
+ // When users click on a column header to sort by other columns.
if ( isset( $_GET['orderby'] ) ) {
$current_orderby = $_GET['orderby'];
+ // In the initial view there's no orderby parameter.
} else {
$current_orderby = '';
}
+ // Not in the initial view and descending order.
if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
$current_order = 'desc';
} else {
+ // The initial view is not always 'asc', we'll take care of this below.
$current_order = 'asc';
}
if ( ! empty( $columns['cb'] ) ) {
static $cb_counter = 1;
- $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
- . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
- $cb_counter++;
+ $columns['cb'] = '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />
+ <label for="cb-select-all-' . $cb_counter . '">' .
+ '<span class="screen-reader-text">' .
+ /* translators: Hidden accessibility text. */
+ __( 'Select All' ) .
+ '</span>' .
+ '</label>';
+ ++$cb_counter;
}
foreach ( $columns as $column_key => $column_display_name ) {
- $class = array( 'manage-column', "column-$column_key" );
+ $class = array( 'manage-column', "column-$column_key" );
+ $aria_sort_attr = '';
+ $abbr_attr = '';
+ $order_text = '';
if ( in_array( $column_key, $hidden, true ) ) {
$class[] = 'hidden';
@@ -1223,14 +1451,41 @@
}
if ( isset( $sortable[ $column_key ] ) ) {
- list( $orderby, $desc_first ) = $sortable[ $column_key ];
+ $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : '';
+ $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false;
+ $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : '';
+ $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : '';
+ $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : '';
+ /*
+ * We're in the initial view and there's no $_GET['orderby'] then check if the
+ * initial sorting information is set in the sortable columns and use that.
+ */
+ if ( '' === $current_orderby && $initial_order ) {
+ // Use the initially sorted column $orderby as current orderby.
+ $current_orderby = $orderby;
+ // Use the initially sorted column asc/desc order as initial order.
+ $current_order = $initial_order;
+ }
+
+ /*
+ * True in the initial view when an initial orderby is set via get_sortable_columns()
+ * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby.
+ */
if ( $current_orderby === $orderby ) {
- $order = 'asc' === $current_order ? 'desc' : 'asc';
+ // The sorted column. The `aria-sort` attribute must be set only on the sorted column.
+ if ( 'asc' === $current_order ) {
+ $order = 'desc';
+ $aria_sort_attr = ' aria-sort="ascending"';
+ } else {
+ $order = 'asc';
+ $aria_sort_attr = ' aria-sort="descending"';
+ }
$class[] = 'sorted';
$class[] = $current_order;
} else {
+ // The other sortable columns.
$order = strtolower( $desc_first );
if ( ! in_array( $order, array( 'desc', 'asc' ), true ) ) {
@@ -1239,12 +1494,33 @@
$class[] = 'sortable';
$class[] = 'desc' === $order ? 'asc' : 'desc';
+
+ /* translators: Hidden accessibility text. */
+ $asc_text = __( 'Sort ascending.' );
+ /* translators: Hidden accessibility text. */
+ $desc_text = __( 'Sort descending.' );
+ $order_text = 'asc' === $order ? $asc_text : $desc_text;
}
+ if ( '' !== $order_text ) {
+ $order_text = ' <span class="screen-reader-text">' . $order_text . '</span>';
+ }
+
+ // Print an 'abbr' attribute if a value is provided via get_sortable_columns().
+ $abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : '';
+
$column_display_name = sprintf(
- '<a href="%s"><span>%s</span><span class="sorting-indicator"></span></a>',
+ '<a href="%1$s">' .
+ '<span>%2$s</span>' .
+ '<span class="sorting-indicators">' .
+ '<span class="sorting-indicator asc" aria-hidden="true"></span>' .
+ '<span class="sorting-indicator desc" aria-hidden="true"></span>' .
+ '</span>' .
+ '%3$s' .
+ '</a>',
esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ),
- $column_display_name
+ $column_display_name,
+ $order_text
);
}
@@ -1256,7 +1532,80 @@
$class = "class='" . implode( ' ', $class ) . "'";
}
- echo "<$tag $scope $id $class>$column_display_name</$tag>";
+ echo "<$tag $scope $id $class $aria_sort_attr $abbr_attr>$column_display_name</$tag>";
+ }
+ }
+
+ /**
+ * Print a table description with information about current sorting and order.
+ *
+ * For the table initial view, information about initial orderby and order
+ * should be provided via get_sortable_columns().
+ *
+ * @since 6.3.0
+ * @access public
+ */
+ public function print_table_description() {
+ list( $columns, $hidden, $sortable ) = $this->get_column_info();
+
+ if ( empty( $sortable ) ) {
+ return;
+ }
+
+ // When users click on a column header to sort by other columns.
+ if ( isset( $_GET['orderby'] ) ) {
+ $current_orderby = $_GET['orderby'];
+ // In the initial view there's no orderby parameter.
+ } else {
+ $current_orderby = '';
+ }
+
+ // Not in the initial view and descending order.
+ if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
+ $current_order = 'desc';
+ } else {
+ // The initial view is not always 'asc', we'll take care of this below.
+ $current_order = 'asc';
+ }
+
+ foreach ( array_keys( $columns ) as $column_key ) {
+
+ if ( isset( $sortable[ $column_key ] ) ) {
+ $orderby = isset( $sortable[ $column_key ][0] ) ? $sortable[ $column_key ][0] : '';
+ $desc_first = isset( $sortable[ $column_key ][1] ) ? $sortable[ $column_key ][1] : false;
+ $abbr = isset( $sortable[ $column_key ][2] ) ? $sortable[ $column_key ][2] : '';
+ $orderby_text = isset( $sortable[ $column_key ][3] ) ? $sortable[ $column_key ][3] : '';
+ $initial_order = isset( $sortable[ $column_key ][4] ) ? $sortable[ $column_key ][4] : '';
+
+ if ( ! is_string( $orderby_text ) || '' === $orderby_text ) {
+ return;
+ }
+ /*
+ * We're in the initial view and there's no $_GET['orderby'] then check if the
+ * initial sorting information is set in the sortable columns and use that.
+ */
+ if ( '' === $current_orderby && $initial_order ) {
+ // Use the initially sorted column $orderby as current orderby.
+ $current_orderby = $orderby;
+ // Use the initially sorted column asc/desc order as initial order.
+ $current_order = $initial_order;
+ }
+
+ /*
+ * True in the initial view when an initial orderby is set via get_sortable_columns()
+ * and true in the sorted views when the actual $_GET['orderby'] is equal to $orderby.
+ */
+ if ( $current_orderby === $orderby ) {
+ /* translators: Hidden accessibility text. */
+ $asc_text = __( 'Ascending.' );
+ /* translators: Hidden accessibility text. */
+ $desc_text = __( 'Descending.' );
+ $order_text = 'asc' === $current_order ? $asc_text : $desc_text;
+ echo '<caption class="screen-reader-text">' . $orderby_text . ' ' . $order_text . '</caption>';
+
+ return;
+ }
+ }
}
}
@@ -1273,6 +1622,7 @@
$this->screen->render_screen_reader_content( 'heading_list' );
?>
<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
+ <?php $this->print_table_description(); ?>
<thead>
<tr>
<?php $this->print_column_headers(); ?>
@@ -1319,7 +1669,7 @@
* Generates the table navigation above or below the table
*
* @since 3.1.0
- * @param string $which
+ * @param string $which The location of the navigation: Either 'top' or 'bottom'.
*/
protected function display_tablenav( $which ) {
if ( 'top' === $which ) {
@@ -1344,7 +1694,7 @@
}
/**
- * Extra controls to be displayed between bulk actions and pagination.
+ * Displays extra controls between bulk actions and pagination.
*
* @since 3.1.0
*
@@ -1422,8 +1772,10 @@
$classes .= ' hidden';
}
- // Comments column uses HTML in the display name with screen reader text.
- // Strip tags to get closer to a user-friendly string.
+ /*
+ * Comments column uses HTML in the display name with screen reader text.
+ * Strip tags to get closer to a user-friendly string.
+ */
$data = 'data-colname="' . esc_attr( wp_strip_all_tags( $column_display_name ) ) . '"';
$attributes = "class='$classes' $data";
@@ -1466,7 +1818,10 @@
* if the current column is not the primary column.
*/
protected function handle_row_actions( $item, $column_name, $primary ) {
- return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
+ return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' .
+ /* translators: Hidden accessibility text. */
+ __( 'Show more details' ) .
+ '</span></button>' : '';
}
/**