How to add parent element’s title to nav menu’s child?

I believe wp_nav_menu() handles the top-level list wrapping so start_lvl method would only handle submenu items ( from my understanding ). It also doesn’t give us access to the $item which we need to get the parent.

Below I’ve setup a boolean variable to track whether we should be showing our title or not. It also keeps it HTML valid, lists can only contain list items so it may be better to style list items with a specific class. The below code is 90% taken from the Core WP_Nav_Menu Walker

First we need to setup our variable:

class Title_Sub_Menus extends Walker_Nav_Menu {

    /**
     * Track Whether to show parent title
     *
     * @var Boolean
     */
    private $show_parent_title = false;

    /* ... */

}

Next, we need to turn on our switch variable whenever a new submenu is created:

class Title_Sub_Menus extends Walker_Nav_Menu {

    /* ... */

    /**
     * Starts the list before the elements are added.
     *
     * @since 3.0.0
     *
     * @see Walker::start_lvl()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
                $t="";
                $n = '';
        } else {
                $t = "\t";
                $n = "\n";
        }

        $indent = str_repeat( $t, $depth );

        // Default class.
        $classes = array( 'sub-menu' );

        // Enable Submenu Title
        $this->show_parent_title = true;

        /**
         * Filters the CSS class(es) applied to a menu list element.
         *
         * @since 4.8.0
         *
         * @param array    $classes The CSS classes that are applied to the menu `<ul>` element.
         * @param stdClass $args    An object of `wp_nav_menu()` arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        $output .= "{$n}{$indent}<ul$class_names>{$n}";

    }

    /* ... */

} // END class Title_Sub_Menus

This is where things get a little tricky. As far as I know, Menu Item Object titles are not saved with the item if it hasn’t changed from the original title. If the original page title is “WordPress Page” and you change the menu item title to “WordPress” after adding it to the menu, then it’s saved to the Menu Item. We also should check against terms. Kudos to Alex Sancho’s answer.

Class Title_Sub_Menus extends Walker_Nav_Menu {

    /* ... */

    /**
     * Starts the element output.
     *
     * @since 3.0.0
     * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
     *
     * @see Walker::start_el()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param WP_Post  $item   Menu item data object.
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     * @param int      $id     Current item ID.
     */
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {

        // Maybe show parent title
        if( 0 === $depth && $this->show_parent_title ) {

            // Grab the title if the user HAS changed it from the original
            $title = get_the_title( $item->menu_item_parent );

            if( empty( $title ) ) {

                // Title is the original title, grab it from the original post or term.
                $object_id  = get_post_meta( $item->menu_item_parent, '_menu_item_object_id', true );
                $object     = get_post_meta( $item->menu_item_parent, '_menu_item_object',    true );
                $type       = get_post_meta( $item->menu_item_parent, '_menu_item_type',      true );

                if ( 'post_type' === $type ) {
                    $title = get_post( $object_id )->post_title;
                } elseif ( 'taxonomy' === $type) {
                    $title = get_term( $object_id, $object )->name;
                }

            }

            $output .= sprintf( '<li class="parentTitle"><h3>%1$s</h3></li>', $title ); // Display our title

            // Parent title displayed, turn off switch
            $this->show_parent_title = false;

        }

        /* ... */

    }

} // END class Title_Sub_Menus

Full Class

class Title_Sub_Menus extends Walker_Nav_Menu {

    /**
     * Track Whether to show parent title
     *
     * @var Boolean
     */
    private $show_parent_title = false;


    /**
     * Starts the list before the elements are added.
     *
     * @since 3.0.0
     *
     * @see Walker::start_lvl()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
                $t="";
                $n = '';
        } else {
                $t = "\t";
                $n = "\n";
        }

        $indent = str_repeat( $t, $depth );

        // Default class.
        $classes = array( 'sub-menu' );

        $this->show_parent_title = true;

        /**
         * Filters the CSS class(es) applied to a menu list element.
         *
         * @since 4.8.0
         *
         * @param array    $classes The CSS classes that are applied to the menu `<ul>` element.
         * @param stdClass $args    An object of `wp_nav_menu()` arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_submenu_css_class', $classes, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        $output .= "{$n}{$indent}<ul$class_names>{$n}";

    }


    /**
     * Starts the element output.
     *
     * @since 3.0.0
     * @since 4.4.0 The {@see 'nav_menu_item_args'} filter was added.
     *
     * @see Walker::start_el()
     *
     * @param string   $output Used to append additional content (passed by reference).
     * @param WP_Post  $item   Menu item data object.
     * @param int      $depth  Depth of menu item. Used for padding.
     * @param stdClass $args   An object of wp_nav_menu() arguments.
     * @param int      $id     Current item ID.
     */
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {

        // Maybe show parent title
        if( 0 === $depth && $this->show_parent_title ) {

            $object_id  = get_post_meta( $item->menu_item_parent, '_menu_item_object_id', true );
            $object     = get_post_meta( $item->menu_item_parent, '_menu_item_object',    true );
            $type       = get_post_meta( $item->menu_item_parent, '_menu_item_type',      true );
            $title      = get_the_title( $item->menu_item_parent );

            if( empty( $title ) ) {

                if ( 'post_type' == $type ) {
                    $title = get_post( $object_id )->post_title;
                } elseif ( 'taxonomy' == $type) {
                    $title = get_term( $object_id, $object )->name;
                }

            }

            $output .= sprintf( '<li class="parentTitle"><h3>%1$s</h3></li>', $title ); // OR post_parent if dynamically generated
            $this->show_parent_title = false;

        }

        if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
                $t="";
                $n = '';
        } else {
                $t = "\t";
                $n = "\n";
        }

        $indent = ( $depth ) ? str_repeat( $t, $depth ) : '';
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;
        $classes[] = 'menu-item-' . $item->ID;

        /**
         * Filters the arguments for a single nav menu item.
         *
         * @since 4.4.0
         *
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param WP_Post  $item  Menu item data object.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
        /**
         * Filters the CSS class(es) applied to a menu item's list item element.
         *
         * @since 3.0.0
         * @since 4.1.0 The `$depth` parameter was added.
         *
         * @param array    $classes The CSS classes that are applied to the menu item's `<li>` element.
         * @param WP_Post  $item    The current menu item.
         * @param stdClass $args    An object of wp_nav_menu() arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
        $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
        /**
         * Filters the ID applied to a menu item's list item element.
         *
         * @since 3.0.1
         * @since 4.1.0 The `$depth` parameter was added.
         *
         * @param string   $menu_id The ID that is applied to the menu item's `<li>` element.
         * @param WP_Post  $item    The current menu item.
         * @param stdClass $args    An object of wp_nav_menu() arguments.
         * @param int      $depth   Depth of menu item. Used for padding.
         */
        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args, $depth );
        $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
        $output .= $indent . '<li' . $id . $class_names .'>';
        $atts = array();
        $atts['title']  = ! empty( $item->attr_title ) ? $item->attr_title : '';
        $atts['target'] = ! empty( $item->target )     ? $item->target     : '';
        $atts['rel']    = ! empty( $item->xfn )        ? $item->xfn        : '';
        $atts['href']   = ! empty( $item->url )        ? $item->url        : '';
        /**
         * Filters the HTML attributes applied to a menu item's anchor element.
         *
         * @since 3.6.0
         * @since 4.1.0 The `$depth` parameter was added.
         *
         * @param array $atts {
         *     The HTML attributes applied to the menu item's `<a>` element, empty strings are ignored.
         *
         *     @type string $title  Title attribute.
         *     @type string $target Target attribute.
         *     @type string $rel    The rel attribute.
         *     @type string $href   The href attribute.
         * }
         * @param WP_Post  $item  The current menu item.
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
        $attributes="";
        foreach ( $atts as $attr => $value ) {
                if ( ! empty( $value ) ) {
                        $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
                        $attributes .= ' ' . $attr . '="' . $value . '"';
                }
        }
        /** This filter is documented in wp-includes/post-template.php */
        $title = apply_filters( 'the_title', $item->title, $item->ID );
        /**
         * Filters a menu item's title.
         *
         * @since 4.4.0
         *
         * @param string   $title The menu item's title.
         * @param WP_Post  $item  The current menu item.
         * @param stdClass $args  An object of wp_nav_menu() arguments.
         * @param int      $depth Depth of menu item. Used for padding.
         */
        $title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';
        $item_output .= $args->link_before . $title . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;
        /**
         * Filters a menu item's starting output.
         *
         * The menu item's starting output only includes `$args->before`, the opening `<a>`,
         * the menu item's title, the closing `</a>`, and `$args->after`. Currently, there is
         * no filter for modifying the opening and closing `<li>` for a menu item.
         *
         * @since 3.0.0
         *
         * @param string   $item_output The menu item's starting HTML output.
         * @param WP_Post  $item        Menu item data object.
         * @param int      $depth       Depth of menu item. Used for padding.
         * @param stdClass $args        An object of wp_nav_menu() arguments.
         */
        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
    }

} // END class Title_Sub_Menus

It seems a bit messy / strange to me there’s not an easier or more straight-forward way to do this so this is one possible solution. Hopefully someone comes along with a more intuitive solution and explanation.