Commit e0a0ab84 authored by Gábor Hojtsy's avatar Gábor Hojtsy
Browse files

Issue #3035446 by seanB, bnjmnm, phenaproxima, andrewmacpherson, Wim Leers,...

Issue #3035446 by seanB, bnjmnm, phenaproxima, andrewmacpherson, Wim Leers, alexpott, rainbreaw: Inform assistive tech users about the outcome of using the MediaLibraryWidget dialog
parent e82e0c05
......@@ -293,12 +293,20 @@
margin: 0 8px;
}
/**
* Style the media library grid items.
*
* The media library item container receives screen reader focus when items are
* removed. Since it is not an interactive element, it does not need an
* outline.
*/
.media-library-item--grid {
justify-content: center;
box-sizing: border-box;
width: 50%;
padding: 8px;
vertical-align: top;
outline: none;
background: #fff;
}
......
......@@ -5,6 +5,7 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AnnounceCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
......@@ -384,6 +385,16 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
'media-library-item--grid',
'js-media-library-item',
],
// Add the tabindex '-1' to allow the focus to be shifted to the next
// media item when an item is removed. We set focus to the container
// because we do not want to set focus to the remove button
// automatically.
// @see ::updateWidget()
'tabindex' => '-1',
// Add a data attribute containing the delta to allow us to easily
// shift the focus to a specific media item.
// @see ::updateWidget()
'data-media-library-item-delta' => $delta,
],
'preview' => [
'#type' => 'container',
......@@ -391,6 +402,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
'#type' => 'submit',
'#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
'#value' => $this->t('Remove'),
'#media_id' => $media_item->id(),
'#attributes' => [
'class' => ['media-library-item__remove'],
'aria-label' => $this->t('Remove @label', ['@label' => $media_item->label()]),
......@@ -398,6 +410,10 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
'#ajax' => [
'callback' => [static::class, 'updateWidget'],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => $this->t('Removing @label.', ['@label' => $media_item->label()]),
],
],
'#submit' => [[static::class, 'removeItem']],
// Prevent errors in other widgets from preventing removal.
......@@ -509,13 +525,17 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
'#ajax' => [
'callback' => [static::class, 'updateWidget'],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => $this->t('Adding selection.'),
],
],
'#attributes' => [
'data-media-library-widget-update' => $field_name . $id_suffix,
'class' => ['js-hide'],
],
'#validate' => [[static::class, 'validateItems']],
'#submit' => [[static::class, 'updateItems']],
'#submit' => [[static::class, 'addItems']],
// Prevent errors in other widgets from preventing updates.
'#limit_validation_errors' => $limit_validation_errors,
];
......@@ -597,35 +617,65 @@ public static function updateWidget(array $form, FormStateInterface $form_state)
// This callback is either invoked from the remove button or the update
// button, which have different nesting levels.
$remove_button = end($triggering_element['#parents']) === 'remove_button';
$length = $remove_button ? -4 : -1;
$is_remove_button = end($triggering_element['#parents']) === 'remove_button';
$length = $is_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']));
}
$parents = array_slice($triggering_element['#array_parents'], 0, $length);
$element = NestedArray::getValue($form, $parents);
// Always clear the textfield selection to prevent duplicate additions.
$element['media_library_selection']['#value'] = '';
$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']);
// Announce the updated content to screen readers.
if ($is_remove_button) {
$announcement = t('Removed @label.', ['@label' => Media::load($field_state['removed_item_id'])->label()]);
}
else {
$new_items = count(static::getNewMediaItems($element, $form_state));
$announcement = \Drupal::translation()->formatPlural($new_items, 'Added one media item.', 'Added @count media 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.
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand("#$wrapper_id", $element));
$response->addCommand(new AnnounceCommand($announcement));
// When the remove button is clicked, shift focus to the next remove button.
// When the last item is deleted, we no longer have a selection and shift
// the focus to the open button.
$removed_last = $is_remove_button && !count($field_state['items']);
if ($is_remove_button && !$removed_last) {
// Find the next media item by weight. The weight of the removed item is
// added to the field state when it is removed in ::removeItem(). If there
// is no item with a bigger weight, we automatically shift the focus to
// the previous media item.
// @see ::removeItem()
$removed_item_weight = $field_state['removed_item_weight'];
$delta_to_focus = 0;
foreach ($field_state['items'] as $delta => $item_fields) {
$delta_to_focus = $delta;
if ($item_fields['weight'] > $removed_item_weight) {
// Stop directly when we find an item with a bigger weight. We also
// have to subtract 1 from the delta in this case, since the delta's
// are renumbered when rebuilding the form.
$delta_to_focus--;
break;
}
}
$response->addCommand(new InvokeCommand("#$wrapper_id [data-media-library-item-delta=$delta_to_focus]", 'focus'));
}
// Shift focus to the open button if the user removed the last selected
// item, or when the user has added items to the selection and is allowed to
// select more items. 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) {
elseif ($removed_last || (!$is_remove_button && !isset($element['media_library_open_button']['#attributes']['data-disabled-focus']))) {
$response->addCommand(new InvokeCommand("#$wrapper_id .js-media-library-open-button", 'focus'));
}
......@@ -648,8 +698,6 @@ public static function removeItem(array $form, FormStateInterface $form_state) {
throw new \LogicException('Expected the remove button to be more than four levels deep in the form. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
}
$parents = array_slice($triggering_element['#array_parents'], 0, -4);
// Get the delta of the item being removed.
$delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
$element = NestedArray::getValue($form, $parents);
// Get the field state.
......@@ -657,9 +705,14 @@ public static function removeItem(array $form, FormStateInterface $form_state) {
$values = NestedArray::getValue($form_state->getValues(), $path);
$field_state = static::getFieldState($element, $form_state);
// Remove the item from the field state and update it.
// Get the delta of the item being removed.
$delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
if (isset($values['selection'][$delta])) {
array_splice($values['selection'], $delta, 1);
// Add the weight of the removed item to the field state so we can shift
// focus to the next/previous item in an easy way.
$field_state['removed_item_weight'] = $values['selection'][$delta]['weight'];
$field_state['removed_item_id'] = $triggering_element['#media_id'];
unset($values['selection'][$delta]);
$field_state['items'] = $values['selection'];
static::setFieldState($element, $form_state, $field_state);
}
......@@ -737,7 +790,7 @@ public static function validateItems(array $form, FormStateInterface $form_state
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function updateItems(array $form, FormStateInterface $form_state) {
public static function addItems(array $form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
......
......@@ -524,6 +524,7 @@ public function testWidget() {
$assert_session->linkExists('Table');
// Select the item.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
// Ensure that the selection completed successfully.
$assert_session->pageTextNotContains('Add or select media');
......@@ -533,6 +534,7 @@ public function testWidget() {
// Clear the selection.
$assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Dog');
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$this->assertNotEmpty($assert_session->waitForText('Removed Dog.'));
$assert_session->assertWaitOnAjaxRequest();
// Assert adding a single media item and removing it.
......@@ -542,15 +544,41 @@ public function testWidget() {
$this->assertGreaterThanOrEqual(1, count($checkboxes));
$checkboxes[0]->click();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$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();
$this->assertNotEmpty($assert_session->waitForText('Removed Dog.'));
$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 we can select the same media item twice.
$assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$page->checkField('Select Dog');
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]')->click();
$assert_session->assertWaitOnAjaxRequest();
$page->checkField('Select Dog');
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
// Assert the same has been added twice and remove the items again.
$this->assertCount(2, $page->findAll('css', '.field--name-field-twin-media .media-library-item'));
$assert_session->hiddenFieldValueEquals('field_twin_media[selection][0][target_id]', 4);
$assert_session->hiddenFieldValueEquals('field_twin_media[selection][1][target_id]', 4);
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$this->assertNotEmpty($assert_session->waitForText('Removed Dog.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$this->assertNotEmpty($assert_session->waitForText('Removed Dog.'));
$assert_session->assertWaitOnAjaxRequest();
// 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();
......@@ -624,6 +652,7 @@ public function testWidget() {
$this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
// Select the items.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added 2 media items.'));
$assert_session->assertWaitOnAjaxRequest();
// Assert the open button is disabled.
$open_button = $assert_session->elementExists('css', '.media-library-open-button[name^="field_twin_media"]');
......@@ -633,19 +662,20 @@ public function testWidget() {
// Ensure that the selection completed successfully.
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextNotContains('Dog');
$assert_session->pageTextContains('Cat');
$assert_session->pageTextContains('Turtle');
$assert_session->pageTextNotContains('Snake');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Dog');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Cat');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Turtle');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Snake');
// Remove "Cat" (happens to be the first remove button on the page).
$assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Cat');
$assert_session->elementExists('css', '.media-library-item__remove')->click();
$this->assertNotEmpty($assert_session->waitForText('Removed Cat.'));
$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 focus is set to the wrapper of the other selected item.
$this->assertJsCondition('jQuery("#field_twin_media-media-library-wrapper .media-library-item").is(":focus")');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Cat');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', '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'));
......@@ -659,11 +689,12 @@ public function testWidget() {
$this->assertGreaterThanOrEqual(1, count($checkboxes));
$checkboxes[0]->click();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains('Dog');
$assert_session->pageTextNotContains('Cat');
$assert_session->pageTextContains('Turtle');
$assert_session->pageTextNotContains('Snake');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Dog');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Cat');
$assert_session->elementTextContains('css', '#field_twin_media-media-library-wrapper', 'Turtle');
$assert_session->elementTextNotContains('css', '#field_twin_media-media-library-wrapper', 'Snake');
// 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'));
......@@ -694,6 +725,7 @@ public function testWidget() {
$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));
$this->assertFalse($checkboxes[0]->isChecked());
$this->assertFalse($checkboxes[1]->isChecked());
$this->assertFalse($checkboxes[2]->isChecked());
......@@ -704,14 +736,14 @@ public function testWidget() {
$assert_session->elementExists('css', '.js-media-library-widget-toggle-weight')->click();
$this->submitForm([
'title[0][value]' => 'My page',
'field_twin_media[selection][0][weight]' => '2',
'field_twin_media[selection][0][weight]' => '3',
], 'Save');
$assert_session->pageTextContains('Basic Page My page has been created');
// We removed this item earlier.
$assert_session->pageTextNotContains('Cat');
// This item was never selected.
$assert_session->pageTextNotContains('Snake');
// "Dog" should come after "Turtle", since we changed the weight.
// "Turtle" should come after "Dog", since we changed the weight.
$assert_session->elementExists('css', '.field--name-field-twin-media > .field__items > .field__item:last-child:contains("Turtle")');
// Make sure everything that was selected shows up.
$assert_session->pageTextContains('Dog');
......@@ -737,6 +769,7 @@ public function testWidget() {
$checkboxes[2]->click();
$checkboxes[3]->click();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added 4 media items.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains('Dog');
$assert_session->pageTextContains('Cat');
......@@ -780,6 +813,7 @@ public function testWidgetAnonymous() {
// Select the first media item (should be Dog).
$page->find('css', '.media-library-view .js-click-to-select-checkbox input')->click();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
// Ensure that the selection completed successfully.
......@@ -924,6 +958,7 @@ public function testWidgetUpload() {
$assert_session->hiddenFieldValueEquals('current_selection', $added_media->id());
// Ensure the created item is added in the widget.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains($png_image->filename);
......@@ -944,6 +979,7 @@ public function testWidgetUpload() {
$assert_session->assertWaitOnAjaxRequest();
$page->fillField('Alternative text', $this->randomString());
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save and insert');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains($file_system->basename($png_uri_2));
......@@ -998,6 +1034,7 @@ public function testWidgetUpload() {
$this->assertCount(2, $selected_checkboxes);
// Ensure the created item is added in the widget.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains('Unlimited Cardinality Image');
......@@ -1039,6 +1076,7 @@ public function testWidgetUpload() {
$assert_session->pageTextContains($file_system->basename($jpg_uri_2));
// Ensure the created item is added in the widget.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains($file_system->basename($jpg_uri_2));
......@@ -1079,6 +1117,7 @@ public function testWidgetUpload() {
$assert_session->checkboxNotChecked("Select $existing_media_name");
$assert_session->hiddenFieldValueEquals('current_selection', $added_media->id());
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains($file_system->basename($png_uri_5));
......@@ -1232,6 +1271,7 @@ public function testWidgetOEmbed() {
// Assert the created oEmbed video is correctly added to the widget.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains($youtube_title);
......@@ -1304,6 +1344,7 @@ public function testWidgetOEmbed() {
$this->assertCount(2, $selected_checkboxes);
// Ensure the created item is added in the widget.
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added 2 media items.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains('Custom video title');
......@@ -1318,6 +1359,7 @@ public function testWidgetOEmbed() {
$page->pressButton('Add');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save and insert');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains($vimeo_title);
......@@ -1355,6 +1397,7 @@ public function testWidgetOEmbed() {
$assert_session->checkboxNotChecked("Select $vimeo_title");
$assert_session->hiddenFieldValueEquals('current_selection', $added_media->id());
$assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected');
$this->assertNotEmpty($assert_session->waitForText('Added one media item.'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('Add or select media');
$assert_session->pageTextContains('Another video');
......
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