Issue #3086096 by oknate, phenaproxima, tedbow, Wim Leers, andrewmacpherson, droplet, bnjmnm: Add a generic Ajax Message command
parent
f702e8c453
commit
7dde35cfd0
|
@ -7,6 +7,15 @@ use Drupal\Core\Asset\AttachedAssets;
|
||||||
/**
|
/**
|
||||||
* AJAX command for a JavaScript Drupal.announce() call.
|
* AJAX command for a JavaScript Drupal.announce() call.
|
||||||
*
|
*
|
||||||
|
* Developers should be extra careful if this command and
|
||||||
|
* \Drupal\Core\Ajax\MessageCommand are included in the same response. By
|
||||||
|
* default, MessageCommmand will also call Drupal.announce() and announce the
|
||||||
|
* message to the screen reader (unless the option to suppress announcements is
|
||||||
|
* passed to the constructor). Manual testing with a screen reader is strongly
|
||||||
|
* recommended.
|
||||||
|
*
|
||||||
|
* @see \Drupal\Core\Ajax\MessageCommand
|
||||||
|
*
|
||||||
* @ingroup ajax
|
* @ingroup ajax
|
||||||
*/
|
*/
|
||||||
class AnnounceCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
|
class AnnounceCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\Core\Ajax;
|
||||||
|
|
||||||
|
use Drupal\Core\Asset\AttachedAssets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX command for a JavaScript Drupal.message() call.
|
||||||
|
*
|
||||||
|
* Developers should be extra careful if this command and
|
||||||
|
* \Drupal\Core\Ajax\AnnounceCommand are included in the same response. Unless
|
||||||
|
* the `announce` option is set to an empty string (''), this command will
|
||||||
|
* result in the message being announced to screen readers. When combined with
|
||||||
|
* AnnounceCommand, this may result in unexpected behavior. Manual testing with
|
||||||
|
* a screen reader is strongly recommended.
|
||||||
|
*
|
||||||
|
* Here are examples of how to suppress announcements:
|
||||||
|
* @code
|
||||||
|
* $command = new MessageCommand("I won't be announced", NULL, [
|
||||||
|
* 'announce' => '',
|
||||||
|
* ]);
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @see \Drupal\Core\Ajax\AnnounceCommand
|
||||||
|
*
|
||||||
|
* @ingroup ajax
|
||||||
|
*/
|
||||||
|
class MessageCommand implements CommandInterface, CommandWithAttachedAssetsInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message text.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to clear previous messages.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $clearPrevious;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query selector for the element the message will appear in.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $wrapperQuerySelector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options passed to Drupal.message().add().
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a MessageCommand object.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* The text of the message.
|
||||||
|
* @param string|null $wrapper_query_selector
|
||||||
|
* The query selector of the element to display messages in when they
|
||||||
|
* should be displayed somewhere other than the default.
|
||||||
|
* @see Drupal.Message.defaultWrapper()
|
||||||
|
* @param array $options
|
||||||
|
* The options passed to Drupal.message().add().
|
||||||
|
* @param bool $clear_previous
|
||||||
|
* If TRUE, previous messages will be cleared first.
|
||||||
|
*/
|
||||||
|
public function __construct($message, $wrapper_query_selector = NULL, array $options = [], $clear_previous = TRUE) {
|
||||||
|
$this->message = $message;
|
||||||
|
$this->wrapperQuerySelector = $wrapper_query_selector;
|
||||||
|
$this->options = $options;
|
||||||
|
$this->clearPrevious = $clear_previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function render() {
|
||||||
|
return [
|
||||||
|
'command' => 'message',
|
||||||
|
'message' => $this->message,
|
||||||
|
'messageWrapperQuerySelector' => $this->wrapperQuerySelector,
|
||||||
|
'messageOptions' => $this->options,
|
||||||
|
'clearPrevious' => $this->clearPrevious,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getAttachedAssets() {
|
||||||
|
$assets = new AttachedAssets();
|
||||||
|
$assets->setLibraries(['core/drupal.message']);
|
||||||
|
return $assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1562,5 +1562,31 @@
|
||||||
} while (match);
|
} while (match);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to add a message to the message area.
|
||||||
|
*
|
||||||
|
* @param {Drupal.Ajax} [ajax]
|
||||||
|
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
|
||||||
|
* @param {object} response
|
||||||
|
* The response from the Ajax request.
|
||||||
|
* @param {string} response.messageWrapperQuerySelector
|
||||||
|
* The zone where to add the message. If null, the default will be used.
|
||||||
|
* @param {string} response.message
|
||||||
|
* The message text.
|
||||||
|
* @param {string} response.messageOptions
|
||||||
|
* The options argument for Drupal.Message().add().
|
||||||
|
* @param {bool} response.clearPrevious
|
||||||
|
* If true, clear previous messages.
|
||||||
|
*/
|
||||||
|
message(ajax, response) {
|
||||||
|
const messages = new Drupal.Message(
|
||||||
|
document.querySelector(response.messageWrapperQuerySelector),
|
||||||
|
);
|
||||||
|
if (response.clearPrevious) {
|
||||||
|
messages.clear();
|
||||||
|
}
|
||||||
|
messages.add(response.message, response.messageOptions);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
})(jQuery, window, Drupal, drupalSettings);
|
})(jQuery, window, Drupal, drupalSettings);
|
||||||
|
|
|
@ -639,6 +639,13 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
|
||||||
document.styleSheets[0].addImport(match[1]);
|
document.styleSheets[0].addImport(match[1]);
|
||||||
} while (match);
|
} while (match);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
message: function message(ajax, response) {
|
||||||
|
var messages = new Drupal.Message(document.querySelector(response.messageWrapperQuerySelector));
|
||||||
|
if (response.clearPrevious) {
|
||||||
|
messages.clear();
|
||||||
|
}
|
||||||
|
messages.add(response.message, response.messageOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})(jQuery, window, Drupal, drupalSettings);
|
})(jQuery, window, Drupal, drupalSettings);
|
|
@ -77,3 +77,11 @@ ajax_test.render_error:
|
||||||
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderError'
|
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderError'
|
||||||
requirements:
|
requirements:
|
||||||
_access: 'TRUE'
|
_access: 'TRUE'
|
||||||
|
|
||||||
|
ajax_test.message_form:
|
||||||
|
path: '/ajax-test/message'
|
||||||
|
defaults:
|
||||||
|
_title: 'Ajax Message Form'
|
||||||
|
_form: '\Drupal\ajax_test\Form\AjaxTestMessageCommandForm'
|
||||||
|
requirements:
|
||||||
|
_access: 'TRUE'
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\ajax_test\Form;
|
||||||
|
|
||||||
|
use Drupal\Core\Ajax\AjaxResponse;
|
||||||
|
use Drupal\Core\Ajax\MessageCommand;
|
||||||
|
use Drupal\Core\Form\FormInterface;
|
||||||
|
use Drupal\Core\Form\FormStateInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form for testing AJAX MessageCommand.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class AjaxTestMessageCommandForm implements FormInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getFormId() {
|
||||||
|
return 'ajax_test_message_command_form';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||||
|
$form['alternate-message-container'] = [
|
||||||
|
'#type' => 'container',
|
||||||
|
'#id' => 'alternate-message-container',
|
||||||
|
];
|
||||||
|
$form['button_default'] = [
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#name' => 'makedefaultmessage',
|
||||||
|
'#value' => 'Make Message In Default Location',
|
||||||
|
'#ajax' => [
|
||||||
|
'callback' => '::makeMessageDefault',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$form['button_alternate'] = [
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#name' => 'makealternatemessage',
|
||||||
|
'#value' => 'Make Message In Alternate Location',
|
||||||
|
'#ajax' => [
|
||||||
|
'callback' => '::makeMessageAlternate',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$form['button_warning'] = [
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#name' => 'makewarningmessage',
|
||||||
|
'#value' => 'Make Warning Message',
|
||||||
|
'#ajax' => [
|
||||||
|
'callback' => '::makeMessageWarning',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for testing MessageCommand with default settings.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||||
|
* The AJAX response.
|
||||||
|
*/
|
||||||
|
public function makeMessageDefault() {
|
||||||
|
$response = new AjaxResponse();
|
||||||
|
return $response->addCommand(new MessageCommand('I am a message in the default location.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for testing MessageCommand using an alternate message location.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||||
|
* The AJAX response.
|
||||||
|
*/
|
||||||
|
public function makeMessageAlternate() {
|
||||||
|
$response = new AjaxResponse();
|
||||||
|
return $response->addCommand(new MessageCommand('I am a message in an alternate location.', '#alternate-message-container', [], FALSE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for testing MessageCommand with warning status.
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||||
|
* The AJAX response.
|
||||||
|
*/
|
||||||
|
public function makeMessageWarning() {
|
||||||
|
$response = new AjaxResponse();
|
||||||
|
return $response->addCommand(new MessageCommand('I am a warning message in the default location.', NULL, ['type' => 'warning', 'announce' => '']));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Drupal\FunctionalJavascriptTests\Ajax;
|
||||||
|
|
||||||
|
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests adding messages via AJAX command.
|
||||||
|
*
|
||||||
|
* @group Ajax
|
||||||
|
*/
|
||||||
|
class MessageCommandTest extends WebDriverTestBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected static $modules = ['ajax_test'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test AJAX MessageCommand use in a form.
|
||||||
|
*/
|
||||||
|
public function testMessageCommand() {
|
||||||
|
$page = $this->getSession()->getPage();
|
||||||
|
$assert_session = $this->assertSession();
|
||||||
|
|
||||||
|
$this->drupalGet('ajax-test/message');
|
||||||
|
$page->pressButton('Make Message In Default Location');
|
||||||
|
$this->waitForMessageVisible('I am a message in the default location.');
|
||||||
|
$this->assertAnnounceContains('I am a message in the default location.');
|
||||||
|
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
|
||||||
|
|
||||||
|
$page->pressButton('Make Message In Alternate Location');
|
||||||
|
$this->waitForMessageVisible('I am a message in an alternate location.', '#alternate-message-container');
|
||||||
|
$assert_session->pageTextContains('I am a message in the default location.');
|
||||||
|
$this->assertAnnounceContains('I am a message in an alternate location.');
|
||||||
|
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
|
||||||
|
$assert_session->elementsCount('css', '#alternate-message-container .messages', 1);
|
||||||
|
|
||||||
|
$page->pressButton('Make Warning Message');
|
||||||
|
$this->waitForMessageVisible('I am a warning message in the default location.', NULL, 'warning');
|
||||||
|
$assert_session->pageTextNotContains('I am a message in the default location.');
|
||||||
|
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
|
||||||
|
$assert_session->elementsCount('css', '#alternate-message-container .messages', 1);
|
||||||
|
|
||||||
|
$this->drupalGet('ajax-test/message');
|
||||||
|
// Test that by default, previous messages in a location are removed.
|
||||||
|
for ($i = 0; $i < 6; $i++) {
|
||||||
|
$page->pressButton('Make Message In Default Location');
|
||||||
|
$this->waitForMessageVisible('I am a message in the default location.');
|
||||||
|
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
|
||||||
|
|
||||||
|
$page->pressButton('Make Warning Message');
|
||||||
|
$this->waitForMessageVisible('I am a warning message in the default location.', NULL, 'warning');
|
||||||
|
// Test that setting MessageCommand::$option['announce'] => '' supresses
|
||||||
|
// screen reader announcement.
|
||||||
|
$this->assertAnnounceNotContains('I am a warning message in the default location.');
|
||||||
|
$this->waitForMessageRemoved('I am a message in the default location.');
|
||||||
|
$assert_session->elementsCount('css', '.messages__wrapper .messages', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that if MessageCommand::clearPrevious is FALSE, messages will not
|
||||||
|
// be cleared.
|
||||||
|
$this->drupalGet('ajax-test/message');
|
||||||
|
for ($i = 1; $i < 7; $i++) {
|
||||||
|
$page->pressButton('Make Message In Alternate Location');
|
||||||
|
$expected_count = $page->waitFor(10, function () use ($i, $page) {
|
||||||
|
return count($page->findAll('css', '#alternate-message-container .messages')) === $i;
|
||||||
|
});
|
||||||
|
$this->assertTrue($expected_count);
|
||||||
|
$this->assertAnnounceContains('I am a message in an alternate location.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that a message of the expected type appears.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* The expected message.
|
||||||
|
* @param string $selector
|
||||||
|
* The selector for the element in which to check for the expected message.
|
||||||
|
* @param string $type
|
||||||
|
* The expected type.
|
||||||
|
*/
|
||||||
|
protected function waitForMessageVisible($message, $selector = '[data-drupal-messages]', $type = 'status') {
|
||||||
|
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', $selector . ' .messages--' . $type . ':contains("' . $message . '")'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that a message of the expected type is removed.
|
||||||
|
*
|
||||||
|
* @param string $message
|
||||||
|
* The expected message.
|
||||||
|
* @param string $selector
|
||||||
|
* The selector for the element in which to check for the expected message.
|
||||||
|
* @param string $type
|
||||||
|
* The expected type.
|
||||||
|
*/
|
||||||
|
protected function waitForMessageRemoved($message, $selector = '[data-drupal-messages]', $type = 'status') {
|
||||||
|
$this->assertNotEmpty($this->assertSession()->waitForElementRemoved('css', $selector . ' .messages--' . $type . ':contains("' . $message . '")'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for inclusion of text in #drupal-live-announce.
|
||||||
|
*
|
||||||
|
* @param string $expected_message
|
||||||
|
* The text expected to be present in #drupal-live-announce.
|
||||||
|
*/
|
||||||
|
protected function assertAnnounceContains($expected_message) {
|
||||||
|
$assert_session = $this->assertSession();
|
||||||
|
$this->assertNotEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for absence of the given text from #drupal-live-announce.
|
||||||
|
*
|
||||||
|
* @param string $expected_message
|
||||||
|
* The text expected to be absent from #drupal-live-announce.
|
||||||
|
*/
|
||||||
|
protected function assertAnnounceNotContains($expected_message) {
|
||||||
|
$assert_session = $this->assertSession();
|
||||||
|
$this->assertEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')", 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||||
use Drupal\js_message_test\Controller\JSMessageTestController;
|
use Drupal\js_message_test\Controller\JSMessageTestController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests core/drupal.messages library.
|
* Tests core/drupal.message library.
|
||||||
*
|
*
|
||||||
* @group Javascript
|
* @group Javascript
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue