MDL-70075 core: Autocomplete selection should always have an active item

Ensure that there is always one active element in the list of selected
autocomplete elements.

Without this we have issues beacuse clicking on the link makes the first
one active if one is not already active, and this turns a click event
into a drag event, which means that it is not deleted.
This commit is contained in:
Andrew Nicols 2020-11-04 11:24:04 +08:00
parent a88838c25f
commit 7d786c7968
3 changed files with 82 additions and 27 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -76,10 +76,58 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
// Tell the input field it has a new active descendant so the item is announced.
selectionElement.attr('aria-activedescendant', itemId);
selectionElement.attr('data-active-value', element.attr('data-value'));
return $.Deferred().resolve();
};
/**
* Get the actively selected element from the state object.
*
* @param {Object} state
* @returns {jQuery}
*/
var getActiveElementFromState = function(state) {
var selectionRegion = $(document.getElementById(state.selectionId));
var activeId = selectionRegion.attr('aria-activedescendant');
if (activeId) {
var activeElement = $(document.getElementById(activeId));
if (activeElement.length) {
// The active descendent still exists.
return activeElement;
}
}
var activeValue = selectionRegion.attr('data-active-value');
return selectionRegion.find('[data-value="' + activeValue + '"]');
};
/**
* Update the active selection from the given state object.
*
* @param {Object} state
*/
var updateActiveSelectionFromState = function(state) {
var activeElement = getActiveElementFromState(state);
var activeValue = activeElement.attr('data-value');
var selectionRegion = $(document.getElementById(state.selectionId));
if (activeValue) {
// Find the index of the currently selected index.
var activeIndex = selectionRegion.find('[aria-selected=true]').index(activeElement);
if (activeIndex !== -1) {
activateSelection(activeIndex, state);
return;
}
}
// Either the active index was not set, or it could not be found.
// Select the first value instead.
activateSelection(0, state);
};
/**
* Update the element that shows the currently selected items.
*
@ -97,12 +145,6 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
// Build up a valid context to re-render the template.
var items = [];
var newSelection = $(document.getElementById(state.selectionId));
var activeId = newSelection.attr('aria-activedescendant');
var activeValue = false;
if (activeId) {
activeValue = $(document.getElementById(activeId)).attr('data-value');
}
originalSelect.children('option').each(function(index, ele) {
if ($(ele).prop('selected')) {
var label;
@ -116,23 +158,24 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
}
}
});
var context = $.extend({items: items}, options, state);
if (!hasItemListChanged(state, items)) {
M.util.js_complete(pendingKey);
return Promise.resolve();
}
state.items = items;
var context = $.extend(options, state);
// Render the template.
return templates.render(options.templates.items, context)
.then(function(html, js) {
// Add it to the page.
templates.replaceNodeContents(newSelection, html, js);
if (activeValue !== false) {
// Reselect any previously selected item.
newSelection.children('[aria-selected=true]').each(function(index, ele) {
if ($(ele).attr('data-value') === activeValue) {
activateSelection(index, state);
}
});
}
updateActiveSelectionFromState(state);
return activeValue;
return;
})
.then(function() {
return M.util.js_complete(pendingKey);
@ -140,6 +183,21 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
.catch(notification.exception);
};
/**
* Check whether the list of items stored in the state has changed.
*
* @param {Object} state
* @param {Array} items
*/
var hasItemListChanged = function(state, items) {
if (state.items.length !== items.length) {
return true;
}
// Check for any items in the state items which are not present in the new items list.
return state.items.filter(item => items.indexOf(item) === -1).length > 0;
};
/**
* Notify of a change in the selection.
*
@ -807,6 +865,7 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
.catch();
});
var selectionElement = $(document.getElementById(state.selectionId));
// Handle clicks on the selected items (will unselect an item).
selectionElement.on('click', '[role=option]', function(e) {
var pendingPromise = addPendingJSPromise('form-autocomplete-clicks');
@ -814,17 +873,12 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
// Remove it from the selection.
pendingPromise.resolve(deselectItem(options, state, $(e.currentTarget), originalSelect));
});
// When listbox is focused, focus on the first option if there is no focused option.
selectionElement.on('focus', function() {
// Find the list of selections.
var selectionsElement = $(document.getElementById(state.selectionId));
// Find the active one.
var element = selectionsElement.children('[data-active-selection]');
if (!element.length) {
activateSelection(0, state);
return;
}
updateActiveSelectionFromState(state);
});
// Keyboard navigation for the selection list.
selectionElement.on('keydown', function(e) {
var pendingPromise = addPendingJSPromise('form-autocomplete-keydown-' + e.keyCode);
@ -1057,7 +1111,8 @@ function($, log, str, templates, notification, LoadingIcon, Aria) {
inputId: 'form_autocomplete_input-' + uniqueId,
suggestionsId: 'form_autocomplete_suggestions-' + uniqueId,
selectionId: 'form_autocomplete_selection-' + uniqueId,
downArrowId: 'form_autocomplete_downarrow-' + uniqueId
downArrowId: 'form_autocomplete_downarrow-' + uniqueId,
items: [],
};
// Increment the unique counter so we don't get duplicates ever.