From cc9419928e759018c0eb84785223dd4c479f88ee Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sat, 18 Oct 2014 10:49:51 -0400 Subject: [PATCH] Working on accessibility This makes quite a few changes, one of the major ones being the removal of classes for marking options as selected or selectable, and instead using the ARIA attributes which should already be present. --- dist/css/select2.css | 9 +- dist/css/select2.min.css | 2 +- dist/js/select2.amd.full.js | 169 ++++++++++++++++++++++++--- dist/js/select2.amd.js | 169 ++++++++++++++++++++++++--- dist/js/select2.full.js | 169 ++++++++++++++++++++++++--- dist/js/select2.full.min.js | 4 +- dist/js/select2.js | 169 ++++++++++++++++++++++++--- dist/js/select2.min.js | 2 +- docs/examples.html | 4 + src/js/select2/core.js | 17 ++- src/js/select2/dropdown/search.js | 10 +- src/js/select2/keys.js | 39 +++++++ src/js/select2/results.js | 69 +++++++++-- src/js/select2/selection/multiple.js | 2 +- src/js/select2/selection/single.js | 31 ++++- src/scss/_dropdown.scss | 5 +- src/scss/theme/default/layout.scss | 6 +- 17 files changed, 774 insertions(+), 102 deletions(-) create mode 100644 src/js/select2/keys.js diff --git a/dist/css/select2.css b/dist/css/select2.css index 52e27ac5..3cbb898c 100644 --- a/dist/css/select2.css +++ b/dist/css/select2.css @@ -55,10 +55,11 @@ margin: 0; padding: 0; } .select2-container .dropdown .results .options .option { - cursor: pointer; padding: 6px; user-select: none; -webkit-user-select: none; } + .select2-container .dropdown .results .options .option[aria-selected] { + cursor: pointer; } .select2-container.open .dropdown { border-top: none; border-top-left-radius: 0; @@ -115,11 +116,11 @@ cursor: default; display: block; padding: 6px; } - .select2-container.select2-theme-default .dropdown .results .options .option.disabled { + .select2-container.select2-theme-default .dropdown .results .options .option[aria-disabled=true] { color: #666; } - .select2-container.select2-theme-default .dropdown .results .options .option.selected { + .select2-container.select2-theme-default .dropdown .results .options .option[aria-selected=true] { background-color: #ddd; } - .select2-container.select2-theme-default .dropdown .results .options .option.highlightable.highlighted { + .select2-container.select2-theme-default .dropdown .results .options .option[aria-selected].highlighted { background-color: #5897fb; color: white; } diff --git a/dist/css/select2.min.css b/dist/css/select2.min.css index bd9890a3..7141691a 100644 --- a/dist/css/select2.min.css +++ b/dist/css/select2.min.css @@ -1 +1 @@ -.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle;}.select2-container .selection .single-select{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none;}.select2-container .selection .single-select .rendered-selection{display:block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;}.select2-container .selection .multiple-select{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none;}.select2-container .selection .multiple-select .rendered-selection{display:block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;}.select2-container .dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:100;}.select2-container .dropdown .search{display:block;padding:4px;}.select2-container .dropdown .search input{outline:0;padding:4px;width:100%;}.select2-container .dropdown .results{display:block;}.select2-container .dropdown .results .options{list-style:none;margin:0;padding:0;}.select2-container .dropdown .results .options .option{cursor:pointer;padding:6px;user-select:none;-webkit-user-select:none;}.select2-container.open .dropdown{border-top:none;border-top-left-radius:0;border-top-right-radius:0;left:0;}.select2-container.select2-theme-default .selection .single-select{background-color:#fff;border:1px solid #aaa;border-radius:4px;}.select2-container.select2-theme-default .selection .single-select .rendered-selection{color:#444;line-height:28px;}.select2-container.select2-theme-default .selection .single-select .rendered-selection .placeholder{color:#999;}.select2-container.select2-theme-default .selection .multiple-select{background-color:white;border:1px solid #aaa;border-radius:4px;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection{list-style:none;margin:0;padding:5px;padding-bottom:0;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .placeholder{color:#999;float:left;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;float:left;margin-right:5px;margin-bottom:5px;padding:0 5px;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .choice .remove{color:#999;display:inline-block;font-weight:bold;margin-right:2px;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .choice .remove:hover{color:#333;}.select2-container.select2-theme-default.open .selection .single-select,.select2-container.select2-theme-default.open .selection .multiple-select{border-bottom-left-radius:0;border-bottom-right-radius:0;}.select2-container.select2-theme-default .dropdown .search input{border:1px solid #aaa;}.select2-container.select2-theme-default .dropdown .results{max-height:200px;overflow-y:scroll;}.select2-container.select2-theme-default .dropdown .results .options .option.group{padding:0;}.select2-container.select2-theme-default .dropdown .results .options .option.group .group-label{cursor:default;display:block;padding:6px;}.select2-container.select2-theme-default .dropdown .results .options .option.disabled{color:#666;}.select2-container.select2-theme-default .dropdown .results .options .option.selected{background-color:#ddd;}.select2-container.select2-theme-default .dropdown .results .options .option.highlightable.highlighted{background-color:#5897fb;color:white;}.s2-container{margin:0;position:relative;zoom:1;vertical-align:middle;}.s2-container.s2-active{border:1px solid #5897fb;border-top:bottom;}.s2-container .s2-single-select{display:block;height:26px;padding:0 0 0 8px;overflow:hidden;position:relative;border:1px solid #aaa;white-space:nowrap;line-height:26px;color:#444;text-decoration:none;border-radius:4px;background-clip:padding-box;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;background-image:-webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));background-image:-webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);background-image:-moz-linear-gradient(center bottom, #eee 0%, #fff 50%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);background-image:linear-gradient(to top, #eee 0%, #fff 50%);}.s2-container .s2-single-select .s2-selection{margin-right:26px;display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.s2-container .s2-open .select2-container-active .select2-choice,.s2-container .s2-open .select2-container-active .select2-choices{border:1px solid #5897fb;outline:none;-webkit-box-shadow:0 0 5px rgba(0, 0, 0, 0.3);box-shadow:0 0 5px rgba(0, 0, 0, 0.3);}.s2-search input{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}.s2-dropdown{width:100%;margin-top:-1px;position:absolute;z-index:9999;overflow:scroll;background:#fff;color:#000;border:1px solid #5897fb;border-top:none;border-radius:0 0 4px 4px;-webkit-box-shadow:0 4px 5px rgba(0, 0, 0, 0.15);box-shadow:0 4px 5px rgba(0, 0, 0, 0.15);}.s2-hidden{display:none;} \ No newline at end of file +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle;}.select2-container .selection .single-select{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none;}.select2-container .selection .single-select .rendered-selection{display:block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;}.select2-container .selection .multiple-select{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none;}.select2-container .selection .multiple-select .rendered-selection{display:block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;}.select2-container .dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:100;}.select2-container .dropdown .search{display:block;padding:4px;}.select2-container .dropdown .search input{outline:0;padding:4px;width:100%;}.select2-container .dropdown .results{display:block;}.select2-container .dropdown .results .options{list-style:none;margin:0;padding:0;}.select2-container .dropdown .results .options .option{padding:6px;user-select:none;-webkit-user-select:none;}.select2-container .dropdown .results .options .option[aria-selected]{cursor:pointer;}.select2-container.open .dropdown{border-top:none;border-top-left-radius:0;border-top-right-radius:0;left:0;}.select2-container.select2-theme-default .selection .single-select{background-color:#fff;border:1px solid #aaa;border-radius:4px;}.select2-container.select2-theme-default .selection .single-select .rendered-selection{color:#444;line-height:28px;}.select2-container.select2-theme-default .selection .single-select .rendered-selection .placeholder{color:#999;}.select2-container.select2-theme-default .selection .multiple-select{background-color:white;border:1px solid #aaa;border-radius:4px;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection{list-style:none;margin:0;padding:5px;padding-bottom:0;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .placeholder{color:#999;float:left;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;float:left;margin-right:5px;margin-bottom:5px;padding:0 5px;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .choice .remove{color:#999;display:inline-block;font-weight:bold;margin-right:2px;}.select2-container.select2-theme-default .selection .multiple-select .rendered-selection .choice .remove:hover{color:#333;}.select2-container.select2-theme-default.open .selection .single-select,.select2-container.select2-theme-default.open .selection .multiple-select{border-bottom-left-radius:0;border-bottom-right-radius:0;}.select2-container.select2-theme-default .dropdown .search input{border:1px solid #aaa;}.select2-container.select2-theme-default .dropdown .results{max-height:200px;overflow-y:scroll;}.select2-container.select2-theme-default .dropdown .results .options .option.group{padding:0;}.select2-container.select2-theme-default .dropdown .results .options .option.group .group-label{cursor:default;display:block;padding:6px;}.select2-container.select2-theme-default .dropdown .results .options .option[aria-disabled=true]{color:#666;}.select2-container.select2-theme-default .dropdown .results .options .option[aria-selected=true]{background-color:#ddd;}.select2-container.select2-theme-default .dropdown .results .options .option[aria-selected].highlighted{background-color:#5897fb;color:white;}.s2-container{margin:0;position:relative;zoom:1;vertical-align:middle;}.s2-container.s2-active{border:1px solid #5897fb;border-top:bottom;}.s2-container .s2-single-select{display:block;height:26px;padding:0 0 0 8px;overflow:hidden;position:relative;border:1px solid #aaa;white-space:nowrap;line-height:26px;color:#444;text-decoration:none;border-radius:4px;background-clip:padding-box;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;background-image:-webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));background-image:-webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);background-image:-moz-linear-gradient(center bottom, #eee 0%, #fff 50%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);background-image:linear-gradient(to top, #eee 0%, #fff 50%);}.s2-container .s2-single-select .s2-selection{margin-right:26px;display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.s2-container .s2-open .select2-container-active .select2-choice,.s2-container .s2-open .select2-container-active .select2-choices{border:1px solid #5897fb;outline:none;-webkit-box-shadow:0 0 5px rgba(0, 0, 0, 0.3);box-shadow:0 0 5px rgba(0, 0, 0, 0.3);}.s2-search input{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}.s2-dropdown{width:100%;margin-top:-1px;position:absolute;z-index:9999;overflow:scroll;background:#fff;color:#000;border:1px solid #5897fb;border-top:none;border-radius:0 0 4px 4px;-webkit-box-shadow:0 4px 5px rgba(0, 0, 0, 0.15);box-shadow:0 4px 5px rgba(0, 0, 0, 0.15);}.s2-hidden{display:none;} \ No newline at end of file diff --git a/dist/js/select2.amd.full.js b/dist/js/select2.amd.full.js index 8a9a6c2c..7f966006 100644 --- a/dist/js/select2.amd.full.js +++ b/dist/js/select2.amd.full.js @@ -151,7 +151,7 @@ define('select2/results',[ Results.prototype.render = function () { var $results = $( - '' + '' ); this.$results = $results; @@ -191,28 +191,42 @@ define('select2/results',[ return s.id.toString(); }); - self.$results.find('.option.selected').removeClass('selected'); - - var $options = self.$results.find('.option'); + var $options = self.$results.find('.option[aria-selected]'); $options.each(function () { var $option = $(this); var item = $option.data('data'); if (item.id != null && selectedIds.indexOf(item.id.toString()) > -1) { - $option.addClass('selected'); + $option.attr('aria-selected', 'true'); + } else { + $option.attr('aria-selected', 'false'); } }); + + var $selected = $options.filter('[aria-selected=true]'); + + // Check if there are any selected options + if ($selected.length > 0) { + // If there are selected options, highlight the first + $selected.first().trigger('mouseenter'); + } else { + // If there are no selected options, highlight the first option + // in the dropdown + $options.first().trigger('mouseenter'); + } }); }; Results.prototype.option = function (data) { var $option = $( - '
  • ' + '
  • ' ); if (data.children) { - $option.addClass('group').removeClass('highlightable selectable'); + $option + .addClass('group') + .removeAttr('aria-selected'); var $label = $(''); $label.html(data.text); @@ -238,11 +252,13 @@ define('select2/results',[ } if (data.disabled) { - $option.removeClass('selectable highlightable').addClass('disabled'); + $option + .removeAttr('aria-selected') + .attr('aria-disabled', 'true'); } if (data.id == null) { - $option.removeClass('selectable highlightable'); + $option.removeClass('aria-selected'); } $option.data('data', data); @@ -274,11 +290,40 @@ define('select2/results',[ self.setClasses(); }); - this.$results.on('mouseup', '.option.selectable', function (evt) { + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$results.attr('aria-expanded', 'true'); + + self.setClasses(); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$results.attr('aria-expanded', 'false'); + }); + + container.on('results:select', function () { + var $highlighted = self.$results.find('.highlighted'); + + var data = $highlighted.data('data'); + + if ($highlighted.attr('aria-selected') == 'true') { + self.trigger('unselected', { + data: data + }); + } else { + self.trigger('selected', { + data: data + }); + } + }); + + this.$results.on('mouseup', '.option[aria-selected]', function (evt) { var $this = $(this); var data = $this.data('data'); - if ($this.hasClass('selected')) { + + if ($this.attr('aria-selected') === 'true') { self.trigger('unselected', { originalEvent: evt, data: data @@ -293,7 +338,7 @@ define('select2/results',[ }); }); - this.$results.on('mouseenter', '.option.highlightable', function (evt) { + this.$results.on('mouseenter', '.option[aria-selected]', function (evt) { self.$results.find('.option.highlighted').removeClass('highlighted'); $(this).addClass('highlighted'); }); @@ -337,10 +382,51 @@ define('select2/selection/base',[ return BaseSelection; }); +define('select2/keys',[ + +], function () { + var KEYS = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESC: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46, + + isArrow: function (k) { + k = k.which ? k.which : k; + + switch (k) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + return true; + } + + return false; + } + }; + + return KEYS; +}); + define('select2/selection/single',[ './base', - '../utils' -], function (BaseSelection, Utils) { + '../utils', + '../keys' +], function (BaseSelection, Utils, KEYS) { function SingleSelection () { SingleSelection.__super__.constructor.apply(this, arguments); } @@ -349,11 +435,13 @@ define('select2/selection/single',[ SingleSelection.prototype.render = function () { var $selection = $( - '' + + '' + '' + '' ); + $selection.attr('title', this.$element.attr('title')); + this.$selection = $selection; return $selection; @@ -375,6 +463,28 @@ define('select2/selection/single',[ }); }); + this.$selection.on('focus', function (evt) { + // User focuses on the container + }); + + this.$selection.on('blur', function (evt) { + // User exits the container + }); + + this.$selection.on('keyup', function (evt) { + var key = evt.which; + + if (container.isOpen()) { + if (key == KEYS.ENTER) { + self.trigger('results:select'); + } + } else { + if (key == KEYS.ENTER || key == KEYS.SPACE) { + self.trigger('open'); + } + } + }); + container.on('selection:update', function (params) { self.update(params.data); }); @@ -468,7 +578,7 @@ define('select2/selection/multiple',[ MultipleSelection.prototype.selectionContainer = function () { var $container = $( '
  • ' + - '×' + + '×' + '
  • ' ); @@ -898,7 +1008,7 @@ define('select2/dropdown/search',[ var $search = $( '' + - '' + + '' + '' ); @@ -921,6 +1031,14 @@ define('select2/dropdown/search',[ }); }); + container.on('open', function () { + self.$search.attr('tabindex', 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + }); + container.on('results:all', function (params) { if (params.query.term == null || params.query.term === '') { var showSearch = self.showSearch(params); @@ -1108,10 +1226,20 @@ define('select2/core',[ }); }); + this.selection.on('open', function () { + self.trigger('open'); + }); + this.selection.on('close', function () { + self.trigger('close'); + }); this.selection.on('toggle', function () { self.toggleDropdown(); }); + this.selection.on('results:select', function () { + self.trigger('results:select'); + }); + this.selection.on('unselected', function (params) { self.trigger('unselect', params); @@ -1160,18 +1288,23 @@ define('select2/core',[ // Hide the original select $element.hide(); + $element.attr('tabindex', '-1'); }; Utils.Extend(Select2, Utils.Observable); Select2.prototype.toggleDropdown = function () { - if (this.$container.hasClass('open')) { + if (this.isOpen()) { this.trigger('close'); } else { this.trigger('open'); } }; + Select2.prototype.isOpen = function () { + return this.$container.hasClass('open'); + }; + Select2.prototype.render = function () { var $container = $( '' + diff --git a/dist/js/select2.amd.js b/dist/js/select2.amd.js index 8a9a6c2c..7f966006 100644 --- a/dist/js/select2.amd.js +++ b/dist/js/select2.amd.js @@ -151,7 +151,7 @@ define('select2/results',[ Results.prototype.render = function () { var $results = $( - '
      ' + '
        ' ); this.$results = $results; @@ -191,28 +191,42 @@ define('select2/results',[ return s.id.toString(); }); - self.$results.find('.option.selected').removeClass('selected'); - - var $options = self.$results.find('.option'); + var $options = self.$results.find('.option[aria-selected]'); $options.each(function () { var $option = $(this); var item = $option.data('data'); if (item.id != null && selectedIds.indexOf(item.id.toString()) > -1) { - $option.addClass('selected'); + $option.attr('aria-selected', 'true'); + } else { + $option.attr('aria-selected', 'false'); } }); + + var $selected = $options.filter('[aria-selected=true]'); + + // Check if there are any selected options + if ($selected.length > 0) { + // If there are selected options, highlight the first + $selected.first().trigger('mouseenter'); + } else { + // If there are no selected options, highlight the first option + // in the dropdown + $options.first().trigger('mouseenter'); + } }); }; Results.prototype.option = function (data) { var $option = $( - '
      • ' + '
      • ' ); if (data.children) { - $option.addClass('group').removeClass('highlightable selectable'); + $option + .addClass('group') + .removeAttr('aria-selected'); var $label = $(''); $label.html(data.text); @@ -238,11 +252,13 @@ define('select2/results',[ } if (data.disabled) { - $option.removeClass('selectable highlightable').addClass('disabled'); + $option + .removeAttr('aria-selected') + .attr('aria-disabled', 'true'); } if (data.id == null) { - $option.removeClass('selectable highlightable'); + $option.removeClass('aria-selected'); } $option.data('data', data); @@ -274,11 +290,40 @@ define('select2/results',[ self.setClasses(); }); - this.$results.on('mouseup', '.option.selectable', function (evt) { + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$results.attr('aria-expanded', 'true'); + + self.setClasses(); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$results.attr('aria-expanded', 'false'); + }); + + container.on('results:select', function () { + var $highlighted = self.$results.find('.highlighted'); + + var data = $highlighted.data('data'); + + if ($highlighted.attr('aria-selected') == 'true') { + self.trigger('unselected', { + data: data + }); + } else { + self.trigger('selected', { + data: data + }); + } + }); + + this.$results.on('mouseup', '.option[aria-selected]', function (evt) { var $this = $(this); var data = $this.data('data'); - if ($this.hasClass('selected')) { + + if ($this.attr('aria-selected') === 'true') { self.trigger('unselected', { originalEvent: evt, data: data @@ -293,7 +338,7 @@ define('select2/results',[ }); }); - this.$results.on('mouseenter', '.option.highlightable', function (evt) { + this.$results.on('mouseenter', '.option[aria-selected]', function (evt) { self.$results.find('.option.highlighted').removeClass('highlighted'); $(this).addClass('highlighted'); }); @@ -337,10 +382,51 @@ define('select2/selection/base',[ return BaseSelection; }); +define('select2/keys',[ + +], function () { + var KEYS = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESC: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46, + + isArrow: function (k) { + k = k.which ? k.which : k; + + switch (k) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + return true; + } + + return false; + } + }; + + return KEYS; +}); + define('select2/selection/single',[ './base', - '../utils' -], function (BaseSelection, Utils) { + '../utils', + '../keys' +], function (BaseSelection, Utils, KEYS) { function SingleSelection () { SingleSelection.__super__.constructor.apply(this, arguments); } @@ -349,11 +435,13 @@ define('select2/selection/single',[ SingleSelection.prototype.render = function () { var $selection = $( - '' + + '' + '' + '' ); + $selection.attr('title', this.$element.attr('title')); + this.$selection = $selection; return $selection; @@ -375,6 +463,28 @@ define('select2/selection/single',[ }); }); + this.$selection.on('focus', function (evt) { + // User focuses on the container + }); + + this.$selection.on('blur', function (evt) { + // User exits the container + }); + + this.$selection.on('keyup', function (evt) { + var key = evt.which; + + if (container.isOpen()) { + if (key == KEYS.ENTER) { + self.trigger('results:select'); + } + } else { + if (key == KEYS.ENTER || key == KEYS.SPACE) { + self.trigger('open'); + } + } + }); + container.on('selection:update', function (params) { self.update(params.data); }); @@ -468,7 +578,7 @@ define('select2/selection/multiple',[ MultipleSelection.prototype.selectionContainer = function () { var $container = $( '
      • ' + - '×' + + '×' + '
      • ' ); @@ -898,7 +1008,7 @@ define('select2/dropdown/search',[ var $search = $( '' + - '' + + '' + '' ); @@ -921,6 +1031,14 @@ define('select2/dropdown/search',[ }); }); + container.on('open', function () { + self.$search.attr('tabindex', 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + }); + container.on('results:all', function (params) { if (params.query.term == null || params.query.term === '') { var showSearch = self.showSearch(params); @@ -1108,10 +1226,20 @@ define('select2/core',[ }); }); + this.selection.on('open', function () { + self.trigger('open'); + }); + this.selection.on('close', function () { + self.trigger('close'); + }); this.selection.on('toggle', function () { self.toggleDropdown(); }); + this.selection.on('results:select', function () { + self.trigger('results:select'); + }); + this.selection.on('unselected', function (params) { self.trigger('unselect', params); @@ -1160,18 +1288,23 @@ define('select2/core',[ // Hide the original select $element.hide(); + $element.attr('tabindex', '-1'); }; Utils.Extend(Select2, Utils.Observable); Select2.prototype.toggleDropdown = function () { - if (this.$container.hasClass('open')) { + if (this.isOpen()) { this.trigger('close'); } else { this.trigger('open'); } }; + Select2.prototype.isOpen = function () { + return this.$container.hasClass('open'); + }; + Select2.prototype.render = function () { var $container = $( '' + diff --git a/dist/js/select2.full.js b/dist/js/select2.full.js index 64e1c8e3..907187e2 100644 --- a/dist/js/select2.full.js +++ b/dist/js/select2.full.js @@ -9689,7 +9689,7 @@ define('select2/results',[ Results.prototype.render = function () { var $results = $( - '
          ' + '
            ' ); this.$results = $results; @@ -9729,28 +9729,42 @@ define('select2/results',[ return s.id.toString(); }); - self.$results.find('.option.selected').removeClass('selected'); - - var $options = self.$results.find('.option'); + var $options = self.$results.find('.option[aria-selected]'); $options.each(function () { var $option = $(this); var item = $option.data('data'); if (item.id != null && selectedIds.indexOf(item.id.toString()) > -1) { - $option.addClass('selected'); + $option.attr('aria-selected', 'true'); + } else { + $option.attr('aria-selected', 'false'); } }); + + var $selected = $options.filter('[aria-selected=true]'); + + // Check if there are any selected options + if ($selected.length > 0) { + // If there are selected options, highlight the first + $selected.first().trigger('mouseenter'); + } else { + // If there are no selected options, highlight the first option + // in the dropdown + $options.first().trigger('mouseenter'); + } }); }; Results.prototype.option = function (data) { var $option = $( - '
          • ' + '
          • ' ); if (data.children) { - $option.addClass('group').removeClass('highlightable selectable'); + $option + .addClass('group') + .removeAttr('aria-selected'); var $label = $(''); $label.html(data.text); @@ -9776,11 +9790,13 @@ define('select2/results',[ } if (data.disabled) { - $option.removeClass('selectable highlightable').addClass('disabled'); + $option + .removeAttr('aria-selected') + .attr('aria-disabled', 'true'); } if (data.id == null) { - $option.removeClass('selectable highlightable'); + $option.removeClass('aria-selected'); } $option.data('data', data); @@ -9812,11 +9828,40 @@ define('select2/results',[ self.setClasses(); }); - this.$results.on('mouseup', '.option.selectable', function (evt) { + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$results.attr('aria-expanded', 'true'); + + self.setClasses(); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$results.attr('aria-expanded', 'false'); + }); + + container.on('results:select', function () { + var $highlighted = self.$results.find('.highlighted'); + + var data = $highlighted.data('data'); + + if ($highlighted.attr('aria-selected') == 'true') { + self.trigger('unselected', { + data: data + }); + } else { + self.trigger('selected', { + data: data + }); + } + }); + + this.$results.on('mouseup', '.option[aria-selected]', function (evt) { var $this = $(this); var data = $this.data('data'); - if ($this.hasClass('selected')) { + + if ($this.attr('aria-selected') === 'true') { self.trigger('unselected', { originalEvent: evt, data: data @@ -9831,7 +9876,7 @@ define('select2/results',[ }); }); - this.$results.on('mouseenter', '.option.highlightable', function (evt) { + this.$results.on('mouseenter', '.option[aria-selected]', function (evt) { self.$results.find('.option.highlighted').removeClass('highlighted'); $(this).addClass('highlighted'); }); @@ -9875,10 +9920,51 @@ define('select2/selection/base',[ return BaseSelection; }); +define('select2/keys',[ + +], function () { + var KEYS = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESC: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46, + + isArrow: function (k) { + k = k.which ? k.which : k; + + switch (k) { + case KEY.LEFT: + case KEY.RIGHT: + case KEY.UP: + case KEY.DOWN: + return true; + } + + return false; + } + }; + + return KEYS; +}); + define('select2/selection/single',[ './base', - '../utils' -], function (BaseSelection, Utils) { + '../utils', + '../keys' +], function (BaseSelection, Utils, KEYS) { function SingleSelection () { SingleSelection.__super__.constructor.apply(this, arguments); } @@ -9887,11 +9973,13 @@ define('select2/selection/single',[ SingleSelection.prototype.render = function () { var $selection = $( - '' + + '' + '' + '' ); + $selection.attr('title', this.$element.attr('title')); + this.$selection = $selection; return $selection; @@ -9913,6 +10001,28 @@ define('select2/selection/single',[ }); }); + this.$selection.on('focus', function (evt) { + // User focuses on the container + }); + + this.$selection.on('blur', function (evt) { + // User exits the container + }); + + this.$selection.on('keyup', function (evt) { + var key = evt.which; + + if (container.isOpen()) { + if (key == KEYS.ENTER) { + self.trigger('results:select'); + } + } else { + if (key == KEYS.ENTER || key == KEYS.SPACE) { + self.trigger('open'); + } + } + }); + container.on('selection:update', function (params) { self.update(params.data); }); @@ -10006,7 +10116,7 @@ define('select2/selection/multiple',[ MultipleSelection.prototype.selectionContainer = function () { var $container = $( '
          • ' + - '×' + + '×' + '
          • ' ); @@ -10436,7 +10546,7 @@ define('select2/dropdown/search',[ var $search = $( '' + - '' + + '' + '' ); @@ -10459,6 +10569,14 @@ define('select2/dropdown/search',[ }); }); + container.on('open', function () { + self.$search.attr('tabindex', 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + }); + container.on('results:all', function (params) { if (params.query.term == null || params.query.term === '') { var showSearch = self.showSearch(params); @@ -10646,10 +10764,20 @@ define('select2/core',[ }); }); + this.selection.on('open', function () { + self.trigger('open'); + }); + this.selection.on('close', function () { + self.trigger('close'); + }); this.selection.on('toggle', function () { self.toggleDropdown(); }); + this.selection.on('results:select', function () { + self.trigger('results:select'); + }); + this.selection.on('unselected', function (params) { self.trigger('unselect', params); @@ -10698,18 +10826,23 @@ define('select2/core',[ // Hide the original select $element.hide(); + $element.attr('tabindex', '-1'); }; Utils.Extend(Select2, Utils.Observable); Select2.prototype.toggleDropdown = function () { - if (this.$container.hasClass('open')) { + if (this.isOpen()) { this.trigger('close'); } else { this.trigger('open'); } }; + Select2.prototype.isOpen = function () { + return this.$container.hasClass('open'); + }; + Select2.prototype.render = function () { var $container = $( '' + diff --git a/dist/js/select2.full.min.js b/dist/js/select2.full.min.js index 127f5bdc..55dfd2b7 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("