From a5a8967cd48ac86ddba619449c921306c879365c Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Fri, 13 Sep 2024 18:36:51 +0100 Subject: [PATCH] Issue #3447837 by finnsky, m4olivei, plopesc, kostyashupenko, kanchan bhogade, smustgrave, alexpott: Special Menu items are rendered as empty links in navigation (cherry picked from commit 1c67d3b55935ed8cb993bd49966d0603e62175e3) --- .../templates/navigation-menu.html.twig | 50 +++++--- .../templates/toolbar-button.html.twig | 2 +- ...ckTest.php => NavigationMenuBlockTest.php} | 120 ++++++++++++++---- .../src/Kernel/NavigationMenuMarkupTest.php | 10 +- 4 files changed, 139 insertions(+), 43 deletions(-) rename core/modules/navigation/tests/src/Kernel/{SystemMenuNavigationBlockTest.php => NavigationMenuBlockTest.php} (66%) diff --git a/core/modules/navigation/templates/navigation-menu.html.twig b/core/modules/navigation/templates/navigation-menu.html.twig index f1242c9b58a3..7e9ec356e9a6 100644 --- a/core/modules/navigation/templates/navigation-menu.html.twig +++ b/core/modules/navigation/templates/navigation-menu.html.twig @@ -8,16 +8,33 @@ {% macro menu_items(items, attributes, menu_level) %} {% for item in items %} + + {% set item_link_tag = 'a' %} + + {% if item.url.isRouted %} + {% if item.url.routeName == '<nolink>' %} + {% set item_link_tag = constant('\\Drupal\\Core\\GeneratedNoLink::TAG') %} + {% elseif item.url.routeName == '<button>' %} + {% set item_link_tag = constant('\\Drupal\\Core\\GeneratedButton::TAG') %} + {% endif %} + {% endif %} + + {% if item.url.options.attributes is empty %} + {% set item_link_attributes = create_attribute() %} + {% else %} + {% set item_link_attributes = create_attribute(item.url.options.attributes) %} + {% endif %} + {% set item_id = ('navigation-link--' ~ item.original_link.pluginId)|clean_unique_id %} {% if menu_level == 0 %} - {% if item.below is empty %} + {% if item.below is empty %} <li id="{{ item_id }}" class="toolbar-block__list-item"> {% include '@navigation/toolbar-button.html.twig' with { - attributes: create_attribute({ 'href': item.url|render, 'data-drupal-tooltip': item.title, 'data-drupal-tooltip-class': 'admin-toolbar__tooltip' }), + attributes: item_link_attributes.setAttribute('href', item.url|render|default(null)).setAttribute('data-drupal-tooltip', item.title).setAttribute('data-drupal-tooltip-class', 'admin-toolbar__tooltip'), icon: item.class|clean_class, - html_tag: 'a', + html_tag: item_link_tag, text: item.title, - extra_classes: 'toolbar-button--collapsible', + extra_classes: 'toolbar-button--collapsible ' ~ (item_link_tag == 'span' ? 'toolbar-button--non-interactive'), } only %} </li> {% else %} @@ -32,7 +49,7 @@ } only %} <div class="toolbar-popover__wrapper" data-toolbar-popover-wrapper inert> {% if item.url %} - {{ link(item.title, item.url, create_attribute({'class': ['toolbar-popover__header', 'toolbar-button', 'toolbar-button--large', 'toolbar-button--dark']})) }} + {{ link(item.title, item.url, item_link_attributes.addClass(['toolbar-popover__header', 'toolbar-button', 'toolbar-button--large', 'toolbar-button--dark'])) }} {% else %} <span class="toolbar-popover__header toolbar-button toolbar-button--large toolbar-button--dark toolbar-button--non-interactive">{{ item.title }}</span> {% endif %} @@ -46,11 +63,12 @@ {% elseif menu_level == 1 %} <li class="toolbar-menu__item toolbar-menu__item--level-{{ menu_level }}"> {% if item.below is empty %} - <a - href="{{ item.url }}" - class="toolbar-button" - data-index-text="{{ item.title|first|lower }}" - >{{ item.title }}</a> + {% include '@navigation/toolbar-button.html.twig' with { + attributes: item_link_attributes.setAttribute('href', item.url|render|default(null)), + text: item.title, + html_tag: item_link_tag, + extra_classes: item_link_tag == 'span' ? 'toolbar-button--non-interactive', + } only %} {% else %} <button class="toolbar-button toolbar-button--expand--down" @@ -69,11 +87,13 @@ {% else %} <li class="toolbar-menu__item toolbar-menu__item--level-{{ menu_level }}"> {% if item.below is empty %} - <a - href="{{ item.url }}" - class="toolbar-menu__link toolbar-menu__link--{{ menu_level }}" - data-index-text="{{ item.title|first|lower }}" - >{{ item.title }}</a> + {{ link(item.title, item.url, { + 'class': [ + 'toolbar-menu__link', + 'toolbar-menu__link--' ~ menu_level, + ], + 'data-index-text': item.title|first|lower + }) }} {% else %} <button class="toolbar-menu__link toolbar-menu__link--{{ menu_level }}" diff --git a/core/modules/navigation/templates/toolbar-button.html.twig b/core/modules/navigation/templates/toolbar-button.html.twig index 7ba0fc8d185f..a209398068c9 100644 --- a/core/modules/navigation/templates/toolbar-button.html.twig +++ b/core/modules/navigation/templates/toolbar-button.html.twig @@ -10,7 +10,7 @@ appear after main classes #} <{{html_tag|default('button')}} {{ attributes.addClass(classes) }} data-index-text="{{ text|first|lower }}" - data-icon-text={{- text|render|trim|slice(0, 2)|join('') }} + data-icon-text="{{- text|render|trim|slice(0, 2)|join('') -}}" > {% if avatar %} <span class="toolbar-button__avatar">{{~ avatar ~}}</span> diff --git a/core/modules/navigation/tests/src/Kernel/SystemMenuNavigationBlockTest.php b/core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php similarity index 66% rename from core/modules/navigation/tests/src/Kernel/SystemMenuNavigationBlockTest.php rename to core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php index d591e2723855..d6ccfc6983b0 100644 --- a/core/modules/navigation/tests/src/Kernel/SystemMenuNavigationBlockTest.php +++ b/core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php @@ -6,7 +6,9 @@ use Drupal\block\Entity\Block; use Drupal\Core\Render\Element; +use Drupal\Core\Render\MetadataBubblingUrlGenerator; use Drupal\Core\Routing\RouteObjectInterface; +use Drupal\Core\Routing\UrlGenerator; use Drupal\KernelTests\KernelTestBase; use Drupal\navigation\Plugin\Block\NavigationMenuBlock; use Drupal\system\Entity\Menu; @@ -20,7 +22,7 @@ use Symfony\Component\Routing\RouteCollection; /** - * Tests \Drupal\navigation\Plugin\Block\SystemMenuNavigationBlock. + * Tests \Drupal\navigation\Plugin\Block\NavigationMenuBlock. * * @group navigation * @see \Drupal\navigation\Plugin\Derivative\SystemMenuNavigationBlock @@ -28,7 +30,7 @@ * @todo Expand test coverage to all SystemMenuNavigationBlock functionality, * including block_menu_delete(). */ -class SystemMenuNavigationBlockTest extends KernelTestBase { +class NavigationMenuBlockTest extends KernelTestBase { /** * {@inheritdoc} @@ -102,19 +104,31 @@ protected function setUp(): void { $routes = new RouteCollection(); $requirements = ['_access' => 'TRUE']; $options = ['_access_checks' => ['access_check.default']]; - $routes->add('example1', new Route('/example1', [], $requirements, $options)); + $special_options = $options + ['_no_path' => TRUE]; $routes->add('example2', new Route('/example2', [], $requirements, $options)); - $routes->add('example3', new Route('/example3', [], $requirements, $options)); $routes->add('example4', new Route('/example4', [], $requirements, $options)); - $routes->add('example5', new Route('/example5', [], $requirements, $options)); - $routes->add('example6', new Route('/example6', [], $requirements, $options)); - $routes->add('example7', new Route('/example7', [], $requirements, $options)); - $routes->add('example8', new Route('/example8', [], $requirements, $options)); $routes->add('example9', new Route('/example9', [], $requirements, $options)); + $routes->add('example11', new Route('/example11', [], $requirements, $options)); + // Mock special routes defined in system.routing.yml. + $routes->add('<nolink>', new Route('', [], $requirements, $special_options)); + $routes->add('<button>', new Route('', [], $requirements, $special_options)); + + // Define our RouteProvider mock. $mock_route_provider = new MockRouteProvider($routes); $this->container->set('router.route_provider', $mock_route_provider); + // Define our UrlGenerator service that use the new RouteProvider. + $url_generator_non_bubbling = new UrlGenerator( + $mock_route_provider, + $this->container->get('path_processor_manager'), + $this->container->get('route_processor_manager'), + $this->container->get('request_stack'), + $this->container->getParameter('filter_protocols') + ); + $url_generator = new MetadataBubblingUrlGenerator($url_generator_non_bubbling, $this->container->get('renderer')); + $this->container->set('url_generator', $url_generator); + // Add a new custom menu. $menu_name = 'mock'; $label = $this->randomMachineName(16); @@ -127,27 +141,33 @@ protected function setUp(): void { $this->menu->save(); // This creates a tree with the following structure: - // - 1 + // - 1 (nolink) // - 2 - // - 3 + // - 3 (nolink) // - 4 // - 9 - // - 5 - // - 7 + // - 5 (button) + // - 7 (button) + // - 10 (nolink) // - 6 - // - 8 + // - 8 (nolink) + // - 11 + // - 12 (button) // With link 6 being the only external link. // phpcs:disable $links = [ - 1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => 'example1', 'title' => 'title 1', 'parent' => '', 'weight' => 0]), + 1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => '<nolink>', 'title' => 'title 1', 'parent' => '', 'weight' => 0]), 2 => MenuLinkMock::create(['id' => 'test.example2', 'route_name' => 'example2', 'title' => 'title 2', 'parent' => '', 'route_parameters' => ['foo' => 'bar'], 'weight' => 1]), - 3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => 'example3', 'title' => 'title 3', 'parent' => 'test.example2', 'weight' => 2]), + 3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => '<nolink>', 'title' => 'title 3', 'parent' => 'test.example2', 'weight' => 2]), 4 => MenuLinkMock::create(['id' => 'test.example4', 'route_name' => 'example4', 'title' => 'title 4', 'parent' => 'test.example3', 'weight' => 3]), - 5 => MenuLinkMock::create(['id' => 'test.example5', 'route_name' => 'example5', 'title' => 'title 5', 'parent' => '', 'expanded' => TRUE, 'weight' => 4]), - 6 => MenuLinkMock::create(['id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'title 6', 'parent' => '', 'weight' => 5]), - 7 => MenuLinkMock::create(['id' => 'test.example7', 'route_name' => 'example7', 'title' => 'title 7', 'parent' => 'test.example5', 'weight' => 6]), - 8 => MenuLinkMock::create(['id' => 'test.example8', 'route_name' => 'example8', 'title' => 'title 8', 'parent' => '', 'weight' => 7]), + 5 => MenuLinkMock::create(['id' => 'test.example5', 'route_name' => '<button>', 'title' => 'title 5', 'parent' => '', 'expanded' => TRUE, 'weight' => 4]), + 6 => MenuLinkMock::create(['id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'title 6', 'parent' => '', 'weight' => 5, 'options' => ['attributes' => ['target' => '_blank', 'class' => ['external-link']]]]), + 7 => MenuLinkMock::create(['id' => 'test.example7', 'route_name' => '<button>', 'title' => 'title 7', 'parent' => 'test.example5', 'weight' => 6]), + 8 => MenuLinkMock::create(['id' => 'test.example8', 'route_name' => '<nolink>', 'title' => 'title 8', 'parent' => '', 'weight' => 7]), 9 => MenuLinkMock::create(['id' => 'test.example9', 'route_name' => 'example9', 'title' => 'title 9', 'parent' => 'test.example4', 'weight' => 7]), + 10 => MenuLinkMock::create(['id' => 'test.example10', 'route_name' => '<nolink>', 'title' => 'title 10', 'parent' => 'test.example7', 'weight' => 7]), + 11 => MenuLinkMock::create(['id' => 'test.example11', 'route_name' => 'example11', 'title' => 'title 11', 'parent' => 'test.example8', 'weight' => 7]), + 12 => MenuLinkMock::create(['id' => 'test.example12', 'route_name' => '<button>', 'title' => 'title 12', 'parent' => 'test.example11', 'weight' => 7]), ]; // phpcs:enable foreach ($links as $instance) { @@ -218,9 +238,12 @@ public function testConfigLevelDepth(): void { $expectations['level_2_only'] = [ 'test.example3' => [], 'test.example7' => [], + 'test.example11' => [], ]; $expectations['level_3_only'] = [ 'test.example4' => [], + 'test.example10' => [], + 'test.example12' => [], ]; $expectations['level_1_and_beyond'] = [ 'test.example1' => [], @@ -230,10 +253,16 @@ public function testConfigLevelDepth(): void { ], ], 'test.example5' => [ - 'test.example7' => [], + 'test.example7' => [ + 'test.example10' => [], + ], ], 'test.example6' => [], - 'test.example8' => [], + 'test.example8' => [ + 'test.example11' => [ + 'test.example12' => [], + ], + ], ]; $expectations['level_2_and_beyond'] = [ 'test.example3' => [ @@ -241,12 +270,19 @@ public function testConfigLevelDepth(): void { 'test.example9' => [], ], ], - 'test.example7' => [], + 'test.example7' => [ + 'test.example10' => [], + ], + 'test.example11' => [ + 'test.example12' => [], + ], ]; $expectations['level_3_and_beyond'] = [ 'test.example4' => [ 'test.example9' => [], ], + 'test.example10' => [], + 'test.example12' => [], ]; // Scenario 1: test all navigation block instances when there's no active // trail. @@ -277,6 +313,46 @@ public function testConfigLevelDepth(): void { } } + /** + * Tests the generated HTML markup. + */ + public function testHtmlMarkup() { + $block = $this->blockManager->createInstance('navigation_menu:' . $this->menu->id(), [ + 'region' => 'content', + 'id' => 'machine_name', + 'level' => 1, + 'depth' => NavigationMenuBlock::NAVIGATION_MAX_DEPTH - 1, + ]); + + $block_build = $block->build(); + $render = \Drupal::service('renderer')->renderRoot($block_build); + + $dom = new \DOMDocument(); + $dom->loadHTML((string) $render); + $xpath = new \DOMXPath($dom); + + $items_query = [ + "//li[contains(@class,'toolbar-block__list-item')]/span/span[text()='title 1']", + "//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 2']", + "//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 3']", + "//li[contains(@class,'toolbar-menu__item--level-2')]/a[text()='title 4']", + "//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 5']", + "//li[contains(@class,'toolbar-block__list-item')]/a/span[text()='title 6']", + "//li[contains(@class,'toolbar-block__list-item')]/a[contains(@class, 'external-link')]", + "//li[contains(@class,'toolbar-block__list-item')]/a[contains(@class, 'external-link')]", + "//li[contains(@class,'toolbar-block__list-item')]/a[@target='_blank']", + "//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 7']", + "//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 8']", + "//li[contains(@class,'toolbar-menu__item--level-2')]/span[text()='title 10']", + "//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 11']", + "//li[contains(@class,'toolbar-menu__item--level-2')]/button[text()='title 12']", + ]; + foreach ($items_query as $query) { + $span = $xpath->query($query); + $this->assertEquals(1, $span->length, $query); + } + } + /** * Helper method to allow for easy menu link tree structure assertions. * diff --git a/core/modules/navigation/tests/src/Kernel/NavigationMenuMarkupTest.php b/core/modules/navigation/tests/src/Kernel/NavigationMenuMarkupTest.php index d1c3d2fd9b7c..23cb623f9f43 100644 --- a/core/modules/navigation/tests/src/Kernel/NavigationMenuMarkupTest.php +++ b/core/modules/navigation/tests/src/Kernel/NavigationMenuMarkupTest.php @@ -118,7 +118,7 @@ protected function setUp(): void { $links = [ 1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => 'example1', 'title' => 'title 1', 'parent' => '', 'weight' => 0]), 2 => MenuLinkMock::create(['id' => 'test.example2', 'route_name' => 'example2', 'title' => 'Another title', 'parent' => '', 'route_parameters' => ['foo' => 'bar'], 'weight' => 1]), - 3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => 'example3', 'title' => 'A menu link', 'parent' => 'test.example2', 'weight' => 2]), + 3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => 'example3', 'title' => 'Nested menu link', 'parent' => 'test.example2', 'weight' => 2]), ]; // phpcs:enable foreach ($links as $instance) { @@ -149,12 +149,12 @@ public function testToolbarButtonAttributes(): void { "//li[contains(@class,'toolbar-block__list-item')]/a[@data-icon-text='ti']", "//li[contains(@class,'toolbar-block__list-item')]/button[@data-index-text='a']", "//li[contains(@class,'toolbar-block__list-item')]/button[@data-icon-text='An']", - "//li[contains(@class,'toolbar-menu__item--level-1')]/a[@data-index-text='a']", - "//li[contains(@class,'toolbar-menu__item--level-1')]/a[not(@data-icon-text)]", + "//li[contains(@class,'toolbar-menu__item--level-1')]/a[@data-index-text='n']", + "//li[contains(@class,'toolbar-menu__item--level-1')]/a[@data-icon-text='Ne']", ]; foreach ($items_query as $query) { - $span = $xpath->query($query); - $this->assertEquals(1, $span->length, $query); + $element = $xpath->query($query); + $this->assertEquals(1, $element->length, $query); } } -- GitLab