diff --git a/src/js/select2/dropdown/infiniteScroll.js b/src/js/select2/dropdown/infiniteScroll.js index 00cc1340..f4c6c028 100644 --- a/src/js/select2/dropdown/infiniteScroll.js +++ b/src/js/select2/dropdown/infiniteScroll.js @@ -78,7 +78,7 @@ define([ var $option = $( '
  • ' + 'role="option" aria-disabled="true">' ); var message = this.options.get('translations').get('loadingMore'); diff --git a/src/js/select2/dropdown/search.js b/src/js/select2/dropdown/search.js index 78693892..2700e06d 100644 --- a/src/js/select2/dropdown/search.js +++ b/src/js/select2/dropdown/search.js @@ -11,7 +11,7 @@ define([ '' + '' + + ' spellcheck="false" role="searchbox" aria-autocomplete="list" />' + '' ); @@ -26,6 +26,8 @@ define([ Search.prototype.bind = function (decorated, container, $container) { var self = this; + var resultsId = container.id + '-results'; + decorated.call(this, container, $container); this.$search.on('keydown', function (evt) { @@ -48,6 +50,7 @@ define([ container.on('open', function () { self.$search.attr('tabindex', 0); + self.$search.attr('aria-controls', resultsId); self.$search.trigger('focus'); @@ -58,6 +61,8 @@ define([ container.on('close', function () { self.$search.attr('tabindex', -1); + self.$search.removeAttr('aria-controls'); + self.$search.removeAttr('aria-activedescendant'); self.$search.val(''); self.$search.trigger('blur'); @@ -80,6 +85,14 @@ define([ } } }); + + container.on('results:focus', function (params) { + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } + }); }; Search.prototype.handleSearch = function (evt) { diff --git a/src/js/select2/results.js b/src/js/select2/results.js index e76d106b..c00f77dd 100644 --- a/src/js/select2/results.js +++ b/src/js/select2/results.js @@ -14,7 +14,7 @@ define([ Results.prototype.render = function () { var $results = $( - '' + '' ); if (this.options.get('multiple')) { @@ -37,7 +37,7 @@ define([ this.hideLoading(); var $message = $( - '
  • ' ); @@ -171,7 +171,7 @@ define([ option.className = 'select2-results__option'; var attrs = { - 'role': 'treeitem', + 'role': 'option', 'aria-selected': 'false' }; diff --git a/src/js/select2/selection/search.js b/src/js/select2/selection/search.js index 163a1817..dd889d2e 100644 --- a/src/js/select2/selection/search.js +++ b/src/js/select2/selection/search.js @@ -12,7 +12,7 @@ define([ '' ); @@ -29,14 +29,18 @@ define([ Search.prototype.bind = function (decorated, container, $container) { var self = this; + var resultsId = container.id + '-results'; + decorated.call(this, container, $container); container.on('open', function () { + self.$search.attr('aria-controls', resultsId); self.$search.trigger('focus'); }); container.on('close', function () { self.$search.val(''); + self.$search.removeAttr('aria-controls'); self.$search.removeAttr('aria-activedescendant'); self.$search.trigger('focus'); }); @@ -56,7 +60,11 @@ define([ }); container.on('results:focus', function (params) { - self.$search.attr('aria-activedescendant', params.id); + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } }); this.$selection.on('focusin', '.select2-search--inline', function (evt) { diff --git a/tests/a11y/search-tests.js b/tests/a11y/search-tests.js deleted file mode 100644 index 58e56492..00000000 --- a/tests/a11y/search-tests.js +++ /dev/null @@ -1,51 +0,0 @@ -module('Accessibility - Search'); - -var MultipleSelection = require('select2/selection/multiple'); -var InlineSearch = require('select2/selection/search'); - -var $ = require('jquery'); - -var Utils = require('select2/utils'); -var Options = require('select2/options'); -var options = new Options({}); - -test('aria-autocomplete attribute is present', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - var selection = new CustomSelection($select, options); - var $selection = selection.render(); - - // Update the selection so the search is rendered - selection.update([]); - - assert.equal( - $selection.find('input').attr('aria-autocomplete'), - 'list', - 'The search box is marked as autocomplete' - ); -}); - -test('aria-activedescendant should be removed when closed', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - var selection = new CustomSelection($select, options); - var $selection = selection.render(); - - var container = new MockContainer(); - selection.bind(container, $('')); - - // Update the selection so the search is rendered - selection.update([]); - - var $search = $selection.find('input'); - $search.attr('aria-activedescendant', 'something'); - - container.trigger('close'); - - assert.ok( - !$search.attr('aria-activedescendant'), - 'There is no active descendant when the dropdown is closed' - ); -}); diff --git a/tests/dropdown/search-a11y-tests.js b/tests/dropdown/search-a11y-tests.js new file mode 100644 index 00000000..0876de9f --- /dev/null +++ b/tests/dropdown/search-a11y-tests.js @@ -0,0 +1,185 @@ +module('Dropdown - Search - Accessibility'); + +var Utils = require('select2/utils'); + +var Dropdown = require('select2/dropdown'); +var DropdownSearch = Utils.Decorate( + Dropdown, + require('select2/dropdown/search') +); + +var $ = require('jquery'); + +var Options = require('select2/options'); +var options = new Options({}); + +test('role attribute is set to searchbox', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + assert.equal( + $dropdown.find('input').attr('role'), + 'searchbox', + 'The search box is marked as a search box' + ); +}); + +test('aria-autocomplete attribute is present', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + assert.equal( + $dropdown.find('input').attr('aria-autocomplete'), + 'list', + 'The search box is marked as autocomplete' + ); +}); + +test('aria-activedescendant should not be set initiailly', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + var $search = $dropdown.find('input'); + + assert.ok( + !$search.attr('aria-activedescendant'), + 'The search box should not point to anything when it is first rendered' + ); +}); + +test('aria-activedescendant should be set after highlight', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + container.trigger('results:focus', { + data: { + _resultId: 'test' + } + }); + + var $search = $dropdown.find('input'); + + assert.equal( + $search.attr('aria-activedescendant'), + 'test', + 'The search is pointing to the focused result' + ); +}); + +test('activedescendant should remove if there is no ID', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + var $search = $dropdown.find('input'); + $search.attr('aria-activedescendant', 'test'); + + container.trigger('results:focus', { + data: {} + }); + + assert.ok( + !$search.attr('aria-activedescendant'), + 'There is no result for the search to be pointing to' + ); +}); + +test('aria-activedescendant should be removed when closed', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + var $search = $dropdown.find('input'); + $search.attr('aria-activedescendant', 'something'); + + container.trigger('close'); + + assert.ok( + !$search.attr('aria-activedescendant'), + 'There is no active descendant when the dropdown is closed' + ); +}); + +test('aria-controls should not be set initiailly', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + var $search = $dropdown.find('input'); + + assert.ok( + !$search.attr('aria-controls'), + 'The search box should not point to the results when it is first rendered' + ); +}); + +test('aria-controls should be set when opened', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + var $search = $dropdown.find('input'); + + container.trigger('open'); + + assert.ok( + $search.attr('aria-controls'), + 'The search should point to the results when it is opened' + ); +}); + +test('aria-controls should be removed when closed', function (assert) { + var $select = $('#qunit-fixture .single'); + + var dropdown = new DropdownSearch($select, options); + var $dropdown = dropdown.render(); + + var container = new MockContainer(); + dropdown.bind(container, $('')); + + var $search = $dropdown.find('input'); + $search.attr('aria-controls', 'something'); + + container.trigger('close'); + + assert.ok( + !$search.attr('aria-controls'), + 'There are no results for the search box to point to when it is closed' + ); +}); diff --git a/tests/results/a11y-tests.js b/tests/results/a11y-tests.js new file mode 100644 index 00000000..c5293265 --- /dev/null +++ b/tests/results/a11y-tests.js @@ -0,0 +1,25 @@ +module('Results - Accessibility'); + +var $ = require('jquery'); + +var Options = require('select2/options'); + +var Results = require('select2/results'); + +test('role of results should be a listbox', function (assert) { + var results = new Results($(''), new Options({})); + + var $results = results.render(); + + assert.equal($results.attr('role'), 'listbox'); +}); + +test('multiple select should have aria-multiselectable', function (assert) { + var results = new Results($(''), new Options({ + multiple: true + })); + + var $results = results.render(); + + assert.equal($results.attr('aria-multiselectable'), 'true'); +}); diff --git a/tests/results/option-tests.js b/tests/results/option-tests.js index f98937e7..96570501 100644 --- a/tests/results/option-tests.js +++ b/tests/results/option-tests.js @@ -60,4 +60,57 @@ test('option in disabled optgroup is disabled', function (assert) { }); assert.equal(option.getAttribute('aria-disabled'), 'true'); -}); \ No newline at end of file +}); + +test('options are not selected by default', function (assert) { + var results = new Results($(''), new Options({})); + + var $option = $(''); + var option = results.option({ + id: 'test', + element: $option[0] + }); + + assert.equal(option.getAttribute('aria-selected'), 'false'); +}); + +test('options with children are given the group role', function(assert) { + var results = new Results($(''), new Options({})); + + var $option = $(''); + var option = results.option({ + children: [{ + id: 'test' + }], + element: $option[0] + }); + + assert.equal(option.getAttribute('role'), 'group'); +}); + +test('options with children have the aria-label set', function (assert) { + var results = new Results($(''), new Options({})); + + var $option = $(''); + var option = results.option({ + children: [{ + id: 'test' + }], + element: $option[0], + text: 'test' + }); + + assert.equal(option.getAttribute('aria-label'), 'test'); +}); + +test('non-group options are given the option role', function (assert) { + var results = new Results($(''), new Options({})); + + var $option = $(''); + var option = results.option({ + id: 'test', + element: $option[0] + }); + + assert.equal(option.getAttribute('role'), 'option'); +}); diff --git a/tests/selection/search-a11y-tests.js b/tests/selection/search-a11y-tests.js new file mode 100644 index 00000000..b2628df5 --- /dev/null +++ b/tests/selection/search-a11y-tests.js @@ -0,0 +1,217 @@ +module('Selection containers - Inline search - Accessibility'); + +var MultipleSelection = require('select2/selection/multiple'); +var InlineSearch = require('select2/selection/search'); + +var $ = require('jquery'); + +var Utils = require('select2/utils'); +var Options = require('select2/options'); +var options = new Options({}); + +test('role attribute is set to searchbox', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + assert.equal( + $selection.find('input').attr('role'), + 'searchbox', + 'The search box is marked as a search box' + ); +}); + +test('aria-autocomplete attribute is present', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + assert.equal( + $selection.find('input').attr('aria-autocomplete'), + 'list', + 'The search box is marked as autocomplete' + ); +}); + +test('aria-activedescendant should not be set initiailly', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + var $search = $selection.find('input'); + + assert.ok( + !$search.attr('aria-activedescendant'), + 'The search box should not point to anything when it is first rendered' + ); +}); + +test('aria-activedescendant should be set after highlight', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + container.trigger('results:focus', { + data: { + _resultId: 'test' + } + }); + + var $search = $selection.find('input'); + + assert.equal( + $search.attr('aria-activedescendant'), + 'test', + 'The search is pointing to the focused result' + ); +}); + +test('activedescendant should remove if there is no ID', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + var $search = $selection.find('input'); + $search.attr('aria-activedescendant', 'test'); + + container.trigger('results:focus', { + data: {} + }); + + assert.ok( + !$search.attr('aria-activedescendant'), + 'There is no result for the search to be pointing to' + ); +}); + +test('aria-activedescendant should be removed when closed', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + var $search = $selection.find('input'); + $search.attr('aria-activedescendant', 'something'); + + container.trigger('close'); + + assert.ok( + !$search.attr('aria-activedescendant'), + 'There is no active descendant when the dropdown is closed' + ); +}); + +test('aria-controls should not be set initiailly', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + var $search = $selection.find('input'); + + assert.ok( + !$search.attr('aria-controls'), + 'The search box should not point to the results when it is first rendered' + ); +}); + +test('aria-controls should be set when opened', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + var $search = $selection.find('input'); + + container.trigger('open'); + + assert.ok( + $search.attr('aria-controls'), + 'The search should point to the results when it is opened' + ); +}); + +test('aria-controls should be removed when closed', function (assert) { + var $select = $('#qunit-fixture .multiple'); + + var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); + var selection = new CustomSelection($select, options); + var $selection = selection.render(); + + var container = new MockContainer(); + selection.bind(container, $('')); + + // Update the selection so the search is rendered + selection.update([]); + + var $search = $selection.find('input'); + $search.attr('aria-controls', 'something'); + + container.trigger('close'); + + assert.ok( + !$search.attr('aria-controls'), + 'There are no results for the search box to point to when it is closed' + ); +}); diff --git a/tests/unit-jq1.html b/tests/unit-jq1.html index bdbe1e04..dc2ca21e 100644 --- a/tests/unit-jq1.html +++ b/tests/unit-jq1.html @@ -57,7 +57,6 @@ - @@ -72,6 +71,7 @@ + @@ -81,6 +81,7 @@ + @@ -91,6 +92,7 @@ + diff --git a/tests/unit-jq2.html b/tests/unit-jq2.html index e3f23023..52500510 100644 --- a/tests/unit-jq2.html +++ b/tests/unit-jq2.html @@ -57,7 +57,6 @@ - @@ -72,6 +71,7 @@ + @@ -81,6 +81,7 @@ + @@ -91,6 +92,7 @@ + diff --git a/tests/unit-jq3.html b/tests/unit-jq3.html index d19a88df..d038818a 100644 --- a/tests/unit-jq3.html +++ b/tests/unit-jq3.html @@ -57,7 +57,6 @@ - @@ -72,6 +71,7 @@ + @@ -81,6 +81,7 @@ + @@ -91,6 +92,7 @@ +