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

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));
......
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