MDL-85041 core: Improve autocomplete/tag a11y for assistive tech

- Updated autocomplete behavior to announce dynamic changes
via ARIA live regions
- Fixed focus management for keyboard and screen reader navigation
- Added proper role/aria tags for interactive tag components
This commit is contained in:
meirzamoodle
2025-06-02 10:38:29 +07:00
parent 7acd85a1aa
commit bff7dffe16
5 changed files with 54 additions and 6 deletions
+1
View File
@@ -1819,6 +1819,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
+50 -4
View File
@@ -237,6 +237,10 @@ define([
}
}
});
const selectedItemText = item[0].childNodes[2].textContent?.trim();
announceChanges(state.selectionId, selectedItemText, 'removed');
// Rerender the selection list.
return updateSelectionList(options, state, originalSelect)
.then(function() {
@@ -620,6 +624,8 @@ define([
}
});
announceChanges(state.selectionId, query.trim(), 'added');
return updateSelectionList(options, state, originalSelect)
.then(function() {
// Notify that the selection changed.
@@ -647,9 +653,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) {
var selectCurrentItem = function(options, state, originalSelect, selectedItem) {
// Find the elements in the page.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
@@ -670,6 +677,8 @@ define([
}
});
announceChanges(state.selectionId, selectedItem, 'added');
return updateSelectionList(options, state, originalSelect)
.then(function() {
// Notify that the selection changed.
@@ -810,8 +819,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));
@@ -930,12 +940,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));
@@ -1283,6 +1294,41 @@ 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;
}
try {
const result = await str.get_string(action, 'core', tagname);
status.textContent = result;
} catch (error) {
notification.exception(error);
}
// 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="sr-only" id="{{selectionId}}-label">{{#str}}selecteditems, form{{/str}}</span>
<div id="{{selectionId}}-announcer" aria-live="polite" role="status" class="sr-only"></div>
<div{{!
}} class="form-autocomplete-selection w-100 {{#multiple}}form-autocomplete-multiple{{/multiple}}"{{!
}} id="{{selectionId}}"{{!