Make WordPress Core

Changeset 56067


Ignore:
Timestamp:
06/27/2023 03:25:00 PM (8 months ago)
Author:
audrasjb
Message:

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.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-walker-nav-menu.php

    r55261 r56067  
    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 ) . '"' : '';
    77 
    78         $output .= "{$n}{$indent}<ul$class_names>{$n}";
     76
     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
     
    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        /**
     
    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 ) . '"' : '';
    174 
    175         $output .= $indent . '<li' . $id . $class_names . '>';
     190
     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();
     
    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 */
     
    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}
  • trunk/tests/phpunit/tests/menu/walker-nav-menu.php

    r55261 r56067  
    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}
Note: See TracChangeset for help on using the changeset viewer.