diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css index edb0c48cb7..2853da8b24 100644 --- a/core/modules/layout_builder/css/layout-builder.css +++ b/core/modules/layout_builder/css/layout-builder.css @@ -56,6 +56,10 @@ padding: 1.5em; } +.layout-section .layout-builder--layout__region .block [tabindex="-1"] { + pointer-events: none; +} + .layout-section .remove-section { position: relative; background: url(../../../misc/icons/bebebe/ex.svg) #fff center center / 16px 16px no-repeat; diff --git a/core/modules/layout_builder/js/layout-builder.es6.js b/core/modules/layout_builder/js/layout-builder.es6.js index f8f7b0be46..68eebcf4bf 100644 --- a/core/modules/layout_builder/js/layout-builder.es6.js +++ b/core/modules/layout_builder/js/layout-builder.es6.js @@ -1,5 +1,19 @@ -(($, { ajax, behaviors }) => { - behaviors.layoutBuilder = { +/** + * @file + * Attaches the behaviors for the Layout Builder module. + */ + +(($, Drupal) => { + const { ajax, behaviors } = Drupal; + /** + * Provides the ability to drag blocks to new positions in the layout. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attach block drag behavior to the Layout Builder UI. + */ + behaviors.layoutBuilderBlockDrag = { attach(context) { $(context) .find('.layout-builder--layout__region') @@ -51,4 +65,36 @@ }); }, }; + /** + * Disables interactive elements in previewed blocks. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attach disabling interactive elements behavior to the Layout Builder UI. + */ + behaviors.layoutBuilderDisableInteractiveElements = { + attach(context) { + // Disable interactive elements inside preview blocks. + const $blocks = $('#layout-builder [data-layout-block-uuid]'); + $blocks.find('input, textarea, select').prop('disabled', true); + $blocks.find('a').on('click mouseup touchstart', e => { + e.preventDefault(); + e.stopPropagation(); + }); + + /* + * In preview blocks, remove from the tabbing order all input elements + * and elements specifically assigned a tab index. + */ + $blocks + .find( + 'button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"]):not(.tabbable)', + ) + .attr('tabindex', -1); + + // Don't allow links to be used as a sortable selection. + $('.layout-builder--layout__region').sortable({ cancel: 'a' }); + }, + }; })(jQuery, Drupal); diff --git a/core/modules/layout_builder/js/layout-builder.js b/core/modules/layout_builder/js/layout-builder.js index 0f0de275ef..43a833fcd7 100644 --- a/core/modules/layout_builder/js/layout-builder.js +++ b/core/modules/layout_builder/js/layout-builder.js @@ -5,11 +5,11 @@ * @preserve **/ -(function ($, _ref) { - var ajax = _ref.ajax, - behaviors = _ref.behaviors; +(function ($, Drupal) { + var ajax = Drupal.ajax, + behaviors = Drupal.behaviors; - behaviors.layoutBuilder = { + behaviors.layoutBuilderBlockDrag = { attach: function attach(context) { $(context).find('.layout-builder--layout__region').sortable({ items: '> .draggable', @@ -32,4 +32,19 @@ }); } }; + + behaviors.layoutBuilderDisableInteractiveElements = { + attach: function attach(context) { + var $blocks = $('#layout-builder [data-layout-block-uuid]'); + $blocks.find('input, textarea, select').prop('disabled', true); + $blocks.find('a').on('click mouseup touchstart', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + + $blocks.find('button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"]):not(.tabbable)').attr('tabindex', -1); + + $('.layout-builder--layout__region').sortable({ cancel: 'a' }); + } + }; })(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 71265e2d46..c5d5c5ded2 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -34,6 +34,7 @@ function layout_builder_help($route_name, RouteMatchInterface $route_match) { else { $output .= '

' . t('To manage other areas of the page, use the block administration page.') . '

'; } + $output .= '

' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '

'; return $output; } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php new file mode 100644 index 0000000000..62bc312293 --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php @@ -0,0 +1,186 @@ +drupalPlaceBlock('local_tasks_block'); + + $this->createContentType(['type' => 'bundle_with_section_field']); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The first node title', + 'body' => [ + [ + 'value' => 'Node body', + ], + ], + ]); + + $bundle = BlockContentType::create([ + 'id' => 'basic', + 'label' => 'Basic block', + 'revision' => 1, + ]); + $bundle->save(); + block_content_add_body_field($bundle->id()); + + BlockContent::create([ + 'type' => 'basic', + 'info' => 'Block with link', + 'body' => [ + // Create a link that should be disabled in Layout Builder preview. + 'value' => 'Take me away', + 'format' => 'full_html', + ], + ])->save(); + + BlockContent::create([ + 'type' => 'basic', + 'info' => 'Block with iframe', + 'body' => [ + // Add iframe that should be non-interactive in Layout Builder preview. + 'value' => '', + 'format' => 'full_html', + ], + ])->save(); + } + + /** + * Tests that forms and links are disabled in the Layout Builder preview. + */ + public function testFormsLinksDisabled() { + $assert_session = $this->assertSession(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer node fields', + 'search content', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + + $this->drupalPostForm("$field_ui_prefix/display", ['layout[enabled]' => TRUE], 'Save'); + $assert_session->linkExists('Manage layout'); + $this->clickLink('Manage layout'); + + $this->drupalGet("$field_ui_prefix/display-layout/default"); + + // Add a block with a form, another with a link, and one with an iframe. + $this->addBlock('Search form', '#layout-builder .search-block-form'); + $this->drupalGet("$field_ui_prefix/display-layout/default"); + $this->addBlock('Block with link', '#link-that-should-be-disabled'); + $this->drupalGet("$field_ui_prefix/display-layout/default"); + $this->addBlock('Block with iframe', '#iframe-that-should-be-disabled'); + + // Ensure the links and forms are disabled using the defaults before the + // layout is saved. + $this->assertLinksFormIframeNotInteractive(); + $this->clickLink('Save Layout'); + $this->clickLink('Manage layout'); + + // Ensure the links and forms are disabled using the defaults. + $this->assertLinksFormIframeNotInteractive(); + + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + $this->drupalGet('node/1/layout'); + + // Ensure the links and forms are also disabled in using the override. + $this->assertLinksFormIframeNotInteractive(); + } + + /** + * Adds a block in the Layout Builder. + * + * @param string $block_link_text + * The link text to add the block. + * @param string $rendered_locator + * The CSS locator to confirm the block was rendered. + */ + protected function addBlock($block_link_text, $rendered_locator) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + // Add a new block. + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#layout-builder a:contains(\'Add Block\')')); + $this->clickLink('Add Block'); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas')); + $assert_session->assertWaitOnAjaxRequest(); + + $assert_session->linkExists($block_link_text); + $this->clickLink($block_link_text); + // Wait for off-canvas dialog to reopen with block form. + $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".layout-builder-add-block")); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Add Block'); + + // Wait for block form to be rendered in the Layout Builder. + $this->assertNotEmpty($assert_session->waitForElement('css', $rendered_locator)); + } + + /** + * Checks if element is unclickable. + * + * @param \Behat\Mink\Element\NodeElement $element + * Element being checked for. + */ + protected function assertElementUnclickable(NodeElement $element) { + try { + $element->click(); + $tag_name = $element->getTagName(); + $this->fail(new FormattableMarkup("@tag_name was clickable when it shouldn't have been", ['@tag_name' => $tag_name])); + } + catch (UnknownError $e) { + $this->assertContains('is not clickable at point', $e->getMessage()); + } + } + + /** + * Asserts that forms, links, and iframes in preview are non-interactive. + */ + protected function assertLinksFormIframeNotInteractive() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->assertNotEmpty($assert_session->waitForElement('css', '#layout-builder .search-block-form')); + $searchButton = $assert_session->buttonExists('Search'); + $this->assertElementUnclickable($searchButton); + $assert_session->linkExists('Take me away'); + $this->assertElementUnclickable($page->findLink('Take me away')); + $iframe = $assert_session->elementExists('css', '#iframe-that-should-be-disabled'); + $this->assertElementUnclickable($iframe); + } + +}