Issue #3086096 by oknate, phenaproxima, tedbow, Wim Leers, andrewmacpherson, droplet, bnjmnm: Add a generic Ajax Message command

merge-requests/55/head
Lee Rowlands 2019-10-10 19:38:23 +10:00
parent f702e8c453
commit 7dde35cfd0
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
8 changed files with 383 additions and 1 deletions

View File

@ -7,6 +7,15 @@ use Drupal\Core\Asset\AttachedAssets;
/**
* 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
*/
class AnnounceCommand implements CommandInterface, CommandWithAttachedAssetsInterface {

View File

@ -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;
}
}

View File

@ -1562,5 +1562,31 @@
} 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);

View File

@ -639,6 +639,13 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr
document.styleSheets[0].addImport(match[1]);
} 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);

View File

@ -77,3 +77,11 @@ ajax_test.render_error:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::renderError'
requirements:
_access: 'TRUE'
ajax_test.message_form:
path: '/ajax-test/message'
defaults:
_title: 'Ajax Message Form'
_form: '\Drupal\ajax_test\Form\AjaxTestMessageCommandForm'
requirements:
_access: 'TRUE'

View File

@ -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' => '']));
}
}

View File

@ -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));
}
}

View File

@ -6,7 +6,7 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\js_message_test\Controller\JSMessageTestController;
/**
* Tests core/drupal.messages library.
* Tests core/drupal.message library.
*
* @group Javascript
*/