Unverified Commit 40d6aca4 authored by lauriii's avatar lauriii

Issue #2994909 by bnjmnm, tedbow, kostyashupenko, tim.plunkett, lauriii,...

Issue #2994909 by bnjmnm, tedbow, kostyashupenko, tim.plunkett, lauriii, samuel.mortenson, Kristen Pol, xjm, andrewmacpherson, alwaysworking, DyanneNova, jrockowitz, worldlinemine, phenaproxima, ckrina, benjifisher, rainbreaw, webchick: Highlight active element while working with dialogs in Layout Builder
parent ba442448
......@@ -68,6 +68,7 @@
border-radius: 26px;
margin-left: -10px;
margin-right: 6px;
z-index: 2;
}
.layout-builder__link--remove:hover {
......@@ -121,3 +122,22 @@
display: block;
padding: 15px 0 15px 25px;
}
.layout-builder__add-section.is-layout-builder-highlighted {
margin-bottom: calc(1.5em - 8px);
outline: none;
}
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted,
.layout-builder__add-block.is-layout-builder-highlighted {
margin: -4px -2px;
position: relative;
z-index: 1;
}
.layout-builder__add-block.is-layout-builder-highlighted,
.layout-builder__add-section.is-layout-builder-highlighted,
.layout-builder__layout.is-layout-builder-highlighted:before,
.layout-builder__layout.is-layout-builder-highlighted,
.layout-builder-block.is-layout-builder-highlighted {
border: 4px solid #000;
}
......@@ -201,4 +201,86 @@
.attr('tabindex', -1);
},
};
// After a dialog opens, highlight element that the dialog is acting on.
$(window).on('dialog:aftercreate', (event, dialog, $element) => {
if (Drupal.offCanvas.isOffCanvas($element)) {
// Start by removing any existing highlighted elements.
$('.is-layout-builder-highlighted').removeClass(
'is-layout-builder-highlighted',
);
/*
* Every dialog has a single 'data-layout-builder-target-highlight-id'
* attribute. Every dialog-opening element has a unique
* 'data-layout-builder-highlight-id' attribute.
*
* When the value of data-layout-builder-target-highlight-id matches
* an element's value of data-layout-builder-highlight-id, the class
* 'is-layout-builder-highlighted' is added to element.
*/
const id = $element
.find('[data-layout-builder-target-highlight-id]')
.attr('data-layout-builder-target-highlight-id');
if (id) {
$(`[data-layout-builder-highlight-id="${id}"]`).addClass(
'is-layout-builder-highlighted',
);
}
}
});
/*
* When a Layout Builder dialog is triggered, the main canvas resizes. After
* the resize transition is complete, see if the target element is still
* visible in viewport. If not, scroll page so the target element is again
* visible.
*
* @todo Replace this custom solution when a general solution is made
* available with https://www.drupal.org/node/3033410
*/
if (document.querySelector('[data-off-canvas-main-canvas]')) {
const mainCanvas = document.querySelector('[data-off-canvas-main-canvas]');
// This event fires when canvas CSS transitions are complete.
mainCanvas.addEventListener('transitionend', () => {
const $target = $('.is-layout-builder-highlighted');
if ($target.length > 0) {
// These four variables are used to determine if the element is in the
// viewport.
const targetTop = $target.offset().top;
const targetBottom = targetTop + $target.outerHeight();
const viewportTop = $(window).scrollTop();
const viewportBottom = viewportTop + $(window).height();
// If the element is not in the viewport, scroll it into view.
if (targetBottom < viewportTop || targetTop > viewportBottom) {
const viewportMiddle = (viewportBottom + viewportTop) / 2;
const scrollAmount = targetTop - viewportMiddle;
// Check whether the browser supports scrollBy(options). If it does
// not, use scrollBy(x-coord, y-coord) instead.
if ('scrollBehavior' in document.documentElement.style) {
window.scrollBy({
top: scrollAmount,
left: 0,
behavior: 'smooth',
});
} else {
window.scrollBy(0, scrollAmount);
}
}
}
});
}
// When a dialog closes, remove the highlight from all elements.
$(window).on('dialog:afterclose', (event, dialog, $element) => {
if (Drupal.offCanvas.isOffCanvas($element)) {
$('.is-layout-builder-highlighted').removeClass(
'is-layout-builder-highlighted',
);
}
});
})(jQuery, Drupal);
......@@ -93,4 +93,51 @@
}).attr('tabindex', -1);
}
};
$(window).on('dialog:aftercreate', function (event, dialog, $element) {
if (Drupal.offCanvas.isOffCanvas($element)) {
$('.is-layout-builder-highlighted').removeClass('is-layout-builder-highlighted');
var id = $element.find('[data-layout-builder-target-highlight-id]').attr('data-layout-builder-target-highlight-id');
if (id) {
$('[data-layout-builder-highlight-id="' + id + '"]').addClass('is-layout-builder-highlighted');
}
}
});
if (document.querySelector('[data-off-canvas-main-canvas]')) {
var mainCanvas = document.querySelector('[data-off-canvas-main-canvas]');
mainCanvas.addEventListener('transitionend', function () {
var $target = $('.is-layout-builder-highlighted');
if ($target.length > 0) {
var targetTop = $target.offset().top;
var targetBottom = targetTop + $target.outerHeight();
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
if (targetBottom < viewportTop || targetTop > viewportBottom) {
var viewportMiddle = (viewportBottom + viewportTop) / 2;
var scrollAmount = targetTop - viewportMiddle;
if ('scrollBehavior' in document.documentElement.style) {
window.scrollBy({
top: scrollAmount,
left: 0,
behavior: 'smooth'
});
} else {
window.scrollBy(0, scrollAmount);
}
}
}
});
}
$(window).on('dialog:afterclose', function (event, dialog, $element) {
if (Drupal.offCanvas.isOffCanvas($element)) {
$('.is-layout-builder-highlighted').removeClass('is-layout-builder-highlighted');
}
});
})(jQuery, Drupal);
\ No newline at end of file
......@@ -9,6 +9,7 @@
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -21,6 +22,7 @@ class ChooseBlockController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use StringTranslationTrait;
/**
......@@ -124,6 +126,7 @@ public function build(SectionStorageInterface $section_storage, $delta, $region)
$block_categories['#type'] = 'container';
$block_categories['#attributes']['class'][] = 'block-categories';
$block_categories['#attributes']['class'][] = 'js-layout-builder-categories';
$block_categories['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
// @todo Explicitly cast delta to an integer, remove this in
// https://www.drupal.org/project/drupal/issues/2984509.
......@@ -188,6 +191,7 @@ public function inlineBlockList(SectionStorageInterface $section_storage, $delta
'#attributes' => $this->getAjaxAttributes(),
];
}
$build['links']['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
return $build;
}
......
......@@ -8,6 +8,7 @@
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -19,6 +20,7 @@
class ChooseSectionController implements ContainerInjectionInterface {
use AjaxHelperTrait;
use LayoutBuilderHighlightTrait;
use StringTranslationTrait;
/**
......@@ -96,6 +98,7 @@ public function build(SectionStorageInterface $section_storage, $delta) {
'class' => [
'layout-selection',
],
'data-layout-builder-target-highlight-id' => $this->sectionAddHighlightId($delta),
],
];
......
......@@ -10,6 +10,7 @@
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorageInterface;
......@@ -24,6 +25,7 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter
use AjaxHelperTrait;
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
/**
* The layout tempstore repository.
......@@ -212,6 +214,7 @@ protected function buildAddSectionLink(SectionStorageInterface $section_storage,
'#type' => 'container',
'#attributes' => [
'class' => ['layout-builder__add-section'],
'data-layout-builder-highlight-id' => $this->sectionAddHighlightId($delta),
],
];
}
......@@ -242,6 +245,7 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s
foreach (Element::children($build[$region]) as $uuid) {
$build[$region][$uuid]['#attributes']['class'][] = 'draggable';
$build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
$build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid);
$build[$region][$uuid]['#contextual_links'] = [
'layout_builder_block' => [
'route_parameters' => [
......@@ -281,7 +285,10 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s
),
];
$build[$region]['layout_builder_add_block']['#type'] = 'container';
$build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['layout-builder__add-block']];
$build[$region]['layout_builder_add_block']['#attributes'] = [
'class' => ['layout-builder__add-block'],
'data-layout-builder-highlight-id' => $this->blockAddHighlightId($delta, $region),
];
$build[$region]['layout_builder_add_block']['#weight'] = 1000;
$build[$region]['#attributes']['data-region'] = $region;
$build[$region]['#attributes']['class'][] = 'layout-builder__region';
......@@ -296,8 +303,10 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
])->toString();
$build['#attributes']['data-layout-delta'] = $delta;
$build['#attributes']['class'][] = 'layout-builder__layout';
$build['#attributes']['data-layout-builder-highlight-id'] = $this->sectionUpdateHighlightId($delta);
return [
'#type' => 'container',
......
......@@ -3,6 +3,7 @@
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorageInterface;
......@@ -13,6 +14,8 @@
*/
class AddBlockForm extends ConfigureBlockFormBase {
use LayoutBuilderHighlightTrait;
/**
* {@inheritdoc}
*/
......@@ -53,6 +56,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
$section_storage->getSection($delta)->appendComponent($component);
$form_state->set('layout_builder__component', $component);
}
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockAddHighlightId($delta, $region);
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
......@@ -24,6 +25,7 @@
class ConfigureSectionForm extends FormBase {
use AjaxFormHelperTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
/**
......@@ -127,6 +129,8 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
}
$target_highlight_id = $this->isUpdate ? $this->sectionUpdateHighlightId($delta) : $this->sectionAddHighlightId($delta);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id;
return $form;
}
......
......@@ -6,6 +6,7 @@
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -18,6 +19,7 @@
abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
use AjaxFormHelperTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
/**
......@@ -79,6 +81,8 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
if ($this->isAjax()) {
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
$form['actions']['cancel']['#attributes']['class'][] = 'dialog-cancel';
$target_highlight_id = !empty($this->uuid) ? $this->blockUpdateHighlightId($this->uuid) : $this->sectionUpdateHighlightId($delta);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id;
}
return $form;
......
......@@ -3,6 +3,7 @@
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutBuilderHighlightTrait;
use Drupal\layout_builder\SectionStorageInterface;
/**
......@@ -12,6 +13,8 @@
*/
class UpdateBlockForm extends ConfigureBlockFormBase {
use LayoutBuilderHighlightTrait;
/**
* {@inheritdoc}
*/
......@@ -40,6 +43,7 @@ public function getFormId() {
*/
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
$component = $section_storage->getSection($delta)->getComponent($uuid);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
......
<?php
namespace Drupal\layout_builder;
/**
* A trait for generating IDs used to highlight active UI elements.
*/
trait LayoutBuilderHighlightTrait {
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The section the block is in.
* @param string $region
* The section region in which the block is placed.
*
* @return string
* The highlight ID of the block.
*/
protected function blockAddHighlightId($delta, $region) {
return "block-$delta-$region";
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $uuid
* The uuid of the block.
*
* @return string
* The highlight ID of the block.
*/
protected function blockUpdateHighlightId($uuid) {
return $uuid;
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The location of the section.
*
* @return string
* The highlight ID of the section.
*/
protected function sectionAddHighlightId($delta) {
return "section-$delta";
}
/**
* Provides the ID used to highlight the active Layout Builder UI element.
*
* @param string $delta
* The location of the section.
*
* @return string
* The highlight ID of the section.
*/
protected function sectionUpdateHighlightId($delta) {
return "section-update-$delta";
}
}
......@@ -231,19 +231,20 @@ protected function assertContextualLinksClickable() {
protected function assertContextualLinkRetainsMouseup() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$body_field_selector = '.block-field-blocknodebundle-with-section-fieldbody';
$body_block = $page->find('css', '.block-field-blocknodebundle-with-section-fieldbody');
$body_block = $page->find('css', $body_field_selector);
$this->assertNotEmpty($body_block);
// Get the current Y position of the body block.
$body_block_y_position = $this->getSession()->evaluateScript("document.getElementsByClassName('block-field-blocknodebundle-with-section-fieldbody')[0].getBoundingClientRect().top + window.pageYOffset");
$body_block_top_position = $this->getElementVerticalPosition($body_field_selector, 'top');
$body_block_contextual_link_button = $body_block->find('css', '.trigger');
$this->assertNotEmpty($body_block_contextual_link_button);
// If the body block contextual link is hidden, make it visible.
if ($body_block_contextual_link_button->hasClass('visually-hidden')) {
$this->toggleContextualTriggerVisibility('.block-field-blocknodebundle-with-section-fieldbody');
$this->toggleContextualTriggerVisibility($body_field_selector);
}
// For the purposes of this test, the contextual link must be accessed with
......@@ -254,13 +255,41 @@ protected function assertContextualLinkRetainsMouseup() {
$assert_session->assertWaitOnAjaxRequest();
// After the contextual link opens the dialog, move the mouse pointer
// elsewhere on the page.
// elsewhere on the page. If mouse up were not working correctly this would
// actually drag the body field too.
$this->movePointerTo('#iframe-that-should-be-disabled');
// If mouseup is working properly, the body block should be in the same
// position it was when $body_block_y_position was declared.
$new_body_block_y_position = $this->getSession()->evaluateScript("document.getElementsByClassName('block-field-blocknodebundle-with-section-fieldbody')[0].getBoundingClientRect().top + window.pageYOffset");
$this->assertEquals($body_block_y_position, $new_body_block_y_position);
$new_body_block_bottom_position = $this->getElementVerticalPosition($body_field_selector, 'bottom');
$iframe_top_position = $this->getElementVerticalPosition('#iframe-that-should-be-disabled', 'top');
$minimum_distance_mouse_moved = $iframe_top_position - $new_body_block_bottom_position;
$this->assertGreaterThan(200, $minimum_distance_mouse_moved, 'The mouse moved at least 200 pixels');
// If mouseup is working properly, the body block should be nearly in same
// position as it was when $body_block_y_position was declared. It will have
// moved slightly because the current block being configured will have a
// border that was not present when the dialog was not open.
$new_body_block_top_position = $this->getElementVerticalPosition($body_field_selector, 'top');
$distance_body_block_moved = abs($body_block_top_position - $new_body_block_top_position);
// Confirm that body moved only slightly compared to the distance the mouse
// moved and therefore was not dragged when the mouse moved.
$this->assertGreaterThan($distance_body_block_moved * 20, $minimum_distance_mouse_moved);
}
/**
* Gets the element position.
*
* @param string $css_selector
* The CSS selector of the element.
* @param string $position_type
* The position type to get, either 'top' or 'bottom'.
*
* @return int
* The element position.
*/
protected function getElementVerticalPosition($css_selector, $position_type) {
$this->assertTrue(in_array($position_type, ['top', 'bottom']), 'Expected position type.');
return (int) $this->getSession()->evaluateScript("document.querySelector('$css_selector').getBoundingClientRect().$position_type + window.pageYOffset");
}
/**
......
......@@ -2,7 +2,9 @@
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
/**
* Tests the Layout Builder UI.
......@@ -11,6 +13,8 @@
*/
class LayoutBuilderUiTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* Path prefix for the field UI for the test bundle.
*
......@@ -22,6 +26,10 @@ class LayoutBuilderUiTest extends WebDriverTestBase {
'layout_builder',
'block',
'node',
'block_content',
'contextual',
'views',
'layout_builder_test_css_transitions',
];
/**
......@@ -40,6 +48,7 @@ protected function setUp() {
'configure any layout',
'administer node display',
'administer node fields',
'access contextual links',
]));
// Enable layout builder.
......@@ -116,4 +125,156 @@ protected function assertModifiedLayout($path) {
$assert_session->pageTextContainsOnce('You have unsaved changes.');
}
/**
* Tests that elements that open the dialog are properly highlighted.
*/
public function testAddHighlights() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$bundle = BlockContentType::create([
'id' => 'basic',
'label' => 'Basic block',
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$assert_session->elementsCount('css', '.layout-builder__add-section', 2);
$assert_session->elementNotExists('css', '.is-layout-builder-highlighted');
$page->clickLink('Add Section');
$this->assertNotEmpty($assert_session->waitForElement('css', '#drupal-off-canvas .item-list'));
$assert_session->assertWaitOnAjaxRequest();
// Highlight is present with AddSectionController.
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-0"]');
$page->clickLink('Two column');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[type="submit"][value="Add section"]'));
$assert_session->assertWaitOnAjaxRequest();
// The highlight is present with ConfigureSectionForm.
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-0"]');
// Submit the form to add the section and then confirm that no element is
// highlighted anymore.
$page->pressButton("Add section");
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightNotExists();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-layout-delta="1"]'));
$assert_session->elementsCount('css', '.layout-builder__add-block', 3);
// Add a custom block.
$page->clickLink('Add Block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'a:contains("Create custom block")'));
$assert_session->assertWaitOnAjaxRequest();
// Highlight is present with ChooseBlockController::build().
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->clickLink('Create custom block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[value="Add Block"]'));
$assert_session->assertWaitOnAjaxRequest();
// Highlight is present with ChooseBlockController::inlineBlockList().
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// The highlight should persist with all block config dialogs.
$page->clickLink('Add Block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'a:contains("Recent content")'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->clickLink('Recent content');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[value="Add Block"]'));
// The highlight is present with ConfigureBlockFormBase::doBuildForm().
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// The highlight is present when the "Configure section" dialog is open.
$page->clickLink('Configure section');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-update-0"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// The highlight is present when the "Remove section" dialog is open.
$page->clickLink('Remove section');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-update-0"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// A block is highlighted when its "Configure" contextual link is clicked.
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody', 'Configure');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('.block-field-blocknodebundle-with-section-fieldbody');
// Make sure the highlight remains when contextual links are revealed with
// the mouse.
$this->toggleContextualTriggerVisibility('.block-field-blocknodebundle-with-section-fieldbody');
$active_section = $page->find('css', '.block-field-blocknodebundle-with-section-fieldbody');
$active_section->pressButton('Open configuration options');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-field-blocknodebundle-with-section-fieldbody .contextual.open'));
$page->pressButton('Close');
$this->assertHighlightNotExists();
// @todo Remove the reload once https://www.drupal.org/node/2918718 is
// completed.
$this->getSession()->reload();
// Block is highlighted when its "Remove block" contextual link is clicked.
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody', 'Remove block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('.block-field-blocknodebundle-with-section-fieldbody');
$page->pressButton('Close');
$this->assertHighlightNotExists();
}
/**
* Confirms the presence of the 'is-layout-builder-highlighted' class.
*
* @param string $selector
* The highlighted element must also match this selector.
*/
private function assertHighlightedElement($selector) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// There is only one highlighted element.
$assert_session->elementsCount('css', '.is-layout-builder-highlighted', 1);
// The selector is also the highlighted element.
$this->assertTrue($page->find('css', $selector)->hasClass('is-layout-builder-highlighted'));
}
/**
* Waits for the dialog to close and confirms no highlights are present.
*/
private function assertHighlightNotExists() {
$this->waitForNoElement('#drupal-off-canvas');
$this->waitForNoElement('.is-layout-builder-highlighted');
}
/**
* Waits for an element to be removed from the page.
*
* @param string $selector