From 09fb3ea03ec554fe2ede5fb65118ab64a751e1df Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Wed, 21 Jun 2023 21:45:52 +0000 Subject: [PATCH] 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. git-svn-id: https://develop.svn.wordpress.org/trunk@55971 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/list-tables.css | 58 +++---- .../includes/class-wp-comments-list-table.php | 12 +- .../includes/class-wp-links-list-table.php | 8 +- src/wp-admin/includes/class-wp-list-table.php | 144 ++++++++++++++++-- .../includes/class-wp-media-list-table.php | 10 +- .../includes/class-wp-ms-sites-list-table.php | 15 +- .../class-wp-ms-themes-list-table.php | 2 +- .../includes/class-wp-ms-users-list-table.php | 8 +- .../includes/class-wp-posts-list-table.php | 28 +++- .../includes/class-wp-terms-list-table.php | 18 ++- .../includes/class-wp-users-list-table.php | 4 +- 11 files changed, 234 insertions(+), 73 deletions(-) diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index 2976d2f982..4d0e6be813 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -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; diff --git a/src/wp-admin/includes/class-wp-comments-list-table.php b/src/wp-admin/includes/class-wp-comments-list-table.php index c7dfcc6159..49c3dc16ff 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -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 { ?> + ' . __( 'Ordered by Comment Date, descending.' ) . '

'; + } else { + $this->print_table_description(); + } + ?> print_column_headers(); ?> diff --git a/src/wp-admin/includes/class-wp-links-list-table.php b/src/wp-admin/includes/class-wp-links-list-table.php index b676629453..f29cf7b3e2 100644 --- a/src/wp-admin/includes/class-wp-links-list-table.php +++ b/src/wp-admin/includes/class-wp-links-list-table.php @@ -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.' ) ), ); } diff --git a/src/wp-admin/includes/class-wp-list-table.php b/src/wp-admin/includes/class-wp-list-table.php index 1e6a8ea505..4dfb825996 100644 --- a/src/wp-admin/includes/class-wp-list-table.php +++ b/src/wp-admin/includes/class-wp-list-table.php @@ -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 = ' ' . $order_text . ''; } - $column_display_name = sprintf( - '%s', - 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 = '' . $column_display_name . '' . $order_text . ''; } $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"; + echo "<$tag $scope $id $class $aria_sort_attr $abbr_attr>$column_display_name"; + } + } + + /** + * 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 '
' . $orderby_text . ' ' . $order_text . '

'; + + return; + } + } } } @@ -1384,6 +1501,7 @@ class WP_List_Table { $this->screen->render_screen_reader_content( 'heading_list' ); ?> + print_table_description(); ?> print_column_headers(); ?> diff --git a/src/wp-admin/includes/class-wp-media-list-table.php b/src/wp-admin/includes/class-wp-media-list-table.php index cbe419d920..5ae5b25240 100644 --- a/src/wp-admin/includes/class-wp-media-list-table.php +++ b/src/wp-admin/includes/class-wp-media-list-table.php @@ -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' ), ); } diff --git a/src/wp-admin/includes/class-wp-ms-sites-list-table.php b/src/wp-admin/includes/class-wp-ms-sites-list-table.php index 2416dd1f73..1359687235 100644 --- a/src/wp-admin/includes/class-wp-ms-sites-list-table.php +++ b/src/wp-admin/includes/class-wp-ms-sites-list-table.php @@ -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' ), ); } diff --git a/src/wp-admin/includes/class-wp-ms-themes-list-table.php b/src/wp-admin/includes/class-wp-ms-themes-list-table.php index d1d467f19e..6dfe3fe71d 100644 --- a/src/wp-admin/includes/class-wp-ms-themes-list-table.php +++ b/src/wp-admin/includes/class-wp-ms-themes-list-table.php @@ -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' ), ); } diff --git a/src/wp-admin/includes/class-wp-ms-users-list-table.php b/src/wp-admin/includes/class-wp-ms-users-list-table.php index e1eec05cba..2e6772f69d 100644 --- a/src/wp-admin/includes/class-wp-ms-users-list-table.php +++ b/src/wp-admin/includes/class-wp-ms-users-list-table.php @@ -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.' ) ), ); } diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index 73f6194812..54bd19441d 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -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; } /** diff --git a/src/wp-admin/includes/class-wp-terms-list-table.php b/src/wp-admin/includes/class-wp-terms-list-table.php index 0671f8580d..3c277d6a61 100644 --- a/src/wp-admin/includes/class-wp-terms-list-table.php +++ b/src/wp-admin/includes/class-wp-terms-list-table.php @@ -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.' ) ), ); } diff --git a/src/wp-admin/includes/class-wp-users-list-table.php b/src/wp-admin/includes/class-wp-users-list-table.php index 4c4d5a9f6d..593479abc0 100644 --- a/src/wp-admin/includes/class-wp-users-list-table.php +++ b/src/wp-admin/includes/class-wp-users-list-table.php @@ -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;