diff --git a/core/modules/navigation/layouts/navigation.html.twig b/core/modules/navigation/layouts/navigation.html.twig index 3748c239ea7..accf9e96f6d 100644 --- a/core/modules/navigation/layouts/navigation.html.twig +++ b/core/modules/navigation/layouts/navigation.html.twig @@ -70,6 +70,7 @@ } only %} + {{ content.content_top }} {{ content.content }} diff --git a/core/modules/navigation/navigation.api.php b/core/modules/navigation/navigation.api.php new file mode 100644 index 00000000000..f2824f29726 --- /dev/null +++ b/core/modules/navigation/navigation.api.php @@ -0,0 +1,57 @@ + [ + '#markup' => \Drupal::config('system.site')->get('name'), + '#cache' => [ + 'tags' => ['config:system.site'], + ], + ], + 'navigation_bar' => [ + '#markup' => 'bar', + ], + 'navigation_baz' => [ + '#markup' => 'baz', + ], + ]; +} + +/** + * Alter replacement values for placeholder tokens. + * + * @param $content_top + * An associative array of content returned by hook_navigation_content_top(). + * + * @see hook_navigation_content_top() + */ +function hook_navigation_content_top_alter(array &$content_top): void { + // Remove a specific element. + unset($content_top['navigation_foo']); + // Modify an element. + $content_top['navigation_bar']['#markup'] = 'new bar'; + // Change weight. + $content_top['navigation_baz']['#weight'] = '-100'; +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/core/modules/navigation/src/Hook/NavigationHooks.php b/core/modules/navigation/src/Hook/NavigationHooks.php index 17d9b618d93..eb1ddb5b3a2 100644 --- a/core/modules/navigation/src/Hook/NavigationHooks.php +++ b/core/modules/navigation/src/Hook/NavigationHooks.php @@ -99,6 +99,11 @@ class NavigationHooks { ], ]; $items['menu_region__footer'] = ['variables' => ['items' => [], 'title' => NULL, 'menu_name' => NULL]]; + $items['navigation_content_top'] = [ + 'variables' => [ + 'items' => [], + ], + ]; return $items; } diff --git a/core/modules/navigation/src/NavigationRenderer.php b/core/modules/navigation/src/NavigationRenderer.php index cb3ecaa0ea1..8e29298092d 100644 --- a/core/modules/navigation/src/NavigationRenderer.php +++ b/core/modules/navigation/src/NavigationRenderer.php @@ -3,6 +3,7 @@ namespace Drupal\navigation; use Drupal\Component\Utility\NestedArray; +use Drupal\Component\Utility\SortArray; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheableMetadata; @@ -18,6 +19,7 @@ use Drupal\Core\Image\ImageFactory; use Drupal\Core\Menu\LocalTaskManagerInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Security\Attribute\TrustedCallback; use Drupal\Core\Session\AccountInterface; @@ -128,6 +130,7 @@ final class NavigationRenderer { if ($storage) { foreach ($storage->getSections() as $delta => $section) { $build[$delta] = $section->toRenderArray([]); + $build[$delta]['#cache']['contexts'] = ['user.permissions', 'theme', 'languages:language_interface']; } } // The render array is built based on decisions made by SectionStorage @@ -157,6 +160,8 @@ final class NavigationRenderer { ]; $build[0] = NestedArray::mergeDeepArray([$build[0], $defaults]); + $build[0]['content_top'] = $this->getContentTop(); + if ($logo_provider === self::LOGO_PROVIDER_CUSTOM) { $logo_path = $logo_settings->get('logo.path'); if (!empty($logo_path) && is_file($logo_path)) { @@ -169,10 +174,36 @@ final class NavigationRenderer { } } } - $build[0]['#cache']['contexts'] = ['user.permissions', 'theme', 'languages:language_interface']; return $build; } + /** + * Gets the content for content_top section. + * + * @return array + * The content_top section content. + */ + protected function getContentTop(): array { + $content_top = [ + '#theme' => 'navigation_content_top', + ]; + $content_top_items = $this->moduleHandler->invokeAll('navigation_content_top'); + $this->moduleHandler->alter('navigation_content_top', $content_top_items); + uasort($content_top_items, [SortArray::class, 'sortByWeightElement']); + // Filter out empty items, taking care to merge any cacheability metadata. + $cacheability = new CacheableMetadata(); + $content_top_items = array_filter($content_top_items, function ($item) use (&$cacheability) { + if (Element::isEmpty($item)) { + $cacheability = $cacheability->merge(CacheableMetadata::createFromRenderArray($item)); + return FALSE; + } + return TRUE; + }); + $cacheability->applyTo($content_top); + $content_top['#items'] = $content_top_items; + return $content_top; + } + /** * Build the top bar for content entity pages. * diff --git a/core/modules/navigation/templates/navigation-content-top.html.twig b/core/modules/navigation/templates/navigation-content-top.html.twig new file mode 100644 index 00000000000..ba9c002d4cf --- /dev/null +++ b/core/modules/navigation/templates/navigation-content-top.html.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Default theme implementation to display the navigation content_top section. + * + * Available variables: + * - items: An associative array of renderable elements to display in the + * content_top section. + * + * @ingroup themeable + */ +#} +{% if items is not empty %} +
+{% endif %} diff --git a/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php index 2333d04f603..7071cee422c 100644 --- a/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php +++ b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestHooks.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\navigation_test\Hook; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\State\StateInterface; @@ -34,4 +35,53 @@ class NavigationTestHooks { } } + /** + * Implements hook_navigation_content_top(). + */ + #[Hook('navigation_content_top')] + public function navigationContentTop(): array { + if (\Drupal::keyValue('navigation_test')->get('content_top')) { + $items = [ + 'navigation_foo' => [ + '#markup' => 'foo', + ], + 'navigation_bar' => [ + '#markup' => 'bar', + ], + 'navigation_baz' => [ + '#markup' => 'baz', + ], + ]; + } + else { + $items = [ + 'navigation_foo' => [], + 'navigation_bar' => [], + 'navigation_baz' => [], + ]; + } + // Add cache tags to our items to express a made up dependency to test + // cacheability. Note that as we're always returning the same items, + // sometimes only with cacheability metadata. By doing this we're testing + // conditional rendering of content_top items. + foreach ($items as &$element) { + CacheableMetadata::createFromRenderArray($element) + ->addCacheTags(['navigation_test']) + ->applyTo($element); + } + return $items; + } + + /** + * Implements hook_navigation_content_top_alter(). + */ + #[Hook('navigation_content_top_alter')] + public function navigationContentTopAlter(&$content_top): void { + if (\Drupal::keyValue('navigation_test')->get('content_top_alter')) { + unset($content_top['navigation_foo']); + $content_top['navigation_bar']['#markup'] = 'new bar'; + $content_top['navigation_baz']['#weight'] = '-100'; + } + } + } diff --git a/core/modules/navigation/tests/src/Functional/NavigationContentTopTest.php b/core/modules/navigation/tests/src/Functional/NavigationContentTopTest.php new file mode 100644 index 00000000000..981b628259c --- /dev/null +++ b/core/modules/navigation/tests/src/Functional/NavigationContentTopTest.php @@ -0,0 +1,58 @@ +drupalLogin($this->createUser([ + 'access navigation', + ])); + } + + /** + * Tests behavior of content_top section hooks. + */ + public function testNavigationContentTop(): void { + $test_page_url = Url::fromRoute('test_page_test.test_page'); + $this->drupalGet($test_page_url); + $this->assertSession()->elementNotExists('css', '.admin-toolbar__content-top'); + \Drupal::keyValue('navigation_test')->set('content_top', 1); + Cache::invalidateTags(['navigation_test']); + $this->drupalGet($test_page_url); + $this->assertSession()->elementTextContains('css', '.admin-toolbar__content-top', 'foobarbaz'); + \Drupal::keyValue('navigation_test')->set('content_top_alter', 1); + Cache::invalidateTags(['navigation_test']); + $this->drupalGet($test_page_url); + $this->assertSession()->elementTextContains('css', '.admin-toolbar__content-top', 'baznew bar'); + } + +}