| @@ -15,9 +15,9 @@ | ||
| 15 | 15 | * Backward compatibility: WordPress applies default attribute values, so submenuVisibility |
| 16 | 16 | * will always have a value even for legacy blocks. We check the legacy openSubmenusOnClick |
| 17 | 17 | * attribute first to preserve original behavior for blocks saved before the migration. |
| 18 | 18 | * |
| 19 | - * @since 6.9.0 | |
| 19 | + * @since 7.0.0 | |
| 20 | 20 | * |
| 21 | 21 | * @param array $attributes Block attributes containing submenuVisibility and/or openSubmenusOnClick. |
| 22 | 22 | * @return string The visibility mode: 'hover', 'click', or 'always'. |
| 23 | 23 | */ |
| @@ -40,8 +40,47 @@ | ||
| 40 | 40 | return $submenu_visibility ?? 'hover'; |
| 41 | 41 | } |
| 42 | 42 | |
| 43 | 43 | /** |
| 44 | + * Returns the custom properties used by the Navigation block for a layout. | |
| 45 | + * | |
| 46 | + * @since 7.1.0 | |
| 47 | + * | |
| 48 | + * @param array $layout Layout configuration. | |
| 49 | + * @return array Navigation layout custom property declarations. | |
| 50 | + */ | |
| 51 | +function gutenberg_block_core_navigation_get_layout_custom_property_declarations( $layout ) { | |
| 52 | + $justification_values = array( | |
| 53 | + 'left' => 'flex-start', | |
| 54 | + 'center' => 'center', | |
| 55 | + 'right' => 'flex-end', | |
| 56 | + 'space-between' => 'space-between', | |
| 57 | + ); | |
| 58 | + $justify_content = is_array( $layout ) ? ( $layout['justifyContent'] ?? 'left' ) : 'left'; | |
| 59 | + if ( ! is_string( $justify_content ) || ! isset( $justification_values[ $justify_content ] ) ) { | |
| 60 | + $justify_content = 'left'; | |
| 61 | + } | |
| 62 | + | |
| 63 | + $justification = $justification_values[ $justify_content ]; | |
| 64 | + $is_vertical = is_array( $layout ) && 'vertical' === ( $layout['orientation'] ?? null ); | |
| 65 | + $align = 'center'; | |
| 66 | + $justify = $justification; | |
| 67 | + | |
| 68 | + if ( $is_vertical ) { | |
| 69 | + $align = in_array( $justify_content, array( 'center', 'right' ), true ) ? $justification : 'flex-start'; | |
| 70 | + $justify = 'left' === $justify_content ? 'initial' : $justification; | |
| 71 | + } | |
| 72 | + | |
| 73 | + return array( | |
| 74 | + '--navigation-layout-justification-setting' => $justification, | |
| 75 | + '--navigation-layout-direction' => $is_vertical ? 'column' : 'row', | |
| 76 | + '--navigation-layout-wrap' => is_array( $layout ) && 'nowrap' === ( $layout['flexWrap'] ?? null ) ? 'nowrap' : 'wrap', | |
| 77 | + '--navigation-layout-justify' => $justify, | |
| 78 | + '--navigation-layout-align' => $align, | |
| 79 | + ); | |
| 80 | +} | |
| 81 | + | |
| 82 | +/** | |
| 44 | 83 | * Helper functions used to render the navigation block. |
| 45 | 84 | * |
| 46 | 85 | * @since 6.5.0 |
| 47 | 86 | */ |
| @@ -239,10 +278,16 @@ | ||
| 239 | 278 | $is_list_open = false; |
| 240 | 279 | |
| 241 | 280 | foreach ( $inner_blocks as $inner_block ) { |
| 242 | 281 | $inner_block_markup = static::get_markup_for_inner_block( $inner_block ); |
| 243 | - $p = new WP_HTML_Tag_Processor( $inner_block_markup ); | |
| 244 | - $is_list_item = $p->next_tag( 'LI' ); | |
| 282 | + // Skip hidden blocks (e.g. hidden via block visibility) that render | |
| 283 | + // as an empty string. Without this check, empty markup is mistaken | |
| 284 | + // for a non-list-item and incorrectly closes the open <ul>. | |
| 285 | + if ( '' === $inner_block_markup ) { | |
| 286 | + continue; | |
| 287 | + } | |
| 288 | + $p = new WP_HTML_Tag_Processor( $inner_block_markup ); | |
| 289 | + $is_list_item = $p->next_tag( 'LI' ); | |
| 245 | 290 | |
| 246 | 291 | if ( $is_list_item && ! $is_list_open ) { |
| 247 | 292 | $is_list_open = true; |
| 248 | 293 | $inner_blocks_html .= sprintf( |
| @@ -371,9 +416,9 @@ | ||
| 371 | 416 | * |
| 372 | 417 | * @since 6.5.0 |
| 373 | 418 | * |
| 374 | 419 | * @param string $overlay_template_part_id The overlay template part ID in format "theme//slug". |
| 375 | - * @param array $attributes The block attributes. | |
| 420 | + * @param array $attributes The block attributes. | |
| 376 | 421 | * @return WP_Block_List Returns the inner blocks for the overlay template part. |
| 377 | 422 | */ |
| 378 | 423 | private static function get_overlay_blocks_from_template_part( $overlay_template_part_id, $attributes ) { |
| 379 | 424 | if ( empty( $overlay_template_part_id ) || ! is_string( $overlay_template_part_id ) ) { |
| @@ -472,9 +517,9 @@ | ||
| 472 | 517 | * |
| 473 | 518 | * @since 6.5.0 |
| 474 | 519 | * |
| 475 | 520 | * @param array $attributes The block attributes. |
| 476 | - * @param WP_Block $block The parsed block. | |
| 521 | + * @param WP_Block $block The parsed block. | |
| 477 | 522 | * @return WP_Block_List Returns the inner blocks for the navigation block. |
| 478 | 523 | */ |
| 479 | 524 | private static function get_inner_blocks( $attributes, $block ) { |
| 480 | 525 | $inner_blocks = $block->inner_blocks; |
| @@ -575,14 +620,15 @@ | ||
| 575 | 620 | 'center' => 'items-justified-center', |
| 576 | 621 | 'space-between' => 'items-justified-space-between', |
| 577 | 622 | ); |
| 578 | 623 | |
| 579 | - $layout_class = ''; | |
| 624 | + $layout_class = ''; | |
| 625 | + $nav_justify_content = $attributes['layout']['justifyContent'] ?? null; | |
| 580 | 626 | if ( |
| 581 | - isset( $attributes['layout']['justifyContent'] ) && | |
| 582 | - isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) | |
| 627 | + is_string( $nav_justify_content ) && | |
| 628 | + isset( $layout_justification[ $nav_justify_content ] ) | |
| 583 | 629 | ) { |
| 584 | - $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; | |
| 630 | + $layout_class .= $layout_justification[ $nav_justify_content ]; | |
| 585 | 631 | } |
| 586 | 632 | if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { |
| 587 | 633 | $layout_class .= ' is-vertical'; |
| 588 | 634 | } |
| @@ -642,11 +688,16 @@ | ||
| 642 | 688 | * |
| 643 | 689 | * @since 7.0.0 |
| 644 | 690 | * |
| 645 | 691 | * @param bool $is_hidden_by_default Whether the responsive menu is hidden by default. |
| 646 | - * @param bool $has_custom_overlay Whether a custom overlay is used. | |
| 647 | - * @param array $colors The colors array. | |
| 692 | + * @param bool $has_custom_overlay Whether a custom overlay is used. | |
| 693 | + * @param array $colors The colors array. | |
| 648 | 694 | * @return array Returns the responsive container classes. |
| 695 | + * | |
| 696 | + * @phpstan-param array{ | |
| 697 | + * overlay_css_classes: list<string>, | |
| 698 | + * ... | |
| 699 | + * } $colors | |
| 649 | 700 | */ |
| 650 | 701 | private static function get_responsive_container_classes( $is_hidden_by_default, $has_custom_overlay, $colors ) { |
| 651 | 702 | $responsive_container_classes = array( 'wp-block-navigation__responsive-container' ); |
| 652 | 703 | |
| @@ -669,10 +720,16 @@ | ||
| 669 | 720 | * Get overlay inline styles for the navigation block. |
| 670 | 721 | * |
| 671 | 722 | * @since 7.0.0 |
| 672 | 723 | * |
| 673 | - * @param array $colors The colors array. | |
| 724 | + * @param bool $has_custom_overlay Whether a custom overlay is used. | |
| 725 | + * @param array $colors The colors array. | |
| 674 | 726 | * @return string Returns the overlay inline styles. |
| 727 | + * | |
| 728 | + * @phpstan-param array{ | |
| 729 | + * overlay_inline_styles: string, | |
| 730 | + * ... | |
| 731 | + * } $colors | |
| 675 | 732 | */ |
| 676 | 733 | private static function get_overlay_inline_styles( $has_custom_overlay, $colors ) { |
| 677 | 734 | $overlay_inline_styles = $has_custom_overlay ? '' : esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ); |
| 678 | 735 | return ( ! empty( $overlay_inline_styles ) ) ? "style=\"$overlay_inline_styles\"" : ''; |
| @@ -682,10 +739,10 @@ | ||
| 682 | 739 | * Get the responsive container markup |
| 683 | 740 | * |
| 684 | 741 | * @since 6.5.0 |
| 685 | 742 | * |
| 686 | - * @param array $attributes The block attributes. | |
| 687 | - * @param WP_Block_List $inner_blocks The list of inner blocks. | |
| 743 | + * @param array $attributes The block attributes. | |
| 744 | + * @param WP_Block_List $inner_blocks The list of inner blocks. | |
| 688 | 745 | * @param string $inner_blocks_html The markup for the inner blocks. |
| 689 | 746 | * @return string Returns the container markup. |
| 690 | 747 | */ |
| 691 | 748 | private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { |
| @@ -774,8 +831,16 @@ | ||
| 774 | 831 | '; |
| 775 | 832 | $responsive_container_content_directives = ' |
| 776 | 833 | data-wp-watch="callbacks.focusFirstElement" |
| 777 | 834 | '; |
| 835 | + | |
| 836 | + // The default overlay displays every submenu it contains; a custom overlay | |
| 837 | + // opts out through the `disable-default-overlay` class on this container. | |
| 838 | + // Submenus inherit this flag to tell the two apart, derived from the same | |
| 839 | + // value as that class so the markup and the styles cannot disagree. | |
| 840 | + if ( $has_custom_overlay ) { | |
| 841 | + $responsive_container_directives .= ' ' . wp_interactivity_data_wp_context( array( 'hasCustomOverlay' => true ) ); | |
| 842 | + } | |
| 778 | 843 | } |
| 779 | 844 | |
| 780 | 845 | // Don't apply overlay inline styles if using a custom overlay template part. |
| 781 | 846 | // The custom overlay is responsible for its own styling. |
| @@ -834,10 +899,10 @@ | ||
| 834 | 899 | * Get the wrapper attributes |
| 835 | 900 | * |
| 836 | 901 | * @since 6.5.0 |
| 837 | 902 | * |
| 838 | - * @param array $attributes The block attributes. | |
| 839 | - * @param WP_Block_List $inner_blocks A list of inner blocks. | |
| 903 | + * @param array $attributes The block attributes. | |
| 904 | + * @param WP_Block_List $inner_blocks A list of inner blocks. | |
| 840 | 905 | * @return string Returns the navigation block markup. |
| 841 | 906 | */ |
| 842 | 907 | private static function get_nav_attributes( $attributes, $inner_blocks ) { |
| 843 | 908 | $is_interactive = static::is_interactive( $attributes, $inner_blocks ); |
| @@ -921,9 +986,9 @@ | ||
| 921 | 986 | * Returns the markup for the navigation block. |
| 922 | 987 | * |
| 923 | 988 | * @since 6.5.0 |
| 924 | 989 | * |
| 925 | - * @param array $attributes The block attributes. | |
| 990 | + * @param array $attributes The block attributes. | |
| 926 | 991 | * @param WP_Block_List $inner_blocks The list of inner blocks. |
| 927 | 992 | * @return string Returns the navigation wrapper markup. |
| 928 | 993 | */ |
| 929 | 994 | private static function get_inner_block_markup( $attributes, $inner_blocks ) { |
| @@ -990,8 +1055,12 @@ | ||
| 990 | 1055 | } |
| 991 | 1056 | |
| 992 | 1057 | unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); |
| 993 | 1058 | |
| 1059 | + // Submenu detection is memoized while a navigation block renders. Reset it so | |
| 1060 | + // the result of a previously rendered navigation block is not reused for this one. | |
| 1061 | + static::$has_submenus = false; | |
| 1062 | + | |
| 994 | 1063 | $inner_blocks = static::get_inner_blocks( $attributes, $block ); |
| 995 | 1064 | // Prevent navigation blocks referencing themselves from rendering. |
| 996 | 1065 | if ( gutenberg_block_core_navigation_block_tree_has_block_type( |
| 997 | 1066 | $inner_blocks, |
| @@ -1210,9 +1279,9 @@ | ||
| 1210 | 1279 | 'class_name' => 'wp-block-navigation-submenu__toggle', |
| 1211 | 1280 | ) |
| 1212 | 1281 | ) ) { |
| 1213 | 1282 | $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' ); |
| 1214 | - $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isMenuOpen' ); | |
| 1283 | + $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isSubmenuOpen' ); | |
| 1215 | 1284 | // The `aria-expanded` attribute for SSR is already added in the submenu block. |
| 1216 | 1285 | } |
| 1217 | 1286 | // Add directives to the submenu. |
| 1218 | 1287 | if ( $tags->next_tag( |
| @@ -1557,8 +1626,137 @@ | ||
| 1557 | 1626 | |
| 1558 | 1627 | add_action( 'init', 'gutenberg_register_block_core_navigation', 20 ); |
| 1559 | 1628 | |
| 1560 | 1629 | /** |
| 1630 | + * Adds Navigation block support classes to inner list containers. | |
| 1631 | + * | |
| 1632 | + * State block support adds the generated `wp-states-*` class to the outer | |
| 1633 | + * block wrapper. The Navigation block renders its menu items inside an inner | |
| 1634 | + * `wp-block-navigation__container` list, so the same state class is also needed | |
| 1635 | + * there for state styles to apply directly to the menu list. | |
| 1636 | + * | |
| 1637 | + * Navigation also uses layout classes on its outer wrapper to define custom | |
| 1638 | + * properties consumed by its inner containers. Viewport layout styles cannot | |
| 1639 | + * change those classes, so equivalent custom properties and a scoping class | |
| 1640 | + * are generated for each configured viewport layout. | |
| 1641 | + * | |
| 1642 | + * Currently this is required as a workaround because of how difficult it is for nav | |
| 1643 | + * child blocks to inherit styles through the complex responsive nav block html. The | |
| 1644 | + * bug in https://github.com/WordPress/gutenberg/issues/62690 also prevents inheritance. | |
| 1645 | + * | |
| 1646 | + * @since 7.1.0 | |
| 1647 | + * | |
| 1648 | + * @param string $block_content The block content. | |
| 1649 | + * @param array $block The full block, including name and attributes. | |
| 1650 | + * @return string The updated block content. | |
| 1651 | + */ | |
| 1652 | +function gutenberg_block_core_navigation_add_support_classes_to_container( $block_content, $block ) { | |
| 1653 | + if ( 'core/navigation' !== ( $block['blockName'] ?? null ) || empty( $block_content ) ) { | |
| 1654 | + return $block_content; | |
| 1655 | + } | |
| 1656 | + | |
| 1657 | + $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); | |
| 1658 | + $style = is_array( $attributes['style'] ?? null ) ? $attributes['style'] : array(); | |
| 1659 | + if ( | |
| 1660 | + defined( 'IS_GUTENBERG_PLUGIN' ) && | |
| 1661 | + IS_GUTENBERG_PLUGIN && | |
| 1662 | + function_exists( 'gutenberg_resolve_style_state_aliases' ) | |
| 1663 | + ) { | |
| 1664 | + $style = gutenberg_resolve_style_state_aliases( $style, 'core/navigation' ); | |
| 1665 | + } | |
| 1666 | + | |
| 1667 | + $global_settings = gutenberg_get_global_settings(); | |
| 1668 | + $viewport_settings = $global_settings['viewport'] ?? null; | |
| 1669 | + $responsive_media_queries = array(); | |
| 1670 | + if ( method_exists( 'WP_Theme_JSON_Gutenberg', 'get_viewport_media_queries' ) ) { | |
| 1671 | + $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings ); | |
| 1672 | + } elseif ( method_exists( 'WP_Theme_JSON', 'get_viewport_media_queries' ) ) { | |
| 1673 | + $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings ); | |
| 1674 | + } | |
| 1675 | + | |
| 1676 | + $styles = array(); | |
| 1677 | + $base_layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array(); | |
| 1678 | + foreach ( $responsive_media_queries as $breakpoint => $media_query ) { | |
| 1679 | + $viewport_style = is_array( $style[ $breakpoint ] ?? null ) ? $style[ $breakpoint ] : array(); | |
| 1680 | + $viewport_layout = is_array( $viewport_style['layout'] ?? null ) ? $viewport_style['layout'] : array(); | |
| 1681 | + if ( empty( $viewport_layout ) ) { | |
| 1682 | + continue; | |
| 1683 | + } | |
| 1684 | + | |
| 1685 | + $styles[] = array( | |
| 1686 | + 'declarations' => gutenberg_block_core_navigation_get_layout_custom_property_declarations( | |
| 1687 | + array_replace( $base_layout, $viewport_layout ) | |
| 1688 | + ), | |
| 1689 | + 'rules_group' => $media_query, | |
| 1690 | + ); | |
| 1691 | + } | |
| 1692 | + | |
| 1693 | + $processor = new WP_HTML_Tag_Processor( $block_content ); | |
| 1694 | + if ( ! $processor->next_tag() ) { | |
| 1695 | + return $block_content; | |
| 1696 | + } | |
| 1697 | + | |
| 1698 | + $class_attribute = $processor->get_attribute( 'class' ); | |
| 1699 | + $state_class = null; | |
| 1700 | + if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute, $matches ) ) { | |
| 1701 | + $state_class = $matches[0]; | |
| 1702 | + } | |
| 1703 | + | |
| 1704 | + $layout_class = null; | |
| 1705 | + if ( ! empty( $styles ) ) { | |
| 1706 | + $layout_class = wp_unique_id( 'wp-block-navigation-' ); | |
| 1707 | + // The inner selector includes both Navigation classes so it overrides the | |
| 1708 | + // default layout custom properties set by `.wp-block-navigation.items-*`. | |
| 1709 | + $selector = ".wp-block-navigation.{$layout_class},.wp-block-navigation.wp-block-navigation__container.{$layout_class}"; | |
| 1710 | + foreach ( $styles as &$style_rule ) { | |
| 1711 | + $style_rule['selector'] = $selector; | |
| 1712 | + } | |
| 1713 | + unset( $style_rule ); | |
| 1714 | + | |
| 1715 | + $processor->add_class( $layout_class ); | |
| 1716 | + gutenberg_style_engine_get_stylesheet_from_css_rules( | |
| 1717 | + $styles, | |
| 1718 | + array( 'context' => 'block-supports' ) | |
| 1719 | + ); | |
| 1720 | + } | |
| 1721 | + | |
| 1722 | + if ( null === $state_class && null === $layout_class ) { | |
| 1723 | + return $block_content; | |
| 1724 | + } | |
| 1725 | + | |
| 1726 | + while ( $processor->next_tag() ) { | |
| 1727 | + // Custom overlay content can include nested Navigation blocks. | |
| 1728 | + // Avoid applying the outer Navigation classes to an inner nav block. | |
| 1729 | + if ( $processor->has_class( 'wp-block-navigation' ) && ! $processor->has_class( 'wp-block-navigation__container' ) ) { | |
| 1730 | + break; | |
| 1731 | + } | |
| 1732 | + | |
| 1733 | + if ( ! $processor->has_class( 'wp-block-navigation__container' ) ) { | |
| 1734 | + continue; | |
| 1735 | + } | |
| 1736 | + | |
| 1737 | + if ( null !== $layout_class ) { | |
| 1738 | + $processor->add_class( $layout_class ); | |
| 1739 | + } | |
| 1740 | + | |
| 1741 | + if ( null === $state_class ) { | |
| 1742 | + continue; | |
| 1743 | + } | |
| 1744 | + | |
| 1745 | + $class_attribute = $processor->get_attribute( 'class' ); | |
| 1746 | + if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute ) ) { | |
| 1747 | + continue; | |
| 1748 | + } | |
| 1749 | + | |
| 1750 | + $processor->add_class( $state_class ); | |
| 1751 | + } | |
| 1752 | + | |
| 1753 | + return $processor->get_updated_html(); | |
| 1754 | +} | |
| 1755 | + | |
| 1756 | +add_filter( 'render_block', 'gutenberg_block_core_navigation_add_support_classes_to_container', 11, 2 ); | |
| 1757 | + | |
| 1758 | +/** | |
| 1561 | 1759 | * Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly. |
| 1562 | 1760 | * |
| 1563 | 1761 | * @since 5.9.0 |
| 1564 | 1762 | * |
| @@ -1599,14 +1797,14 @@ | ||
| 1599 | 1797 | * @since 5.9.0 |
| 1600 | 1798 | * |
| 1601 | 1799 | * @deprecated 6.3.0 Use WP_Navigation_Fallback::parse_blocks_from_menu_items() instead. |
| 1602 | 1800 | * |
| 1603 | - * @param array $menu_items An array of menu items that represent | |
| 1604 | - * an individual level of a menu. | |
| 1605 | - * @param array $menu_items_by_parent_id An array keyed by the id of the | |
| 1606 | - * parent menu where each element is an | |
| 1607 | - * array of menu items that belong to | |
| 1608 | - * that parent. | |
| 1801 | + * @param array $menu_items An array of menu items that represent | |
| 1802 | + * an individual level of a menu. | |
| 1803 | + * @param array $menu_items_by_parent_id An array keyed by the id of the | |
| 1804 | + * parent menu where each element is an | |
| 1805 | + * array of menu items that belong to | |
| 1806 | + * that parent. | |
| 1609 | 1807 | * @return array An array of parsed block data. |
| 1610 | 1808 | */ |
| 1611 | 1809 | function gutenberg_block_core_navigation_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) { |
| 1612 | 1810 | |
| @@ -1704,9 +1902,9 @@ | ||
| 1704 | 1902 | * @since 6.2.0 |
| 1705 | 1903 | * |
| 1706 | 1904 | * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback_blocks() instead. |
| 1707 | 1905 | * |
| 1708 | - * @param object $classic_nav_menu WP_Term The classic navigation object to convert. | |
| 1906 | + * @param object $classic_nav_menu WP_Term The classic navigation object to convert. | |
| 1709 | 1907 | * @return array the normalized parsed blocks. |
| 1710 | 1908 | */ |
| 1711 | 1909 | function gutenberg_block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) { |
| 1712 | 1910 | |