Skip to content

Commit b66fcb5

Browse files
committed
Menus: Allow themes and plugins to pass HTML attributes to various Nav Walker outputs.
This introduces a new set of hooks that can be used to filter various HTML elements of the Nav Walker, in order to output the desired HTML attributes: - List items: `nav_menu_item_attributes` - Submenu `<ul>` element: `nav_menu_submenu_attributes` Props davidwebca, danyk4, costdev, peterwilsoncc, audrasjb, oglekler. Fixes #57140. git-svn-id: https://develop.svn.wordpress.org/trunk@56067 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 1d543a5 commit b66fcb5

2 files changed

Lines changed: 177 additions & 14 deletions

File tree

src/wp-includes/class-walker-nav-menu.php

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,27 @@ public function start_lvl( &$output, $depth = 0, $args = null ) {
7373
* @param int $depth Depth of menu item. Used for padding.
7474
*/
7575
$class_names = implode( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
76-
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
7776

78-
$output .= "{$n}{$indent}<ul$class_names>{$n}";
77+
$atts = array();
78+
$atts['class'] = ! empty( $class_names ) ? $class_names : '';
79+
80+
/**
81+
* Filters the HTML attributes applied to a menu list element.
82+
*
83+
* @since 6.3.0
84+
*
85+
* @param array $atts {
86+
* The HTML attributes applied to the `<ul>` element, empty strings are ignored.
87+
*
88+
* @type string $class HTML CSS class attribute.
89+
* }
90+
* @param stdClass $args An object of `wp_nav_menu()` arguments.
91+
* @param int $depth Depth of menu item. Used for padding.
92+
*/
93+
$atts = apply_filters( 'nav_menu_submenu_attributes', $atts, $args, $depth );
94+
$attributes = $this->build_atts( $atts );
95+
96+
$output .= "{$n}{$indent}<ul{$attributes}>{$n}";
7997
}
8098

8199
/**
@@ -156,7 +174,6 @@ public function start_el( &$output, $data_object, $depth = 0, $args = null, $cur
156174
* @param int $depth Depth of menu item. Used for padding.
157175
*/
158176
$class_names = implode( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $menu_item, $args, $depth ) );
159-
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
160177

161178
/**
162179
* Filters the ID attribute applied to a menu item's list item element.
@@ -170,9 +187,30 @@ public function start_el( &$output, $data_object, $depth = 0, $args = null, $cur
170187
* @param int $depth Depth of menu item. Used for padding.
171188
*/
172189
$id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $menu_item->ID, $menu_item, $args, $depth );
173-
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
174190

175-
$output .= $indent . '<li' . $id . $class_names . '>';
191+
$li_atts = array();
192+
$li_atts['id'] = ! empty( $id ) ? $id : '';
193+
$li_atts['class'] = ! empty( $class_names ) ? $class_names : '';
194+
195+
/**
196+
* Filters the HTML attributes applied to a menu's list item element.
197+
*
198+
* @since 6.3.0
199+
*
200+
* @param array $li_atts {
201+
* The HTML attributes applied to the menu item's `<li>` element, empty strings are ignored.
202+
*
203+
* @type string $class HTML CSS class attribute.
204+
* @type string $id HTML id attribute.
205+
* }
206+
* @param WP_Post $menu_item The current menu item object.
207+
* @param stdClass $args An object of wp_nav_menu() arguments.
208+
* @param int $depth Depth of menu item. Used for padding.
209+
*/
210+
$li_atts = apply_filters( 'nav_menu_item_attributes', $li_atts, $menu_item, $args, $depth );
211+
$li_attributes = $this->build_atts( $li_atts );
212+
213+
$output .= $indent . '<li' . $li_attributes . '>';
176214

177215
$atts = array();
178216
$atts['title'] = ! empty( $menu_item->attr_title ) ? $menu_item->attr_title : '';
@@ -214,15 +252,8 @@ public function start_el( &$output, $data_object, $depth = 0, $args = null, $cur
214252
* @param stdClass $args An object of wp_nav_menu() arguments.
215253
* @param int $depth Depth of menu item. Used for padding.
216254
*/
217-
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $menu_item, $args, $depth );
218-
219-
$attributes = '';
220-
foreach ( $atts as $attr => $value ) {
221-
if ( is_scalar( $value ) && '' !== $value && false !== $value ) {
222-
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
223-
$attributes .= ' ' . $attr . '="' . $value . '"';
224-
}
225-
}
255+
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $menu_item, $args, $depth );
256+
$attributes = $this->build_atts( $atts );
226257

227258
/** This filter is documented in wp-includes/post-template.php */
228259
$title = apply_filters( 'the_title', $menu_item->title, $menu_item->ID );
@@ -286,4 +317,23 @@ public function end_el( &$output, $data_object, $depth = 0, $args = null ) {
286317
$output .= "</li>{$n}";
287318
}
288319

320+
/**
321+
* Builds a string of HTML attributes from an array of key/value pairs.
322+
* Empty values are ignored.
323+
*
324+
* @since 6.3.0
325+
*
326+
* @param array $atts Optional. An array of HTML attribute key/value pairs. Default empty array.
327+
* @return string A string of HTML attributes.
328+
*/
329+
protected function build_atts( $atts = array() ) {
330+
$attribute_string = '';
331+
foreach ( $atts as $attr => $value ) {
332+
if ( false !== $value && '' !== $value && is_scalar( $value ) ) {
333+
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
334+
$attribute_string .= ' ' . $attr . '="' . $value . '"';
335+
}
336+
}
337+
return $attribute_string;
338+
}
289339
}

tests/phpunit/tests/menu/walker-nav-menu.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,4 +365,117 @@ public function test_walker_nav_menu_start_el_should_add_rel_privacy_policy_when
365365

366366
$this->assertStringContainsString( 'rel="privacy-policy"', $output );
367367
}
368+
369+
/**
370+
* Tests that `Walker_Nav_Menu::start_lvl()` applies 'nav_menu_submenu_attributes' filters.
371+
*
372+
* @ticket 57278
373+
*
374+
* @covers Walker_Nav_Menu::start_lvl
375+
*/
376+
public function test_start_lvl_should_apply_nav_menu_submenu_attributes_filters() {
377+
$output = '';
378+
$args = (object) array(
379+
'before' => '',
380+
'after' => '',
381+
'link_before' => '',
382+
'link_after' => '',
383+
);
384+
385+
$filter = new MockAction();
386+
add_filter( 'nav_menu_submenu_attributes', array( $filter, 'filter' ) );
387+
388+
$this->walker->start_lvl( $output, 0, $args );
389+
390+
$this->assertSame( 1, $filter->get_call_count() );
391+
}
392+
393+
/**
394+
* Tests that `Walker_Nav_Menu::start_el()` applies 'nav_menu_item_attributes' filters.
395+
*
396+
* @ticket 57278
397+
*
398+
* @covers Walker_Nav_Menu::start_el
399+
*/
400+
public function test_start_el_should_apply_nav_menu_item_attributes_filters() {
401+
$output = '';
402+
$post_id = self::factory()->post->create();
403+
$item = (object) array(
404+
'ID' => $post_id,
405+
'object_id' => $post_id,
406+
'title' => get_the_title( $post_id ),
407+
'target' => '',
408+
'xfn' => '',
409+
'current' => false,
410+
);
411+
$args = (object) array(
412+
'before' => '',
413+
'after' => '',
414+
'link_before' => '',
415+
'link_after' => '',
416+
);
417+
418+
$filter = new MockAction();
419+
add_filter( 'nav_menu_item_attributes', array( $filter, 'filter' ) );
420+
421+
$this->walker->start_el( $output, $item, 0, $args );
422+
423+
$this->assertSame( 1, $filter->get_call_count() );
424+
}
425+
426+
/**
427+
* Tests that `Walker_Nav_Menu::build_atts()` builds attributes correctly.
428+
*
429+
* @ticket 57278
430+
*
431+
* @covers Walker_Nav_Menu::build_atts
432+
*
433+
* @dataProvider data_build_atts_should_build_attributes
434+
*
435+
* @param array $atts An array of HTML attribute key/value pairs.
436+
* @param string $expected The expected built attributes.
437+
*/
438+
public function test_build_atts_should_build_attributes( $atts, $expected ) {
439+
$build_atts_reflection = new ReflectionMethod( $this->walker, 'build_atts' );
440+
441+
$build_atts_reflection->setAccessible( true );
442+
$actual = $build_atts_reflection->invoke( $this->walker, $atts );
443+
$build_atts_reflection->setAccessible( false );
444+
445+
$this->assertSame( $expected, $actual );
446+
}
447+
448+
/**
449+
* Data provider.
450+
*
451+
* @return array[]
452+
*/
453+
public function data_build_atts_should_build_attributes() {
454+
return array(
455+
'an empty attributes array' => array(
456+
'atts' => array(),
457+
'expected' => '',
458+
),
459+
'attributes containing a (bool) false value' => array(
460+
'atts' => array( 'disabled' => false ),
461+
'expected' => '',
462+
),
463+
'attributes containing an empty string value' => array(
464+
'atts' => array( 'id' => '' ),
465+
'expected' => '',
466+
),
467+
'attributes containing a non-scalar value' => array(
468+
'atts' => array( 'data-items' => new stdClass() ),
469+
'expected' => '',
470+
),
471+
'attributes containing a "href" -> should escape the URL' => array(
472+
'atts' => array( 'href' => 'https://example.org/A File With Spaces.pdf' ),
473+
'expected' => ' href="https://example.org/A%20File%20With%20Spaces.pdf"',
474+
),
475+
'attributes containing a non-"href" attribute -> should escape the value' => array(
476+
'atts' => array( 'id' => 'hello&goodbye' ),
477+
'expected' => ' id="hello&amp;goodbye"',
478+
),
479+
);
480+
}
368481
}

0 commit comments

Comments
 (0)