diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css index 900329e8edfd997ded5f2626f32b6e81a4cd7e4d..29f9a2b2055e8d4027c364ed0d0b3f1edbde0017 100644 --- a/core/modules/layout_builder/css/layout-builder.css +++ b/core/modules/layout_builder/css/layout-builder.css @@ -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; +} diff --git a/core/modules/layout_builder/js/layout-builder.es6.js b/core/modules/layout_builder/js/layout-builder.es6.js index bdd17e3e5b693a71cfe8ceb3c3084c5f198a3b81..6b25f4e4e876dd21678cbaf4e1070d5c3dd8efd4 100644 --- a/core/modules/layout_builder/js/layout-builder.es6.js +++ b/core/modules/layout_builder/js/layout-builder.es6.js @@ -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); diff --git a/core/modules/layout_builder/js/layout-builder.js b/core/modules/layout_builder/js/layout-builder.js index b6cb26ca1e04acf50b2bb4d378a820b26a88ece1..13b6b09aa627899f4efcbf547a9f71078ba99401 100644 --- a/core/modules/layout_builder/js/layout-builder.js +++ b/core/modules/layout_builder/js/layout-builder.js @@ -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 diff --git a/core/modules/layout_builder/src/Controller/ChooseBlockController.php b/core/modules/layout_builder/src/Controller/ChooseBlockController.php index ea721b7717fe7ec6f3db53a21d48edc4be2fa992..af9a3b3c2105410211abfd750abcf19aaa5d53ec 100644 --- a/core/modules/layout_builder/src/Controller/ChooseBlockController.php +++ b/core/modules/layout_builder/src/Controller/ChooseBlockController.php @@ -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; } diff --git a/core/modules/layout_builder/src/Controller/ChooseSectionController.php b/core/modules/layout_builder/src/Controller/ChooseSectionController.php index f7c19c2118e3b97022513d734aa6d88fbee8455a..714c4770f82870f62d8c9afb027207f8a2fcdf36 100644 --- a/core/modules/layout_builder/src/Controller/ChooseSectionController.php +++ b/core/modules/layout_builder/src/Controller/ChooseSectionController.php @@ -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), ], ]; diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index e53819611af132df6b8784ba7f871d02fdc54c05..9c98847e8907662e3d5b274c3ec5ad2c386493a4 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -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', diff --git a/core/modules/layout_builder/src/Form/AddBlockForm.php b/core/modules/layout_builder/src/Form/AddBlockForm.php index 1d873308a1f2f5571db6ab2c65f6d688fd3f879a..ccfea97e2fcd742b1e8bb1dece9d7a95114d0577 100644 --- a/core/modules/layout_builder/src/Form/AddBlockForm.php +++ b/core/modules/layout_builder/src/Form/AddBlockForm.php @@ -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); } diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index 92584b58935ffd34253dfe017ad0cd780021c9ce..82fce6156b2f09e454af29c8c505c253edc72700 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -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; } diff --git a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php index d6c05f87bf1226ce061c8fc1c15eca51110c9dc4..06f09ae9b703736bbaf6107c9d6701534b0cea34 100644 --- a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php +++ b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php @@ -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; diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php index c00b406eb2cb2a5e666410fd185ad408cf2659cd..59cf91fd56a284ca4d61cf68952b8ef20f63ef4e 100644 --- a/core/modules/layout_builder/src/Form/UpdateBlockForm.php +++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php @@ -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); } diff --git a/core/modules/layout_builder/src/LayoutBuilderHighlightTrait.php b/core/modules/layout_builder/src/LayoutBuilderHighlightTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..5c3e53c7b5546fce5c21c31f3b1836566253a7ff --- /dev/null +++ b/core/modules/layout_builder/src/LayoutBuilderHighlightTrait.php @@ -0,0 +1,64 @@ +<?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"; + } + +} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php index e4b342ba86ca0beac96057b94adc599a58244521..b7168a6610a2e30da24cb845bd7efad6c4302468 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php @@ -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"); } /** diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php index 06b80e76a473c873ce6c743fd1d74f5d992faaa5..2b3ba8ebfcb96cb8134fce784e68ea8942a96621 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderUiTest.php @@ -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 + * CSS selector. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + * + * @todo Remove in https://www.drupal.org/node/2892440. + */ + protected function waitForNoElement($selector, $timeout = 10000) { + $condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)"; + $this->assertJsCondition($condition, $timeout); + } + }