Issue #3463142 by plopesc, m4olivei, larowlan, penyaskito: Allow modules to hook into top of content section of new core navigation

merge-requests/10896/head^2
Lee Rowlands 2025-01-13 12:07:05 +10:00
parent 4761995e1a
commit 17b0403efa
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
7 changed files with 220 additions and 1 deletions

View File

@ -70,6 +70,7 @@
} only %}
</div>
{{ content.content_top }}
{{ content.content }}
</nav>

View File

@ -0,0 +1,57 @@
<?php
/**
* @file
* Hooks related to the Navigation module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Provide content for Navigation content_top section.
*
* @return array
* An associative array of renderable elements.
*
* @see hook_navigation_content_top_alter()
*/
function hook_navigation_content_top(): array {
return [
'navigation_foo' => [
'#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".
*/

View File

@ -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;
}

View File

@ -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.
*

View File

@ -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 %}
<div class="admin-toolbar__content-top">
{{ items }}
</div>
{% endif %}

View File

@ -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';
}
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore foobarbaz baznew
/**
* Tests for navigation content_top section.
*
* @group navigation
*/
class NavigationContentTopTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['navigation', 'navigation_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->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');
}
}