')
+ .addClass('tagger-audible-status')
+ .attr('role', 'status')
+ .attr('aria-live', 'polite')
+ .attr('aria-atomic', 'true')
+ .appendTo(this.taggerWidget);
+
if (!this.readonly) {
// If not readonly, stub out an empty suggestion list
this.taggerSuggestions = $('
')
@@ -553,7 +590,7 @@
}
}
else if (event.which === this.keyCodes.DOWN) { // Down Arrow
- if (isMainInput) {
+ if (isMainInput && !event.altKey) {
if (!this.options.ajaxURL || this.taggerSuggestions.is(":visible")) {
this._showSuggestions(true);
}
@@ -592,19 +629,28 @@
this.taggerWidget.find("input[tabindex]:visible").first().focus();
}
else {
- // If the suggestion list is visible already, then toggle it off
- if (this.taggerSuggestions.is(":visible")) {
- this._hideSuggestions();
- }
- // otherwise show it
- else {
- this._showSuggestions(true);
- }
+ this._toggleShowSuggestions();
}
event.preventDefault();
}
},
+ /**
+ * Toggle whether suggestions are shown or not
+ *
+ * @private
+ */
+ _toggleShowSuggestions: function() {
+ // If the suggestion list is visible already, then toggle it off
+ if (this.taggerSuggestions.is(":visible")) {
+ this._hideSuggestions();
+ }
+ // otherwise show it
+ else {
+ this._showSuggestions(true);
+ }
+ },
+
/**
* When keypress events fire on the tagger widget redirect them to the filter input
*
@@ -707,7 +753,7 @@
this._inputExpand(this.taggerInput);
// Clear filtered suggestions
- this._loadSuggestions(this.tagsByID, true);
+ this._loadSuggestions(this.tagsByID, true, false);
// Set the flag to show it's not loaded filtered results
this.loadedFiltered = false;
}, this), 250);
@@ -752,7 +798,7 @@
}
}
// Load filtered results into the suggestion list
- this._loadSuggestions(filteredResults, false);
+ this._loadSuggestions(filteredResults, false, true);
this.loadedFiltered = true;
},
@@ -796,7 +842,7 @@
}
});
self.tagsByID = data;
- self._loadSuggestions(data, false);
+ self._loadSuggestions(data, false, true);
self.loadedFiltered = true;
self._showSuggestions(false);
},
@@ -933,9 +979,10 @@
* Load tags into the suggestion list
* @param {object} suggestableTags - Object containing members of tagID to tag object
* @param {boolean} allowIndent - Allow indenting of suggestion lists if true
+ * @param {boolean} setAudibleStatus - Set the audible status of the suggestions list if true
* @protected
*/
- _loadSuggestions: function (suggestableTags, allowIndent) {
+ _loadSuggestions: function (suggestableTags, allowIndent, setAudibleStatus) {
// Clear out suggestion list
this.taggerSuggestionsList.children().remove();
@@ -992,6 +1039,8 @@
.appendTo(this.taggerSuggestionsList);
}
+ var audibleStatusSet = false;
+
// Add message if filtering meant no items to suggest and the noSuggestText option is not empty and the user has actually typed something
if (suggestableTagArray.length === 0) {
if (this.options.noSuggestText.length > 0 && this._getVisibleInput().val().length > 0) {
@@ -1002,6 +1051,11 @@
.addClass('missing')
.text(this.options.noSuggestText)
.appendTo(this.taggerSuggestionsList);
+
+ if (setAudibleStatus) {
+ this._setAudibleStatus(this.options.noSuggestText);
+ audibleStatusSet = true;
+ }
}
}
else if (this.taggerSuggestionsList.children().length === 0) {
@@ -1013,6 +1067,11 @@
.addClass('missing')
.text(this.options.emptyListText)
.appendTo(this.taggerSuggestionsList);
+
+ if (setAudibleStatus) {
+ this._setAudibleStatus(this.options.emptyListText);
+ audibleStatusSet = true;
+ }
}
if (suggestableTags.limited) {
@@ -1023,6 +1082,16 @@
.addClass('limited')
.text(this.options.limitedText)
.appendTo(this.taggerSuggestionsList);
+
+ if (setAudibleStatus) {
+ this._setAudibleStatus(this.options.limitedText);
+ audibleStatusSet = true;
+ }
+ }
+
+ if (!audibleStatusSet && setAudibleStatus) {
+ // Announce the number of options available
+ this._setAudibleStatus(suggestableTagArray.length + ((suggestableTagArray.length === 1) ? ' option is' : ' options are') + ' available');
}
},
@@ -1095,11 +1164,15 @@
// Handle suggestion adding
var suggestionItem = $(event.target).closest('li');
if (suggestionItem.data('tagid') && !suggestionItem.data('freetext')) {
- this._addTagFromID(suggestionItem.data('tagid'));
+ var tagId = suggestionItem.data('tagid')
+ this._addTagFromID(tagId);
+ this._setAudibleStatus("Selected " + this.tagsByID[tagId].key);
this._selectionReset(true, true);
}
else if (suggestionItem.data('freetext') && !suggestionItem.data('tagid')) {
- this._addFreeText(suggestionItem.data('freetext'));
+ var freetext = suggestionItem.data('freetext');
+ this._addFreeText(freetext);
+ this._setAudibleStatus("Added " + freetext);
this._selectionReset(true, true);
}
else {
@@ -1234,7 +1307,7 @@
this._hideSuggestions();
}
// Reload in all suggestions
- this._loadSuggestions(this.tagsByID, true);
+ this._loadSuggestions(this.tagsByID, true, true);
// Clear the flag
this.loadedFiltered = false;
}
@@ -1256,7 +1329,7 @@
this.taggerSuggestions.show();
// Mark the aria expanded attr to true
- this.taggerInput.attr('aria-expanded', 'true');
+ this.taggerWidget.attr('aria-expanded', 'true');
// Show the filter if necessary
if (this.singleValue && this.taggerFilterInput && this.tagCount === 1) {
@@ -1268,7 +1341,7 @@
var self = this;
var loadSuggestionsInternal = function () {
- self._loadSuggestions(self.tagsByID, true);
+ self._loadSuggestions(self.tagsByID, true, true);
// Set the flag to show it's not loaded filtered results
self.loadedFiltered = false;
// Focus the first item in the list, which may be the filter, or may be an option
@@ -1308,7 +1381,7 @@
this.taggerSuggestions.hide();
// Mark the aria expanded attr to false
- this.taggerInput.attr('aria-expanded', 'false');
+ this.taggerWidget.attr('aria-expanded', 'false');
},
/**
@@ -1358,7 +1431,7 @@
// Expand properly
this._inputExpand(this.taggerInput);
// Clear filtered suggestions
- this._loadSuggestions(this.tagsByID, true);
+ this._loadSuggestions(this.tagsByID, true, !shouldHideMenu);
// Set the flag to show it's not loaded filtered results
this.loadedFiltered = false;
// Focus input
@@ -1416,15 +1489,16 @@
tag = $('
')
.addClass('tag')
.attr("tabindex", this.tabIndex)
+ .attr("aria-label", tagData.key)
.text($('
').html(tagData.key).text())
.data("tagid", tagID)
- .insertBefore(this.taggerInput);
+ .appendTo(this.taggerSelectedTags);
if (tagData.freetext) {
tag.addClass('freetext');
}
- var tagRemover = $('
');
+ var tagRemover = $('
');
// Reusable tag removal closure
var tagRemoveProcessing = function () {
@@ -1475,9 +1549,9 @@
}
}, this));
- // Bind event to the whole tag to deal with backspaces, arrow keys
+ // Bind event to the whole tag to deal with backspaces, delete key and arrow keys
tag.bind('keydown', $.proxy(function (event) {
- if (event.which === this.keyCodes.BACKSPACE) { // Backspace
+ if (event.which === this.keyCodes.BACKSPACE || event.which === this.keyCodes.DELETE) { // Backspace or delete
this._removeTagByElem($(event.target), false, true);
if (tagRemover) {
tagRemover.remove();
@@ -1528,7 +1602,7 @@
}
}
else {
- tag = $('
').prependTo(this.taggerWidget);
+ tag = $('
').appendTo(this.taggerSelectedTags);
tag.text($('
').html(tagData.key).text());
if (this.singleValue) {
tag.addClass('tag-single');
@@ -1627,12 +1701,24 @@
this.taggerInput.show();
}
+ // Announce the removal
+ this._setAudibleStatus("Removed " + this.tagsByID[tagID].key);
+
// Fire onchange action
if (this.canFireActions) {
this._fireOnChangeAction();
}
},
+ /**
+ * Set the text in the audible status div, which will be read out by screen readers
+ * @param {String} status - the text to read out
+ * @protected
+ */
+ _setAudibleStatus: function (status) {
+ this.audibleStatus.text(status);
+ },
+
/**
* If there is any onchange function defined on the original element, run it
* @protected