Navigation Sub-menu Buttons

    The preceding example of sub-menu dropdowns is primarily a desktop use case. On mobile viewports it is common to have toggle buttons with each sub-menu to expand in a touch-friendly way. When JavaScript is not available, the buttons are not inserted and all of the sub-menus are shown as expanded. In AMP however, we can prevent showing all submenus expanded by limiting the CSS rule selector to only apply if JS is disabled and it is not an AMP response:

    .no-js:not([amp]) .main-navigation ul ul {
        display: block;
    }

    Now, to allow the submenus to be expanded we need to inject the buttons in an AMP-compatible way. We can do so using a PHP filter to add the buttons. Along with each toggle button there is also is an amp-state added which contains whether or not the sub-menu is expanded.

    /**
     * Filter the HTML output of a nav menu item to add the AMP dropdown button to reveal the sub-menu.
     *
     * This is only used for AMP since in JS it is added via initMainNavigation() in navigation.js.
     *
     * @param string $item_output   Nav menu item HTML.
     * @param object $item          Nav menu item.
     * @return string Modified nav menu item HTML.
     */
    function example_add_nav_sub_menu_buttons( $item_output, $item ) {
    
        // Only add the buttons in AMP responses.
        if ( ! example_is_amp() ) ) {
            return $item_output;
        }
    
        // Skip when the item has no sub-menu.
        if ( ! in_array( 'menu-item-has-children', $item->classes, true ) ) {
            return $item_output;
        }
    
        // Obtain the initial expanded state.
        $expanded = in_array( 'current-menu-ancestor', $item->classes, true );
    
        // Generate a unique state ID.
        static $nav_menu_item_number = 0;
        $nav_menu_item_number++;
        $expanded_state_id = 'navMenuItemExpanded' . $nav_menu_item_number;
    
        // Create new state for managing storing the whether the sub-menu is expanded.
        $item_output .= sprintf(
            '<amp-state id="%s"><script type="application/json">%s</script></amp-state>',
            esc_attr( $expanded_state_id ),
            wp_json_encode( $expanded )
        );
    
        /*
         * Create the toggle button which mutates the state and which has class and
         * aria-expanded attributes which react to the state changes.
         */
        $dropdown_button  = '<button';
        $dropdown_class   = 'dropdown-toggle';
        $toggled_class    = 'toggled-on';
        $dropdown_button .= sprintf(
            ' class="%s" [class]="%s"',
            esc_attr( $dropdown_class . ( $expanded ? " $toggled_class" : '' ) ),
            esc_attr( sprintf( "%s + ( $expanded_state_id ? %s : '' )", wp_json_encode( $dropdown_class ), wp_json_encode( " $toggled_class" ) ) )
        );
        $dropdown_button .= sprintf(
            ' aria-expanded="%s" [aria-expanded]="%s"',
            esc_attr( wp_json_encode( $expanded ) ),
            esc_attr( "$expanded_state_id ? 'true' : 'false'" )
        );
        $dropdown_button .= sprintf(
            ' on="%s"',
            esc_attr( "tap:AMP.setState( { $expanded_state_id: ! $expanded_state_id } )" )
        );
        $dropdown_button .= '>';
    
        // Let the screen reader text in the button also update based on the expanded state.
        $dropdown_button .= sprintf(
            '<span class="screen-reader-text" [text]="%s">%s</span>',
            esc_attr( sprintf( "$expanded_state_id ? %s : %s", wp_json_encode( __( 'collapse child menu', 'example' ) ), wp_json_encode( __( 'expand child menu', 'example' ) ) ) ),
            esc_html( $expanded ? __( 'collapse child menu', 'example' ) : __( 'expand child menu', 'example' ) )
        );
    
        $dropdown_button .= '</button>';
    
        $item_output .= $dropdown_button;
        return $item_output;
    }
    add_filter( 'walker_nav_menu_start_el', 'example_add_nav_sub_menu_buttons', 10, 2 );

    Lastly, the CSS used for showing the expanded sub-menu can make use of the sibling selector to show the list when the button gets its toggled-on class added (which applies when using JavaScript as well):

    .main-navigation ul button.toggled-on + ul {
        display: block;
    }