Merge branch 'MDL-85041-500' of https://github.com/junpataleta/moodle into MOODLE_500_STABLE

This commit is contained in:
Jun Pataleta
2025-06-04 17:48:18 +08:00
6 changed files with 79 additions and 51 deletions
+1
View File
@@ -1827,6 +1827,7 @@ $string['remoteappuser'] = 'Remote {$a} User';
$string['remove'] = 'Remove';
$string['removeadmin'] = 'Remove admin';
$string['removecreator'] = 'Remove course creator';
$string['removed'] = 'Removed {$a}';
$string['removestudent'] = 'Remove student';
$string['removeteacher'] = 'Remove teacher';
$string['rename'] = 'Rename';
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+74 -48
View File
@@ -221,7 +221,7 @@ define([
* @param {Element} originalSelect The original select list.
* @return {Promise}
*/
var deselectItem = function(options, state, item, originalSelect) {
const deselectItem = async(options, state, item, originalSelect) => {
var selectedItemValue = $(item).attr('data-value');
// Preprend an empty option to the select list to avoid having a default selected option.
@@ -239,14 +239,15 @@ define([
}
}
});
// Rerender the selection list.
return updateSelectionList(options, state, originalSelect)
.then(function() {
// Notify that the selection changed.
notifyChange(originalSelect);
return;
});
const selectedItemText = item[0].childNodes[2].textContent?.trim();
await announceChanges(state.selectionId, selectedItemText, 'removed');
// Rerender the selection list.
await updateSelectionList(options, state, originalSelect);
// Notify that the selection changed.
notifyChange(originalSelect);
};
/**
@@ -589,7 +590,7 @@ define([
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @return {Promise}
*/
var createItem = function(options, state, originalSelect) {
const createItem = async(options, state, originalSelect) => {
// Find the element in the DOM.
var inputElement = $(document.getElementById(state.inputId));
// Get the current text in the input field.
@@ -624,23 +625,17 @@ define([
}
});
return updateSelectionList(options, state, originalSelect)
.then(function() {
// Notify that the selection changed.
notifyChange(originalSelect);
// Announce the changes to the assistive technology.
await announceChanges(state.selectionId, query.trim(), 'added');
return;
})
.then(function() {
// Clear the input field.
inputElement.val('');
await updateSelectionList(options, state, originalSelect);
// Notify that the selection changed.
notifyChange(originalSelect);
return;
})
.then(function() {
// Close the suggestions list.
return closeSuggestions(state);
});
// Clear the input field.
inputElement.val('');
// Close the suggestions list.
await closeSuggestions(state);
};
/**
@@ -651,9 +646,10 @@ define([
* @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @param {string|null} selectedItem The item to be selected.
* @return {Promise}
*/
var selectCurrentItem = function(options, state, originalSelect) {
const selectCurrentItem = async(options, state, originalSelect, selectedItem) => {
// Find the elements in the page.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
@@ -668,32 +664,30 @@ define([
originalSelect.children('option').prop('selected', false);
}
// Look for a match, and toggle the selected property if there is a match.
originalSelect.children('option').each(function(index, ele) {
originalSelect.children('option').each(function (index, ele) {
if ($(ele).attr('value') == selectedItemValue) {
$(ele).prop('selected', true);
}
});
return updateSelectionList(options, state, originalSelect)
.then(function() {
// Notify that the selection changed.
notifyChange(originalSelect);
await announceChanges(state.selectionId, selectedItem, 'added');
return;
})
.then(function() {
if (options.closeSuggestionsOnSelect) {
// Clear the input element.
inputElement.val('');
// Close the list of suggestions.
return closeSuggestions(state);
} else {
// Focus on the input element so the suggestions does not auto-close.
inputElement.focus();
// Remove the last selected item from the suggestions list.
return updateSuggestions(options, state, inputElement.val(), originalSelect);
}
});
await updateSelectionList(options, state, originalSelect);
// Notify that the selection changed.
notifyChange(originalSelect);
if (options.closeSuggestionsOnSelect) {
// Clear the input element.
inputElement.val('');
// Close the list of suggestions.
await closeSuggestions(state);
} else {
// Focus on the input element so the suggestions does not auto-close.
inputElement.focus();
// Remove the last selected item from the suggestions list.
await updateSuggestions(options, state, inputElement.val(), originalSelect);
}
};
/**
@@ -814,8 +808,9 @@ define([
var suggestionsElement = $(document.getElementById(state.suggestionsId));
if ((inputElement.attr('aria-expanded') === "true") &&
(suggestionsElement.children('[aria-selected=true]').length > 0)) {
const selectedItemText = suggestionsElement.children('[aria-selected=true]')[0].textContent.trim();
// If the suggestion list has an active item, select it.
pendingJsPromise.resolve(selectCurrentItem(options, state, originalSelect));
pendingJsPromise.resolve(selectCurrentItem(options, state, originalSelect, selectedItemText));
} else if (options.tags) {
// If tags are enabled, create a tag.
pendingJsPromise.resolve(createItem(options, state, originalSelect));
@@ -934,12 +929,13 @@ define([
activateItem(current, state)
.then(function() {
// And select it.
return selectCurrentItem(options, state, originalSelect);
const selectedItemText = element[0].textContent.trim();
return selectCurrentItem(options, state, originalSelect, selectedItemText);
})
.then(function() {
return pendingPromise.resolve();
})
.catch();
.catch(notification.exception);
});
var selectionElement = $(document.getElementById(state.selectionId));
@@ -1297,6 +1293,36 @@ define([
});
};
/**
* Announces changes to a tag in the autocomplete form.
*
* Updates the text content of a status element to inform users about the addition or removal
* of a tag. This is useful for accessibility purposes, ensuring screen readers can notify users
* of changes in the autocomplete component.
*
* @param {string} selectionId - The ID of the selection element used to locate the announcer element.
* @param {string|null|undefined} tagname - The name of the tag that was added or removed.
* @param {string} action - The action performed on the tag (e.g., "added" or "removed").
*/
const announceChanges = async(selectionId, tagname, action) => {
if (!tagname) {
return;
}
const status = document.getElementById(`${selectionId}-announcer`);
if (!status) {
return;
}
status.textContent = await str.get_string(action, 'core', tagname);
// Remove the status message after 4 seconds to prevent screen readers from announcing it.
setTimeout(() => {
status.textContent = '';
}, 4000);
};
return {
// Public variables and functions.
enhanceField: enhanceField,
@@ -38,6 +38,7 @@
], "noSelectionString": "No selection" }
}}
<span class="visually-hidden" id="{{selectionId}}-label">{{#str}}selecteditems, form{{/str}}</span>
<div id="{{selectionId}}-announcer" aria-live="polite" role="status" class="visually-hidden"></div>
<div{{!
}} class="form-autocomplete-selection w-100 {{#multiple}}form-autocomplete-multiple{{/multiple}}"{{!
}} id="{{selectionId}}"{{!
@@ -77,7 +77,7 @@ Feature: Use the qbank plugin manager page for bulkmove
When I open the autocomplete suggestions list in the ".search-banks" "css_element"
Then I should not see "C3 - Question bank 3" in the ".search-banks" "css_element"
And I click on "C1 - Question bank 1" item in the autocomplete list
Then I should not see "Test questions 1" in the ".search-categories" "css_element"
Then I should not see "Test questions 1" in the ".search-categories .form-autocomplete-selection" "css_element"
And I open the autocomplete suggestions list in the ".search-categories" "css_element"
And "Test questions 2" "autocomplete_suggestions" should exist
And "Test questions 3" "autocomplete_suggestions" should not exist