*/ function tlig_nav_meta( int $item_id ): array { $meta = get_post_meta( $item_id, '_tlig_nav', true ); return is_array( $meta ) ? $meta : []; } /** * The three icons the entire site uses, inline. * * A whole icon font was loaded for these. Counted across all 15 menus: 24 * `fa-search`, 14 `fa-book-open`, 14 `fa-headphones`, and nothing else. Inline * SVG costs no request and inherits `currentColor`, so an icon on the dark * ground needs no second rule. * * @param string $name Icon key. * @return string SVG markup, or '' for an unknown name. */ function tlig_nav_icon( string $name ): string { static $icons = [ 'search' => '', 'book' => '', 'headphones' => '', ]; if ( ! isset( $icons[ $name ] ) ) { return ''; } return sprintf( '', $icons[ $name ] ); } /** * Renders the primary menu. * * @param string $location Registered menu location. */ function tlig_nav( string $location = 'primary' ): void { if ( ! has_nav_menu( $location ) ) { return; } // The toggle is part of the menu's own markup rather than the header's, so // `aria-controls` can name the list it opens without the template having to // know the id. Below the tablet breakpoint it is the entire mobile menu. $toggle = sprintf( '', 'tlig-menu', esc_html__( 'Menu', 'tlig' ) ); wp_nav_menu( [ 'theme_location' => $location, 'container' => 'nav', 'container_class' => 'site-nav', 'container_aria_label' => __( 'Primary navigation', 'tlig' ), 'menu_id' => 'tlig-menu', 'menu_class' => 'site-nav__bar', 'items_wrap' => $toggle . '
    %3$s
', 'depth' => 3, 'walker' => new TLIG_Nav_Walker(), 'fallback_cb' => false, ] ); } /** * Turns a three-level menu into a bar of mega panels. * * `Walker_Nav_Menu` hands `start_lvl()` a depth but not the item that owns the * level, so the current item is stashed in `start_el()` — the panel wrapper has * to know whose panel it is in order to give it an id for `aria-controls`. */ class TLIG_Nav_Walker extends Walker_Nav_Menu { /** @var WP_Post|null The item most recently opened, for start_lvl(). */ private $current; /** @var array> Settings of the open ancestors, by depth. */ private $stack = []; /** @var array How many children each item has, by item ID. */ private $siblings = []; /** * Counts each item's children before walking. * * A column with no width of its own should share the row with its * neighbours, which means knowing how many there are. UberMenu stored that * as `columns: auto` and worked it out at render time; the walker is handed * the whole tree here, so it can do the same. * * @param array $elements All menu items. * @param int $max_depth Depth limit. * @param mixed ...$args Passed through. * @return string */ public function walk( $elements, $max_depth, ...$args ) { $this->siblings = []; foreach ( (array) $elements as $element ) { $parent = (int) ( $element->menu_item_parent ?? 0 ); $this->siblings[ $parent ] = ( $this->siblings[ $parent ] ?? 0 ) + 1; } return parent::walk( $elements, $max_depth, ...$args ); } /** * Opens a submenu level. * * Depth 0 → the panel and its grid. Depth 1 → the list of links in a column. * * @param string $output Accumulated markup, by reference. * @param int $depth Depth of the *parent* item. * @param stdClass $args wp_nav_menu arguments. */ public function start_lvl( &$output, $depth = 0, $args = null ) { $indent = str_repeat( "\t", $depth ); if ( 0 === $depth ) { $id = $this->current ? 'tlig-panel-' . (int) $this->current->ID : ''; $output .= sprintf( "\n%s
\n", $indent, esc_attr( $id ) ); // The panel repeats its own item as a link, and the stylesheet shows // it only in the stacked layout. There, a tap on the bar item opens // the panel instead of following the link — which is what a phone // user expects, and what the old site got wrong in the opposite // direction: with hover the only way in, its top-level pages were // unreachable on a phone. This is the way back to that page. if ( $this->current && '' !== trim( (string) $this->current->url ) && '#' !== $this->current->url ) { $output .= sprintf( "%s\t%s\n", $indent, esc_url( (string) $this->current->url ), esc_html( trim( wp_strip_all_tags( $this->current->title ) ) ) ); } $output .= sprintf( "%s\t
    \n", $indent ); return; } $output .= sprintf( "\n%s
      \n", $indent ); } /** * Closes a submenu level. * * @param string $output Accumulated markup, by reference. * @param int $depth Depth of the parent item. * @param stdClass $args wp_nav_menu arguments. */ public function end_lvl( &$output, $depth = 0, $args = null ) { $indent = str_repeat( "\t", $depth ); $output .= 0 === $depth ? sprintf( "%s\t
    \n%s
\n", $indent, $indent ) : sprintf( "%s\n", $indent ); } /** * Opens an item. * * @param string $output Accumulated markup, by reference. * @param WP_Post $item Menu item. * @param int $depth Depth of the item. * @param stdClass $args wp_nav_menu arguments. * @param int $current_object_id Unused. */ public function start_el( &$output, $item, $depth = 0, $args = null, $current_object_id = 0 ) { $this->current = $item; $nav = tlig_nav_meta( (int) $item->ID ); $this->stack[ $depth ] = $nav; $has_children = in_array( 'menu-item-has-children', (array) $item->classes, true ); $indent = str_repeat( "\t", $depth + 1 ); switch ( $depth ) { case 0: $output .= $indent . $this->bar_item( $item, $nav, $has_children ); break; case 1: $output .= $indent . $this->column( $item, $nav, $has_children ); break; default: // A leaf can carry a block or a feed too — the English // TESTIMONIES panel is two of them, and dropping them here is // how they went missing the first time. $output .= $indent . '\n"; } /** * A top-level item: the thing you see in the bar. * * @param WP_Post $item Menu item. * @param array $nav Its TLIG settings. * @param bool $has_children Whether it opens a panel. * @return string */ private function bar_item( WP_Post $item, array $nav, bool $has_children ): string { $panel = $has_children && ! empty( $nav['panel'] ); $classes = [ 'site-nav__item' ]; if ( $panel ) { $classes[] = 'site-nav__item--panel'; } if ( in_array( 'current-menu-item', (array) $item->classes, true ) ) { $classes[] = 'is-current'; } $attrs = ''; if ( $panel ) { // The link still goes where it always went; the button beside it is // what opens the panel. A single element cannot both navigate and // disclose, and hover alone leaves touch and keyboard out. $attrs = sprintf( ' aria-expanded="false" aria-controls="%s"', esc_attr( 'tlig-panel-' . (int) $item->ID ) ); } $out = sprintf( '
  • ', esc_attr( implode( ' ', $classes ) ) ); $out .= $this->link( $item, $nav, 'site-nav__top' ); if ( $panel ) { $out .= sprintf( '', $attrs, /* translators: %s: menu item label. */ esc_html( sprintf( __( 'Open the %s menu', 'tlig' ), wp_strip_all_tags( $item->title ) ) ) ); } return $out; } /** * A column inside a panel. * * The width is a grid span, not a pixel height: `--tlig-span` feeds * `grid-column: span var(--tlig-span)`. UberMenu stored it as a fraction of * the row, which is the same thing said less directly. * * @param WP_Post $item Menu item. * @param array $nav Its TLIG settings. * @param bool $has_children Whether it holds links. * @return string */ private function column( WP_Post $item, array $nav, bool $has_children ): string { $span = isset( $nav['span'] ) ? max( 1, min( 12, (int) $nav['span'] ) ) : 0; // No width of its own: divide the row by however many columns this // panel has. Without this every unsized column fell back to a quarter, // which is right for a panel of four and wrong for the 28 panels — // UNITY in DIVERSITY, MISSION, SEARCH, across the languages — that // hold a single column. Those were rendering a 1200px banner into a // quarter of the width. if ( ! $span ) { $count = max( 1, (int) ( $this->siblings[ (int) $item->menu_item_parent ] ?? 1 ) ); $span = max( 1, min( 12, (int) round( 12 / $count ) ) ); } $style = sprintf( ' style="--tlig-span:%d"', $span ); $out = sprintf( '