diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css index 900329e8ed..29f9a2b205 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 bdd17e3e5b..8358644be2 100644 --- a/core/modules/layout_builder/js/layout-builder.es6.js +++ b/core/modules/layout_builder/js/layout-builder.es6.js @@ -201,4 +201,85 @@ .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 elements value of data-layout-builder-highlight-id, the class + * 'is-layout-builder-highlighted' is added to it. + */ + 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 target element is still visible + * in viewport. If not, scroll page so 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 element in viewport. + const targetTop = $target.offset().top; + const targetBottom = targetTop + $target.outerHeight(); + const viewportTop = $(window).scrollTop(); + const viewportBottom = viewportTop + $(window).height(); + + // If element not in viewport, scroll it into view. + if (targetBottom < viewportTop || targetTop > viewportBottom) { + const viewportMiddle = (viewportBottom + viewportTop) / 2; + const scrollAmount = targetTop - viewportMiddle; + + // Check if browser supports scrollBy(options) if 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 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 b6cb26ca1e..13b6b09aa6 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 ea721b7717..af9a3b3c21 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 f7c19c2118..714c4770f8 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 37042f761a..83bf6c1b24 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'; @@ -291,8 +298,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 1d873308a1..ccfea97e2f 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 92584b5893..82fce6156b 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 d6c05f87bf..06f09ae9b7 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 c00b406eb2..59cf91fd56 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 0000000000..65a470c31b --- /dev/null +++ b/core/modules/layout_builder/src/LayoutBuilderHighlightTrait.php @@ -0,0 +1,64 @@ +pageTextContainsOnce('You have unsaved changes.'); } + /** + * Tests that dialog opening elements 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(); + + // Highlight is present with ConfigureSectionForm. + $this->assertHighlightedElement('[data-layout-builder-highlight-id="section-0"]'); + + // Submit form to add section then confirm no element highlighted. + $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 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(); + + // 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"]')); + + // Highlight is present with ConfigureBlockFormBase::doBuildForm. + $this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]'); + $page->pressButton('Close'); + $this->assertHighlightNotExists(); + + // Highlight is present when Configure section dialog 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(); + + // Highlight is present when Remove section dialog 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(); + + // 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 highlight remains when mouse reveals contextual links. + $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 reload after https://www.drupal.org/node/2918718 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(); + } + + /** + * Confirm presence of is-layout-builder-highlighted element. + * + * @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 dialog to close and confirms no highlights 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); + } + }