From 9ada3712f756390e2ec434dbfcb880243259babf Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sun, 19 Oct 2014 16:13:57 -0400 Subject: [PATCH] Single select accessibility Now the Select2 instance is correctly recognized as a combobox, and the selected option is read aloud when it is focused, just like in a standard select box. This works by generating semi-random ids that are used for the ARIA attributes. These are not intended to be consistent by any means, they are just generated to make the instance accessible by screen readers. --- dist/js/select2.amd.full.js | 58 ++++++++++++++++++++++++++++-- dist/js/select2.amd.js | 58 ++++++++++++++++++++++++++++-- dist/js/select2.full.js | 58 ++++++++++++++++++++++++++++-- dist/js/select2.full.min.js | 4 +-- dist/js/select2.js | 58 ++++++++++++++++++++++++++++-- dist/js/select2.min.js | 2 +- src/js/select2/data/base.js | 19 ++++++++++ src/js/select2/data/select.js | 4 +++ src/js/select2/results.js | 8 ++++- src/js/select2/selection/single.js | 27 +++++++++++++- 10 files changed, 283 insertions(+), 13 deletions(-) diff --git a/dist/js/select2.amd.full.js b/dist/js/select2.amd.full.js index e312499c..bf832c2c 100644 --- a/dist/js/select2.amd.full.js +++ b/dist/js/select2.amd.full.js @@ -258,7 +258,11 @@ define('select2/results',[ } if (data.id == null) { - $option.removeClass('aria-selected'); + $option.removeAttr('aria-selected'); + } + + if (data._resultId != null) { + $option.attr('id', data._resultId); } $option.data('data', data); @@ -363,6 +367,8 @@ define('select2/results',[ var $next = $options.eq(nextIndex); $next.trigger('mouseenter'); + console.log($next.offset().top, self.$results.parent().scrollTop()); + //self.$results.parents().scrollTop($next.offset().top); }); this.$results.on('mouseup', '.option[aria-selected]', function (evt) { @@ -482,13 +488,24 @@ define('select2/selection/single',[ SingleSelection.prototype.render = function () { var $selection = $( - '' + + '' ); $selection.attr('title', this.$element.attr('title')); + var id = 'select2-container-'; + + for (var i = 0; i < 4; i++) { + var r = Math.floor(Math.random() * 16); + id += r.toString(16); + } + + $selection.find('.rendered-selection').attr('id', id); + $selection.attr('aria-labelledby', id); + this.$selection = $selection; return $selection; @@ -510,6 +527,16 @@ define('select2/selection/single',[ }); }); + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$selection.attr('aria-expanded', 'true'); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$selection.attr('aria-expanded', 'false'); + }); + this.$selection.on('focus', function (evt) { // User focuses on the container }); @@ -564,6 +591,10 @@ define('select2/selection/single',[ var formatted = this.display(selection); this.$selection.find('.rendered-selection').html(formatted); + + if (data[0]._resultId != null) { + this.$selection.attr('aria-activedescendent', data[0]._resultId); + } }; return SingleSelection; @@ -727,6 +758,25 @@ define('select2/data/base',[ // Can be implemented in subclasses }; + BaseAdapter.prototype.generateResultId = function (data) { + var id = ''; + + for (var i = 0; i < 4; i++) { + var r = Math.floor(Math.random() * 16); + id += r.toString(16); + } + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + for (var s = 0; s < 4; s++) { + var idChar = Math.floor(Math.random() * 16); + id += idChar.toString(16); + } + } + return id; + }; + return BaseAdapter; }); @@ -878,6 +928,10 @@ define('select2/data/select',[ data.children = children; } + if (data.id) { + data._resultId = this.generateResultId(data); + } + $option.data('data', data); } diff --git a/dist/js/select2.amd.js b/dist/js/select2.amd.js index e312499c..bf832c2c 100644 --- a/dist/js/select2.amd.js +++ b/dist/js/select2.amd.js @@ -258,7 +258,11 @@ define('select2/results',[ } if (data.id == null) { - $option.removeClass('aria-selected'); + $option.removeAttr('aria-selected'); + } + + if (data._resultId != null) { + $option.attr('id', data._resultId); } $option.data('data', data); @@ -363,6 +367,8 @@ define('select2/results',[ var $next = $options.eq(nextIndex); $next.trigger('mouseenter'); + console.log($next.offset().top, self.$results.parent().scrollTop()); + //self.$results.parents().scrollTop($next.offset().top); }); this.$results.on('mouseup', '.option[aria-selected]', function (evt) { @@ -482,13 +488,24 @@ define('select2/selection/single',[ SingleSelection.prototype.render = function () { var $selection = $( - '' + + '' ); $selection.attr('title', this.$element.attr('title')); + var id = 'select2-container-'; + + for (var i = 0; i < 4; i++) { + var r = Math.floor(Math.random() * 16); + id += r.toString(16); + } + + $selection.find('.rendered-selection').attr('id', id); + $selection.attr('aria-labelledby', id); + this.$selection = $selection; return $selection; @@ -510,6 +527,16 @@ define('select2/selection/single',[ }); }); + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$selection.attr('aria-expanded', 'true'); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$selection.attr('aria-expanded', 'false'); + }); + this.$selection.on('focus', function (evt) { // User focuses on the container }); @@ -564,6 +591,10 @@ define('select2/selection/single',[ var formatted = this.display(selection); this.$selection.find('.rendered-selection').html(formatted); + + if (data[0]._resultId != null) { + this.$selection.attr('aria-activedescendent', data[0]._resultId); + } }; return SingleSelection; @@ -727,6 +758,25 @@ define('select2/data/base',[ // Can be implemented in subclasses }; + BaseAdapter.prototype.generateResultId = function (data) { + var id = ''; + + for (var i = 0; i < 4; i++) { + var r = Math.floor(Math.random() * 16); + id += r.toString(16); + } + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + for (var s = 0; s < 4; s++) { + var idChar = Math.floor(Math.random() * 16); + id += idChar.toString(16); + } + } + return id; + }; + return BaseAdapter; }); @@ -878,6 +928,10 @@ define('select2/data/select',[ data.children = children; } + if (data.id) { + data._resultId = this.generateResultId(data); + } + $option.data('data', data); } diff --git a/dist/js/select2.full.js b/dist/js/select2.full.js index 9009fad9..bc207836 100644 --- a/dist/js/select2.full.js +++ b/dist/js/select2.full.js @@ -9796,7 +9796,11 @@ define('select2/results',[ } if (data.id == null) { - $option.removeClass('aria-selected'); + $option.removeAttr('aria-selected'); + } + + if (data._resultId != null) { + $option.attr('id', data._resultId); } $option.data('data', data); @@ -9901,6 +9905,8 @@ define('select2/results',[ var $next = $options.eq(nextIndex); $next.trigger('mouseenter'); + console.log($next.offset().top, self.$results.parent().scrollTop()); + //self.$results.parents().scrollTop($next.offset().top); }); this.$results.on('mouseup', '.option[aria-selected]', function (evt) { @@ -10020,13 +10026,24 @@ define('select2/selection/single',[ SingleSelection.prototype.render = function () { var $selection = $( - '' + + '' ); $selection.attr('title', this.$element.attr('title')); + var id = 'select2-container-'; + + for (var i = 0; i < 4; i++) { + var r = Math.floor(Math.random() * 16); + id += r.toString(16); + } + + $selection.find('.rendered-selection').attr('id', id); + $selection.attr('aria-labelledby', id); + this.$selection = $selection; return $selection; @@ -10048,6 +10065,16 @@ define('select2/selection/single',[ }); }); + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$selection.attr('aria-expanded', 'true'); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$selection.attr('aria-expanded', 'false'); + }); + this.$selection.on('focus', function (evt) { // User focuses on the container }); @@ -10102,6 +10129,10 @@ define('select2/selection/single',[ var formatted = this.display(selection); this.$selection.find('.rendered-selection').html(formatted); + + if (data[0]._resultId != null) { + this.$selection.attr('aria-activedescendent', data[0]._resultId); + } }; return SingleSelection; @@ -10265,6 +10296,25 @@ define('select2/data/base',[ // Can be implemented in subclasses }; + BaseAdapter.prototype.generateResultId = function (data) { + var id = ''; + + for (var i = 0; i < 4; i++) { + var r = Math.floor(Math.random() * 16); + id += r.toString(16); + } + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + for (var s = 0; s < 4; s++) { + var idChar = Math.floor(Math.random() * 16); + id += idChar.toString(16); + } + } + return id; + }; + return BaseAdapter; }); @@ -10416,6 +10466,10 @@ define('select2/data/select',[ data.children = children; } + if (data.id) { + data._resultId = this.generateResultId(data); + } + $option.data('data', data); } diff --git a/dist/js/select2.full.min.js b/dist/js/select2.full.min.js index 820b2033..395f022b 100644 --- a/dist/js/select2.full.min.js +++ b/dist/js/select2.full.min.js @@ -1,4 +1,4 @@ var requirejs,require,define;!function(a){function b(a,b){return r.call(a,b)}function c(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=p.map,q=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(n=n.slice(0,n.length-1),a=a.split("/"),g=a.length-1,p.nodeIdCompat&&t.test(a[g])&&(a[g]=a[g].replace(t,"")),a=n.concat(a),k=0;k0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||q)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&q&&q[d]&&(i=q[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function d(b,c){return function(){return k.apply(a,s.call(arguments,0).concat([b,c]))}}function e(a){return function(b){return c(b,a)}}function f(a){return function(b){n[a]=b}}function g(c){if(b(o,c)){var d=o[c];delete o[c],q[c]=!0,j.apply(a,d)}if(!b(n,c)&&!b(q,c))throw new Error("No "+c);return n[c]}function h(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function i(a){return function(){return p&&p.config&&p.config[a]||{}}}var j,k,l,m,n={},o={},p={},q={},r=Object.prototype.hasOwnProperty,s=[].slice,t=/\.js$/;l=function(a,b){var d,f=h(a),i=f[0];return a=f[1],i&&(i=c(i,b),d=g(i)),i?a=d&&d.normalize?d.normalize(a,e(b)):c(a,b):(a=c(a,b),f=h(a),i=f[0],a=f[1],i&&(d=g(i))),{f:i?i+"!"+a:a,n:a,pr:i,p:d}},m={require:function(a){return d(a)},exports:function(a){var b=n[a];return"undefined"!=typeof b?b:n[a]={}},module:function(a){return{id:a,uri:"",exports:n[a],config:i(a)}}},j=function(c,e,h,i){var j,k,p,r,s,t,u=[],v=typeof h;if(i=i||c,"undefined"===v||"function"===v){for(e=!e.length&&h.length?["require","exports","module"]:e,s=0;s0&&b-1 in a}function d(a,b,c){if(ab.isFunction(b))return ab.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return ab.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(hb.test(b))return ab.filter(b,a,c);b=ab.filter(b,a)}return ab.grep(a,function(a){return U.call(b,a)>=0!==c})}function e(a,b){for(;(a=a[b])&&1!==a.nodeType;);return a}function f(a){var b=ob[a]={};return ab.each(a.match(nb)||[],function(a,c){b[c]=!0}),b}function g(){$.removeEventListener("DOMContentLoaded",g,!1),a.removeEventListener("load",g,!1),ab.ready()}function h(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=ab.expando+Math.random()}function i(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(ub,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:tb.test(c)?ab.parseJSON(c):c}catch(e){}sb.set(a,b,c)}else c=void 0;return c}function j(){return!0}function k(){return!1}function l(){try{return $.activeElement}catch(a){}}function m(a,b){return ab.nodeName(a,"table")&&ab.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function n(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function o(a){var b=Kb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function p(a,b){for(var c=0,d=a.length;d>c;c++)rb.set(a[c],"globalEval",!b||rb.get(b[c],"globalEval"))}function q(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(rb.hasData(a)&&(f=rb.access(a),g=rb.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)ab.event.add(b,e,j[e][c])}sb.hasData(a)&&(h=sb.access(a),i=ab.extend({},h),sb.set(b,i))}}function r(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&ab.nodeName(a,b)?ab.merge([a],c):c}function s(a,b){var c=b.nodeName.toLowerCase();"input"===c&&yb.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}function t(b,c){var d=ab(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:ab.css(d[0],"display");return d.detach(),e}function u(a){var b=$,c=Ob[a];return c||(c=t(a,b),"none"!==c&&c||(Nb=(Nb||ab("