diff --git a/core/core.services.yml b/core/core.services.yml index d9388365ec985291ec86670f98fcbf4aeb7c03e1..cbcccfe20c82a153098b8ff54c84034ef40fc265 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1096,6 +1096,11 @@ services: arguments: ['@title_resolver', '@renderer'] tags: - { name: render.main_content_renderer, format: drupal_dialog.off_canvas } + main_content_renderer.off_canvas_top: + class: Drupal\Core\Render\MainContent\OffCanvasRenderer + arguments: ['@title_resolver', '@renderer', 'top'] + tags: + - { name: render.main_content_renderer, format: drupal_dialog.off_canvas_top } main_content_renderer.modal: class: Drupal\Core\Render\MainContent\ModalRenderer arguments: ['@title_resolver'] diff --git a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php index da6a26e35a5278360e89f7a9699e33903079f8bd..78c406b3d8f02f504b44c857f961c9091ecda77b 100644 --- a/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php +++ b/core/lib/Drupal/Core/Ajax/OpenOffCanvasDialogCommand.php @@ -34,19 +34,22 @@ class OpenOffCanvasDialogCommand extends OpenDialogCommand { * (optional) Custom settings that will be passed to the Drupal behaviors * on the content of the dialog. If left empty, the settings will be * populated automatically from the current request. + * @param string $position + * (optional) The position to render the off-canvas dialog. */ - public function __construct($title, $content, array $dialog_options = [], $settings = NULL) { + public function __construct($title, $content, array $dialog_options = [], $settings = NULL, $position = 'side') { parent::__construct('#drupal-off-canvas', $title, $content, $dialog_options, $settings); $this->dialogOptions['modal'] = FALSE; $this->dialogOptions['autoResize'] = FALSE; $this->dialogOptions['resizable'] = 'w'; $this->dialogOptions['draggable'] = FALSE; $this->dialogOptions['drupalAutoButtons'] = FALSE; + $this->dialogOptions['drupalOffCanvasPosition'] = $position; // @todo drupal.ajax.js does not respect drupalAutoButtons properly, pass an // empty set of buttons until https://www.drupal.org/node/2793343 is in. $this->dialogOptions['buttons'] = []; if (empty($dialog_options['dialogClass'])) { - $this->dialogOptions['dialogClass'] = 'ui-dialog-off-canvas'; + $this->dialogOptions['dialogClass'] = "ui-dialog-off-canvas ui-dialog-position-$position"; } // If no width option is provided then use the default width to avoid the // dialog staying at the width of the previous instance when opened diff --git a/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php b/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php index 55bf8eb7d1aef1ed582815fa915e63e0f11f904a..b8f0e73e5d98b7835af1e640983c2c9e612c877f 100644 --- a/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/OffCanvasRenderer.php @@ -23,6 +23,13 @@ class OffCanvasRenderer extends DialogRenderer { */ protected $renderer; + /** + * The position to render the off-canvas dialog. + * + * @var string + */ + protected $position; + /** * Constructs a new OffCanvasRenderer. * @@ -30,10 +37,13 @@ class OffCanvasRenderer extends DialogRenderer { * The title resolver. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. + * @param string $position + * (optional) The position to render the off-canvas dialog. */ - public function __construct(TitleResolverInterface $title_resolver, RendererInterface $renderer) { + public function __construct(TitleResolverInterface $title_resolver, RendererInterface $renderer, $position = 'side') { parent::__construct($title_resolver); $this->renderer = $renderer; + $this->position = $position; } /** @@ -55,7 +65,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch // Determine the title: use the title provided by the main content if any, // otherwise get it from the routing information. $options = $request->request->get('dialogOptions', []); - $response->addCommand(new OpenOffCanvasDialogCommand($title, $content, $options)); + $response->addCommand(new OpenOffCanvasDialogCommand($title, $content, $options, NULL, $this->position)); return $response; } diff --git a/core/misc/dialog/off-canvas.es6.js b/core/misc/dialog/off-canvas.es6.js index 0068e44e1b1fac98b2f7fbb69936abbbe4a4b314..a4de6060ddc602b3dc5fc2cc971092a27c32035c 100644 --- a/core/misc/dialog/off-canvas.es6.js +++ b/core/misc/dialog/off-canvas.es6.js @@ -13,6 +13,19 @@ * @namespace */ Drupal.offCanvas = { + /** + * Storage for position information about the tray. + * + * @type {?String} + */ + position: null, + + /** + * The minimum height of the tray when opened at the top of the page. + * + * @type {Number} + */ + minimumHeight: 30, /** * The minimum width to use body displace needs to match the width at which @@ -75,10 +88,14 @@ }; /** - * Applies initial height to dialog based on window height. + * Applies initial height and with to dialog based depending on position. * @see http://api.jqueryui.com/dialog for all dialog options. */ - settings.height = $(window).height(); + const position = settings.drupalOffCanvasPosition; + const height = position === 'side' ? $(window).height() : settings.height; + const width = position === 'side' ? settings.width : '100%'; + settings.height = height; + settings.width = width; }, /** @@ -90,8 +107,7 @@ $('body').removeClass('js-off-canvas-dialog-open'); // Remove all *.off-canvas events Drupal.offCanvas.removeOffCanvasEvents($element); - - Drupal.offCanvas.$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, 0); + Drupal.offCanvas.resetPadding(); }, /** @@ -168,11 +184,27 @@ * Data attached to the event. */ resetSize(event) { - const offsets = displace.offsets; const $element = event.data.$element; const container = Drupal.offCanvas.getContainer($element); + const position = event.data.settings.drupalOffCanvasPosition; + + // Only remove the `data-offset-*` attribute if the value previously + // exists and the orientation is changing. + if ( + Drupal.offCanvas.position && + Drupal.offCanvas.position !== position) { + container.removeAttr(`data-offset-${Drupal.offCanvas.position}`); + } + // Set a minimum height on $element + if (position === 'top') { + $element.css('min-height', `${Drupal.offCanvas.minimumHeight}px`); + } + + displace(); + + const offsets = displace.offsets; - const topPosition = (offsets.top !== 0 ? `+${offsets.top}` : ''); + const topPosition = position === 'side' && offsets.top !== 0 ? `+${offsets.top}` : ''; const adjustedOptions = { // @see http://api.jqueryui.com/position/ position: { @@ -182,14 +214,17 @@ }, }; + const height = position === 'side' ? `${$(window).height() - (offsets.top + offsets.bottom)}px` : event.data.settings.height; container.css({ position: 'fixed', - height: `${$(window).height() - (offsets.top + offsets.bottom)}px`, + height, }); $element .dialog('option', adjustedOptions) .trigger('dialogContentResize.off-canvas'); + + Drupal.offCanvas.position = position; }, /** @@ -201,20 +236,29 @@ * Data attached to the event. */ bodyPadding(event) { - if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) { + const position = event.data.settings.drupalOffCanvasPosition; + if (position === 'side' && $('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) { return; } + Drupal.offCanvas.resetPadding(); const $element = event.data.$element; const $container = Drupal.offCanvas.getContainer($element); const $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper; const width = $container.outerWidth(); const mainCanvasPadding = $mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`); - if (width !== mainCanvasPadding) { + if (position === 'side' && width !== mainCanvasPadding) { $mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, `${width}px`); $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width); displace(); } + + const height = $container.outerHeight(); + if (position === 'top') { + $mainCanvasWrapper.css('padding-top', `${height}px`); + $container.attr('data-offset-top', height); + displace(); + } }, /** @@ -238,6 +282,15 @@ getEdge() { return document.documentElement.dir === 'rtl' ? 'left' : 'right'; }, + + /** + * Resets main canvas wrapper and toolbar padding / margin. + */ + resetPadding() { + Drupal.offCanvas.$mainCanvasWrapper.css(`padding-${Drupal.offCanvas.getEdge()}`, 0); + Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0); + displace(); + }, }; /** diff --git a/core/misc/dialog/off-canvas.js b/core/misc/dialog/off-canvas.js index 1de5f675659b458f62d9900205605a4410e9b601..85498b7c5112abf48e50d4d267723700744ef8b9 100644 --- a/core/misc/dialog/off-canvas.js +++ b/core/misc/dialog/off-canvas.js @@ -7,6 +7,10 @@ (function ($, Drupal, debounce, displace) { Drupal.offCanvas = { + position: null, + + minimumHeight: 30, + minDisplaceWidth: 768, $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'), @@ -33,7 +37,11 @@ of: window }; - settings.height = $(window).height(); + var position = settings.drupalOffCanvasPosition; + var height = position === 'side' ? $(window).height() : settings.height; + var width = position === 'side' ? settings.width : '100%'; + settings.height = height; + settings.width = width; }, beforeClose: function beforeClose(_ref2) { var $element = _ref2.$element; @@ -41,8 +49,7 @@ $('body').removeClass('js-off-canvas-dialog-open'); Drupal.offCanvas.removeOffCanvasEvents($element); - - Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0); + Drupal.offCanvas.resetPadding(); }, afterCreate: function afterCreate(_ref3) { var $element = _ref3.$element, @@ -79,11 +86,23 @@ $element.height(modalHeight - offset - scrollOffset); }, resetSize: function resetSize(event) { - var offsets = displace.offsets; var $element = event.data.$element; var container = Drupal.offCanvas.getContainer($element); + var position = event.data.settings.drupalOffCanvasPosition; + + if (Drupal.offCanvas.position && Drupal.offCanvas.position !== position) { + container.removeAttr('data-offset-' + Drupal.offCanvas.position); + } + + if (position === 'top') { + $element.css('min-height', Drupal.offCanvas.minimumHeight + 'px'); + } - var topPosition = offsets.top !== 0 ? '+' + offsets.top : ''; + displace(); + + var offsets = displace.offsets; + + var topPosition = position === 'side' && offsets.top !== 0 ? '+' + offsets.top : ''; var adjustedOptions = { position: { my: Drupal.offCanvas.getEdge() + ' top', @@ -92,34 +111,51 @@ } }; + var height = position === 'side' ? $(window).height() - (offsets.top + offsets.bottom) + 'px' : event.data.settings.height; container.css({ position: 'fixed', - height: $(window).height() - (offsets.top + offsets.bottom) + 'px' + height: height }); $element.dialog('option', adjustedOptions).trigger('dialogContentResize.off-canvas'); + + Drupal.offCanvas.position = position; }, bodyPadding: function bodyPadding(event) { - if ($('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) { + var position = event.data.settings.drupalOffCanvasPosition; + if (position === 'side' && $('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth) { return; } + Drupal.offCanvas.resetPadding(); var $element = event.data.$element; var $container = Drupal.offCanvas.getContainer($element); var $mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper; var width = $container.outerWidth(); var mainCanvasPadding = $mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge()); - if (width !== mainCanvasPadding) { + if (position === 'side' && width !== mainCanvasPadding) { $mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), width + 'px'); $container.attr('data-offset-' + Drupal.offCanvas.getEdge(), width); displace(); } + + var height = $container.outerHeight(); + if (position === 'top') { + $mainCanvasWrapper.css('padding-top', height + 'px'); + $container.attr('data-offset-top', height); + displace(); + } }, getContainer: function getContainer($element) { return $element.dialog('widget'); }, getEdge: function getEdge() { return document.documentElement.dir === 'rtl' ? 'left' : 'right'; + }, + resetPadding: function resetPadding() { + Drupal.offCanvas.$mainCanvasWrapper.css('padding-' + Drupal.offCanvas.getEdge(), 0); + Drupal.offCanvas.$mainCanvasWrapper.css('padding-top', 0); + displace(); } }; diff --git a/core/misc/dialog/off-canvas.motion.css b/core/misc/dialog/off-canvas.motion.css index b3158e988afe3d6bd582620546cfe837bacb5747..60d8d6a1dd2d29ac7dad4a83e7b28768b3e157a0 100644 --- a/core/misc/dialog/off-canvas.motion.css +++ b/core/misc/dialog/off-canvas.motion.css @@ -7,5 +7,5 @@ */ .dialog-off-canvas-main-canvas { - transition: all 0.7s ease; + transition: padding-right 0.7s ease, padding-left 0.7s ease, padding-top 0.3s ease; } diff --git a/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php b/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php index ea310fa0bdf8291132cc373924b88310d0cd78f5..fbfa5a7bdd187e0ac510bc8a7f5caa27418f61f4 100644 --- a/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php +++ b/core/modules/system/tests/modules/off_canvas_test/src/Controller/TestController.php @@ -45,17 +45,22 @@ public function thing2() { public function linksDisplay() { return [ 'off_canvas_link_1' => [ - '#title' => 'Click Me 1!', + '#title' => 'Open side panel 1', '#type' => 'link', '#url' => Url::fromRoute('off_canvas_test.thing1'), '#attributes' => [ 'class' => ['use-ajax'], 'data-dialog-type' => 'dialog', 'data-dialog-renderer' => 'off_canvas', + 'data-dialog-options' => Json::encode([ + 'classes' => [ + "ui-dialog" => "ui-corner-all side-1", + ], + ]), ], ], 'off_canvas_link_2' => [ - '#title' => 'Click Me 2!', + '#title' => 'Open side panel 2', '#type' => 'link', '#url' => Url::fromRoute('off_canvas_test.thing2'), '#attributes' => [ @@ -64,6 +69,43 @@ public function linksDisplay() { 'data-dialog-renderer' => 'off_canvas', 'data-dialog-options' => Json::encode([ 'width' => 555, + 'classes' => [ + "ui-dialog" => "ui-corner-all side-2", + ], + ]), + ], + ], + 'off_canvas_top_link_1' => [ + '#title' => 'Open top panel 1', + '#type' => 'link', + '#url' => Url::fromRoute('off_canvas_test.thing1'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas_top', + 'data-dialog-options' => Json::encode([ + 'width' => 555, + 'classes' => [ + "ui-dialog" => "ui-corner-all top-1", + ], + ]), + ], + + ], + 'off_canvas_top_link_2' => [ + '#title' => 'Open top panel 2', + '#type' => 'link', + '#url' => Url::fromRoute('off_canvas_test.thing2'), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas_top', + 'data-dialog-options' => Json::encode([ + 'height' => 421, + 'classes' => [ + "ui-dialog" => "ui-corner-all top-2", + ], + ]), ], ], diff --git a/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php b/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php index 222ebc45d7337da3acb515f20eb79273fc57eed8..c7867221231aa2353f5ad522a859f1a7c67544c6 100644 --- a/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php +++ b/core/modules/system/tests/src/Functional/Ajax/OffCanvasDialogTest.php @@ -23,8 +23,10 @@ class OffCanvasDialogTest extends BrowserTestBase { /** * Test sending AJAX requests to open and manipulate off-canvas dialog. + * + * @dataProvider dialogPosition */ - public function testDialog() { + public function testDialog($position) { // Ensure the elements render without notices or exceptions. $this->drupalGet('ajax-test/dialog'); @@ -45,8 +47,9 @@ public function testDialog() { 'resizable' => 'w', 'draggable' => FALSE, 'drupalAutoButtons' => FALSE, + 'drupalOffCanvasPosition' => $position ?: 'side', 'buttons' => [], - 'dialogClass' => 'ui-dialog-off-canvas', + 'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-' . ($position ?: 'side'), 'width' => 300, ], 'effect' => 'fade', @@ -54,9 +57,23 @@ public function testDialog() { ]; // Emulate going to the JS version of the page and check the JSON response. - $ajax_result = $this->drupalGet('ajax-test/dialog-contents', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_dialog.off_canvas']]); + $wrapper_format = $position && ($position !== 'side') ? 'drupal_dialog.off_canvas_' . $position : 'drupal_dialog.off_canvas'; + $ajax_result = $this->drupalGet('ajax-test/dialog-contents', ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => $wrapper_format]]); $ajax_result = Json::decode($ajax_result); - $this->assertEqual($off_canvas_expected_response, $ajax_result[3], 'off-canvas dialog JSON response matches.'); + $this->assertEquals($off_canvas_expected_response, $ajax_result[3], 'off-canvas dialog JSON response matches.'); + } + + /** + * The data provider for potential dialog positions. + * + * @return array + */ + public static function dialogPosition() { + return [ + [NULL], + ['side'], + ['top'], + ]; } } diff --git a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php index 0cadecee5b06131780f953cb2ad190f8c8f30b71..7719634118c45d83a6db3260f6c3f8a56772dc5e 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php +++ b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTest.php @@ -9,6 +9,15 @@ */ class OffCanvasTest extends OffCanvasTestBase { + /** + * Stores to the class that should be on the last dialog. + * + * @var string + * + * @see \Drupal\off_canvas_test\Controller\TestController::linksDisplay. + */ + protected $lastDialogClass; + /** * {@inheritdoc} */ @@ -18,60 +27,83 @@ class OffCanvasTest extends OffCanvasTestBase { /** * Tests that non-contextual links will work with the off-canvas dialog. + * + * @dataProvider themeDataProvider */ - public function testOffCanvasLinks() { - // Test the same functionality on multiple themes. - foreach ($this->getTestThemes() as $theme) { - $this->enableTheme($theme); - $this->drupalGet('/off-canvas-test-links'); + public function testOffCanvasLinks($theme) { + $this->enableTheme($theme); + $this->drupalGet('/off-canvas-test-links'); - $page = $this->getSession()->getPage(); - $web_assert = $this->assertSession(); - - // Make sure off-canvas dialog is on page when first loaded. - $web_assert->elementNotExists('css', '#drupal-off-canvas'); + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); - // Check opening and closing with two separate links. - // Make sure tray updates to new content. - // Check the first link again to make sure the empty title class is - // removed. - foreach (['1', '2', '1'] as $link_index) { - // Click the first test like that should open the page. - $page->clickLink("Click Me $link_index!"); + // Make sure off-canvas dialog is on page when first loaded. + $web_assert->elementNotExists('css', '#drupal-off-canvas'); + + // Check opening and closing with two separate links. + // Make sure tray updates to new content. + // Check the first link again to make sure the empty title class is + // removed. + foreach (['1', '2', '1'] as $link_index) { + $this->assertOffCanvasDialog($link_index, 'side'); + $header_text = $this->getOffCanvasDialog()->find('css', '.ui-dialog-title')->getText(); + if ($link_index == '2') { + // Check no title behavior. + $web_assert->elementExists('css', '.ui-dialog-empty-title'); + $this->assertEquals("\xc2\xa0", $header_text); + + $style = $page->find('css', '.ui-dialog-off-canvas')->getAttribute('style'); + $this->assertTrue(strstr($style, 'width: 555px;') !== FALSE, 'Dialog width respected.'); + $page->clickLink("Open side panel 1"); $this->waitForOffCanvasToOpen(); + $style = $page->find('css', '.ui-dialog-off-canvas')->getAttribute('style'); + $this->assertTrue(strstr($style, 'width: 555px;') === FALSE, 'Dialog width reset to default.'); + } + else { + // Check that header is correct. + $this->assertEquals("Thing $link_index", $header_text); + $web_assert->elementNotExists('css', '.ui-dialog-empty-title'); + } + } + + // Test the off_canvas_top tray. + foreach ([1, 2] as $link_index) { + $this->assertOffCanvasDialog($link_index, 'top'); - // Check that the canvas is not on the page. - $web_assert->elementExists('css', '#drupal-off-canvas'); - // Check that response text is on page. - $web_assert->pageTextContains("Thing $link_index says hello"); - $off_canvas_tray = $this->getOffCanvasDialog(); - - // Check that tray is visible. - $this->assertEquals(TRUE, $off_canvas_tray->isVisible()); - $header_text = $off_canvas_tray->find('css', '.ui-dialog-title')->getText(); - - $tray_text = $off_canvas_tray->findById('drupal-off-canvas')->getText(); - $this->assertEquals("Thing $link_index says hello", $tray_text); - - if ($link_index == '2') { - // Check no title behavior. - $web_assert->elementExists('css', '.ui-dialog-empty-title'); - $this->assertEquals("\xc2\xa0", $header_text); - - $style = $page->find('css', '.ui-dialog-off-canvas')->getAttribute('style'); - $this->assertTrue(strstr($style, 'width: 555px;') !== FALSE, 'Dialog width respected.'); - $page->clickLink("Click Me 1!"); - $this->waitForOffCanvasToOpen(); - $style = $page->find('css', '.ui-dialog-off-canvas')->getAttribute('style'); - $this->assertTrue(strstr($style, 'width: 555px;') === FALSE, 'Dialog width reset to default.'); - } - else { - // Check that header is correct. - $this->assertEquals("Thing $link_index", $header_text); - $web_assert->elementNotExists('css', '.ui-dialog-empty-title'); - } + $style = $page->find('css', '.ui-dialog-off-canvas')->getAttribute('style'); + if ($link_index === 1) { + $this->assertTrue((bool) strstr($style, 'height: auto;')); + } + else { + $this->assertTrue((bool) strstr($style, 'height: 421px;')); } } + + // Ensure an off-canvas link opened from inside the off-canvas dialog will + // work. + $this->drupalGet('/off-canvas-test-links'); + $page->clickLink('Display more links!'); + $this->waitForOffCanvasToOpen(); + $web_assert->linkExists('Off_canvas link!'); + // Click off-canvas link inside off-canvas dialog + $page->clickLink('Off_canvas link!'); + /* @var \Behat\Mink\Element\NodeElement $dialog */ + $this->waitForOffCanvasToOpen(); + $web_assert->elementTextContains('css', '.ui-dialog[aria-describedby="drupal-off-canvas"]', 'Thing 2 says hello'); + + // Ensure an off-canvas link opened from inside the off-canvas dialog will + // work after another dialog has been opened. + $this->drupalGet('/off-canvas-test-links'); + $page->clickLink("Open side panel 1"); + $this->waitForOffCanvasToOpen(); + $page->clickLink('Display more links!'); + $this->waitForOffCanvasToOpen(); + $web_assert->linkExists('Off_canvas link!'); + // Click off-canvas link inside off-canvas dialog + $page->clickLink('Off_canvas link!'); + /* @var \Behat\Mink\Element\NodeElement $dialog */ + $this->waitForOffCanvasToOpen(); + $web_assert->elementTextContains('css', '.ui-dialog[aria-describedby="drupal-off-canvas"]', 'Thing 2 says hello'); } /** @@ -91,7 +123,7 @@ public function testNarrowWidth() { $this->getSession()->resizeWindow($narrow_width_breakpoint + $offset, $height); $this->drupalGet('/off-canvas-test-links'); $this->assertFalse($page->find('css', '.dialog-off-canvas-main-canvas')->hasAttribute('style'), 'Body not padded on wide page load.'); - $page->clickLink("Click Me 1!"); + $page->clickLink("Open side panel 1"); $this->waitForOffCanvasToOpen(); // Check that the main canvas is padded when page is not narrow width and // tray is open. @@ -101,10 +133,40 @@ public function testNarrowWidth() { $this->getSession()->resizeWindow($narrow_width_breakpoint - $offset, $height); $this->drupalGet('/off-canvas-test-links'); $this->assertFalse($page->find('css', '.dialog-off-canvas-main-canvas')->hasAttribute('style'), 'Body not padded on narrow page load.'); - $page->clickLink("Click Me 1!"); + $page->clickLink("Open side panel 1"); $this->waitForOffCanvasToOpen(); $this->assertFalse($page->find('css', '.dialog-off-canvas-main-canvas')->hasAttribute('style'), 'Body not padded on narrow page with tray open.'); } } + /** + * @param int $link_index + * The index of the link to test. + * @param string $position + * The position of the dialog to test. + */ + protected function assertOffCanvasDialog($link_index, $position) { + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + $link_text = "Open $position panel $link_index"; + + // Click the first test like that should open the page. + $page->clickLink($link_text); + if ($this->lastDialogClass) { + $this->waitForNoElement('.' . $this->lastDialogClass); + } + $this->waitForOffCanvasToOpen($position); + $this->lastDialogClass = "$position-$link_index"; + + // Check that response text is on page. + $web_assert->pageTextContains("Thing $link_index says hello"); + $off_canvas_tray = $this->getOffCanvasDialog(); + + // Check that tray is visible. + $this->assertEquals(TRUE, $off_canvas_tray->isVisible()); + + $tray_text = $off_canvas_tray->findById('drupal-off-canvas')->getText(); + $this->assertEquals("Thing $link_index says hello", $tray_text); + } + } diff --git a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php index d3f446cf6a23d12ebd9cd9af38cfb0054fdc8b32..293d9f70a04d5fec9244169ec66483a43d8c14b0 100644 --- a/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php +++ b/core/modules/system/tests/src/FunctionalJavascript/OffCanvasTestBase.php @@ -60,14 +60,21 @@ protected function enableTheme($theme) { /** * Waits for off-canvas dialog to open. + * + * @param string $position + * The position of the dialog. + * + * @throws \Behat\Mink\Exception\ElementNotFoundException */ - protected function waitForOffCanvasToOpen() { + protected function waitForOffCanvasToOpen($position = 'side') { $web_assert = $this->assertSession(); // Wait just slightly longer than the off-canvas dialog CSS animation. // @see core/misc/dialog/off-canvas.motion.css $this->getSession()->wait(800); $web_assert->assertWaitOnAjaxRequest(); $this->assertElementVisibleAfterWait('css', '#drupal-off-canvas'); + // Check that the canvas is positioned on the side. + $web_assert->elementExists('css', '.ui-dialog-position-' . $position); } /** @@ -128,4 +135,18 @@ protected function assertElementVisibleAfterWait($selector, $locator, $timeout = $this->assertNotEmpty($this->assertSession()->waitForElementVisible($selector, $locator, $timeout)); } + /** + * Dataprovider that returns theme name as the sole argument. + */ + public function themeDataProvider() { + $themes = $this->getTestThemes(); + $data = []; + foreach ($themes as $theme) { + $data[$theme] = [ + $theme, + ]; + } + return $data; + } + } diff --git a/core/modules/toolbar/js/toolbar.es6.js b/core/modules/toolbar/js/toolbar.es6.js index 4149d27da7c7b2e7c9787cb9128df5315fe9f360..3113776fd3a5cf89c975e41f3f53d95c88e8154d 100644 --- a/core/modules/toolbar/js/toolbar.es6.js +++ b/core/modules/toolbar/js/toolbar.es6.js @@ -143,6 +143,27 @@ activeTab: $('.toolbar-bar .toolbar-tab:not(.home-toolbar-tab) a').get(0), }); } + + $(window).on({ + 'dialog:aftercreate': (event, dialog, $element, settings) => { + const $toolbar = $('#toolbar-bar'); + $toolbar.css('margin-top', '0'); + + // When off-canvas is positioned in top, toolbar has to be moved down. + if (settings.drupalOffCanvasPosition === 'top') { + const height = Drupal.offCanvas.getContainer($element).outerHeight(); + $toolbar.css('margin-top', `${height}px`); + + $element.on('dialogContentResize.off-canvas', () => { + const newHeight = Drupal.offCanvas.getContainer($element).outerHeight(); + $toolbar.css('margin-top', `${newHeight}px`); + }); + } + }, + 'dialog:beforeclose': () => { + $('#toolbar-bar').css('margin-top', '0'); + }, + }); }); }, }; diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js index 7246420f8888ddc6cbe7407f1b39a890629d3bc2..f705a9b0153cb2ea22b7e9a3b729e895ffc18e97 100644 --- a/core/modules/toolbar/js/toolbar.js +++ b/core/modules/toolbar/js/toolbar.js @@ -97,6 +97,26 @@ activeTab: $('.toolbar-bar .toolbar-tab:not(.home-toolbar-tab) a').get(0) }); } + + $(window).on({ + 'dialog:aftercreate': function dialogAftercreate(event, dialog, $element, settings) { + var $toolbar = $('#toolbar-bar'); + $toolbar.css('margin-top', '0'); + + if (settings.drupalOffCanvasPosition === 'top') { + var height = Drupal.offCanvas.getContainer($element).outerHeight(); + $toolbar.css('margin-top', height + 'px'); + + $element.on('dialogContentResize.off-canvas', function () { + var newHeight = Drupal.offCanvas.getContainer($element).outerHeight(); + $toolbar.css('margin-top', newHeight + 'px'); + }); + } + }, + 'dialog:beforeclose': function dialogBeforeclose() { + $('#toolbar-bar').css('margin-top', '0'); + } + }); }); } }; diff --git a/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php b/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php index e2d933a6577d6e1a563efec19ecbf37038c2582e..eca0c58f8a99f7c543aa1fe15b1a8afd0c56cafd 100644 --- a/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php +++ b/core/tests/Drupal/Tests/Core/Ajax/OpenOffCanvasDialogCommandTest.php @@ -13,9 +13,11 @@ class OpenOffCanvasDialogCommandTest extends UnitTestCase { /** * @covers ::render + * + * @dataProvider dialogPosition */ - public function testRender() { - $command = new OpenOffCanvasDialogCommand('Title', '

Text!

', ['url' => 'example']); + public function testRender($position) { + $command = new OpenOffCanvasDialogCommand('Title', '

Text!

', ['url' => 'example'], NULL, $position); $expected = [ 'command' => 'openDialog', @@ -31,8 +33,9 @@ public function testRender() { 'draggable' => FALSE, 'drupalAutoButtons' => FALSE, 'buttons' => [], - 'dialogClass' => 'ui-dialog-off-canvas', + 'dialogClass' => 'ui-dialog-off-canvas ui-dialog-position-' . $position, 'width' => 300, + 'drupalOffCanvasPosition' => $position, ], 'effect' => 'fade', 'speed' => 1000, @@ -40,4 +43,16 @@ public function testRender() { $this->assertEquals($expected, $command->render()); } + /** + * The data provider for potential dialog positions. + * + * @return array + */ + public static function dialogPosition() { + return [ + ['side'], + ['top'], + ]; + } + } diff --git a/core/themes/stable/css/core/dialog/off-canvas.motion.css b/core/themes/stable/css/core/dialog/off-canvas.motion.css index b3158e988afe3d6bd582620546cfe837bacb5747..60d8d6a1dd2d29ac7dad4a83e7b28768b3e157a0 100644 --- a/core/themes/stable/css/core/dialog/off-canvas.motion.css +++ b/core/themes/stable/css/core/dialog/off-canvas.motion.css @@ -7,5 +7,5 @@ */ .dialog-off-canvas-main-canvas { - transition: all 0.7s ease; + transition: padding-right 0.7s ease, padding-left 0.7s ease, padding-top 0.3s ease; }