diff options
Diffstat (limited to 'core/modules/navigation')
35 files changed, 779 insertions, 110 deletions
diff --git a/core/modules/navigation/components/badge/badge.component.yml b/core/modules/navigation/components/badge/badge.component.yml index a7bb04f963eb..a7940d1efa54 100644 --- a/core/modules/navigation/components/badge/badge.component.yml +++ b/core/modules/navigation/components/badge/badge.component.yml @@ -21,6 +21,9 @@ props: enum: - info - success + meta:enum: + info: Information + success: Success slots: label: type: string diff --git a/core/modules/navigation/components/title/title.component.yml b/core/modules/navigation/components/title/title.component.yml index 696960d455f5..1001c42c109f 100644 --- a/core/modules/navigation/components/title/title.component.yml +++ b/core/modules/navigation/components/title/title.component.yml @@ -23,6 +23,9 @@ props: enum: - ellipsis - xs + meta:enum: + ellipsis: Ellipsis + xs: 'Extra-small' extra_classes: type: array title: Extra classes. @@ -43,6 +46,15 @@ props: - h5 - h6 - span + meta:enum: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + span: Inline + x-translation-context: HTML tag # Provide a default value default: h2 icon: diff --git a/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml b/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml index 87e29a14bf44..130cca22e1cb 100644 --- a/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml +++ b/core/modules/navigation/components/toolbar-button/toolbar-button.component.yml @@ -37,6 +37,17 @@ props: - primary - small-offset - weight--400 + meta:enum: + collapsible: Collapsible + dark: Dark + expand--down: Expands down + expand--side: Expands to the side + large: Large + non-interactive: Non-Interactive + primary: Primary + small-offset: Small offset + weight--400: Weight 400 + x-translation-context: Toolbar button modifiers extra_classes: type: array title: Extra classes. @@ -53,6 +64,11 @@ props: - a - button - span + meta:enum: + a: Link + button: Button + span: Inline + x-translation-context: HTML tag # Provide a default value default: button icon: diff --git a/core/modules/navigation/components/toolbar-button/toolbar-button.css b/core/modules/navigation/components/toolbar-button/toolbar-button.css index 3af3cbfdd39d..85bfb00883cf 100644 --- a/core/modules/navigation/components/toolbar-button/toolbar-button.css +++ b/core/modules/navigation/components/toolbar-button/toolbar-button.css @@ -36,7 +36,7 @@ text-align: start; -webkit-text-decoration: none; text-decoration: none; - word-break: break-word; + overflow-wrap: break-word; color: var(--toolbar-button-color); border: 0; border-radius: var(--admin-toolbar-space-8); diff --git a/core/modules/navigation/components/toolbar-button/toolbar-button.pcss.css b/core/modules/navigation/components/toolbar-button/toolbar-button.pcss.css index 9b40b8fcb460..0e1d5f3bdd57 100644 --- a/core/modules/navigation/components/toolbar-button/toolbar-button.pcss.css +++ b/core/modules/navigation/components/toolbar-button/toolbar-button.pcss.css @@ -33,7 +33,7 @@ cursor: pointer; text-align: start; text-decoration: none; - word-break: break-word; + overflow-wrap: break-word; color: var(--toolbar-button-color); border: 0; border-radius: var(--admin-toolbar-space-8); diff --git a/core/modules/navigation/css/base/variables.css b/core/modules/navigation/css/base/variables.css index 91063f3f09d9..c56c7322b6f4 100644 --- a/core/modules/navigation/css/base/variables.css +++ b/core/modules/navigation/css/base/variables.css @@ -74,14 +74,14 @@ We need it root to calculate the size of the displace in .dialog-off-canvas-main /* White. */ --admin-toolbar-color-white: var(--drupal-admin-color-white, #fff); /* Expanded background color. */ - --admin-toolbar-color-expanded: rgba(231, 234, 243, 0.5); /* --admin-toolbar-color-gray-050 */ + --admin-toolbar-color-expanded: rgb(231, 234, 243, 0.5); /* --admin-toolbar-color-gray-050 */ /* Fonts. */ --admin-toolbar-font-family: inter, sans-serif; /* Shadows. */ - --admin-toolbar-color-shadow-0: rgba(0, 0, 0, 0); - --admin-toolbar-color-shadow-6: rgba(0, 0, 0, 0.06); - --admin-toolbar-color-shadow-8: rgba(0, 0, 0, 0.08); - --admin-toolbar-color-shadow-15: rgba(0, 0, 0, 0.15); + --admin-toolbar-color-shadow-0: rgb(0, 0, 0, 0); + --admin-toolbar-color-shadow-6: rgb(0, 0, 0, 0.06); + --admin-toolbar-color-shadow-8: rgb(0, 0, 0, 0.08); + --admin-toolbar-color-shadow-15: rgb(0, 0, 0, 0.15); /** * Spaces. diff --git a/core/modules/navigation/css/base/variables.pcss.css b/core/modules/navigation/css/base/variables.pcss.css index ae9655a7ad0e..844e0ba048a7 100644 --- a/core/modules/navigation/css/base/variables.pcss.css +++ b/core/modules/navigation/css/base/variables.pcss.css @@ -71,14 +71,14 @@ We need it root to calculate the size of the displace in .dialog-off-canvas-main /* White. */ --admin-toolbar-color-white: var(--drupal-admin-color-white, #fff); /* Expanded background color. */ - --admin-toolbar-color-expanded: rgba(231, 234, 243, 0.5); /* --admin-toolbar-color-gray-050 */ + --admin-toolbar-color-expanded: rgb(231, 234, 243, 0.5); /* --admin-toolbar-color-gray-050 */ /* Fonts. */ --admin-toolbar-font-family: inter, sans-serif; /* Shadows. */ - --admin-toolbar-color-shadow-0: rgba(0, 0, 0, 0); - --admin-toolbar-color-shadow-6: rgba(0, 0, 0, 0.06); - --admin-toolbar-color-shadow-8: rgba(0, 0, 0, 0.08); - --admin-toolbar-color-shadow-15: rgba(0, 0, 0, 0.15); + --admin-toolbar-color-shadow-0: rgb(0, 0, 0, 0); + --admin-toolbar-color-shadow-6: rgb(0, 0, 0, 0.06); + --admin-toolbar-color-shadow-8: rgb(0, 0, 0, 0.08); + --admin-toolbar-color-shadow-15: rgb(0, 0, 0, 0.15); /** * Spaces. diff --git a/core/modules/navigation/css/components/admin-toolbar.css b/core/modules/navigation/css/components/admin-toolbar.css index 25fd9ca1cc02..8e8636d8a855 100644 --- a/core/modules/navigation/css/components/admin-toolbar.css +++ b/core/modules/navigation/css/components/admin-toolbar.css @@ -94,7 +94,7 @@ body { .admin-toolbar { block-size: calc(100vh - var(--drupal-displace-offset-top, 0px)); transform: none; - inset-block-start: var(--drupal-displace-offset-top, 0); + inset-block-start: 0; } } @media only screen and (max-height: 18.75rem) { @@ -333,7 +333,7 @@ body { display: none; width: 100vw; height: 100vh; - background-color: rgba(0, 0, 0, 0.14); + background-color: rgb(0, 0, 0, 0.14); } :where([data-admin-toolbar="expanded"]) .admin-toolbar-overlay { display: block; diff --git a/core/modules/navigation/css/components/admin-toolbar.pcss.css b/core/modules/navigation/css/components/admin-toolbar.pcss.css index 0b13b08252e1..55ffd803e693 100644 --- a/core/modules/navigation/css/components/admin-toolbar.pcss.css +++ b/core/modules/navigation/css/components/admin-toolbar.pcss.css @@ -95,7 +95,7 @@ body { @media (--admin-toolbar-desktop) { block-size: calc(100vh - var(--drupal-displace-offset-top, 0px)); transform: none; - inset-block-start: var(--drupal-displace-offset-top, 0); + inset-block-start: 0; } @media only screen and (max-height: 300px) { @@ -354,7 +354,7 @@ body { display: none; width: 100vw; height: 100vh; - background-color: rgba(0, 0, 0, 0.14); + background-color: rgb(0, 0, 0, 0.14); :where([data-admin-toolbar="expanded"]) & { display: block; diff --git a/core/modules/navigation/css/components/toolbar-dropdown.css b/core/modules/navigation/css/components/toolbar-dropdown.css index f21bf044137a..eca3b63b27e4 100644 --- a/core/modules/navigation/css/components/toolbar-dropdown.css +++ b/core/modules/navigation/css/components/toolbar-dropdown.css @@ -50,7 +50,7 @@ padding: var(--admin-toolbar-space-8) var(--admin-toolbar-space-16); border-radius: var(--admin-toolbar-space-12); background: white; - box-shadow: 0 14px 30px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 14px 30px 0 rgb(0, 0, 0, 0.1); } [data-drupal-dropdown][aria-expanded="true"] + .toolbar-dropdown__menu { display: block; diff --git a/core/modules/navigation/css/components/toolbar-dropdown.pcss.css b/core/modules/navigation/css/components/toolbar-dropdown.pcss.css index 6c462a08ba86..91cd88d0a5fc 100644 --- a/core/modules/navigation/css/components/toolbar-dropdown.pcss.css +++ b/core/modules/navigation/css/components/toolbar-dropdown.pcss.css @@ -49,7 +49,7 @@ padding: var(--admin-toolbar-space-8) var(--admin-toolbar-space-16); border-radius: var(--admin-toolbar-space-12); background: white; - box-shadow: 0 14px 30px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 14px 30px 0 rgb(0, 0, 0, 0.1); } [data-drupal-dropdown][aria-expanded="true"] + .toolbar-dropdown__menu { diff --git a/core/modules/navigation/css/components/toolbar-menu.css b/core/modules/navigation/css/components/toolbar-menu.css index 33e2af3bcb9a..9b411c7cba75 100644 --- a/core/modules/navigation/css/components/toolbar-menu.css +++ b/core/modules/navigation/css/components/toolbar-menu.css @@ -51,7 +51,7 @@ -webkit-text-decoration: none; text-decoration: none; letter-spacing: var(--admin-toolbar-letter-spacing-0-06); - word-break: break-word; + overflow-wrap: break-word; color: var(--admin-toolbar-color-gray-800); border: none; background-color: transparent; @@ -133,5 +133,5 @@ rotate: -90deg; } [dir="rtl"] :is([aria-expanded="true"] .toolbar-menu__chevron) { - rotate: 0; + rotate: none; } diff --git a/core/modules/navigation/css/components/toolbar-menu.pcss.css b/core/modules/navigation/css/components/toolbar-menu.pcss.css index 7f2c98950cc1..60fc599d7122 100644 --- a/core/modules/navigation/css/components/toolbar-menu.pcss.css +++ b/core/modules/navigation/css/components/toolbar-menu.pcss.css @@ -55,7 +55,7 @@ text-align: start; text-decoration: none; letter-spacing: var(--admin-toolbar-letter-spacing-0-06); - word-break: break-word; + overflow-wrap: break-word; color: var(--admin-toolbar-color-gray-800); border: none; background-color: transparent; @@ -150,7 +150,7 @@ rotate: -90deg; [dir="rtl"] & { - rotate: 0; + rotate: none; } } } diff --git a/core/modules/navigation/css/components/toolbar-message.css b/core/modules/navigation/css/components/toolbar-message.css index 11b50e9459be..8a852a0fc703 100644 --- a/core/modules/navigation/css/components/toolbar-message.css +++ b/core/modules/navigation/css/components/toolbar-message.css @@ -15,7 +15,7 @@ text-align: start; -webkit-text-decoration: none; text-decoration: none; - word-break: break-word; + overflow-wrap: break-word; color: var(--admin-toolbar-color-gray-800); border: 0; border-radius: var(--admin-toolbar-space-8); diff --git a/core/modules/navigation/css/components/toolbar-message.pcss.css b/core/modules/navigation/css/components/toolbar-message.pcss.css index 9689014aab43..9e0f401fb4a5 100644 --- a/core/modules/navigation/css/components/toolbar-message.pcss.css +++ b/core/modules/navigation/css/components/toolbar-message.pcss.css @@ -7,7 +7,7 @@ cursor: pointer; text-align: start; text-decoration: none; - word-break: break-word; + overflow-wrap: break-word; color: var(--admin-toolbar-color-gray-800); border: 0; border-radius: var(--admin-toolbar-space-8); diff --git a/core/modules/navigation/css/components/toolbar-popover.css b/core/modules/navigation/css/components/toolbar-popover.css index 76f587a325e6..532e873d96e6 100644 --- a/core/modules/navigation/css/components/toolbar-popover.css +++ b/core/modules/navigation/css/components/toolbar-popover.css @@ -48,9 +48,9 @@ padding-block-start: var(--admin-toolbar-space-16); transform: translateX(0); box-shadow: - 0 0 72px rgba(0, 0, 0, 0.2), - 0 0 8px rgba(0, 0, 0, 0.04), - 0 0 40px rgba(0, 0, 0, 0.06); + 0 0 72px rgb(0, 0, 0, 0.2), + 0 0 8px rgb(0, 0, 0, 0.04), + 0 0 40px rgb(0, 0, 0, 0.06); inline-size: var(--admin-toolbar-popover-width); inset-block-start: var(--drupal-displace-offset-top, 0); inset-inline-start: 1px; diff --git a/core/modules/navigation/css/components/toolbar-popover.pcss.css b/core/modules/navigation/css/components/toolbar-popover.pcss.css index 6a8b2a019c24..a12ebe955edb 100644 --- a/core/modules/navigation/css/components/toolbar-popover.pcss.css +++ b/core/modules/navigation/css/components/toolbar-popover.pcss.css @@ -47,9 +47,9 @@ padding-block-start: var(--admin-toolbar-space-16); transform: translateX(0); box-shadow: - 0 0 72px rgba(0, 0, 0, 0.2), - 0 0 8px rgba(0, 0, 0, 0.04), - 0 0 40px rgba(0, 0, 0, 0.06); + 0 0 72px rgb(0, 0, 0, 0.2), + 0 0 8px rgb(0, 0, 0, 0.04), + 0 0 40px rgb(0, 0, 0, 0.06); inline-size: var(--admin-toolbar-popover-width); inset-block-start: var(--drupal-displace-offset-top, 0); inset-inline-start: 1px; diff --git a/core/modules/navigation/css/components/top-bar.css b/core/modules/navigation/css/components/top-bar.css index 65a489cd78b7..134b0a506c75 100644 --- a/core/modules/navigation/css/components/top-bar.css +++ b/core/modules/navigation/css/components/top-bar.css @@ -22,7 +22,7 @@ .top-bar { block-size: var(--admin-toolbar-top-bar-height); position: fixed; - inset-block-start: var(--drupal-displace-offset-top, 0); + inset-block-start: 0; inset-inline-start: 0; width: 100vw; padding-block: var(--admin-toolbar-space-12); diff --git a/core/modules/navigation/css/components/top-bar.pcss.css b/core/modules/navigation/css/components/top-bar.pcss.css index b55604dfa569..0a34a4b17dae 100644 --- a/core/modules/navigation/css/components/top-bar.pcss.css +++ b/core/modules/navigation/css/components/top-bar.pcss.css @@ -18,7 +18,7 @@ @media (--admin-toolbar-desktop) { block-size: var(--admin-toolbar-top-bar-height); position: fixed; - inset-block-start: var(--drupal-displace-offset-top, 0); + inset-block-start: 0; inset-inline-start: 0; width: 100vw; padding-block: var(--admin-toolbar-space-12); diff --git a/core/modules/navigation/js/admin-toolbar-wrapper.js b/core/modules/navigation/js/admin-toolbar-wrapper.js index c9e2ecb9cae9..6c32da0b6f6c 100644 --- a/core/modules/navigation/js/admin-toolbar-wrapper.js +++ b/core/modules/navigation/js/admin-toolbar-wrapper.js @@ -81,31 +81,35 @@ Drupal.displace(true); }); - - /** - * Initialize Drupal.displace() - * - * We add the displace attribute to a separate full width element because we - * don't want this element to have transitions. Note that this element and the - * navbar share the same exact width. - */ - const initDisplace = () => { - const displaceElement = doc - .querySelector('.admin-toolbar') - ?.querySelector('.admin-toolbar__displace-placeholder'); - const edge = document.documentElement.dir === 'rtl' ? 'right' : 'left'; - displaceElement?.setAttribute(`data-offset-${edge}`, ''); - Drupal.displace(true); - }; - - initDisplace(); } + /** + * Initialize Drupal.displace() + * + * We add the displace attribute to a separate full width element because we + * don't want this element to have transitions. Note that this element and the + * navbar share the same exact width. + * + * @param {HTMLElement} el - The admin toolbar wrapper. + */ + const initDisplace = (el) => { + const displaceElement = el.querySelector( + '.admin-toolbar__displace-placeholder', + ); + const edge = document.documentElement.dir === 'rtl' ? 'right' : 'left'; + displaceElement?.setAttribute(`data-offset-${edge}`, ''); + Drupal.displace(true); + }; + // Any triggers on page. Inside or outside sidebar. // For now button in sidebar + mobile header and background. Drupal.behaviors.navigationProcessToolbarTriggers = { attach: (context) => { + once('navigation-displace', '.admin-toolbar', context).forEach( + initDisplace, + ); + const triggers = once( 'admin-toolbar-trigger', '[aria-controls="admin-toolbar"]', diff --git a/core/modules/navigation/navigation.install b/core/modules/navigation/navigation.install index e4280b472ac2..77390203123a 100644 --- a/core/modules/navigation/navigation.install +++ b/core/modules/navigation/navigation.install @@ -22,25 +22,6 @@ function navigation_install(bool $is_syncing): void { } /** - * Implements hook_requirements(). - */ -function navigation_requirements($phase): array { - $requirements = []; - - if ($phase === 'runtime') { - if (\Drupal::moduleHandler()->moduleExists('toolbar')) { - $requirements['toolbar'] = [ - 'title' => t('Toolbar and Navigation modules are both installed'), - 'value' => t('The Navigation module is a complete replacement for the Toolbar module and disables its functionality when both modules are installed. If you are planning to continue using Navigation module, you can uninstall the Toolbar module now.'), - 'severity' => REQUIREMENT_WARNING, - ]; - } - } - - return $requirements; -} - -/** * Reorganizes the values for the logo settings. */ function navigation_update_11001(array &$sandbox): void { diff --git a/core/modules/navigation/navigation.services.yml b/core/modules/navigation/navigation.services.yml index 88b6826409a2..925efe58a4e1 100644 --- a/core/modules/navigation/navigation.services.yml +++ b/core/modules/navigation/navigation.services.yml @@ -45,3 +45,6 @@ services: class: Drupal\navigation\TopBarItemManager parent: default_plugin_manager Drupal\navigation\TopBarItemManagerInterface: '@plugin.manager.top_bar_item' + + Drupal\navigation\Menu\NavigationMenuLinkTreeManipulators: + autowire: true diff --git a/core/modules/navigation/src/Hook/NavigationHooks.php b/core/modules/navigation/src/Hook/NavigationHooks.php index ed4eb8283668..540a2659af53 100644 --- a/core/modules/navigation/src/Hook/NavigationHooks.php +++ b/core/modules/navigation/src/Hook/NavigationHooks.php @@ -62,7 +62,7 @@ class NavigationHooks { $output = ''; $output .= '<h3>' . $this->t('About') . '</h3>'; $output .= '<p>' . $this->t('The Navigation module provides a left-aligned, collapsible, vertical sidebar navigation.') . '</p>'; - $output .= '<p>' . $this->t('For more information, see the <a href=":docs">online documentation for the Navigation module</a>.', [':docs' => 'https://www.drupal.org/project/navigation']) . '</p>'; + $output .= '<p>' . $this->t('For more information, see the <a href=":docs">online documentation for the Navigation module</a>.', [':docs' => 'https://www.drupal.org/docs/develop/core-modules-and-themes/core-modules/navigation-module']) . '</p>'; return $output; } $configuration_route = 'layout_builder.navigation.'; diff --git a/core/modules/navigation/src/Hook/NavigationRequirements.php b/core/modules/navigation/src/Hook/NavigationRequirements.php new file mode 100644 index 000000000000..f72877c04e43 --- /dev/null +++ b/core/modules/navigation/src/Hook/NavigationRequirements.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\navigation\Hook; + +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\Requirement\RequirementSeverity; +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\StringTranslation\StringTranslationTrait; + +/** + * Requirements for the navigation module. + */ +class NavigationRequirements { + + use StringTranslationTrait; + + public function __construct( + protected readonly ModuleHandlerInterface $moduleHandler, + ) {} + + /** + * Implements hook_runtime_requirements(). + */ + #[Hook('runtime_requirements')] + public function runtime(): array { + $requirements = []; + if ($this->moduleHandler->moduleExists('toolbar')) { + $requirements['toolbar'] = [ + 'title' => $this->t('Toolbar and Navigation modules are both installed'), + 'value' => $this->t('The Navigation module is a complete replacement for the Toolbar module and disables its functionality when both modules are installed. If you are planning to continue using Navigation module, you can uninstall the Toolbar module now.'), + 'severity' => RequirementSeverity::Warning, + ]; + } + return $requirements; + } + +} diff --git a/core/modules/navigation/src/Menu/NavigationMenuLinkTreeManipulators.php b/core/modules/navigation/src/Menu/NavigationMenuLinkTreeManipulators.php new file mode 100644 index 000000000000..75fefdf15417 --- /dev/null +++ b/core/modules/navigation/src/Menu/NavigationMenuLinkTreeManipulators.php @@ -0,0 +1,159 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\navigation\Menu; + +use Drupal\Core\Menu\MenuLinkDefault; +use Drupal\Core\Menu\MenuLinkTreeElement; +use Drupal\Core\Menu\StaticMenuLinkOverridesInterface; +use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\system\Controller\SystemController; +use Symfony\Component\Routing\Exception\RouteNotFoundException; + +/** + * Provides a menu link tree manipulator for the navigation menu block. + */ +class NavigationMenuLinkTreeManipulators { + + use StringTranslationTrait; + + public function __construct( + protected readonly RouteProviderInterface $routeProvider, + protected readonly StaticMenuLinkOverridesInterface $overrides, + TranslationInterface $translation, + ) { + $this->setStringTranslation($translation); + } + + /** + * Adds an "overview" child link to second level menu links with children. + * + * In the navigation menu, a second-level menu item is a link if it does not + * have children, but if it does, it instead becomes a button that opens + * its child menu. To provide a way to access the page a second-level menu + * item links to, add an "overview" link that links to the page as a child + * (third-level) menu item. + * + * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree + * The menu link tree to manipulate. + * + * @return \Drupal\Core\Menu\MenuLinkTreeElement[] + * The manipulated menu link tree. + */ + public function addSecondLevelOverviewLinks(array $tree): array { + if (!$tree) { + return []; + } + + foreach ($tree as $item) { + if (!$this->isEnabledAndAccessible($item)) { + continue; + } + foreach ($item->subtree as $sub_item) { + if ($this->shouldAddOverviewLink($sub_item)) { + $this->addOverviewLink($sub_item); + } + } + } + + return $tree; + } + + /** + * Whether a menu tree element should have an overview link added to it. + * + * @param \Drupal\Core\Menu\MenuLinkTreeElement $element + * The menu link tree element to check. + * + * @return bool + * TRUE if menu tree element should have a child overview link added. + */ + protected function shouldAddOverviewLink(MenuLinkTreeElement $element): bool { + if (empty($element->subtree) || !$this->isEnabledAndAccessible($element)) { + return FALSE; + } + + $route_name = $element->link->getRouteName(); + if (in_array($route_name, ['<nolink>', '<button>'])) { + return FALSE; + } + + $has_visible_children = FALSE; + foreach ($element->subtree as $sub_element) { + // Do not add overview link if there are no accessible or enabled + // children. + if ($this->isEnabledAndAccessible($sub_element)) { + $has_visible_children = TRUE; + } + + // Do not add overview link if there is already a child linking to the + // same URL. + if ($sub_element->link->getRouteName() === $route_name) { + return FALSE; + } + } + + if (!$has_visible_children) { + return FALSE; + } + + // The systemAdminMenuBlockPage() method in SystemController returns a list + // of child menu links for the page. If the second-level menu item link's + // route uses that controller, do not add the overview link, because that + // duplicates what is already in the navigation menu. + try { + $controller = ltrim($this->routeProvider->getRouteByName($route_name)->getDefault('_controller') ?? '', "\\"); + return $controller !== SystemController::class . '::systemAdminMenuBlockPage'; + } + catch (RouteNotFoundException) { + return TRUE; + } + } + + /** + * Checks whether the menu link tree element is accessible and enabled. + * + * Generally, the 'checkAccess' manipulator should run before this manipulator + * does, so the access objects should be set on all the links, but if it is + * not, treat the link as accessible for the purpose of adding the overview + * child link. + * + * @param \Drupal\Core\Menu\MenuLinkTreeElement $element + * The menu link tree element to be checked. + * + * @return bool + * TRUE if the menu link tree element is enabled and has access allowed. + */ + protected function isEnabledAndAccessible(MenuLinkTreeElement $element): bool { + return $element->link->isEnabled() && (!isset($element->access) || $element->access->isAllowed()); + } + + /** + * Adds "overview" menu tree element as child of a menu tree element. + * + * @param \Drupal\Core\Menu\MenuLinkTreeElement $element + * The menu link tree element to add the overview child element to. + */ + protected function addOverviewLink(MenuLinkTreeElement $element): void { + // Copy the menu link for the menu link element to a new menu link + // definition, except with overrides to make 'Overview' the title, set the + // parent to the original menu link, and set weight to a low number so that + // it likely goes to the top. + $definition = [ + 'title' => $this->t('Overview', [ + '@title' => $element->link->getTitle(), + ]), + 'parent' => $element->link->getPluginId(), + 'provider' => 'navigation', + 'weight' => -1000, + ] + $element->link->getPluginDefinition(); + $link = new MenuLinkDefault([], $element->link->getPluginId() . '.navigation_overview', $definition, $this->overrides); + $overview_element = new MenuLinkTreeElement($link, FALSE, $element->depth + 1, $element->inActiveTrail, []); + $overview_element->access = $element->access ? clone $element->access : NULL; + $element->subtree[$element->link->getPluginId() . '.navigation_overview'] = $overview_element; + } + +} diff --git a/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php b/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php index 380cc8540aa3..8b75563b92f1 100644 --- a/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php +++ b/core/modules/navigation/src/Plugin/Block/NavigationMenuBlock.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\navigation\Menu\NavigationMenuLinkTreeManipulators; use Drupal\navigation\Plugin\Derivative\SystemMenuNavigationBlock as SystemMenuNavigationBlockDeriver; use Drupal\system\Plugin\Block\SystemMenuBlock; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -85,6 +86,7 @@ final class NavigationMenuBlock extends SystemMenuBlock implements ContainerFact $tree = $this->menuTree->load($menu_name, $parameters); $manipulators = [ ['callable' => 'menu.default_tree_manipulators:checkAccess'], + ['callable' => NavigationMenuLinkTreeManipulators::class . ':addSecondLevelOverviewLinks'], ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], ]; $tree = $this->menuTree->transform($tree, $manipulators); diff --git a/core/modules/navigation/src/WorkspacesLazyBuilder.php b/core/modules/navigation/src/WorkspacesLazyBuilder.php index 3134fa4745ed..146f7d83dc9b 100644 --- a/core/modules/navigation/src/WorkspacesLazyBuilder.php +++ b/core/modules/navigation/src/WorkspacesLazyBuilder.php @@ -59,6 +59,9 @@ final class WorkspacesLazyBuilder { 'title' => $active_workspace ? $active_workspace->label() : $this->t('Live'), 'url' => $url, 'class' => 'workspaces', + 'icon' => [ + 'icon_id' => 'workspaces', + ], ], ], '#attached' => [ diff --git a/core/modules/navigation/templates/top-bar.html.twig b/core/modules/navigation/templates/top-bar.html.twig index 6efdeed523f9..319f97f67478 100644 --- a/core/modules/navigation/templates/top-bar.html.twig +++ b/core/modules/navigation/templates/top-bar.html.twig @@ -13,17 +13,19 @@ */ #} {% set attributes = create_attribute() %} -<aside {{ attributes.addClass('top-bar').setAttribute('data-drupal-admin-styles', '').setAttribute('aria-labelledby', 'top-bar__title') }}> - <h3 id="top-bar__title" class="visually-hidden">{{ 'Administrative top bar'|t }}</h3> - <div class="top-bar__content"> - <div class="top-bar__tools"> - {{- tools -}} +{% if tools or context|render or actions|render %} + <aside {{ attributes.addClass('top-bar').setAttribute('data-drupal-admin-styles', '').setAttribute('aria-labelledby', 'top-bar__title').setAttribute('data-offset-top', true) }}> + <h3 id="top-bar__title" class="visually-hidden">{{ 'Administrative top bar'|t }}</h3> + <div class="top-bar__content"> + <div class="top-bar__tools"> + {{- tools -}} + </div> + <div class="top-bar__context"> + {{- context -}} + </div> + <div class="top-bar__actions"> + {{- actions -}} + </div> </div> - <div class="top-bar__context"> - {{- context -}} - </div> - <div class="top-bar__actions"> - {{- actions -}} - </div> - </div> -</aside> + </aside> +{% endif %} diff --git a/core/modules/navigation/tests/navigation_test/navigation_test.module b/core/modules/navigation/tests/navigation_test/navigation_test.module deleted file mode 100644 index 3c7eb2fade87..000000000000 --- a/core/modules/navigation/tests/navigation_test/navigation_test.module +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -/** - * @file - * Contains main module functions. - */ - -declare(strict_types=1); - -use Drupal\Component\Utility\Html; - -/** - * Implements hook_preprocess_HOOK(). - */ -function navigation_test_preprocess_block__navigation(&$variables): void { - // Add some additional classes so we can target the correct contextual link - // in tests. - $variables['attributes']['class'][] = Html::cleanCssIdentifier('block-' . $variables['elements']['#plugin_id']); -} diff --git a/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php new file mode 100644 index 000000000000..9020deed81d9 --- /dev/null +++ b/core/modules/navigation/tests/navigation_test/src/Hook/NavigationTestThemeHooks.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\navigation_test\Hook; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Theme hook implementations for navigation_test module. + */ +class NavigationTestThemeHooks { + + /** + * Implements hook_preprocess_HOOK(). + */ + #[Hook('preprocess_block__navigation')] + public function preprocessBlockNavigation(&$variables): void { + // Add some additional classes so we can target the correct contextual link + // in tests. + $variables['attributes']['class'][] = Html::cleanCssIdentifier('block-' . $variables['elements']['#plugin_id']); + } + +} diff --git a/core/modules/navigation/tests/src/Functional/NavigationWorkspacesUiTest.php b/core/modules/navigation/tests/src/Functional/NavigationWorkspacesUiTest.php new file mode 100644 index 000000000000..48de404b65ae --- /dev/null +++ b/core/modules/navigation/tests/src/Functional/NavigationWorkspacesUiTest.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\navigation\Functional; + +use Drupal\Tests\BrowserTestBase; + +/** + * Tests for \Drupal\navigation\WorkspacesLazyBuilder. + * + * @group navigation + */ +class NavigationWorkspacesUiTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['navigation', 'workspaces_ui']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $admin_user = $this->drupalCreateUser([ + 'access navigation', + 'administer workspaces', + ]); + $this->drupalLogin($admin_user); + } + + /** + * Tests the Workspaces button in the navigation bar. + */ + public function testWorkspacesNavigationButton(): void { + $this->drupalGet('<front>'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--workspaces svg', 'width', '20'); + $this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--workspaces svg', 'class', 'toolbar-button__icon'); + } + +} diff --git a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php index 3264d769c195..5bf9d2477f09 100644 --- a/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php +++ b/core/modules/navigation/tests/src/FunctionalJavascript/PerformanceTest.php @@ -73,14 +73,14 @@ class PerformanceTest extends PerformanceTestBase { $expected = [ 'QueryCount' => 4, - 'CacheGetCount' => 49, + 'CacheGetCount' => 47, 'CacheGetCountByBin' => [ 'config' => 11, 'data' => 4, 'discovery' => 10, 'bootstrap' => 6, 'dynamic_page_cache' => 1, - 'render' => 16, + 'render' => 14, 'menu' => 1, ], 'CacheSetCount' => 2, @@ -89,9 +89,9 @@ class PerformanceTest extends PerformanceTestBase { ], 'CacheDeleteCount' => 0, 'CacheTagInvalidationCount' => 0, - 'CacheTagLookupQueryCount' => 14, + 'CacheTagLookupQueryCount' => 13, 'ScriptCount' => 3, - 'ScriptBytes' => 215500, + 'ScriptBytes' => 167569, 'StylesheetCount' => 2, 'StylesheetBytes' => 46000, ]; diff --git a/core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php b/core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php index 496b7d1f7af5..d9cf1f70a090 100644 --- a/core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php +++ b/core/modules/navigation/tests/src/Kernel/NavigationMenuBlockTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Routing\RouteObjectInterface; use Drupal\Core\Routing\UrlGenerator; use Drupal\KernelTests\KernelTestBase; use Drupal\navigation\Plugin\Block\NavigationMenuBlock; +use Drupal\system\Controller\SystemController; use Drupal\system\Entity\Menu; use Drupal\system\Tests\Routing\MockRouteProvider; use Drupal\Tests\Core\Menu\MenuLinkMock; @@ -106,9 +107,16 @@ class NavigationMenuBlockTest extends KernelTestBase { $options = ['_access_checks' => ['access_check.default']]; $special_options = $options + ['_no_path' => TRUE]; $routes->add('example2', new Route('/example2', [], $requirements, $options)); - $routes->add('example4', new Route('/example4', [], $requirements, $options)); + $routes->add('example4', new Route('/example4', ['_controller' => SystemController::class . '::systemAdminMenuBlockPage'], $requirements, $options)); $routes->add('example9', new Route('/example9', [], $requirements, $options)); - $routes->add('example11', new Route('/example11', [], $requirements, $options)); + $routes->add('example11', new Route('/example11', ['_controller' => SystemController::class . '::systemAdminMenuBlockPage'], $requirements, $options)); + $routes->add('example13', new Route('/example13', [], $requirements, $options)); + $routes->add('example14', new Route('/example14', [], $requirements, $options)); + $routes->add('example15', new Route('/example15', [], $requirements, $options)); + $routes->add('example16', new Route('/example16', [], $requirements, $options)); + $routes->add('example17', new Route('/example17', [], $requirements, $options)); + $routes->add('example18', new Route('/example18', [], $requirements, $options)); + $routes->add('example19', new Route('/example19', [], ['_access' => 'FALSE'], $options)); // Mock special routes defined in system.routing.yml. $routes->add('<nolink>', new Route('', [], $requirements, $special_options)); @@ -144,15 +152,23 @@ class NavigationMenuBlockTest extends KernelTestBase { // - 1 (nolink) // - 2 // - 3 (nolink) - // - 4 + // - 4 (list of child links) // - 9 // - 5 (button) // - 7 (button) // - 10 (nolink) // - 6 // - 8 (nolink) - // - 11 + // - 11 (list of child links) // - 12 (button) + // - 13 + // - 14 (not a list of child links) + // - 15 + // - 16 + // - 17 + // - 18 (disabled) + // - 19 (access denied) + // - 20 (links to same routed URL as 17) // With link 6 being the only external link. // phpcs:disable $links = [ @@ -168,6 +184,14 @@ class NavigationMenuBlockTest extends KernelTestBase { 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]), + 13 => MenuLinkMock::create(['id' => 'test.example13', 'route_name' => 'example13', 'title' => 'title 13', 'parent' => '', 'weight' => 8]), + 14 => MenuLinkMock::create(['id' => 'test.example14', 'route_name' => 'example14', 'title' => 'title 14', 'parent' => 'test.example13', 'weight' => 8]), + 15 => MenuLinkMock::create(['id' => 'test.example15', 'route_name' => 'example15', 'title' => 'title 15', 'parent' => 'test.example14', 'weight' => 8]), + 16 => MenuLinkMock::create(['id' => 'test.example16', 'route_name' => 'example16', 'title' => 'title 16', 'parent' => '', 'weight' => 9]), + 17 => MenuLinkMock::create(['id' => 'test.example17', 'route_name' => 'example17', 'title' => 'title 17', 'parent' => 'test.example16', 'weight' => 9]), + 18 => MenuLinkMock::create(['id' => 'test.example18', 'route_name' => 'example18', 'title' => 'title 18', 'parent' => 'test.example17', 'weight' => 9, 'enabled' => FALSE]), + 19 => MenuLinkMock::create(['id' => 'test.example19', 'route_name' => 'example19', 'title' => 'title 19', 'parent' => 'test.example17', 'weight' => 9]), + 20 => MenuLinkMock::create(['id' => 'test.example20', 'route_name' => 'example17', 'title' => 'title 20', 'parent' => 'test.example17', 'weight' => 9]), ]; // phpcs:enable foreach ($links as $instance) { @@ -234,16 +258,22 @@ class NavigationMenuBlockTest extends KernelTestBase { 'test.example5' => [], 'test.example6' => [], 'test.example8' => [], + 'test.example13' => [], + 'test.example16' => [], ]; $expectations['level_2_only'] = [ 'test.example3' => [], 'test.example7' => [], 'test.example11' => [], + 'test.example14' => [], + 'test.example17' => [], ]; $expectations['level_3_only'] = [ 'test.example4' => [], 'test.example10' => [], 'test.example12' => [], + 'test.example15' => [], + 'test.example20' => [], ]; $expectations['level_1_and_beyond'] = [ 'test.example1' => [], @@ -263,6 +293,20 @@ class NavigationMenuBlockTest extends KernelTestBase { 'test.example12' => [], ], ], + 'test.example13' => [ + 'test.example14' => [ + 'test.example14.navigation_overview' => [], + 'test.example15' => [], + ], + ], + 'test.example16' => [ + // 17 only has inaccessible and disabled child links, and a child item + // that links to the same url as 17, so there should be no overview link + // child added. + 'test.example17' => [ + 'test.example20' => [], + ], + ], ]; $expectations['level_2_and_beyond'] = [ 'test.example3' => [ @@ -276,6 +320,12 @@ class NavigationMenuBlockTest extends KernelTestBase { 'test.example11' => [ 'test.example12' => [], ], + 'test.example14' => [ + 'test.example15' => [], + ], + 'test.example17' => [ + 'test.example20' => [], + ], ]; $expectations['level_3_and_beyond'] = [ 'test.example4' => [ @@ -283,6 +333,8 @@ class NavigationMenuBlockTest extends KernelTestBase { ], 'test.example10' => [], 'test.example12' => [], + 'test.example15' => [], + 'test.example20' => [], ]; // Scenario 1: test all navigation block instances when there's no active // trail. @@ -346,6 +398,10 @@ class NavigationMenuBlockTest extends KernelTestBase { "//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']", + "//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 13']", + "//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 14']", + "//li[contains(@class,'toolbar-menu__item--level-2')]/a[text()='Overview']", + "//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 17']", ]; foreach ($items_query as $query) { $span = $xpath->query($query); diff --git a/core/modules/navigation/tests/src/Nightwatch/Tests/navigationDisplaceTest.js b/core/modules/navigation/tests/src/Nightwatch/Tests/navigationDisplaceTest.js new file mode 100644 index 000000000000..ce9acfacc514 --- /dev/null +++ b/core/modules/navigation/tests/src/Nightwatch/Tests/navigationDisplaceTest.js @@ -0,0 +1,26 @@ +/** + * Verify that Drupal.displace() attribute is properly added by JavaScript. + */ +module.exports = { + '@tags': ['core', 'navigation'], + browser(browser) { + browser + .drupalInstall() + .drupalInstallModule('navigation', true) + .drupalInstallModule('big_pipe') + .setWindowSize(1220, 800); + }, + after(browser) { + browser.drupalUninstall(); + }, + + 'Verify displace attribute': (browser) => { + browser.drupalLoginAsAdmin(() => { + browser + .drupalRelativeURL('/admin/') + .waitForElementPresent( + '.admin-toolbar__displace-placeholder[data-offset-left]', + ); + }); + }, +}; diff --git a/core/modules/navigation/tests/src/Unit/NavigationMenuLinkTreeManipulatorsTest.php b/core/modules/navigation/tests/src/Unit/NavigationMenuLinkTreeManipulatorsTest.php new file mode 100644 index 000000000000..257033b5feac --- /dev/null +++ b/core/modules/navigation/tests/src/Unit/NavigationMenuLinkTreeManipulatorsTest.php @@ -0,0 +1,308 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\navigation\Unit; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Menu\MenuLinkTreeElement; +use Drupal\Core\Menu\StaticMenuLinkOverridesInterface; +use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\navigation\Menu\NavigationMenuLinkTreeManipulators; +use Drupal\system\Controller\SystemController; +use Drupal\Tests\Core\Menu\MenuLinkMock; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\Routing\Route; + +/** + * Tests the navigation menu link tree manipulator. + * + * @group navigation + * + * @coversDefaultClass \Drupal\navigation\Menu\NavigationMenuLinkTreeManipulators + */ +class NavigationMenuLinkTreeManipulatorsTest extends UnitTestCase { + + /** + * Tests the addSecondLevelOverviewLinks() tree manipulator. + * + * @covers ::addSecondLevelOverviewLinks + */ + public function testAddSecondLevelOverviewLinks(): void { + $routeProvider = $this->createMock(RouteProviderInterface::class); + // For only the route named 'child_list', return a route object with the + // SystemController::systemAdminMenuBlockPage as the controller. + $childListRoute = new Route('/test-child-list', ['_controller' => SystemController::class . '::systemAdminMenuBlockPage']); + $routeProvider->expects($this->any()) + ->method('getRouteByName') + ->willReturnCallback(static fn ($name) => $name === 'child_list' ? $childListRoute : new Route("/$name")); + $overrides = $this->createMock(StaticMenuLinkOverridesInterface::class); + $translation = $this->createMock(TranslationInterface::class); + $translation + ->method('translateString') + ->willReturnCallback(static fn ($string) => $string); + $manipulator = new NavigationMenuLinkTreeManipulators($routeProvider, $overrides, $translation); + + $originalTree = $this->mockTree(); + // Make sure overview links do not already exist. + $this->assertArrayNotHasKey('test.example3.navigation_overview', $originalTree[2]->subtree[3]->subtree); + $this->assertArrayNotHasKey('test.example6.navigation_overview', $originalTree[5]->subtree[6]->subtree); + $tree = $manipulator->addSecondLevelOverviewLinks($originalTree); + + // First level menu items should not have any children added. + $this->assertEmpty($tree[1]->subtree); + $this->assertEquals($originalTree[2]->subtree, $tree[2]->subtree); + $this->assertEquals($originalTree[5]->subtree, $tree[5]->subtree); + $this->assertEquals($originalTree[8]->subtree, $tree[8]->subtree); + $this->assertEquals($originalTree[11]->subtree, $tree[11]->subtree); + $this->assertEquals($originalTree[13]->subtree, $tree[13]->subtree); + $this->assertEquals($originalTree[16]->subtree, $tree[16]->subtree); + $this->assertEquals($originalTree[19]->subtree, $tree[19]->subtree); + + // Leaves should not have any children added. + $this->assertEmpty($tree[2]->subtree[3]->subtree[4]->subtree); + $this->assertEmpty($tree[5]->subtree[6]->subtree[7]->subtree); + $this->assertEmpty($tree[8]->subtree[9]->subtree[10]->subtree); + $this->assertEmpty($tree[11]->subtree[12]->subtree); + $this->assertEmpty($tree[13]->subtree[14]->subtree[15]->subtree); + $this->assertEmpty($tree[16]->subtree[17]->subtree[18]->subtree); + $this->assertEmpty($tree[19]->subtree[20]->subtree[21]->subtree); + $this->assertEmpty($tree[19]->subtree[20]->subtree[22]->subtree); + + // Links 3 and 6 should have overview children, even though 6 is unrouted. + $this->assertArrayHasKey('test.example3.navigation_overview', $tree[2]->subtree[3]->subtree); + $this->assertArrayHasKey('test.example6.navigation_overview', $tree[5]->subtree[6]->subtree); + + // Link 9 is a child list page, so it should not have an overview child. + $this->assertArrayNotHasKey('test.example9.navigation_overview', $tree[8]->subtree[9]->subtree); + + // Link 14 and Link 17 are <nolink> and <button> routes, so they should not + // have overview children. + $this->assertArrayNotHasKey('test.example14.navigation_overview', $tree[13]->subtree[14]->subtree); + $this->assertArrayNotHasKey('test.example17.navigation_overview', $tree[16]->subtree[17]->subtree); + + // Link 20's child links are either inaccessible, disabled, or link to the + // same route as 20, so it should not have an overview child. + $this->assertArrayNotHasKey('test.example20.navigation_overview', $tree[19]->subtree[20]->subtree); + } + + /** + * Creates a mock tree. + * + * This mocks a tree with the following structure: + * - 1 + * - 2 + * - 3 + * - 4 + * - 5 + * - 6 (external) + * - 7 + * - 8 + * - 9 + * - 10 + * - 11 + * - 12 + * - 13 + * - 14 (nolink) + * - 15 + * - 16 + * - 17 (button) + * - 18 + * - 19 + * - 20 + * - 21 (disabled) + * - 22 (access denied) + * - 23 (links to same routed URL as 20) + * + * With link 9 linking to a page that contains a list of child menu links. + * + * @return \Drupal\Core\Menu\MenuLinkTreeElement[] + * The mock menu tree. + */ + protected function mockTree(): array { + $links = [ + 1 => MenuLinkMock::create([ + 'id' => 'test.example1', + 'route_name' => 'example1', + 'title' => 'foo', + 'parent' => '', + ]), + 2 => MenuLinkMock::create([ + 'id' => 'test.example2', + 'route_name' => 'example2', + 'title' => 'foo', + 'parent' => '', + ]), + 3 => MenuLinkMock::create([ + 'id' => 'test.example3', + 'route_name' => 'example3', + 'title' => 'baz', + 'parent' => 'test.example2', + ]), + 4 => MenuLinkMock::create([ + 'id' => 'test.example4', + 'route_name' => 'example4', + 'title' => 'qux', + 'parent' => 'test.example3', + ]), + 5 => MenuLinkMock::create([ + 'id' => 'test.example5', + 'route_name' => 'example5', + 'title' => 'title5', + 'parent' => '', + ]), + 6 => MenuLinkMock::create([ + 'id' => 'test.example6', + 'route_name' => '', + 'url' => 'https://www.drupal.org/', + 'title' => 'bar_bar', + 'parent' => 'test.example5', + ]), + 7 => MenuLinkMock::create([ + 'id' => 'test.example7', + 'route_name' => 'example7', + 'title' => 'title7', + 'parent' => 'test.example6', + ]), + 8 => MenuLinkMock::create([ + 'id' => 'test.example8', + 'route_name' => 'example8', + 'title' => 'title8', + 'parent' => '', + ]), + 9 => MenuLinkMock::create([ + 'id' => 'test.example9', + 'route_name' => 'child_list', + 'title' => 'title9', + 'parent' => 'test.example8', + ]), + 10 => MenuLinkMock::create([ + 'id' => 'test.example10', + 'route_name' => 'example9', + 'title' => 'title10', + 'parent' => 'test.example9', + ]), + 11 => MenuLinkMock::create([ + 'id' => 'test.example11', + 'route_name' => 'example11', + 'title' => 'title11', + 'parent' => '', + ]), + 12 => MenuLinkMock::create([ + 'id' => 'test.example12', + 'route_name' => 'example12', + 'title' => 'title12', + 'parent' => 'text.example11', + ]), + 13 => MenuLinkMock::create([ + 'id' => 'test.example13', + 'route_name' => 'example13', + 'title' => 'title13', + 'parent' => '', + ]), + 14 => MenuLinkMock::create([ + 'id' => 'test.example14', + 'route_name' => '<nolink>', + 'title' => 'title14', + 'parent' => 'text.example13', + ]), + 15 => MenuLinkMock::create([ + 'id' => 'test.example15', + 'route_name' => 'example15', + 'title' => 'title15', + 'parent' => 'text.example14', + ]), + 16 => MenuLinkMock::create([ + 'id' => 'test.example16', + 'route_name' => 'example16', + 'title' => 'title16', + 'parent' => '', + ]), + 17 => MenuLinkMock::create([ + 'id' => 'test.example17', + 'route_name' => '<button>', + 'title' => 'title17', + 'parent' => 'text.example16', + ]), + 18 => MenuLinkMock::create([ + 'id' => 'test.example18', + 'route_name' => 'example18', + 'title' => 'title18', + 'parent' => 'text.example17', + ]), + 19 => MenuLinkMock::create([ + 'id' => 'test.example19', + 'route_name' => 'example19', + 'title' => 'title19', + 'parent' => '', + ]), + 20 => MenuLinkMock::create([ + 'id' => 'test.example20', + 'route_name' => 'example20', + 'title' => 'title20', + 'parent' => 'test.example19', + ]), + 21 => MenuLinkMock::create([ + 'id' => 'test.example21', + 'route_name' => 'example21', + 'title' => 'title21', + 'parent' => 'test.example20', + 'enabled' => FALSE, + ]), + 22 => MenuLinkMock::create([ + 'id' => 'test.example22', + 'route_name' => 'no_access', + 'title' => 'title22', + 'parent' => 'test.example20', + ]), + 23 => MenuLinkMock::create([ + 'id' => 'test.example23', + 'route_name' => 'example20', + 'title' => 'title23', + 'parent' => 'test.example20', + ]), + ]; + $tree = []; + $tree[1] = new MenuLinkTreeElement($links[1], FALSE, 1, FALSE, []); + $tree[2] = new MenuLinkTreeElement($links[2], TRUE, 1, FALSE, [ + 3 => new MenuLinkTreeElement($links[3], TRUE, 2, FALSE, [ + 4 => new MenuLinkTreeElement($links[4], FALSE, 3, FALSE, []), + ]), + ]); + $tree[5] = new MenuLinkTreeElement($links[5], TRUE, 1, FALSE, [ + 6 => new MenuLinkTreeElement($links[6], TRUE, 2, FALSE, [ + 7 => new MenuLinkTreeElement($links[7], FALSE, 3, FALSE, []), + ]), + ]); + $tree[8] = new MenuLinkTreeElement($links[8], TRUE, 1, FALSE, [ + 9 => new MenuLinkTreeElement($links[9], TRUE, 2, FALSE, [ + 10 => new MenuLinkTreeElement($links[10], FALSE, 3, FALSE, []), + ]), + ]); + $tree[11] = new MenuLinkTreeElement($links[11], TRUE, 1, FALSE, [ + 12 => new MenuLinkTreeElement($links[12], FALSE, 2, FALSE, []), + ]); + $tree[13] = new MenuLinkTreeElement($links[13], TRUE, 1, FALSE, [ + 14 => new MenuLinkTreeElement($links[14], TRUE, 2, FALSE, [ + 15 => new MenuLinkTreeElement($links[15], FALSE, 3, FALSE, []), + ]), + ]); + $tree[16] = new MenuLinkTreeElement($links[16], TRUE, 1, FALSE, [ + 17 => new MenuLinkTreeElement($links[17], TRUE, 2, FALSE, [ + 18 => new MenuLinkTreeElement($links[18], FALSE, 3, FALSE, []), + ]), + ]); + $tree[19] = new MenuLinkTreeElement($links[19], TRUE, 1, FALSE, [ + 20 => new MenuLinkTreeElement($links[20], TRUE, 2, FALSE, [ + 21 => new MenuLinkTreeElement($links[21], FALSE, 3, FALSE, []), + 22 => new MenuLinkTreeElement($links[22], FALSE, 3, FALSE, []), + 23 => new MenuLinkTreeElement($links[23], FALSE, 3, FALSE, []), + ]), + ]); + $tree[19]->subtree[20]->subtree[22]->access = AccessResult::forbidden(); + + return $tree; + } + +} |