Commit befdca55 authored by lauriii's avatar lauriii

Issue #3016807 by seanB, FeyP, phenaproxima, andrewmacpherson, lauriii,...

Issue #3016807 by seanB, FeyP, phenaproxima, andrewmacpherson, lauriii, rainbreaw, larowlan: Improve refocus on submit buttons of Media Library Widget modals
parent 29e5277a
......@@ -435,6 +435,7 @@
* seven theme in https://www.drupal.org/project/drupal/issues/2980769
*/
.button.media-library-open-button {
margin-bottom: 1em;
margin-left: 0; /* LTR */
}
[dir="rtl"] .button.media-library-open-button {
......
......@@ -70,4 +70,34 @@
.hide();
},
};
/**
* Disable the open button when the user is not allowed to add more items.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to disable the media library open button.
*/
Drupal.behaviors.MediaLibraryWidgetDisableButton = {
attach(context) {
// When the user returns from the modal to the widget, we want to shift
// the focus back to the open button. If the user is not allowed to add
// more items, the button needs to be disabled. Since we can't shift the
// focus to disabled elements, the focus is set back to the open button
// via JavaScript by adding the 'data-disabled-focus' attribute.
$('.js-media-library-open-button[data-disabled-focus="true"]', context)
.once('media-library-disable')
.each(function() {
$(this).focus();
// There is a small delay between the focus set by the browser and the
// focus of screen readers. We need to give screen readers time to
// shift the focus as well before the button is disabled.
setTimeout(() => {
$(this).attr('disabled', 'disabled');
}, 50);
});
},
};
})(jQuery, Drupal);
......@@ -36,4 +36,18 @@
$('.js-media-library-item-weight', context).once('media-library-toggle').parent().hide();
}
};
Drupal.behaviors.MediaLibraryWidgetDisableButton = {
attach: function attach(context) {
$('.js-media-library-open-button[data-disabled-focus="true"]', context).once('media-library-disable').each(function () {
var _this = this;
$(this).focus();
setTimeout(function () {
$(_this).attr('disabled', 'disabled');
}, 50);
});
}
};
})(jQuery, Drupal);
\ No newline at end of file
......@@ -5,7 +5,9 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
......@@ -383,7 +385,12 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
else {
$cardinality_message = $this->t('The maximum number of media items have been selected.');
}
$element['#description'] .= '<br />' . $cardinality_message;
// Add a line break between the field message and the cardinality message.
if (!empty($element['#description'])) {
$element['#description'] .= '<br />';
}
$element['#description'] .= $cardinality_message;
}
// Create a new media library URL with the correct state parameters.
......@@ -421,9 +428,19 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
'#submit' => [],
// Allow the media library to be opened even if there are form errors.
'#limit_validation_errors' => [],
'#access' => $cardinality_unlimited || $remaining > 0,
];
// When the user returns from the modal to the widget, we want to shift the
// focus back to the open button. If the user is not allowed to add more
// items, the button needs to be disabled. Since we can't shift the focus to
// disabled elements, the focus is set back to the open button via
// JavaScript by adding the 'data-disabled-focus' attribute.
// @see Drupal.behaviors.MediaLibraryWidgetDisableButton
if (!$cardinality_unlimited && $remaining === 0) {
$element['media_library_open_button']['#attributes']['data-disabled-focus'] = 'true';
$element['media_library_open_button']['#attributes']['class'][] = 'visually-hidden';
}
// This hidden field and button are used to add new items to the widget.
$element['media_library_selection'] = [
'#type' => 'hidden',
......@@ -482,14 +499,17 @@ public function massageFormValues(array $values, array $form, FormStateInterface
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* An array representing the updated widget.
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to update the selection.
*/
public static function updateWidget(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
// This callback is either invoked from the remove button or the update
// button, which have different nesting levels.
$length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
$remove_button = end($triggering_element['#parents']) === 'remove_button';
$length = $remove_button ? -4 : -1;
if (count($triggering_element['#array_parents']) < abs($length)) {
throw new \LogicException('The element that triggered the widget update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
}
......@@ -497,7 +517,30 @@ public static function updateWidget(array $form, FormStateInterface $form_state)
$element = NestedArray::getValue($form, $parents);
// Always clear the textfield selection to prevent duplicate additions.
$element['media_library_selection']['#value'] = '';
return $element;
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand("#$wrapper_id", $element));
$field_state = static::getFieldState($element, $form_state);
// When the remove button is clicked, the focus will be kept in the
// selection area by default. When the last item is deleted, we no longer
// have a selection and shift the focus to the open button.
$removed_last = $remove_button && !count($field_state['items']);
// Shift focus to the open button if the user did not click the remove
// button. When the user is not allowed to add more items, the button needs
// to be disabled. Since we can't shift the focus to disabled elements, the
// focus is set via JavaScript by adding the 'data-disabled-focus' attribute
// and we also don't want to set the focus here.
// @see Drupal.behaviors.MediaLibraryWidgetDisableButton
$select_more = !$remove_button && !isset($element['media_library_open_button']['#attributes']['data-disabled-focus']);
if ($removed_last || $select_more) {
$response->addCommand(new InvokeCommand("#$wrapper_id .js-media-library-open-button", 'focus'));
}
return $response;
}
/**
......
......@@ -99,8 +99,17 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
$form['actions']['submit']['#value'] = $this->t('Select media');
$form['actions']['submit']['#field_id'] = $selection_field_id;
// By default, the AJAX system tries to move the focus back to the element
// that triggered the AJAX request. Since the media library is closed after
// clicking the select button, the focus can't be moved back. We need to set
// the 'data-disable-refocus' attribute to prevent the AJAX system from
// moving focus to a random element. The select button triggers an update in
// the opener, and the opener should be responsible for moving the focus. An
// example of this can be seen in MediaLibraryWidget::updateWidget().
// @see \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::updateWidget()
$form['actions']['submit']['#attributes'] = [
'class' => ['media-library-select'],
'data-disable-refocus' => 'true',
];
}
......
......@@ -242,6 +242,7 @@ public function testWidget() {
// Select a media item, assert the hidden selection field contains the ID of
// the selected item.
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$this->assertGreaterThanOrEqual(1, count($checkboxes));
$checkboxes[0]->click();
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
$assert_session->elementTextContains('css', '.media-library-selected-count', '1 of 1 item selected');
......@@ -366,6 +367,22 @@ public function testWidget() {
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$assert_session->assertWaitOnAjaxRequest();
// Assert adding a single media item and removing it.
$assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$this->assertGreaterThanOrEqual(1, count($checkboxes));
$checkboxes[0]->click();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
$assert_session->assertWaitOnAjaxRequest();
// Assert the focus is set back on the open button of the media field.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":focus")');
$assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Dog');
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$assert_session->assertWaitOnAjaxRequest();
// Assert the focus is set back on the open button of the media field.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .js-media-library-open-button").is(":focus")');
// Assert the selection is persistent in the media library modal, and
// the number of selected items is displayed correctly.
$assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
......@@ -377,6 +394,7 @@ public function testWidget() {
// Select a media item, assert the hidden selection field contains the ID of
// the selected item.
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$this->assertCount(4, $checkboxes);
$checkboxes[0]->click();
$assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
// Assert the number of selected items is displayed correctly.
......@@ -416,6 +434,7 @@ public function testWidget() {
$page->clickLink('Type Two');
$assert_session->assertWaitOnAjaxRequest();
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$this->assertCount(4, $checkboxes);
$checkboxes[0]->click();
// Assert the selection is updated correctly.
$assert_session->elementTextContains('css', '.media-library-selected-count', '2 of 2 items selected');
......@@ -436,6 +455,11 @@ public function testWidget() {
// Select the items.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
$assert_session->assertWaitOnAjaxRequest();
// Assert the open button is disabled.
$open_button = $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]');
$this->assertTrue($open_button->hasAttribute('data-disabled-focus'));
$this->assertTrue($open_button->hasAttribute('disabled'));
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-open-button").is(":disabled")');
// Ensure that the selection completed successfully.
$assert_session->pageTextNotContains('Add or select media');
......@@ -448,13 +472,21 @@ public function testWidget() {
$assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Cat');
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$assert_session->assertWaitOnAjaxRequest();
// Assert the focus is set to the remove button of the other selected item.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-item__remove").is(":focus")');
$assert_session->pageTextNotContains('Cat');
$assert_session->pageTextContains('Turtle');
// Assert the open button is no longer disabled.
$open_button = $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]');
$this->assertFalse($open_button->hasAttribute('data-disabled-focus'));
$this->assertFalse($open_button->hasAttribute('disabled'));
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-open-button").is(":not(:disabled)")');
// Open the media library again and select another item.
$assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$this->assertGreaterThanOrEqual(1, count($checkboxes));
$checkboxes[0]->click();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
$assert_session->assertWaitOnAjaxRequest();
......@@ -462,13 +494,16 @@ public function testWidget() {
$assert_session->pageTextNotContains('Cat');
$assert_session->pageTextContains('Turtle');
$assert_session->pageTextNotContains('Snake');
// Assert we are not allowed to add more items to the field.
$assert_session->elementNotExists('css', '.media-library-open-button[name^="field_twin_media"]');
// Assert the open button is disabled.
$this->assertTrue($assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->hasAttribute('data-disabled-focus'));
$this->assertTrue($assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->hasAttribute('disabled'));
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-open-button").is(":disabled")');
// Assert the selection is cleared when the modal is closed.
$assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$this->assertGreaterThanOrEqual(4, count($checkboxes));
// Nothing is selected yet.
$this->assertFalse($checkboxes[0]->isChecked());
$this->assertFalse($checkboxes[1]->isChecked());
......@@ -476,7 +511,6 @@ public function testWidget() {
$this->assertFalse($checkboxes[3]->isChecked());
$assert_session->elementTextContains('css', '.media-library-selected-count', '0 items selected');
// Select the first 2 items.
$checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
$checkboxes[0]->click();
$assert_session->elementTextContains('css', '.media-library-selected-count', '1 item selected');
$checkboxes[1]->click();
......@@ -527,6 +561,7 @@ public function testWidget() {
// Select all media items of type one (should also contain Dog, again).
$checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
$checkboxes = $page->findAll('css', $checkbox_selector);
$this->assertGreaterThanOrEqual(4, count($checkboxes));
$checkboxes[0]->click();
$checkboxes[1]->click();
$checkboxes[2]->click();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment