@wordpress/block-library 9.37.2-next.79a2f3cdd.0 → 9.37.2-next.ba3aee3a2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/build/code/transforms.cjs +1 -1
  3. package/build/code/transforms.cjs.map +1 -1
  4. package/build/image/block.json +3 -0
  5. package/build/image/image.cjs +42 -4
  6. package/build/image/image.cjs.map +2 -2
  7. package/build/image/save.cjs +3 -0
  8. package/build/image/save.cjs.map +2 -2
  9. package/build/image/utils.cjs +5 -0
  10. package/build/image/utils.cjs.map +2 -2
  11. package/build/navigation/constants.cjs +3 -0
  12. package/build/navigation/constants.cjs.map +2 -2
  13. package/build/navigation/edit/overlay-template-part-selector.cjs +2 -1
  14. package/build/navigation/edit/overlay-template-part-selector.cjs.map +2 -2
  15. package/build/navigation/edit/use-create-overlay.cjs +2 -1
  16. package/build/navigation/edit/use-create-overlay.cjs.map +2 -2
  17. package/build/navigation/view.cjs +14 -2
  18. package/build/navigation/view.cjs.map +2 -2
  19. package/build/private-apis.cjs +3 -1
  20. package/build/private-apis.cjs.map +2 -2
  21. package/build/quote/transforms.cjs +30 -6
  22. package/build/quote/transforms.cjs.map +2 -2
  23. package/build/separator/transforms.cjs +5 -2
  24. package/build/separator/transforms.cjs.map +2 -2
  25. package/build/template-part/edit/index.cjs +9 -2
  26. package/build/template-part/edit/index.cjs.map +2 -2
  27. package/build-module/code/transforms.mjs +1 -1
  28. package/build-module/code/transforms.mjs.map +1 -1
  29. package/build-module/image/block.json +3 -0
  30. package/build-module/image/image.mjs +44 -5
  31. package/build-module/image/image.mjs.map +2 -2
  32. package/build-module/image/save.mjs +3 -0
  33. package/build-module/image/save.mjs.map +2 -2
  34. package/build-module/image/utils.mjs +4 -0
  35. package/build-module/image/utils.mjs.map +2 -2
  36. package/build-module/navigation/constants.mjs +2 -0
  37. package/build-module/navigation/constants.mjs.map +2 -2
  38. package/build-module/navigation/edit/overlay-template-part-selector.mjs +2 -1
  39. package/build-module/navigation/edit/overlay-template-part-selector.mjs.map +2 -2
  40. package/build-module/navigation/edit/use-create-overlay.mjs +2 -1
  41. package/build-module/navigation/edit/use-create-overlay.mjs.map +2 -2
  42. package/build-module/navigation/view.mjs +14 -2
  43. package/build-module/navigation/view.mjs.map +2 -2
  44. package/build-module/private-apis.mjs +3 -1
  45. package/build-module/private-apis.mjs.map +2 -2
  46. package/build-module/quote/transforms.mjs +30 -6
  47. package/build-module/quote/transforms.mjs.map +2 -2
  48. package/build-module/separator/transforms.mjs +6 -3
  49. package/build-module/separator/transforms.mjs.map +2 -2
  50. package/build-module/template-part/edit/index.mjs +9 -2
  51. package/build-module/template-part/edit/index.mjs.map +2 -2
  52. package/build-style/navigation/style-rtl.css +25 -6
  53. package/build-style/navigation/style.css +25 -6
  54. package/build-style/navigation-overlay-close/style-rtl.css +0 -1
  55. package/build-style/navigation-overlay-close/style.css +0 -1
  56. package/build-style/style-rtl.css +25 -7
  57. package/build-style/style.css +25 -7
  58. package/package.json +37 -37
  59. package/src/code/transforms.js +1 -1
  60. package/src/image/block.json +3 -0
  61. package/src/image/image.js +43 -2
  62. package/src/image/save.js +10 -0
  63. package/src/image/utils.js +4 -0
  64. package/src/navigation/constants.js +9 -0
  65. package/src/navigation/edit/overlay-template-part-selector.js +3 -1
  66. package/src/navigation/edit/test/overlay-template-part-selector.js +3 -3
  67. package/src/navigation/edit/test/use-create-overlay.js +5 -5
  68. package/src/navigation/edit/use-create-overlay.js +2 -1
  69. package/src/navigation/index.php +271 -13
  70. package/src/navigation/style.scss +46 -9
  71. package/src/navigation/view.js +24 -4
  72. package/src/navigation-overlay-close/style.scss +0 -1
  73. package/src/private-apis.js +2 -0
  74. package/src/quote/transforms.js +31 -5
  75. package/src/separator/transforms.js +6 -3
  76. package/src/template-part/edit/index.js +21 -4
@@ -41,6 +41,18 @@ class WP_Navigation_Block_Renderer {
41
41
  */
42
42
  private static $seen_menu_names = array();
43
43
 
44
+ /**
45
+ * Returns whether the navigation overlay experiment is enabled.
46
+ *
47
+ * @since 6.5.0
48
+ *
49
+ * @return bool Returns whether the navigation overlay experiment is enabled.
50
+ */
51
+ private static function is_overlay_experiment_enabled() {
52
+ $gutenberg_experiments = get_option( 'gutenberg-experiments' );
53
+ return $gutenberg_experiments && array_key_exists( 'gutenberg-customizable-navigation-overlays', $gutenberg_experiments );
54
+ }
55
+
44
56
  /**
45
57
  * Returns whether or not this is responsive navigation.
46
58
  *
@@ -158,6 +170,22 @@ class WP_Navigation_Block_Renderer {
158
170
  return $inner_block_content;
159
171
  }
160
172
 
173
+ /**
174
+ * Returns the html for blocks from a template part (without navigation container wrapper).
175
+ *
176
+ * @since 6.5.0
177
+ *
178
+ * @param WP_Block_List $blocks The list of blocks to render.
179
+ * @return string Returns the html for the template part blocks.
180
+ */
181
+ private static function get_template_part_blocks_html( $blocks ) {
182
+ $html = '';
183
+ foreach ( $blocks as $block ) {
184
+ $html .= $block->render();
185
+ }
186
+ return $html;
187
+ }
188
+
161
189
  /**
162
190
  * Returns the html for the inner blocks of the navigation block.
163
191
  *
@@ -272,6 +300,127 @@ class WP_Navigation_Block_Renderer {
272
300
  return new WP_Block_List( $fallback_blocks, $attributes );
273
301
  }
274
302
 
303
+ /**
304
+ * Recursively disables overlay menu for navigation blocks within overlay blocks.
305
+ * Prevents nested overlays (inception).
306
+ *
307
+ * @since 6.5.0
308
+ *
309
+ * @param array $blocks Array of parsed block arrays.
310
+ * @return array Modified blocks with overlayMenu set to 'never' for navigation blocks.
311
+ */
312
+ private static function disable_overlay_menu_for_nested_navigation_blocks( $blocks ) {
313
+ if ( empty( $blocks ) || ! is_array( $blocks ) ) {
314
+ return $blocks;
315
+ }
316
+
317
+ foreach ( $blocks as &$block ) {
318
+ if ( ! isset( $block['blockName'] ) ) {
319
+ continue;
320
+ }
321
+
322
+ // If this is a navigation block, disable its overlay menu.
323
+ if ( 'core/navigation' === $block['blockName'] ) {
324
+ if ( ! isset( $block['attrs'] ) ) {
325
+ $block['attrs'] = array();
326
+ }
327
+ $block['attrs']['overlayMenu'] = 'never';
328
+ }
329
+
330
+ // Recursively process inner blocks.
331
+ if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
332
+ $block['innerBlocks'] = static::disable_overlay_menu_for_nested_navigation_blocks( $block['innerBlocks'] );
333
+ }
334
+ }
335
+
336
+ return $blocks;
337
+ }
338
+
339
+ /**
340
+ * Gets the inner blocks for the navigation block from an overlay template part.
341
+ *
342
+ * @since 6.5.0
343
+ *
344
+ * @param string $overlay_template_part_id The overlay template part ID in format "theme//slug".
345
+ * @param array $attributes The block attributes.
346
+ * @return WP_Block_List Returns the inner blocks for the overlay template part.
347
+ */
348
+ private static function get_overlay_blocks_from_template_part( $overlay_template_part_id, $attributes ) {
349
+ if ( empty( $overlay_template_part_id ) || ! is_string( $overlay_template_part_id ) ) {
350
+ return new WP_Block_List( array(), $attributes );
351
+ }
352
+
353
+ // Parse the template part ID (format: "theme//slug").
354
+ $parts = explode( '//', $overlay_template_part_id, 2 );
355
+ if ( count( $parts ) !== 2 ) {
356
+ return new WP_Block_List( array(), $attributes );
357
+ }
358
+
359
+ $theme = $parts[0];
360
+ $slug = $parts[1];
361
+
362
+ // Only query for template parts from the active theme.
363
+ if ( get_stylesheet() !== $theme ) {
364
+ return new WP_Block_List( array(), $attributes );
365
+ }
366
+
367
+ // Query for the template part post.
368
+ $template_part_query = new WP_Query(
369
+ array(
370
+ 'post_type' => 'wp_template_part',
371
+ 'post_status' => 'publish',
372
+ 'post_name__in' => array( $slug ),
373
+ 'tax_query' => array(
374
+ array(
375
+ 'taxonomy' => 'wp_theme',
376
+ 'field' => 'name',
377
+ 'terms' => $theme,
378
+ ),
379
+ ),
380
+ 'posts_per_page' => 1,
381
+ 'no_found_rows' => true,
382
+ 'lazy_load_term_meta' => false, // Do not lazy load term meta, as template parts only have one term.
383
+ )
384
+ );
385
+
386
+ $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null;
387
+
388
+ if ( ! $template_part_post ) {
389
+ // Try to get from theme file if not in database.
390
+ $block_template = get_block_file_template( $overlay_template_part_id, 'wp_template_part' );
391
+ if ( isset( $block_template->content ) ) {
392
+ $parsed_blocks = parse_blocks( $block_template->content );
393
+ $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
394
+ // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays.
395
+ $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks );
396
+ return new WP_Block_List( $blocks, $attributes );
397
+ }
398
+ return new WP_Block_List( array(), $attributes );
399
+ }
400
+
401
+ // Get the template part content.
402
+ $block_template = _build_block_template_result_from_post( $template_part_post );
403
+ if ( ! isset( $block_template->content ) ) {
404
+ return new WP_Block_List( array(), $attributes );
405
+ }
406
+
407
+ $parsed_blocks = parse_blocks( $block_template->content );
408
+
409
+ // 'parse_blocks' includes a null block with '\n\n' as the content when
410
+ // it encounters whitespace. This code strips it.
411
+ $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
412
+
413
+ // Re-serialize, and run Block Hooks algorithm to inject hooked blocks.
414
+ $markup = serialize_blocks( $blocks );
415
+ $markup = apply_block_hooks_to_content_from_post_object( $markup, $template_part_post );
416
+ $blocks = parse_blocks( $markup );
417
+
418
+ // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays.
419
+ $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks );
420
+
421
+ return new WP_Block_List( $blocks, $attributes );
422
+ }
423
+
275
424
  /**
276
425
  * Gets the inner blocks for the navigation block.
277
426
  *
@@ -467,9 +616,44 @@ class WP_Navigation_Block_Renderer {
467
616
 
468
617
  $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu'];
469
618
 
619
+ // Set-up variables for the custom overlay experiment.
620
+ // Values are set to "off" so they don't affect the default behavior.
621
+ $is_overlay_experiment_enabled = static::is_overlay_experiment_enabled();
622
+ $has_custom_overlay = false;
623
+ $close_button_markup = '';
624
+ $has_custom_overlay_close_block = false;
625
+ $overlay_blocks_html = '';
626
+ $custom_overlay_markup = '';
627
+
628
+ if ( $is_overlay_experiment_enabled ) {
629
+ // Check if an overlay template part is selected and render it.
630
+ // This needs to happen before building classes so we know if overlay blocks actually exist.
631
+ if ( ! empty( $attributes['overlay'] ) ) {
632
+ // Get blocks from the overlay template part.
633
+ $overlay_blocks = static::get_overlay_blocks_from_template_part( $attributes['overlay'], $attributes );
634
+ // Check if overlay contains a navigation-overlay-close block.
635
+ $has_custom_overlay_close_block = block_core_navigation_block_tree_has_block_type(
636
+ $overlay_blocks,
637
+ 'core/navigation-overlay-close',
638
+ array( 'core/navigation' ) // Skip navigation blocks, as they cannot contain an overlay close block
639
+ );
640
+ // Render template part blocks directly without navigation container wrapper.
641
+ $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks );
642
+ // Add Interactivity API directives to the overlay close block if present.
643
+ if ( $has_custom_overlay_close_block && $is_interactive ) {
644
+ $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html );
645
+ $overlay_blocks_html = block_core_navigation_add_directives_to_overlay_close( $tags );
646
+ }
647
+ }
648
+
649
+ $has_custom_overlay = ! empty( $overlay_blocks_html );
650
+ }
651
+
652
+ // Only add the disable-default-overlay class if experiment is enabled AND overlay blocks actually rendered.
470
653
  $responsive_container_classes = array(
471
654
  'wp-block-navigation__responsive-container',
472
655
  $is_hidden_by_default ? 'hidden-by-default' : '',
656
+ $has_custom_overlay ? 'disable-default-overlay' : '',
473
657
  implode( ' ', $colors['overlay_css_classes'] ),
474
658
  );
475
659
  $open_button_classes = array(
@@ -523,14 +707,33 @@ class WP_Navigation_Block_Renderer {
523
707
 
524
708
  $overlay_inline_styles = esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) );
525
709
 
710
+ if ( $has_custom_overlay ) {
711
+ $custom_overlay_markup = sprintf(
712
+ '<div class="wp-block-navigation__overlay-container">%s</div>',
713
+ $overlay_blocks_html
714
+ );
715
+ }
716
+
717
+ // Show default close button for all responsive navigation,
718
+ // unless custom overlay has its own close block.
719
+ if ( ! $has_custom_overlay_close_block ) {
720
+ $close_button_markup = sprintf(
721
+ '<button %1$s class="wp-block-navigation__responsive-container-close" %2$s>%3$s</button>',
722
+ $toggle_aria_label_close,
723
+ $close_button_directives,
724
+ $toggle_close_button_content
725
+ );
726
+ }
727
+
526
728
  return sprintf(
527
729
  '<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button>
528
730
  <div class="%5$s" %7$s id="%1$s" %11$s>
529
731
  <div class="wp-block-navigation__responsive-close" tabindex="-1">
530
732
  <div class="wp-block-navigation__responsive-dialog" %12$s>
531
- <button %4$s class="wp-block-navigation__responsive-container-close" %13$s>%9$s</button>
733
+ %13$s
532
734
  <div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content">
533
735
  %2$s
736
+ %15$s
534
737
  </div>
535
738
  </div>
536
739
  </div>
@@ -547,8 +750,9 @@ class WP_Navigation_Block_Renderer {
547
750
  $open_button_directives,
548
751
  $responsive_container_directives,
549
752
  $responsive_dialog_directives,
550
- $close_button_directives,
551
- $responsive_container_content_directives
753
+ $close_button_markup,
754
+ $responsive_container_content_directives,
755
+ $has_custom_overlay ? $custom_overlay_markup : ''
552
756
  );
553
757
  }
554
758
 
@@ -699,7 +903,10 @@ class WP_Navigation_Block_Renderer {
699
903
 
700
904
  $inner_blocks = static::get_inner_blocks( $attributes, $block );
701
905
  // Prevent navigation blocks referencing themselves from rendering.
702
- if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) {
906
+ if ( block_core_navigation_block_tree_has_block_type(
907
+ $inner_blocks,
908
+ 'core/navigation'
909
+ ) ) {
703
910
  return '';
704
911
  }
705
912
 
@@ -798,6 +1005,29 @@ if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
798
1005
  }
799
1006
  }
800
1007
 
1008
+ /**
1009
+ * Add Interactivity API directives to the navigation-overlay-close block
1010
+ * markup using the Tag Processor.
1011
+ *
1012
+ * @since 6.5.0
1013
+ *
1014
+ * @param WP_HTML_Tag_Processor $tags Markup of the navigation block.
1015
+ * @return string Overlay close markup with the directives injected.
1016
+ */
1017
+ function block_core_navigation_add_directives_to_overlay_close( $tags ) {
1018
+ // Find the navigation-overlay-close button.
1019
+ if ( $tags->next_tag(
1020
+ array(
1021
+ 'tag_name' => 'BUTTON',
1022
+ 'class_name' => 'wp-block-navigation-overlay-close',
1023
+ )
1024
+ ) ) {
1025
+ // Add the same close directive as the default close button.
1026
+ $tags->set_attribute( 'data-wp-on--click', 'actions.closeMenuOnClick' );
1027
+ }
1028
+ return $tags->get_updated_html();
1029
+ }
1030
+
801
1031
  /**
802
1032
  * Add Interactivity API directives to the navigation-submenu and page-list
803
1033
  * blocks markup using the Tag Processor.
@@ -1020,26 +1250,54 @@ function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) {
1020
1250
  }
1021
1251
 
1022
1252
  /**
1023
- * Returns true if the navigation block contains a nested navigation block.
1253
+ * Recursively checks if blocks contain a specific block type.
1024
1254
  *
1025
- * @since 6.2.0
1255
+ * @since 7.0.0
1026
1256
  *
1027
- * @param WP_Block_List $inner_blocks Inner block instance to be normalized.
1028
- * @return bool true if the navigation block contains a nested navigation block.
1257
+ * @param WP_Block_List $blocks The list of blocks to check.
1258
+ * @param string $block_type The block type to search for (e.g., 'core/navigation').
1259
+ * @param array $skip_block_types Optional. Block types to skip when recursing. Default empty array.
1260
+ * @return bool Returns true if the specified block type is found.
1029
1261
  */
1030
- function block_core_navigation_block_contains_core_navigation( $inner_blocks ) {
1031
- foreach ( $inner_blocks as $block ) {
1032
- if ( 'core/navigation' === $block->name ) {
1262
+ function block_core_navigation_block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) {
1263
+ if ( empty( $blocks ) ) {
1264
+ return false;
1265
+ }
1266
+
1267
+ foreach ( $blocks as $block ) {
1268
+ if ( $block_type === $block->name ) {
1033
1269
  return true;
1034
1270
  }
1035
- if ( $block->inner_blocks && block_core_navigation_block_contains_core_navigation( $block->inner_blocks ) ) {
1036
- return true;
1271
+
1272
+ // Recursively check inner blocks, skipping specified block types.
1273
+ if ( ! in_array( $block->name, $skip_block_types, true ) && ! empty( $block->inner_blocks ) ) {
1274
+ if ( block_core_navigation_block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) {
1275
+ return true;
1276
+ }
1037
1277
  }
1038
1278
  }
1039
1279
 
1040
1280
  return false;
1041
1281
  }
1042
1282
 
1283
+ /**
1284
+ * Returns true if the navigation block contains a nested navigation block.
1285
+ *
1286
+ * @since 6.2.0
1287
+ * @deprecated 7.0.0 Use block_core_navigation_block_tree_has_block_type() instead.
1288
+ *
1289
+ * @param WP_Block_List $inner_blocks Inner block instance to be normalized.
1290
+ * @return bool true if the navigation block contains a nested navigation block.
1291
+ */
1292
+ function block_core_navigation_block_contains_core_navigation( $inner_blocks ) {
1293
+ _deprecated_function( __FUNCTION__, '7.0.0', 'block_core_navigation_block_tree_has_block_type()' );
1294
+
1295
+ return block_core_navigation_block_tree_has_block_type(
1296
+ $inner_blocks,
1297
+ 'core/navigation'
1298
+ );
1299
+ }
1300
+
1043
1301
  /**
1044
1302
  * Retrieves the appropriate fallback to be used on the front of the
1045
1303
  * site when there is no menu assigned to the Nav block.
@@ -523,10 +523,13 @@ button.wp-block-navigation-item__content {
523
523
  }
524
524
 
525
525
  // Try to inherit any root paddings set, so the X can align to a top-right aligned menu.
526
- padding-top: clamp(1rem, var(--wp--style--root--padding-top), 20rem);
527
- padding-right: clamp(1rem, var(--wp--style--root--padding-right), 20rem);
528
- padding-bottom: clamp(1rem, var(--wp--style--root--padding-bottom), 20rem);
529
- padding-left: clamp(1rem, var(--wp--style--root--padding-left), 20rem);
526
+ // Don't apply default padding when custom overlay is present.
527
+ &:not(.disable-default-overlay) {
528
+ padding-top: clamp(1rem, var(--wp--style--root--padding-top), 20rem);
529
+ padding-right: clamp(1rem, var(--wp--style--root--padding-right), 20rem);
530
+ padding-bottom: clamp(1rem, var(--wp--style--root--padding-bottom), 20rem);
531
+ padding-left: clamp(1rem, var(--wp--style--root--padding-left), 20rem);
532
+ }
530
533
 
531
534
  // Allow modal to scroll.
532
535
  overflow: auto;
@@ -534,10 +537,13 @@ button.wp-block-navigation-item__content {
534
537
  // Give it a z-index just higher than the adminbar.
535
538
  z-index: 100000;
536
539
 
537
- .wp-block-navigation__responsive-container-content {
538
- // Add padding above to accommodate close button.
540
+ // Add padding above to accommodate close button.
541
+ // Don't apply default padding when custom overlay is present.
542
+ &:not(.disable-default-overlay) .wp-block-navigation__responsive-container-content {
539
543
  padding-top: calc(2rem + #{ $navigation-icon-size });
544
+ }
540
545
 
546
+ .wp-block-navigation__responsive-container-content {
541
547
  // Don't crop the focus style.
542
548
  overflow: visible;
543
549
 
@@ -623,6 +629,29 @@ button.wp-block-navigation-item__content {
623
629
  }
624
630
  }
625
631
 
632
+ // Custom overlay container visibility.
633
+ // When there's a custom overlay, by default hide overlay container.
634
+ &.disable-default-overlay {
635
+ .wp-block-navigation__overlay-container {
636
+ display: none;
637
+ width: 100%;
638
+ }
639
+
640
+ // When menu is open with custom overlay, hide default navigation and show overlay.
641
+ &.is-menu-open {
642
+ .wp-block-navigation__responsive-container-content {
643
+ // Hides the content of the non-overlay navigation.
644
+ > .wp-block-navigation__container {
645
+ display: none;
646
+ }
647
+
648
+ .wp-block-navigation__overlay-container {
649
+ display: block;
650
+ }
651
+ }
652
+ }
653
+ }
654
+
626
655
  @include break-small() {
627
656
  &:not(.hidden-by-default) {
628
657
  &:not(.is-menu-open) {
@@ -648,13 +677,14 @@ button.wp-block-navigation-item__content {
648
677
  }
649
678
 
650
679
  // Default menu background and font color.
680
+ // Don't apply default colors when custom overlay is present.
651
681
  .wp-block-navigation:not(.has-background)
652
- .wp-block-navigation__responsive-container.is-menu-open {
682
+ .wp-block-navigation__responsive-container.is-menu-open:not(.disable-default-overlay) {
653
683
  background-color: #fff;
654
684
  }
655
685
 
656
686
  .wp-block-navigation:not(.has-text-color)
657
- .wp-block-navigation__responsive-container.is-menu-open {
687
+ .wp-block-navigation__responsive-container.is-menu-open:not(.disable-default-overlay) {
658
688
  color: #000;
659
689
  }
660
690
 
@@ -721,6 +751,12 @@ button.wp-block-navigation-item__content {
721
751
  }
722
752
  }
723
753
 
754
+ // When default close button is within a custom overlay, add spacing from edges.
755
+ .disable-default-overlay .wp-block-navigation__responsive-container-close {
756
+ top: clamp(1rem, var(--wp--style--root--padding-left), 20rem);
757
+ right: clamp(1rem, var(--wp--style--root--padding-left), 20rem);
758
+ }
759
+
724
760
  // The menu adds wrapping containers.
725
761
  .wp-block-navigation__responsive-close {
726
762
  width: 100%;
@@ -753,7 +789,8 @@ button.wp-block-navigation-item__content {
753
789
 
754
790
  // Adjust open dialog top margin when admin-bar is visible.
755
791
  // Needs to be scoped to .is-menu-open, or it will shift the position of any other navigations that may be present.
756
- .has-modal-open .admin-bar .is-menu-open .wp-block-navigation__responsive-dialog {
792
+ // Don't apply margin when custom overlay is present.
793
+ .has-modal-open .admin-bar .is-menu-open:not(.disable-default-overlay) .wp-block-navigation__responsive-dialog {
757
794
  margin-top: $admin-bar-height-big;
758
795
 
759
796
  // Handle smaller admin-bar.
@@ -18,6 +18,28 @@ const focusableSelectors = [
18
18
  '[tabindex]:not([tabindex^="-"])',
19
19
  ];
20
20
 
21
+ /**
22
+ * Gets all visible focusable elements within a container.
23
+ * Filters out elements that are hidden.
24
+ *
25
+ * @param {HTMLElement} ref - The container element to search within
26
+ * @return {HTMLElement[]} Array of visible focusable elements
27
+ */
28
+ function getFocusableElements( ref ) {
29
+ const focusableElements = ref.querySelectorAll( focusableSelectors );
30
+ return Array.from( focusableElements ).filter( ( element ) => {
31
+ // Use modern checkVisibility API if available (Chrome 105+, Firefox 106+, Safari 17.4+)
32
+ if ( typeof element.checkVisibility === 'function' ) {
33
+ return element.checkVisibility( {
34
+ checkOpacity: false,
35
+ checkVisibilityCSS: true,
36
+ } );
37
+ }
38
+ // Fallback for older browsers
39
+ return element.offsetParent !== null;
40
+ } );
41
+ }
42
+
21
43
  // This is a fix for Safari in iOS/iPadOS. Without it, Safari doesn't focus out
22
44
  // when the user taps in the body. It can be removed once we add an overlay to
23
45
  // capture the clicks, instead of relying on the focusout event.
@@ -198,8 +220,7 @@ const { state, actions } = store(
198
220
  const ctx = getContext();
199
221
  const { ref } = getElement();
200
222
  if ( state.isMenuOpen ) {
201
- const focusableElements =
202
- ref.querySelectorAll( focusableSelectors );
223
+ const focusableElements = getFocusableElements( ref );
203
224
  ctx.modal = ref;
204
225
  ctx.firstFocusableElement = focusableElements[ 0 ];
205
226
  ctx.lastFocusableElement =
@@ -209,8 +230,7 @@ const { state, actions } = store(
209
230
  focusFirstElement() {
210
231
  const { ref } = getElement();
211
232
  if ( state.isMenuOpen ) {
212
- const focusableElements =
213
- ref.querySelectorAll( focusableSelectors );
233
+ const focusableElements = getFocusableElements( ref );
214
234
  focusableElements?.[ 0 ]?.focus();
215
235
  }
216
236
  },
@@ -10,7 +10,6 @@
10
10
  text-decoration: none;
11
11
 
12
12
  &:focus {
13
- outline: 2px solid currentColor;
14
13
  outline-offset: 2px;
15
14
  }
16
15
 
@@ -2,6 +2,7 @@
2
2
  * Internal dependencies
3
3
  */
4
4
  import { default as BlockKeyboardShortcuts } from './block-keyboard-shortcuts';
5
+ import { NAVIGATION_OVERLAY_TEMPLATE_PART_AREA } from './navigation/constants';
5
6
  import { lock } from './lock-unlock';
6
7
 
7
8
  /**
@@ -10,4 +11,5 @@ import { lock } from './lock-unlock';
10
11
  export const privateApis = {};
11
12
  lock( privateApis, {
12
13
  BlockKeyboardShortcuts,
14
+ NAVIGATION_OVERLAY_TEMPLATE_PART_AREA,
13
15
  } );
@@ -141,15 +141,41 @@ const transforms = {
141
141
  {
142
142
  type: 'block',
143
143
  blocks: [ 'core/paragraph' ],
144
- transform: ( { citation }, innerBlocks ) =>
145
- RichText.isEmpty( citation )
146
- ? innerBlocks
144
+ isMatch: ( { citation }, block ) => {
145
+ const innerBlocks = block.innerBlocks;
146
+ if ( ! innerBlocks.length ) {
147
+ return ! RichText.isEmpty( citation );
148
+ }
149
+
150
+ return innerBlocks.every( ( innerBlock ) => {
151
+ if ( innerBlock.name === 'core/paragraph' ) {
152
+ return true;
153
+ }
154
+ const converted = switchToBlockType(
155
+ innerBlock,
156
+ 'core/paragraph'
157
+ );
158
+ return converted !== null;
159
+ } );
160
+ },
161
+ transform: ( { citation }, innerBlocks ) => {
162
+ const paragraphs = innerBlocks.flatMap( ( innerBlock ) => {
163
+ if ( innerBlock.name === 'core/paragraph' ) {
164
+ return innerBlock;
165
+ }
166
+ return (
167
+ switchToBlockType( innerBlock, 'core/paragraph' ) || []
168
+ );
169
+ } );
170
+ return RichText.isEmpty( citation )
171
+ ? paragraphs
147
172
  : [
148
- ...innerBlocks,
173
+ ...paragraphs,
149
174
  createBlock( 'core/paragraph', {
150
175
  content: citation,
151
176
  } ),
152
- ],
177
+ ];
178
+ },
153
179
  },
154
180
  {
155
181
  type: 'block',
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { createBlock } from '@wordpress/blocks';
4
+ import { createBlock, getDefaultBlockName } from '@wordpress/blocks';
5
5
 
6
6
  const transforms = {
7
7
  from: [
8
8
  {
9
- type: 'enter',
9
+ type: 'input',
10
10
  regExp: /^-{3,}$/,
11
- transform: () => createBlock( 'core/separator' ),
11
+ transform: () => [
12
+ createBlock( 'core/separator' ),
13
+ createBlock( getDefaultBlockName() ),
14
+ ],
12
15
  },
13
16
  {
14
17
  type: 'raw',
@@ -40,6 +40,23 @@ import {
40
40
  useTemplatePartArea,
41
41
  } from './utils/hooks';
42
42
 
43
+ const SUPPORTED_AREAS = [ 'header', 'footer' ];
44
+
45
+ /**
46
+ * Returns the list of supported template part areas for pattern replacement.
47
+ * Includes 'overlay' only if the navigation overlays experiment is enabled.
48
+ *
49
+ * @return {string[]} Array of supported area names.
50
+ */
51
+ function getSupportedAreas() {
52
+ const isOverlayExperimentEnabled =
53
+ typeof window !== 'undefined' &&
54
+ window.__experimentalNavigationOverlays === true;
55
+ return isOverlayExperimentEnabled
56
+ ? [ ...SUPPORTED_AREAS, 'navigation-overlay' ]
57
+ : SUPPORTED_AREAS;
58
+ }
59
+
43
60
  function ReplaceButton( {
44
61
  isEntityAvailable,
45
62
  area,
@@ -54,10 +71,9 @@ function ReplaceButton( {
54
71
  templatePartId
55
72
  );
56
73
  const hasReplacements = !! templateParts.length;
74
+ const supportedAreas = getSupportedAreas();
57
75
  const canReplace =
58
- isEntityAvailable &&
59
- hasReplacements &&
60
- ( area === 'header' || area === 'footer' );
76
+ isEntityAvailable && hasReplacements && supportedAreas.includes( area );
61
77
 
62
78
  if ( ! canReplace ) {
63
79
  return null;
@@ -80,10 +96,11 @@ function TemplatesList( { area, clientId, isEntityAvailable, onSelect } ) {
80
96
  // This hook fetches patterns, so don't run it unconditionally in the main
81
97
  // edit function!
82
98
  const blockPatterns = useAlternativeBlockPatterns( area, clientId );
99
+ const supportedAreas = getSupportedAreas();
83
100
  const canReplace =
84
101
  isEntityAvailable &&
85
102
  !! blockPatterns.length &&
86
- ( area === 'header' || area === 'footer' );
103
+ supportedAreas.includes( area );
87
104
 
88
105
  if ( ! canReplace ) {
89
106
  return null;