From 1de3afa54835080f98f2d6a4a716e2dc03241d60 Mon Sep 17 00:00:00 2001 From: Maxerns Date: Thu, 2 Jul 2026 16:56:40 +0100 Subject: [PATCH 1/9] nhance hierarchical display with aria attributes and update tests --- .../includes/class-wp-posts-list-table.php | 32 +++++++- .../phpunit/tests/admin/wpPostsListTable.php | 76 +++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) 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 18c76169eb81c..84d57dfef1f4f 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1131,17 +1131,43 @@ public function column_title( $post ) { echo '
' . $locked_avatar . ' ' . $locked_text . "
\n"; } - $pad = str_repeat( '— ', $this->current_level ); + $pad = str_repeat( + ' ', + $this->current_level + ); + $described_by_attr = ''; + $hierarchy = ''; + + if ( $post->post_parent ) { + $parent = get_post( $post->post_parent ); + + if ( $parent ) { + /** This filter is documented in wp-includes/post-template.php */ + $parent_title = apply_filters( 'the_title', $parent->post_title, $parent->ID ); + + $hierarchy_id = 'post-hierarchy-' . $post->ID; + $described_by_attr = sprintf( ' aria-describedby="%s"', esc_attr( $hierarchy_id ) ); + $hierarchy = sprintf( + '', + esc_attr( $hierarchy_id ), + /* translators: %s: Parent post title. */ + esc_html( sprintf( __( 'Child of %s' ), $parent_title ) ) + ); + } + } + echo ''; $title = _draft_or_post_title(); if ( $can_edit_post && 'trash' !== $post->post_status ) { printf( - '%s%s', + '%2$s%4$s%5$s', get_edit_post_link( $post->ID ), $pad, - $title + $described_by_attr, + $title, + $hierarchy ); } else { printf( diff --git a/tests/phpunit/tests/admin/wpPostsListTable.php b/tests/phpunit/tests/admin/wpPostsListTable.php index 9d2482a034af7..eefdc34ea8075 100644 --- a/tests/phpunit/tests/admin/wpPostsListTable.php +++ b/tests/phpunit/tests/admin/wpPostsListTable.php @@ -196,6 +196,61 @@ public function test_grandchildren_hierarchical_pages_second_page() { ); } + /** + * @ticket 64932 + * + * @covers WP_Posts_List_Table::display_rows + * @covers WP_Posts_List_Table::set_hierarchical_display + * @covers WP_Posts_List_Table::column_title + */ + public function test_child_page_row_title_has_hierarchy_description() { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + $output = $this->get_hierarchical_page_list_output( + array( + self::$top[1], + self::$children[1][1], + ) + ); + + $expected = sprintf( + ' ' . + 'Child 1' . + '', + get_edit_post_link( self::$children[1][1]->ID ), + self::$children[1][1]->ID + ); + + $this->assertStringContainsString( $expected, $output ); + } + + /** + * @ticket 64932 + * + * @covers WP_Posts_List_Table::display_rows + * @covers WP_Posts_List_Table::set_hierarchical_display + * @covers WP_Posts_List_Table::column_title + */ + public function test_top_level_page_row_title_does_not_have_hierarchy_description() { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + $output = $this->get_hierarchical_page_list_output( + array( + self::$top[1], + ) + ); + + $this->assertStringContainsString( + sprintf( + 'Top Level Page 1', + get_edit_post_link( self::$top[1]->ID ) + ), + $output + ); + $this->assertStringNotContainsString( 'aria-describedby="post-hierarchy-', $output ); + $this->assertStringNotContainsString( 'class="hidden">Child of ', $output ); + } + /** * Helper function to test the output of a page which uses `WP_Posts_List_Table`. * @@ -245,6 +300,27 @@ protected function _test_list_hierarchical_page( array $args, array $expected_id } } + /** + * Gets the output for a hierarchical page list table. + * + * @param WP_Post[] $posts Posts to display. + * @return string List table rows output. + */ + protected function get_hierarchical_page_list_output( array $posts ) { + $_REQUEST['paged'] = 1; + $GLOBALS['per_page'] = 20; + + ob_start(); + $this->table->set_hierarchical_display( true ); + $this->table->display_rows( $posts ); + $output = ob_get_clean(); + + unset( $_REQUEST['paged'] ); + unset( $GLOBALS['per_page'] ); + + return $output; + } + /** * @ticket 37407 * From 303135e8db443566c9c3b3b154d5a1cebe3da4d7 Mon Sep 17 00:00:00 2001 From: Maxerns Date: Tue, 7 Jul 2026 21:53:09 +0100 Subject: [PATCH 2/9] rearrange link formatting in posts list table --- src/wp-admin/includes/class-wp-posts-list-table.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 84d57dfef1f4f..b13694f17429a 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1162,9 +1162,9 @@ public function column_title( $post ) { if ( $can_edit_post && 'trash' !== $post->post_status ) { printf( - '%2$s%4$s%5$s', - get_edit_post_link( $post->ID ), + '%1$s%4$s%5$s', $pad, + get_edit_post_link( $post->ID ), $described_by_attr, $title, $hierarchy From 577b3b7487030a01861f3289df80b3a5360f96be Mon Sep 17 00:00:00 2001 From: Maxerns Date: Sun, 12 Jul 2026 12:31:50 +0100 Subject: [PATCH 3/9] trigger CI rerun after flaky wp cli setup failure From 17d9ba72f76cfcc88b9fdc2e555bee2d632d96e0 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 21 Jul 2026 16:14:59 -0500 Subject: [PATCH 4/9] Strip tags before emitting inside `span` to avoid announcing escaped tags. --- src/wp-admin/includes/class-wp-posts-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 37752f505f5b4..54e7f4f4c3d00 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1194,7 +1194,7 @@ public function column_title( $post ) { '', esc_attr( $hierarchy_id ), /* translators: %s: Parent post title. */ - esc_html( sprintf( __( 'Child of %s' ), $parent_title ) ) + esc_html( sprintf( __( 'Child of %s' ), wp_strip_all_tags( $parent_title ) ) ) ); } } From 5ab388d8c6d62aa0afcaf4d91765a0991fd03354 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 21 Jul 2026 16:30:50 -0500 Subject: [PATCH 5/9] Also render relevant information when not editable Span can't take `aria-describedby`, so switch to screen-reader-text and output inside title wrapper. --- src/wp-admin/includes/class-wp-posts-list-table.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 54e7f4f4c3d00..ce84c0a615465 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1219,10 +1219,18 @@ public function column_title( $post ) { $hierarchy ); } else { + $processor = new WP_HTML_Tag_Processor( $hierarchy ); + if ( true === $processor->next_tag( 'span' ) ) { + $processor->remove_class( 'hidden' ); + $processor->add_class( 'screen-reader-text' ); + $hierarchy = $processor->get_updated_html(); + } + printf( - '%s%s', + '%1$s%2$s%3$s', $pad, - $title + $title, + $hierarchy, ); } _post_states( $post ); From 09fc3c64cc0aad3b8dbcd37bb73504eae2548b5f Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 21 Jul 2026 16:31:40 -0500 Subject: [PATCH 6/9] Add a space inside wrapper to add a speech break when used as screen reader text. --- src/wp-admin/includes/class-wp-posts-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ce84c0a615465..b23d97122fca5 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1191,7 +1191,7 @@ public function column_title( $post ) { $hierarchy_id = 'post-hierarchy-' . $post->ID; $described_by_attr = sprintf( ' aria-describedby="%s"', esc_attr( $hierarchy_id ) ); $hierarchy = sprintf( - '', + '', esc_attr( $hierarchy_id ), /* translators: %s: Parent post title. */ esc_html( sprintf( __( 'Child of %s' ), wp_strip_all_tags( $parent_title ) ) ) From 84c901c411115307312d715d8d092c3d2735b19d Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 21 Jul 2026 16:40:03 -0500 Subject: [PATCH 7/9] Don't instantiate HTML API on empty strings; removing trailing comma; update tests for new expectation. --- .../includes/class-wp-posts-list-table.php | 14 ++++++++------ tests/phpunit/tests/admin/wpPostsListTable.php | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) 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 b23d97122fca5..d1469e31b2295 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1219,18 +1219,20 @@ public function column_title( $post ) { $hierarchy ); } else { - $processor = new WP_HTML_Tag_Processor( $hierarchy ); - if ( true === $processor->next_tag( 'span' ) ) { - $processor->remove_class( 'hidden' ); - $processor->add_class( 'screen-reader-text' ); - $hierarchy = $processor->get_updated_html(); + if ( '' !== $hierarchy ) { + $processor = new WP_HTML_Tag_Processor( $hierarchy ); + if ( true === $processor->next_tag( 'span' ) ) { + $processor->remove_class( 'hidden' ); + $processor->add_class( 'screen-reader-text' ); + $hierarchy = $processor->get_updated_html(); + } } printf( '%1$s%2$s%3$s', $pad, $title, - $hierarchy, + $hierarchy ); } _post_states( $post ); diff --git a/tests/phpunit/tests/admin/wpPostsListTable.php b/tests/phpunit/tests/admin/wpPostsListTable.php index 25324dcb4cb7f..701499bb395e1 100644 --- a/tests/phpunit/tests/admin/wpPostsListTable.php +++ b/tests/phpunit/tests/admin/wpPostsListTable.php @@ -216,7 +216,7 @@ public function test_child_page_row_title_has_hierarchy_description() { $expected = sprintf( ' ' . 'Child 1' . - '', + '', get_edit_post_link( self::$children[1][1]->ID ), self::$children[1][1]->ID ); @@ -248,7 +248,7 @@ public function test_top_level_page_row_title_does_not_have_hierarchy_descriptio $output ); $this->assertStringNotContainsString( 'aria-describedby="post-hierarchy-', $output ); - $this->assertStringNotContainsString( 'class="hidden">Child of ', $output ); + $this->assertStringNotContainsString( 'class="hidden"> Child of ', $output ); } /** From 2d62ffbfdfed9e228c25c8b421056a25ac20ae87 Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 21 Jul 2026 17:35:44 -0500 Subject: [PATCH 8/9] Avoid HTML API overhead and make easier to use different output wrappers. --- .../includes/class-wp-posts-list-table.php | 26 +++++++++---------- .../phpunit/tests/admin/wpPostsListTable.php | 4 +-- 2 files changed, 14 insertions(+), 16 deletions(-) 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 d1469e31b2295..b0a5e674bb8b2 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1179,7 +1179,8 @@ public function column_title( $post ) { $this->current_level ); $described_by_attr = ''; - $hierarchy = ''; + $hierarchy_linked = ''; + $hierarchy_nolink = ''; if ( $post->post_parent ) { $parent = get_post( $post->post_parent ); @@ -1190,8 +1191,14 @@ public function column_title( $post ) { $hierarchy_id = 'post-hierarchy-' . $post->ID; $described_by_attr = sprintf( ' aria-describedby="%s"', esc_attr( $hierarchy_id ) ); - $hierarchy = sprintf( - '', + $hierarchy_linked = sprintf( + '', + esc_attr( $hierarchy_id ), + /* translators: %s: Parent post title. */ + esc_html( sprintf( __( 'Child of %s' ), wp_strip_all_tags( $parent_title ) ) ) + ); + $hierarchy_nolink = sprintf( + ' (%2$s)', esc_attr( $hierarchy_id ), /* translators: %s: Parent post title. */ esc_html( sprintf( __( 'Child of %s' ), wp_strip_all_tags( $parent_title ) ) ) @@ -1216,23 +1223,14 @@ public function column_title( $post ) { get_edit_post_link( $post->ID ), $described_by_attr, $title, - $hierarchy + $hierarchy_linked ); } else { - if ( '' !== $hierarchy ) { - $processor = new WP_HTML_Tag_Processor( $hierarchy ); - if ( true === $processor->next_tag( 'span' ) ) { - $processor->remove_class( 'hidden' ); - $processor->add_class( 'screen-reader-text' ); - $hierarchy = $processor->get_updated_html(); - } - } - printf( '%1$s%2$s%3$s', $pad, $title, - $hierarchy + $hierarchy_nolink ); } _post_states( $post ); diff --git a/tests/phpunit/tests/admin/wpPostsListTable.php b/tests/phpunit/tests/admin/wpPostsListTable.php index 701499bb395e1..25324dcb4cb7f 100644 --- a/tests/phpunit/tests/admin/wpPostsListTable.php +++ b/tests/phpunit/tests/admin/wpPostsListTable.php @@ -216,7 +216,7 @@ public function test_child_page_row_title_has_hierarchy_description() { $expected = sprintf( ' ' . 'Child 1' . - '', + '', get_edit_post_link( self::$children[1][1]->ID ), self::$children[1][1]->ID ); @@ -248,7 +248,7 @@ public function test_top_level_page_row_title_does_not_have_hierarchy_descriptio $output ); $this->assertStringNotContainsString( 'aria-describedby="post-hierarchy-', $output ); - $this->assertStringNotContainsString( 'class="hidden"> Child of ', $output ); + $this->assertStringNotContainsString( 'class="hidden">Child of ', $output ); } /** From 912c08ef975439a6c12675f2df7c1674e4fd4efa Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 21 Jul 2026 17:57:15 -0500 Subject: [PATCH 9/9] Fix variable alignment --- src/wp-admin/includes/class-wp-posts-list-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b0a5e674bb8b2..7522f8561ba44 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1197,7 +1197,7 @@ public function column_title( $post ) { /* translators: %s: Parent post title. */ esc_html( sprintf( __( 'Child of %s' ), wp_strip_all_tags( $parent_title ) ) ) ); - $hierarchy_nolink = sprintf( + $hierarchy_nolink = sprintf( ' (%2$s)', esc_attr( $hierarchy_id ), /* translators: %s: Parent post title. */