Unverified Commit ba110231 authored by Alex Pott's avatar Alex Pott
Browse files

feat: #3529464 Make menu trail behaviour in SystemMenuBlock optional

By: catch
By: godotislate
By: alexpott
By: berdir
By: benjifisher
By: rkoller
By: simohell
parent 99a2607a
Loading
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -435,6 +435,7 @@ block.settings.system_menu_block:*:
  label: 'Menu block'
  constraints:
    FullyValidatable: ~
    IgnoreActiveTrail: ~
  mapping:
    level:
      type: integer
@@ -453,6 +454,10 @@ block.settings.system_menu_block:*:
    expand_all_items:
      type: boolean
      label: 'Expand all items'
    ignore_active_trail:
      type: boolean
      label: 'Ignore active trail'
      requiredKey: false

block.settings.local_tasks_block:
  type: block_settings
+93 −6
Original line number Diff line number Diff line
@@ -107,6 +107,36 @@ public function blockForm($form, FormStateInterface $form_state) {
      '#description' => $this->t('Override the option found on each menu link used for expanding children and instead display the whole menu tree as expanded.'),
    ];

    // When only the first level of links are shown, or if all links are
    // expanded, the active trail logic can be skipped.
    $state_conditions = [
      // When the menu level starts at anything other than 1.
      [
        ':input[name="settings[level]"]' => ['!value' => '1'],
      ],
      'or',
      // When links aren't all expanded, and more than one level of links are
      // shown.
      [
        'input[name="settings[expand_all_items]"]' => ['checked' => FALSE],
        ':input[name="settings[depth]"]' => ['!value' => '1'],
      ],
    ];

    // The 'add_active_trail_class' checkbox value is the inverse of the
    // 'ignore_active_trail configuration value. This is because the positive
    // statement is easier to explain in the UI, but the negative statement is
    // easier to implement in the API.
    $form['menu_levels']['add_active_trail_class'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Add a CSS class to ancestors of the current page'),
      '#default_value' => empty($config['ignore_active_trail']),
      '#description' => $this->t('Adds a CSS class to parent menu links when the current page is in the menu. This feature has a performance impact and should only be enabled when the menu appearance should differ based on the current page.'),
      '#states' => [
        'required' => $state_conditions,
      ],
    ];

    return $form;
  }

@@ -120,6 +150,20 @@ public static function processMenuLevelParents(&$element, FormStateInterface $fo
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function blockValidate($form, FormStateInterface $form_state): void {
    $values = $form_state->getValues();
    if (!empty($values['add_active_trail_class'])) {
      return;
    }

    if ((int) $values['level'] !== 1 || (empty($values['expand_all_items']) && (int) $values['depth'] !== 1)) {
      $form_state->setError($form['menu_levels']['add_active_trail_class'], $this->t('"Add a CSS class to ancestors of the current page" is required if the menu if the initial is 1, or if menu items are not all expanded and the number of levels display is more than 1.'));
    }
  }

  /**
   * {@inheritdoc}
   */
@@ -127,6 +171,16 @@ public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['level'] = $form_state->getValue('level');
    $this->configuration['depth'] = $form_state->getValue('depth') ?: NULL;
    $this->configuration['expand_all_items'] = $form_state->getValue('expand_all_items');

    // Reverse the form checkbox value to match the configuration name. While
    // this is counter-intuitive, it simplifies both the UI and the API logic
    // outside of this method.
    if ($form_state->getValue('add_active_trail_class')) {
      unset($this->configuration['ignore_active_trail']);
    }
    else {
      $this->configuration['ignore_active_trail'] = TRUE;
    }
  }

  /**
@@ -134,18 +188,22 @@ public function blockSubmit($form, FormStateInterface $form_state) {
   */
  public function build() {
    $menu_name = $this->getDerivativeId();
    if ($this->configuration['expand_all_items']) {
    $level = $this->configuration['level'];
    $depth = $this->configuration['depth'];
    // If all items are expanded, or if only the first level of the menu is
    // shown, then the links will always be the same on each page.
    if ($this->configuration['expand_all_items'] || ($level == 1 && $depth == 1)) {
      $parameters = new MenuTreeParameters();
      if ($this->shouldSetActiveTrail()) {
        $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
        $parameters->setActiveTrail($active_trail);
      }
    }
    else {
      $parameters = $this->menuTree->getCurrentRouteMenuTreeParameters($menu_name);
    }

    // Adjust the menu tree parameters based on the block's configuration.
    $level = $this->configuration['level'];
    $depth = $this->configuration['depth'];
    $parameters->setMinDepth($level);
    // When the depth is configured to zero, there is no depth limit. When depth
    // is non-zero, it indicates the number of levels that must be displayed.
@@ -218,7 +276,16 @@ public function getCacheContexts() {
    // Additional cache contexts, e.g. those that determine link text or
    // accessibility of a menu, will be bubbled automatically.
    $menu_name = $this->getDerivativeId();
    return Cache::mergeContexts(parent::getCacheContexts(), ['route.menu_active_trails:' . $menu_name]);
    $contexts = parent::getCacheContexts();
    // The active trail context is added when the menu block is not configured
    // to ignore the active trail. Ignoring the active trail only applies when
    // the menu is also configured with all items expanded and start level 1, so
    // if any of those conditions are not true, the active trail context is
    // added.
    if ($this->shouldSetActiveTrail()) {
      $contexts = Cache::mergeContexts($contexts, ['route.menu_active_trails:' . $menu_name]);
    }
    return $contexts;
  }

  /**
@@ -228,4 +295,24 @@ public function createPlaceholder(): bool {
    return TRUE;
  }

  /**
   * Determine whether the menu block should set active trails on the links.
   *
   * The active trail must be set if it's required to build the correct
   * set of links, so setting 'ignore_active_trail' to TRUE only works with
   * certain configurations:
   * - The initial level is 1 and all items are expanded
   * - The initial level is 1 and only 1 level of links are displayed
   *
   * While the form UI and validation should prevent 'ignore_active_trail' from
   * being set to TRUE otherwise, the other settings are checked as well, in
   * case the configuration is somehow in an invalid state.
   *
   * @return bool
   *   TRUE if the menu block should set active trails on the links.
   */
  protected function shouldSetActiveTrail(): bool {
    return empty($this->configuration['ignore_active_trail']) || $this->configuration['level'] !== 1 || (empty($this->configuration['expand_all_items']) && $this->configuration['depth'] !== 1);
  }

}
+27 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\system\Plugin\Validation\Constraint;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;

/**
 * Constraint on the ignore_active_trail configuration in system menu blocks.
 */
#[Constraint(
  id: 'IgnoreActiveTrail',
  label: new TranslatableMarkup('Whether the ignore_active_trail setting is valid', [], ['context' => 'Validation'])
)]
class IgnoreActiveTrailConstraint extends SymfonyConstraint {

  /**
   * The default violation message.
   *
   * @var string
   */
  public string $message = 'The "ignore_active_trail" setting on a system menu block cannot be enabled if "level" is greater than 1 or "expand_all_items" is not enabled and "depth" is greater than 1.';

}
+30 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\system\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Validator for the IgnoreActiveTrail constraint.
 */
class IgnoreActiveTrailConstraintValidator extends ConstraintValidator {

  /**
   * {@inheritdoc}
   */
  public function validate(mixed $value, Constraint $constraint): void {
    assert($constraint instanceof IgnoreActiveTrailConstraint);
    if (!is_array($value)) {
      throw new UnexpectedTypeException($value, 'array');
    }
    if (!empty($value['ignore_active_trail'])
      && ((isset($value['level']) && $value['level'] > 1) || (empty($value['expand_all_items']) && $value['depth'] != 1))) {
      $this->context->addViolation($constraint->message);
    }
  }

}
+82 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Tests\system\FunctionalJavascript\Block;

use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;

/**
 * Test the #states on the system menu block form.
 */
#[Group('Block')]
#[RunTestsInSeparateProcesses]
class SystemMenuBlockUiTest extends WebDriverTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = ['block', 'system'];

  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'stark';

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    // Create and log in an administrative user.
    $user = $this->drupalCreateUser([
      'administer blocks',
      'access administration pages',
    ]);
    $this->drupalLogin($user);
  }

  /**
   * Tests that add_active_trail_class field states based on other form values.
   */
  public function testSystemMenuBlockForm(): void {
    $this->drupalGet(Url::fromRoute('block.admin_add', [
      'plugin_id' => 'system_menu_block:admin',
      'theme' => 'stark',
    ]));
    $page = $this->getSession()->getPage();
    $page->findById('edit-settings-menu-levels')->click();
    $levelField = $page->findField('settings[level]');
    $depthField = $page->findField('settings[depth]');
    // On first form when adding the menu block, the level should be set to 1,
    // "expand_all_items" unchecked, and "add_active_trail_class" checkbox is
    // required.
    $this->assertEquals('1', $levelField->getValue());
    $this->assertEquals('0', $depthField->getValue());
    $expandField = $page->findField('settings[expand_all_items]');
    $this->assertFalse($expandField->isChecked());
    $addActiveTrailField = $page->findField('settings[add_active_trail_class]');
    $this->assertTrue($addActiveTrailField->hasAttribute('required'));

    // Setting the depth value to '1' should mean the checkbox is no longer
    // required.
    $depthField->setValue('1');
    $this->assertFalse($addActiveTrailField->hasAttribute('required'));

    // Clicking on "expand_all_items" makes "add_active_trail_class" not required
    // when level is 1.
    $depthField->setValue('0');
    $this->assertTrue($addActiveTrailField->hasAttribute('required'));
    $expandField->click();
    $this->assertFalse($addActiveTrailField->hasAttribute('required'));

    // Setting level to a value greater than one makes "add_active_trail_class"
    // required.
    $levelField->setValue('2');
    $this->assertTrue($addActiveTrailField->hasAttribute('required'));
  }

}
Loading