Skip to content

Commit 4221931

Browse files
Added accessibility support for hierarchial post type
1 parent e12ddb3 commit 4221931

2 files changed

Lines changed: 150 additions & 1 deletion

File tree

src/wp-admin/includes/class-wp-posts-list-table.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1143,9 +1143,36 @@ public function column_title( $post ) {
11431143
$title = _draft_or_post_title();
11441144

11451145
if ( $can_edit_post && 'trash' !== $post->post_status ) {
1146+
if ( $this->current_level > 0 ) {
1147+
/*
1148+
* When displayed in hierarchical mode, $parent_name is only set for
1149+
* the "accidental level 0" code path above. Derive it from the post's
1150+
* immediate parent for all other hierarchical cases.
1151+
*/
1152+
if ( ! isset( $parent_name ) && $post->post_parent > 0 ) {
1153+
$immediate_parent = get_post( $post->post_parent );
1154+
if ( $immediate_parent instanceof WP_Post ) {
1155+
/** This filter is documented in wp-includes/post-template.php */
1156+
$parent_name = apply_filters( 'the_title', $immediate_parent->post_title, $immediate_parent->ID );
1157+
}
1158+
}
1159+
1160+
if ( isset( $parent_name ) ) {
1161+
/* translators: Accessibility text for a subpage row-title link in the pages list table. 1: Post title. 2: Parent page title. */
1162+
$aria_label = sprintf( __( '“%1$s” (subpage of “%2$s”) (Edit)' ), $title, $parent_name );
1163+
} else {
1164+
/* translators: Accessibility text for a subpage row-title link when parent title is unavailable. %s: Post title. */
1165+
$aria_label = sprintf( __( '“%s” (subpage) (Edit)' ), $title );
1166+
}
1167+
} else {
1168+
/* translators: Accessibility label for a post row-title link in the list table. %s: Post title. */
1169+
$aria_label = sprintf( __( '“%s” (Edit)' ), $title );
1170+
}
1171+
11461172
printf(
1147-
'<a class="row-title" href="%s">%s%s</a>',
1173+
'<a class="row-title" href="%s" aria-label="%s">%s%s</a>',
11481174
get_edit_post_link( $post->ID ),
1175+
esc_attr( $aria_label ),
11491176
$pad,
11501177
$title
11511178
);

tests/phpunit/tests/admin/wpPostsListTable.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,128 @@ protected function _test_list_hierarchical_page( array $args, array $expected_id
245245
}
246246
}
247247

248+
/**
249+
* Tests that a top-level page link has an aria-label with the title and "(Edit)".
250+
*
251+
* @ticket 62006
252+
*
253+
* @covers WP_Posts_List_Table::column_title
254+
*/
255+
public function test_top_level_page_aria_label() {
256+
$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
257+
wp_set_current_user( $admin_id );
258+
259+
$post = self::$top[1];
260+
$title = apply_filters( 'the_title', $post->post_title, $post->ID );
261+
262+
// level=0 → top-level page.
263+
ob_start();
264+
$this->table->single_row( $post, 0 );
265+
$output = ob_get_clean();
266+
267+
// Expected: "Title" (Edit) — using curly quotes as the translatable string uses &#8220;/&#8221;.
268+
$this->assertStringContainsString( 'aria-label=', $output );
269+
$this->assertStringContainsString( esc_attr( $title ), $output );
270+
$this->assertStringContainsString( '(Edit)', html_entity_decode( $output, ENT_QUOTES | ENT_HTML5 ) );
271+
$this->assertStringNotContainsString( 'subpage', $output );
272+
273+
wp_set_current_user( 0 );
274+
}
275+
276+
/**
277+
* Tests that a child page link includes "subpage of" with the parent page title in its aria-label.
278+
*
279+
* @ticket 62006
280+
*
281+
* @covers WP_Posts_List_Table::column_title
282+
*/
283+
public function test_child_page_aria_label_includes_parent_name() {
284+
$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
285+
wp_set_current_user( $admin_id );
286+
287+
$child = self::$children[1][1];
288+
$parent = self::$top[1];
289+
$child_title = apply_filters( 'the_title', $child->post_title, $child->ID );
290+
$parent_title = apply_filters( 'the_title', $parent->post_title, $parent->ID );
291+
292+
// level=1 → direct child page.
293+
ob_start();
294+
$this->table->single_row( $child, 1 );
295+
$output = ob_get_clean();
296+
297+
$decoded = html_entity_decode( $output, ENT_QUOTES | ENT_HTML5 );
298+
299+
$this->assertStringContainsString( 'aria-label=', $output );
300+
$this->assertStringContainsString( 'subpage of', $decoded );
301+
$this->assertStringContainsString( $child_title, $decoded );
302+
$this->assertStringContainsString( $parent_title, $decoded );
303+
$this->assertStringContainsString( '(Edit)', $decoded );
304+
305+
wp_set_current_user( 0 );
306+
}
307+
308+
/**
309+
* Tests that a grandchild page link includes "subpage of" with its immediate parent title in its aria-label.
310+
*
311+
* @ticket 62006
312+
*
313+
* @covers WP_Posts_List_Table::column_title
314+
*/
315+
public function test_grandchild_page_aria_label_includes_immediate_parent_name() {
316+
$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
317+
wp_set_current_user( $admin_id );
318+
319+
$grandchild = self::$grandchildren[3][1][1];
320+
$child = self::$children[3][1];
321+
$grandchild_title = apply_filters( 'the_title', $grandchild->post_title, $grandchild->ID );
322+
$parent_title = apply_filters( 'the_title', $child->post_title, $child->ID );
323+
324+
// level=2 → grandchild page; immediate parent is the child page.
325+
ob_start();
326+
$this->table->single_row( $grandchild, 2 );
327+
$output = ob_get_clean();
328+
329+
$decoded = html_entity_decode( $output, ENT_QUOTES | ENT_HTML5 );
330+
331+
$this->assertStringContainsString( 'aria-label=', $output );
332+
$this->assertStringContainsString( 'subpage of', $decoded );
333+
$this->assertStringContainsString( $grandchild_title, $decoded );
334+
$this->assertStringContainsString( $parent_title, $decoded );
335+
$this->assertStringContainsString( '(Edit)', $decoded );
336+
337+
wp_set_current_user( 0 );
338+
}
339+
340+
/**
341+
* Tests that a non-editable page (trashed) does not receive an aria-label on the title span.
342+
*
343+
* @ticket 62006
344+
*
345+
* @covers WP_Posts_List_Table::column_title
346+
*/
347+
public function test_trashed_page_title_has_no_aria_label() {
348+
$admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
349+
wp_set_current_user( $admin_id );
350+
351+
$trashed = self::factory()->post->create_and_get(
352+
array(
353+
'post_type' => 'page',
354+
'post_status' => 'trash',
355+
'post_parent' => self::$top[1]->ID,
356+
)
357+
);
358+
359+
ob_start();
360+
$this->table->single_row( $trashed, 1 );
361+
$output = ob_get_clean();
362+
363+
// Trashed posts render as <span>, not <a class="row-title">, so no aria-label on the title.
364+
$this->assertStringNotContainsString( 'class="row-title"', $output );
365+
$this->assertStringNotContainsString( 'aria-label="&#8220;', $output );
366+
367+
wp_set_current_user( 0 );
368+
}
369+
248370
/**
249371
* @ticket 37407
250372
*

0 commit comments

Comments
 (0)