Skip to content

Commit ef68ad6

Browse files
committed
Issue #2973921 by tedbow, bnjmnm, yogeshmpawar, Kristen Pol, lauriii, alwaysworking, xjm, andrewmacpherson: Interactive controls inside preview block in the Layout Builder form should be disabled
1 parent 72e0a4d commit ef68ad6

12 files changed

+341
-4
lines changed

core/modules/layout_builder/css/layout-builder.css

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,6 @@
5151
text-align: center;
5252
}
5353

54-
.layout-section .layout-builder--layout__region .block {
55-
padding: 1.5em;
56-
}
57-
5854
.layout-section .remove-section {
5955
position: relative;
6056
background: url(../../../misc/icons/bebebe/ex.svg) #fff center center / 16px 16px no-repeat;
@@ -75,6 +71,14 @@
7571
background-image: url(../../../misc/icons/787878/ex.svg);
7672
}
7773

74+
.layout-builder-block {
75+
padding: 1.5em;
76+
}
77+
78+
.layout-builder-block [tabindex="-1"] {
79+
pointer-events: none;
80+
}
81+
7882
#drupal-off-canvas .layout-selection li {
7983
display: block;
8084
padding-bottom: 1em;

core/modules/layout_builder/js/layout-builder.es6.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,39 @@
160160
});
161161
},
162162
};
163+
164+
/**
165+
* Disables interactive elements in previewed blocks.
166+
*
167+
* @type {Drupal~behavior}
168+
*
169+
* @prop {Drupal~behaviorAttach} attach
170+
* Attach disabling interactive elements behavior to the Layout Builder UI.
171+
*/
172+
behaviors.layoutBuilderDisableInteractiveElements = {
173+
attach() {
174+
// Disable interactive elements inside preview blocks.
175+
const $blocks = $('#layout-builder [data-layout-block-uuid]');
176+
$blocks.find('input, textarea, select').prop('disabled', true);
177+
$blocks.find('a').on('click mouseup touchstart', e => {
178+
e.preventDefault();
179+
e.stopPropagation();
180+
});
181+
182+
/*
183+
* In preview blocks, remove from the tabbing order all input elements
184+
* and elements specifically assigned a tab index, other than those
185+
* related to contextual links.
186+
*/
187+
$blocks
188+
.find(
189+
'button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"]):not(.tabbable)',
190+
)
191+
.not(
192+
(index, element) =>
193+
$(element).closest('[data-contextual-id]').length > 0,
194+
)
195+
.attr('tabindex', -1);
196+
},
197+
};
163198
})(jQuery, Drupal);

core/modules/layout_builder/js/layout-builder.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,19 @@
7676
});
7777
}
7878
};
79+
80+
behaviors.layoutBuilderDisableInteractiveElements = {
81+
attach: function attach() {
82+
var $blocks = $('#layout-builder [data-layout-block-uuid]');
83+
$blocks.find('input, textarea, select').prop('disabled', true);
84+
$blocks.find('a').on('click mouseup touchstart', function (e) {
85+
e.preventDefault();
86+
e.stopPropagation();
87+
});
88+
89+
$blocks.find('button, [href], input, select, textarea, iframe, [tabindex]:not([tabindex="-1"]):not(.tabbable)').not(function (index, element) {
90+
return $(element).closest('[data-contextual-id]').length > 0;
91+
}).attr('tabindex', -1);
92+
}
93+
};
7994
})(jQuery, Drupal);

core/modules/layout_builder/layout_builder.module

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function layout_builder_help($route_name, RouteMatchInterface $route_match) {
4040
else {
4141
$output .= '<p>' . t('To manage other areas of the page, use the block administration page.') . '</p>';
4242
}
43+
$output .= '<p>' . t('Forms and links inside the content of the layout builder tool have been disabled.') . '</p>';
4344
return $output;
4445
}
4546

core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
106106
'#base_plugin_id' => $block->getBaseId(),
107107
'#derivative_plugin_id' => $block->getDerivativeId(),
108108
'#weight' => $event->getComponent()->getWeight(),
109+
'#attributes' => ['class' => ['layout-builder-block']],
109110
'content' => $content,
110111
];
111112
if ($is_content_empty && $is_placeholder_ready) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Remove all transitions for testing.
3+
*/
4+
* {
5+
/* CSS transitions. */
6+
transition: none !important;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# @todo Remove this module & its usages in https://www.drupal.org/node/2901792.
2+
name: 'Layout Builder Test Disable Animations'
3+
type: module
4+
description: 'Disables CSS animations for tests '
5+
package: Testing
6+
version: VERSION
7+
core: 8.x
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
layout_builder.disable_css_transitions:
2+
css:
3+
component:
4+
css/layout_builder_test_css_transitions.test.css: {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* Attaches CSS to disable animations.
6+
*
7+
* CSS animations cause intermittent errors in some tests.
8+
*/
9+
10+
/**
11+
* Implements hook_page_attachments().
12+
*/
13+
function layout_builder_test_css_transitions_page_attachments(array &$attachments) {
14+
// Unconditionally attach an asset to the page.
15+
$attachments['#attached']['library'][] = 'layout_builder_test_css_transitions/layout_builder.disable_css_transitions';
16+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<?php
2+
3+
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
4+
5+
use Behat\Mink\Element\NodeElement;
6+
use Drupal\block_content\Entity\BlockContent;
7+
use Drupal\block_content\Entity\BlockContentType;
8+
use Drupal\Component\Render\FormattableMarkup;
9+
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
10+
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
11+
use WebDriver\Exception\UnknownError;
12+
13+
/**
14+
* Tests the Layout Builder disables interactions of rendered blocks.
15+
*
16+
* @group layout_builder
17+
*/
18+
class LayoutBuilderDisableInteractionsTest extends WebDriverTestBase {
19+
20+
use ContextualLinkClickTrait;
21+
22+
/**
23+
* {@inheritdoc}
24+
*/
25+
protected static $modules = [
26+
'block',
27+
'block_content',
28+
'filter',
29+
'filter_test',
30+
'layout_builder',
31+
'node',
32+
'search',
33+
'contextual',
34+
'layout_builder_test_css_transitions',
35+
];
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
protected function setUp() {
41+
parent::setUp();
42+
43+
// @todo The Layout Builder UI relies on local tasks; fix in
44+
// https://www.drupal.org/project/drupal/issues/2917777.
45+
$this->drupalPlaceBlock('local_tasks_block');
46+
47+
$this->createContentType(['type' => 'bundle_with_section_field']);
48+
$this->createNode([
49+
'type' => 'bundle_with_section_field',
50+
'title' => 'The first node title',
51+
'body' => [
52+
[
53+
'value' => 'Node body',
54+
],
55+
],
56+
]);
57+
58+
$bundle = BlockContentType::create([
59+
'id' => 'basic',
60+
'label' => 'Basic block',
61+
'revision' => 1,
62+
]);
63+
$bundle->save();
64+
block_content_add_body_field($bundle->id());
65+
66+
BlockContent::create([
67+
'type' => 'basic',
68+
'info' => 'Block with link',
69+
'body' => [
70+
// Create a link that should be disabled in Layout Builder preview.
71+
'value' => '<a id="link-that-should-be-disabled" href="/search/node">Take me away</a>',
72+
'format' => 'full_html',
73+
],
74+
])->save();
75+
76+
BlockContent::create([
77+
'type' => 'basic',
78+
'info' => 'Block with iframe',
79+
'body' => [
80+
// Add iframe that should be non-interactive in Layout Builder preview.
81+
'value' => '<iframe id="iframe-that-should-be-disabled" width="560" height="315" src="https://www.youtube.com/embed/gODZzSOelss" frameborder="0"></iframe>',
82+
'format' => 'full_html',
83+
],
84+
])->save();
85+
}
86+
87+
/**
88+
* Tests that forms and links are disabled in the Layout Builder preview.
89+
*/
90+
public function testFormsLinksDisabled() {
91+
$assert_session = $this->assertSession();
92+
$page = $this->getSession()->getPage();
93+
94+
$this->drupalLogin($this->drupalCreateUser([
95+
'configure any layout',
96+
'administer node display',
97+
'administer node fields',
98+
'search content',
99+
'access contextual links',
100+
]));
101+
102+
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
103+
104+
$this->drupalPostForm("$field_ui_prefix/display", ['layout[enabled]' => TRUE], 'Save');
105+
$assert_session->linkExists('Manage layout');
106+
$this->clickLink('Manage layout');
107+
108+
// Add a block with a form, another with a link, and one with an iframe.
109+
$this->addBlock('Search form', '#layout-builder .search-block-form');
110+
$this->addBlock('Block with link', '#link-that-should-be-disabled');
111+
$this->addBlock('Block with iframe', '#iframe-that-should-be-disabled');
112+
113+
// Ensure the links and forms are disabled using the defaults before the
114+
// layout is saved.
115+
$this->assertLinksFormIframeNotInteractive();
116+
117+
$page->pressButton('Save layout');
118+
$this->clickLink('Manage layout');
119+
120+
// Ensure the links and forms are disabled using the defaults.
121+
$this->assertLinksFormIframeNotInteractive();
122+
123+
// Ensure contextual links were not disabled.
124+
$this->assertContextualLinksClickable();
125+
126+
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save');
127+
$this->drupalGet('node/1/layout');
128+
129+
// Ensure the links and forms are also disabled in using the override.
130+
$this->assertLinksFormIframeNotInteractive();
131+
132+
// Ensure contextual links were not disabled.
133+
$this->assertContextualLinksClickable();
134+
}
135+
136+
/**
137+
* Adds a block in the Layout Builder.
138+
*
139+
* @param string $block_link_text
140+
* The link text to add the block.
141+
* @param string $rendered_locator
142+
* The CSS locator to confirm the block was rendered.
143+
*/
144+
protected function addBlock($block_link_text, $rendered_locator) {
145+
$assert_session = $this->assertSession();
146+
$page = $this->getSession()->getPage();
147+
148+
// Add a new block.
149+
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#layout-builder a:contains(\'Add Block\')'));
150+
$this->clickLink('Add Block');
151+
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
152+
$assert_session->assertWaitOnAjaxRequest();
153+
154+
$assert_session->linkExists($block_link_text);
155+
$this->clickLink($block_link_text);
156+
157+
// Wait for off-canvas dialog to reopen with block form.
158+
$this->assertNotEmpty($assert_session->waitForElementVisible('css', ".layout-builder-add-block"));
159+
$assert_session->assertWaitOnAjaxRequest();
160+
$page->pressButton('Add Block');
161+
162+
// Wait for block form to be rendered in the Layout Builder.
163+
$this->assertNotEmpty($assert_session->waitForElement('css', $rendered_locator));
164+
}
165+
166+
/**
167+
* Checks if element is unclickable.
168+
*
169+
* @param \Behat\Mink\Element\NodeElement $element
170+
* Element being checked for.
171+
*/
172+
protected function assertElementUnclickable(NodeElement $element) {
173+
try {
174+
$element->click();
175+
$tag_name = $element->getTagName();
176+
$this->fail(new FormattableMarkup("@tag_name was clickable when it shouldn't have been", ['@tag_name' => $tag_name]));
177+
}
178+
catch (UnknownError $e) {
179+
$this->assertContains('is not clickable at point', $e->getMessage());
180+
}
181+
}
182+
183+
/**
184+
* Asserts that forms, links, and iframes in preview are non-interactive.
185+
*/
186+
protected function assertLinksFormIframeNotInteractive() {
187+
$assert_session = $this->assertSession();
188+
$page = $this->getSession()->getPage();
189+
190+
$this->assertNotEmpty($assert_session->waitForElement('css', '.block-search'));
191+
$searchButton = $assert_session->buttonExists('Search');
192+
$this->assertElementUnclickable($searchButton);
193+
$assert_session->linkExists('Take me away');
194+
$this->assertElementUnclickable($page->findLink('Take me away'));
195+
$iframe = $assert_session->elementExists('css', '#iframe-that-should-be-disabled');
196+
$this->assertElementUnclickable($iframe);
197+
}
198+
199+
/**
200+
* Confirms that Layout Builder contextual links remain active.
201+
*/
202+
protected function assertContextualLinksClickable() {
203+
$assert_session = $this->assertSession();
204+
$page = $this->getSession()->getPage();
205+
$this->drupalGet($this->getUrl());
206+
207+
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody [data-contextual-id^="layout_builder_block"]', 'Configure');
208+
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ui-dialog-titlebar [title="Close"]'));
209+
$page->pressButton('Close');
210+
$this->assertNoElementAfterWait('#drupal-off-canvas');
211+
212+
// Run the steps a second time after closing dialog, which reverses the
213+
// order that behaviors.layoutBuilderDisableInteractiveElements and
214+
// contextual link initialization occurs.
215+
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody [data-contextual-id^="layout_builder_block"]', 'Configure');
216+
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
217+
}
218+
219+
/**
220+
* Waits for an element to be removed from the page.
221+
*
222+
* @param string $selector
223+
* CSS selector.
224+
* @param int $timeout
225+
* (optional) Timeout in milliseconds, defaults to 10000.
226+
* @param string $message
227+
* (optional) Custom message to display with the assertion.
228+
*
229+
* @todo: Remove after https://www.drupal.org/project/drupal/issues/2892440
230+
*/
231+
public function assertNoElementAfterWait($selector, $timeout = 10000, $message = '') {
232+
$page = $this->getSession()->getPage();
233+
if ($message === '') {
234+
$message = "Element '$selector' was not on the page after wait.";
235+
}
236+
$this->assertTrue($page->waitFor($timeout / 1000, function () use ($page, $selector) {
237+
return empty($page->find('css', $selector));
238+
}), $message);
239+
}
240+
241+
}

0 commit comments

Comments
 (0)