Administration: Set accessible state for list table headers.

Implement `aria-sort` and change icon states to indicate current sort for list tables. Allow screen reader users to get context about the current sort and allow sighted users to know how the table is currently sorted.

Props afercia, rianrietveld, joedolson, alexstine, johnjamesjacoby.
Fixes #32170.
Built from https://develop.svn.wordpress.org/trunk@55971


git-svn-id: http://core.svn.wordpress.org/trunk@55483 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
joedolson 2023-06-21 21:47:23 +00:00
parent e2f114db08
commit fd17918a8a
15 changed files with 267 additions and 104 deletions

View File

@ -462,50 +462,64 @@ table.media .column-title .filename {
width: 160px;
}
.sorting-indicators {
display: grid;
}
.sorting-indicator {
display: block;
visibility: hidden;
width: 10px;
height: 4px;
margin-top: 8px;
margin-top: 4px;
margin-right: 7px;
}
.sorting-indicator:before {
content: "\f142";
font: normal 20px/1 dashicons;
speak: never;
display: inline-block;
padding: 0;
top: -4px;
right: -8px;
color: #3c434a;
line-height: 0.5;
position: relative;
vertical-align: top;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-decoration: none !important;
color: #3c434a;
color: #a7aaad;
}
.column-comments .sorting-indicator:before {
top: 0;
right: -10px;
}
th.sorted.asc .sorting-indicator:before,
th.desc:hover span.sorting-indicator:before,
th.desc a:focus span.sorting-indicator:before {
.sorting-indicator.asc:before {
content: "\f142";
}
th.sorted.desc .sorting-indicator:before,
th.asc:hover span.sorting-indicator:before,
th.asc a:focus span.sorting-indicator:before {
.sorting-indicator.desc:before {
content: "\f140";
}
th.sorted.desc .sorting-indicator.desc:before {
color: #1d2327;
}
th.sorted.asc .sorting-indicator.asc:before {
color: #1d2327;
}
th.sorted.asc a:focus .sorting-indicator.asc:before,
th.sorted.asc:hover .sorting-indicator.asc:before,
th.sorted.desc a:focus .sorting-indicator.desc:before,
th.sorted.desc:hover .sorting-indicator.desc:before {
color: #a7aaad;
}
th.sorted.asc a:focus .sorting-indicator.desc:before,
th.sorted.asc:hover .sorting-indicator.desc:before,
th.sorted.desc a:focus .sorting-indicator.asc:before,
th.sorted.desc:hover .sorting-indicator.asc:before {
color: #1d2327;
}
.wp-list-table .toggle-row {
position: absolute;
left: 8px;
@ -614,10 +628,6 @@ tr.wp-locked .row-actions .trash {
display: none;
}
.fixed .column-comments .sorting-indicator {
margin-top: 3px;
}
#menu-locations-wrap .widefat {
width: 60%;
}
@ -645,14 +655,6 @@ th.sorted a span {
cursor: pointer;
}
th.sorted .sorting-indicator,
th.desc:hover span.sorting-indicator,
th.desc a:focus span.sorting-indicator,
th.asc:hover span.sorting-indicator,
th.asc a:focus span.sorting-indicator {
visibility: visible;
}
.tablenav-pages .current-page {
margin: 0 0 0 2px;
font-size: 13px;

File diff suppressed because one or more lines are too long

View File

@ -461,50 +461,64 @@ table.media .column-title .filename {
width: 160px;
}
.sorting-indicators {
display: grid;
}
.sorting-indicator {
display: block;
visibility: hidden;
width: 10px;
height: 4px;
margin-top: 8px;
margin-top: 4px;
margin-left: 7px;
}
.sorting-indicator:before {
content: "\f142";
font: normal 20px/1 dashicons;
speak: never;
display: inline-block;
padding: 0;
top: -4px;
left: -8px;
color: #3c434a;
line-height: 0.5;
position: relative;
vertical-align: top;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-decoration: none !important;
color: #3c434a;
color: #a7aaad;
}
.column-comments .sorting-indicator:before {
top: 0;
left: -10px;
}
th.sorted.asc .sorting-indicator:before,
th.desc:hover span.sorting-indicator:before,
th.desc a:focus span.sorting-indicator:before {
.sorting-indicator.asc:before {
content: "\f142";
}
th.sorted.desc .sorting-indicator:before,
th.asc:hover span.sorting-indicator:before,
th.asc a:focus span.sorting-indicator:before {
.sorting-indicator.desc:before {
content: "\f140";
}
th.sorted.desc .sorting-indicator.desc:before {
color: #1d2327;
}
th.sorted.asc .sorting-indicator.asc:before {
color: #1d2327;
}
th.sorted.asc a:focus .sorting-indicator.asc:before,
th.sorted.asc:hover .sorting-indicator.asc:before,
th.sorted.desc a:focus .sorting-indicator.desc:before,
th.sorted.desc:hover .sorting-indicator.desc:before {
color: #a7aaad;
}
th.sorted.asc a:focus .sorting-indicator.desc:before,
th.sorted.asc:hover .sorting-indicator.desc:before,
th.sorted.desc a:focus .sorting-indicator.asc:before,
th.sorted.desc:hover .sorting-indicator.asc:before {
color: #1d2327;
}
.wp-list-table .toggle-row {
position: absolute;
right: 8px;
@ -613,10 +627,6 @@ tr.wp-locked .row-actions .trash {
display: none;
}
.fixed .column-comments .sorting-indicator {
margin-top: 3px;
}
#menu-locations-wrap .widefat {
width: 60%;
}
@ -644,14 +654,6 @@ th.sorted a span {
cursor: pointer;
}
th.sorted .sorting-indicator,
th.desc:hover span.sorting-indicator,
th.desc a:focus span.sorting-indicator,
th.asc:hover span.sorting-indicator,
th.asc a:focus span.sorting-indicator {
visibility: visible;
}
.tablenav-pages .current-page {
margin: 0 2px 0 0;
font-size: 13px;

File diff suppressed because one or more lines are too long

View File

@ -540,8 +540,8 @@ class WP_Comments_List_Table extends WP_List_Table {
*/
protected function get_sortable_columns() {
return array(
'author' => 'comment_author',
'response' => 'comment_post_ID',
'author' => array( 'comment_author', false, __( 'Author' ), __( 'Table ordered by Comment Author.' ) ),
'response' => array( 'comment_post_ID', false, _x( 'In Response To', 'column name' ), __( 'Table ordered by Post Replied To.' ) ),
'date' => 'comment_date',
);
}
@ -580,6 +580,14 @@ class WP_Comments_List_Table extends WP_List_Table {
?>
<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">' . __( 'Ordered by Comment Date, descending.' ) . '</p>';
} else {
$this->print_table_description();
}
?>
<thead>
<tr>
<?php $this->print_column_headers(); ?>

View File

@ -143,10 +143,10 @@ class WP_Links_List_Table extends WP_List_Table {
*/
protected function get_sortable_columns() {
return array(
'name' => 'name',
'url' => 'url',
'visible' => 'visible',
'rating' => 'rating',
'name' => array( 'name', false, _x( 'Name', 'link name' ), __( 'Table ordered by Name.' ), 'asc' ),
'url' => array( 'url', false, __( 'URL' ), __( 'Table ordered by URL.' ) ),
'visible' => array( 'visible', false, __( 'Visible' ), __( 'Table ordered by Visibility.' ) ),
'rating' => array( 'rating', false, __( 'Rating' ), __( 'Table ordered by Rating.' ) ),
);
}

View File

@ -1109,10 +1109,17 @@ class WP_List_Table {
*
* 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
*/
@ -1253,9 +1260,22 @@ class WP_List_Table {
}
$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;
}
@ -1292,15 +1312,19 @@ class WP_List_Table {
$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 = '';
}
if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
// 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';
}
@ -1317,7 +1341,10 @@ class WP_List_Table {
}
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';
@ -1334,29 +1361,53 @@ class WP_List_Table {
}
if ( isset( $sortable[ $column_key ] ) ) {
list( $orderby, $desc_first ) = $sortable[ $column_key ];
list( $orderby, $desc_first, $abbr, $orderby_text, $initial_order ) = $sortable[ $column_key ];
/*
* 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 ) ) {
$order = $desc_first ? 'desc' : 'asc';
}
$class[] = 'sortable';
$class[] = 'desc' === $order ? 'asc' : 'desc';
$class[] = 'sortable';
$class[] = 'desc' === $order ? 'asc' : 'desc';
$order_text = 'asc' === $order ? __( 'Sort ascending.' ) : __( 'Sort descending.' );
}
if ( '' !== $order_text ) {
$order_text = ' <span class="screen-reader-text">' . $order_text . '</span>';
}
$column_display_name = sprintf(
'<a href="%s"><span>%s</span><span class="sorting-indicator"></span></a>',
esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ),
$column_display_name
);
// Print an 'abbr' attribute if a value is provided via get_sortable_columns().
$abbr_attr = $abbr ? ' abbr="' . esc_attr( $abbr ) . '"' : '';
$column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicators"><span class="sorting-indicator asc" aria-hidden="true"></span><span class="sorting-indicator desc" aria-hidden="true"></span>' . $order_text . '</a>';
}
$tag = ( 'cb' === $column_key ) ? 'td' : 'th';
@ -1367,7 +1418,73 @@ class WP_List_Table {
$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 4.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 ] ) ) {
list( $orderby, $desc_first, $abbr, $orderby_text, $initial_order ) = $sortable[ $column_key ];
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 ) {
$order_text = 'asc' === $current_order ? __( 'Ascending.' ) : __( 'Descending.' );
echo '<caption class="screen-reader-text">' . $orderby_text . ' ' . $order_text . '</p>';
return;
}
}
}
}
@ -1384,6 +1501,7 @@ class WP_List_Table {
$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(); ?>

View File

@ -389,11 +389,11 @@ class WP_Media_List_Table extends WP_List_Table {
*/
protected function get_sortable_columns() {
return array(
'title' => 'title',
'author' => 'author',
'parent' => 'parent',
'comments' => 'comment_count',
'date' => array( 'date', true ),
'title' => array( 'title', false, _x( 'File', 'column name' ), __( 'Table ordered by File Name.' ) ),
'author' => array( 'author', false, __( 'Author' ), __( 'Table ordered by Author.' ) ),
'parent' => array( 'parent', false, _x( 'Uploaded to', 'column name' ), __( 'Table ordered by Uploaded To.' ) ),
'comments' => array( 'comment_count', __( 'Comments' ), false, __( 'Table ordered by Comments.' ) ),
'date' => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ), 'desc' ),
);
}

View File

@ -389,10 +389,19 @@ class WP_MS_Sites_List_Table extends WP_List_Table {
* @return array
*/
protected function get_sortable_columns() {
if ( is_subdomain_install() ) {
$abbr = __( 'Domain' );
$blogname_orderby_text = __( 'Table ordered by Site Domain Name.' );
} else {
$abbr = __( 'Path' );
$blogname_orderby_text = __( 'Table ordered by Site Path.' );
}
return array(
'blogname' => 'blogname',
'lastupdated' => 'lastupdated',
'registered' => 'blog_id',
'blogname' => array( 'blogname', false, $abbr, $blogname_orderby_text ),
'lastupdated' => array( 'lastupdated', true, __( 'Last Updated' ), __( 'Table ordered by Last Updated.' ) ),
'registered' => array( 'blog_id', true, _x( 'Registered', 'site' ), __( 'Table ordered by Site Registered Date.' ), 'desc' ),
);
}

View File

@ -343,7 +343,7 @@ class WP_MS_Themes_List_Table extends WP_List_Table {
*/
protected function get_sortable_columns() {
return array(
'name' => 'name',
'name' => array( 'name', false, __( 'Theme' ), __( 'Table ordered by Theme Name.' ), 'asc' ),
);
}

View File

@ -212,10 +212,10 @@ class WP_MS_Users_List_Table extends WP_List_Table {
*/
protected function get_sortable_columns() {
return array(
'username' => 'login',
'name' => 'name',
'email' => 'email',
'registered' => 'id',
'username' => array( 'login', false, __( 'Username' ), __( 'Table ordered by Username.' ), 'asc' ),
'name' => array( 'name', false, __( 'Name' ), __( 'Table ordered by Name.' ) ),
'email' => array( 'email', false, __( 'E-mail' ), __( 'Table ordered by E-mail.' ) ),
'registered' => array( 'id', false, _x( 'Registered', 'user' ), __( 'Table ordered by User Registered Date.' ) ),
);
}

View File

@ -760,12 +760,28 @@ class WP_Posts_List_Table extends WP_List_Table {
* @return array
*/
protected function get_sortable_columns() {
return array(
'title' => 'title',
'parent' => 'parent',
'comments' => 'comment_count',
'date' => array( 'date', true ),
);
$post_type = $this->screen->post_type;
if ( 'page' === $post_type ) {
$title_orderby_text = isset( $_GET['orderby'] ) ? __( 'Table ordered by Title.' ) : __( 'Table ordered by Hierarchical Menu Order and Title.' );
$sortables = array(
'title' => array( 'title', false, __( 'Title' ), $title_orderby_text, 'asc' ),
'parent' => array( 'parent', false ),
'comments' => array( 'comment_count', false, __( 'Comments' ), __( 'Table ordered by Comments.' ) ),
'date' => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ) ),
);
} else {
$sortables = array(
'title' => array( 'title', false, __( 'Title' ), __( 'Table ordered by Title.' ) ),
'parent' => array( 'parent', false ),
'comments' => array( 'comment_count', false, __( 'Comments' ), __( 'Table ordered by Comments.' ) ),
'date' => array( 'date', true, __( 'Date' ), __( 'Table ordered by Date.' ), 'desc' ),
);
}
// Custom Post Types: there's a filter for that, see get_column_info().
return $sortables;
}
/**

View File

@ -208,12 +208,20 @@ class WP_Terms_List_Table extends WP_List_Table {
* @return array
*/
protected function get_sortable_columns() {
$taxonomy = $this->screen->taxonomy;
if ( ! isset( $_GET['orderby'] ) && is_taxonomy_hierarchical( $taxonomy ) ) {
$name_orderby_text = __( 'Table ordered hierarchically.' );
} else {
$name_orderby_text = __( 'Table ordered by Name.' );
}
return array(
'name' => 'name',
'description' => 'description',
'slug' => 'slug',
'posts' => 'count',
'links' => 'count',
'name' => array( 'name', false, _x( 'Name', 'term name' ), $name_orderby_text, 'asc' ),
'description' => array( 'description', false, __( 'Description' ), __( 'Table ordered by Description.' ) ),
'slug' => array( 'slug', false, __( 'Slug' ), __( 'Table ordered by Slug.' ) ),
'posts' => array( 'count', false, _x( 'Count', 'Number/count of items' ), __( 'Table ordered by Posts Count.' ) ),
'links' => array( 'count', false, __( 'Links' ), __( 'Table ordered by Links.' ) ),
);
}

View File

@ -393,8 +393,8 @@ class WP_Users_List_Table extends WP_List_Table {
*/
protected function get_sortable_columns() {
$columns = array(
'username' => 'login',
'email' => 'email',
'username' => array( 'login', false, __( 'Username' ), __( 'Table ordered by Username.' ), 'asc' ),
'email' => array( 'email', false, __( 'E-mail' ), __( 'Table ordered by E-mail.' ) ),
);
return $columns;

View File

@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
$wp_version = '6.3-alpha-55970';
$wp_version = '6.3-alpha-55971';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.