Commit 22ceff0f authored by Jono Mingard's avatar Jono Mingard Committed by Robert Lyon
Browse files

Improve accessibility of select2 (Bug #1492081)

- Modify the AJAX data adapter and search box so that items are
  read out when moving up and down through them with the keyboard
- Link select2 instances to the label(s) of the original <select>
- Allow removing tags with the keyboard
- Hide original <select> completely instead of leaving it
  accessible to screen readers
- Keep focus position when loading more elements with infinite
  scrolling
- Add behat step to set select2 value(s)

behatnotneeded

Change-Id: I6b380f74525c2ca3c38070744d71d1812cec05b0
parent 67685d07
......@@ -13,3 +13,12 @@ Changes:
* Removed package manager metadata files
* Added styling to decrease size of text in the menus
* Added a 'readonly' status to an option to hide the 'x' (stop it being removable)
* Accessibility improvements
* Modify the AJAX data adapter and search box so that items are read out when moving up and
down through them with the keyboard
* Link select2 instances to the label(s) of the original <select>
* Allow removing tags with the keyboard
* Hide original <select> completely instead of leaving it accessible to screen readers
* Keep focus position when loading more elements with infinite scrolling
* Add label to "Remove tag" button
* Make "Remove tag" button work with the keyboard
......@@ -781,8 +781,9 @@ S2.define('select2/results',[
this.clear();
this.hideLoading();
var messageId = this.data.generateResultId({ id: this.$results.attr('id') }, {});
var $message = $(
'<li role="treeitem" aria-live="assertive"' +
'<li id="' + messageId + '" role="treeitem" aria-live="assertive"' +
' class="select2-results__option"></li>'
);
......@@ -797,6 +798,7 @@ S2.define('select2/results',[
$message[0].className += ' select2-results__message';
this.$results.append($message);
this.trigger('results:speak', { id: messageId });
};
Results.prototype.hideMessages = function () {
......@@ -842,7 +844,7 @@ S2.define('select2/results',[
return sorter(data);
};
Results.prototype.setClasses = function () {
Results.prototype.setClasses = function (highlightIndex) {
var self = this;
this.data.current(function (selected) {
......@@ -871,8 +873,10 @@ S2.define('select2/results',[
var $selected = $options.filter('[aria-selected=true]');
// Check if there are any selected options
if ($selected.length > 0) {
if (highlightIndex >= 0) {
// If a specific index has been given, highlight it
$options.eq(highlightIndex).trigger('mouseenter');
} else if ($selected.length > 0) {
// If there are selected options, highlight the first
$selected.first().trigger('mouseenter');
} else {
......@@ -935,6 +939,14 @@ S2.define('select2/results',[
delete attrs['aria-selected'];
}
if (data.element) {
$element = $(data.element);
if ($element.hasClass('readonly')) {
var currentclass = option.getAttribute('class');
option.setAttribute('class', 'readonly ' + currentclass);
}
}
for (var attr in attrs) {
var val = attrs[attr];
......@@ -994,10 +1006,13 @@ S2.define('select2/results',[
});
container.on('results:append', function (params) {
var previousIndex = self.$results
.find('.select2-results__option[aria-selected]').length - 1;
self.append(params.data);
if (container.isOpen()) {
self.setClasses();
self.setClasses(previousIndex);
}
});
......@@ -1175,10 +1190,12 @@ S2.define('select2/results',[
if ($this.attr('aria-selected') === 'true') {
if (self.options.get('multiple')) {
self.trigger('unselect', {
originalEvent: evt,
data: data
});
if (!$this.hasClass('readonly')) {
self.trigger('unselect', {
originalEvent: evt,
data: data
});
}
} else {
self.trigger('close', {});
}
......@@ -1203,6 +1220,9 @@ S2.define('select2/results',[
data: data,
element: $(this)
});
self.trigger('results:speak', {
id: $(this).attr('id')
});
});
};
......@@ -1303,7 +1323,7 @@ S2.define('select2/selection/base',[
BaseSelection.prototype.render = function () {
var $selection = $(
'<span class="select2-selection" role="combobox" ' +
' aria-haspopup="true" aria-expanded="false">' +
'aria-haspopup="true" aria-expanded="false" aria-labelledby="' + this.options.labels.join(' ') + '">' +
'</span>'
);
......@@ -1347,10 +1367,6 @@ S2.define('select2/selection/base',[
}
});
container.on('results:focus', function (params) {
self.$selection.attr('aria-activedescendant', params.data._resultId);
});
container.on('selection:update', function (params) {
self.update(params.data);
});
......@@ -1568,23 +1584,9 @@ S2.define('select2/selection/multiple',[
MultipleSelection.__super__.bind.apply(this, arguments);
this.$selection.on('click', function (evt) {
self.trigger('toggle', {
originalEvent: evt
});
});
this.$selection.on(
'click',
'.select2-selection__choice__remove',
function (evt) {
// Ignore the event if it is disabled
if (self.options.get('disabled')) {
return;
}
var $remove = $(this);
var $remove = $(evt.target).closest('.select2-selection__choice__remove');
if ($remove.length) {
var $selection = $remove.parent();
var data = $selection.data('data');
self.trigger('unselect', {
......@@ -1592,7 +1594,12 @@ S2.define('select2/selection/multiple',[
data: data
});
}
);
else {
self.trigger('toggle', {
originalEvent: evt
});
}
});
};
MultipleSelection.prototype.clear = function () {
......@@ -1606,15 +1613,25 @@ S2.define('select2/selection/multiple',[
return escapeMarkup(template(data, container));
};
MultipleSelection.prototype.selectionContainer = function (readonly) {
MultipleSelection.prototype.selectionContainer = function (selection, readonly) {
readonly = (typeof readonly !== 'undefined') ? readonly : false;
var state = (readonly) ? 'readonly' : 'remove';
var button = (readonly) ? '' : '&times;';
var state, button;
if (readonly) {
state = 'readonly';
button = '';
}
else {
state = 'remove';
button = '<span class="select2-hidden-accessible">' +
get_string_ajax('element.select.remove', 'pieforms', selection.text || selection.title) +
'</span>&times;';
}
var $container = $(
'<li class="select2-selection__choice">' +
'<span class="select2-selection__choice__' + state + '" role="presentation">' +
'<button class="select2-selection__choice__' + state + '">' +
button +
'</span>' +
'</button>' +
'</li>'
);
......@@ -1634,7 +1651,7 @@ S2.define('select2/selection/multiple',[
var selection = data[d];
var readonly = $(selection.element).hasClass('readonly');
var $selection = this.selectionContainer(readonly);
var $selection = this.selectionContainer(selection, readonly);
var formatted = this.display(selection, $selection);
$selection.append(formatted);
......@@ -1862,6 +1879,10 @@ S2.define('select2/selection/search',[
self.$search.attr('aria-activedescendant', params.id);
});
container.on('results:speak', function (params) {
self.$search.attr('aria-activedescendant', params.id);
});
this.$selection.on('focusin', '.select2-search--inline', function (evt) {
self.trigger('focus', evt);
});
......@@ -3432,6 +3453,11 @@ S2.define('select2/data/ajax',[
var $request = options.transport(options, function (data) {
var results = self.processResults(data, params);
var items = results.results;
for (var i = 0; i < items.length; i++) {
items[i] = self._normalizeItem(items[i]);
}
if (self.options.get('debug') && window.console && console.error) {
// Check to make sure that the response included a `results` key.
if (!results || !results.results || !$.isArray(results.results)) {
......@@ -3829,7 +3855,7 @@ S2.define('select2/dropdown/search',[
'<span class="select2-search select2-search--dropdown">' +
'<input class="select2-search__field" type="search" tabindex="-1"' +
' autocomplete="off" autocorrect="off" autocapitalize="off"' +
' spellcheck="false" role="textbox" />' +
' spellcheck="false" role="textbox" aria-autocomplete="list" />' +
'</span>'
);
......@@ -4952,6 +4978,8 @@ S2.define('select2/core',[
$element.data('select2').destroy();
}
var self = this;
this.$element = $element;
this.id = this._generateId($element);
......@@ -4960,6 +4988,10 @@ S2.define('select2/core',[
this.options = new Options(options, $element);
if (this.options.labels == null) {
this.options.labels = this._findOriginalLabels();
}
Select2.__super__.constructor.call(this);
// Set up the tabindex
......@@ -4997,8 +5029,6 @@ S2.define('select2/core',[
// Bind events
var self = this;
// Bind the container to all of the adapters
this._bindAdapters();
......@@ -5020,8 +5050,7 @@ S2.define('select2/core',[
});
// Hide the original select
$element.addClass('select2-hidden-accessible');
$element.attr('aria-hidden', 'true');
$element.hide();
// Synchronize any monitored attributes
this._syncAttributes();
......@@ -5031,6 +5060,23 @@ S2.define('select2/core',[
Utils.Extend(Select2, Utils.Observable);
Select2.prototype._findOriginalLabels = function() {
var self = this;
var id = this.$element.attr('id');
if (id) {
return jQuery.map($('label[for="' + id + '"]'), function(el, i) {
if (!$(el).attr('id')) {
$(el).attr('id', self.id + '-label-' + i);
}
return $(el).attr('id');
});
}
else {
return [];
}
}
Select2.prototype._generateId = function ($element) {
var id = '';
......@@ -5265,9 +5311,9 @@ S2.define('select2/core',[
}
} else {
if (key === KEYS.ENTER || key === KEYS.SPACE ||
(key === KEYS.DOWN && evt.altKey)) {
(key === KEYS.DOWN && evt.altKey) &&
$(evt.target).hasClass('select2-selection')) {
self.open();
evt.preventDefault();
}
}
......
......@@ -218,12 +218,18 @@ Version: 4.0.0 Timestamp: Jul 1 13:43:36 EDT 2015
text-overflow: ellipsis;
width: auto; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
background: none;
border: none;
color: #999;
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
margin-right: 2px;
padding: 0; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__readonly {
display: none; }
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:focus {
color: #333; }
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
......
Supports Markdown
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