浏览代码

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.
Kevin Brown 10 年之前
父节点
当前提交
cc9419928e

+ 5 - 4
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; }
 

文件差异内容过多而无法显示
+ 0 - 0
dist/css/select2.min.css


+ 151 - 18
dist/js/select2.amd.full.js

@@ -151,7 +151,7 @@ define('select2/results',[
 
   Results.prototype.render = function () {
     var $results = $(
-      '<ul class="options"></ul>'
+      '<ul class="options" role="listbox"></ul>'
     );
 
     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 = $(
-      '<li class="option highlightable selectable"></li>'
+      '<li class="option" role="option" aria-selected="false"></li>'
     );
 
     if (data.children) {
-      $option.addClass('group').removeClass('highlightable selectable');
+      $option
+        .addClass('group')
+        .removeAttr('aria-selected');
 
       var $label = $('<strong class="group-label"></strong>');
       $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 = $(
-      '<span class="single-select">' +
+      '<span class="single-select" tabindex="0">' +
         '<span class="rendered-selection"></span>' +
       '</span>'
     );
 
+    $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 = $(
       '<li class="choice">' +
-        '<span class="remove">&times;</span>' +
+        '<span class="remove" role="presentation">&times;</span>' +
       '</li>'
     );
 
@@ -898,7 +1008,7 @@ define('select2/dropdown/search',[
 
     var $search = $(
       '<span class="search">' +
-        '<input type="search" name="search" />' +
+        '<input type="search" name="search" tabindex="-1" role="textbox" />' +
       '</span>'
     );
 
@@ -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 = $(
       '<span class="select2 select2-container select2-theme-default">' +

+ 151 - 18
dist/js/select2.amd.js

@@ -151,7 +151,7 @@ define('select2/results',[
 
   Results.prototype.render = function () {
     var $results = $(
-      '<ul class="options"></ul>'
+      '<ul class="options" role="listbox"></ul>'
     );
 
     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 = $(
-      '<li class="option highlightable selectable"></li>'
+      '<li class="option" role="option" aria-selected="false"></li>'
     );
 
     if (data.children) {
-      $option.addClass('group').removeClass('highlightable selectable');
+      $option
+        .addClass('group')
+        .removeAttr('aria-selected');
 
       var $label = $('<strong class="group-label"></strong>');
       $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 = $(
-      '<span class="single-select">' +
+      '<span class="single-select" tabindex="0">' +
         '<span class="rendered-selection"></span>' +
       '</span>'
     );
 
+    $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 = $(
       '<li class="choice">' +
-        '<span class="remove">&times;</span>' +
+        '<span class="remove" role="presentation">&times;</span>' +
       '</li>'
     );
 
@@ -898,7 +1008,7 @@ define('select2/dropdown/search',[
 
     var $search = $(
       '<span class="search">' +
-        '<input type="search" name="search" />' +
+        '<input type="search" name="search" tabindex="-1" role="textbox" />' +
       '</span>'
     );
 
@@ -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 = $(
       '<span class="select2 select2-container select2-theme-default">' +

+ 151 - 18
dist/js/select2.full.js

@@ -9689,7 +9689,7 @@ define('select2/results',[
 
   Results.prototype.render = function () {
     var $results = $(
-      '<ul class="options"></ul>'
+      '<ul class="options" role="listbox"></ul>'
     );
 
     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 = $(
-      '<li class="option highlightable selectable"></li>'
+      '<li class="option" role="option" aria-selected="false"></li>'
     );
 
     if (data.children) {
-      $option.addClass('group').removeClass('highlightable selectable');
+      $option
+        .addClass('group')
+        .removeAttr('aria-selected');
 
       var $label = $('<strong class="group-label"></strong>');
       $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 = $(
-      '<span class="single-select">' +
+      '<span class="single-select" tabindex="0">' +
         '<span class="rendered-selection"></span>' +
       '</span>'
     );
 
+    $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 = $(
       '<li class="choice">' +
-        '<span class="remove">&times;</span>' +
+        '<span class="remove" role="presentation">&times;</span>' +
       '</li>'
     );
 
@@ -10436,7 +10546,7 @@ define('select2/dropdown/search',[
 
     var $search = $(
       '<span class="search">' +
-        '<input type="search" name="search" />' +
+        '<input type="search" name="search" tabindex="-1" role="textbox" />' +
       '</span>'
     );
 
@@ -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 = $(
       '<span class="select2 select2-container select2-theme-default">' +

文件差异内容过多而无法显示
+ 0 - 0
dist/js/select2.full.min.js


+ 151 - 18
dist/js/select2.js

@@ -580,7 +580,7 @@ define('select2/results',[
 
   Results.prototype.render = function () {
     var $results = $(
-      '<ul class="options"></ul>'
+      '<ul class="options" role="listbox"></ul>'
     );
 
     this.$results = $results;
@@ -620,28 +620,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 = $(
-      '<li class="option highlightable selectable"></li>'
+      '<li class="option" role="option" aria-selected="false"></li>'
     );
 
     if (data.children) {
-      $option.addClass('group').removeClass('highlightable selectable');
+      $option
+        .addClass('group')
+        .removeAttr('aria-selected');
 
       var $label = $('<strong class="group-label"></strong>');
       $label.html(data.text);
@@ -667,11 +681,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);
@@ -703,11 +719,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
@@ -722,7 +767,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');
     });
@@ -766,10 +811,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);
   }
@@ -778,11 +864,13 @@ define('select2/selection/single',[
 
   SingleSelection.prototype.render = function () {
     var $selection = $(
-      '<span class="single-select">' +
+      '<span class="single-select" tabindex="0">' +
         '<span class="rendered-selection"></span>' +
       '</span>'
     );
 
+    $selection.attr('title', this.$element.attr('title'));
+
     this.$selection = $selection;
 
     return $selection;
@@ -804,6 +892,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);
     });
@@ -897,7 +1007,7 @@ define('select2/selection/multiple',[
   MultipleSelection.prototype.selectionContainer = function () {
     var $container = $(
       '<li class="choice">' +
-        '<span class="remove">&times;</span>' +
+        '<span class="remove" role="presentation">&times;</span>' +
       '</li>'
     );
 
@@ -1327,7 +1437,7 @@ define('select2/dropdown/search',[
 
     var $search = $(
       '<span class="search">' +
-        '<input type="search" name="search" />' +
+        '<input type="search" name="search" tabindex="-1" role="textbox" />' +
       '</span>'
     );
 
@@ -1350,6 +1460,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);
@@ -1537,10 +1655,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);
 
@@ -1589,18 +1717,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 = $(
       '<span class="select2 select2-container select2-theme-default">' +

文件差异内容过多而无法显示
+ 0 - 0
dist/js/select2.min.js


+ 4 - 0
docs/examples.html

@@ -48,6 +48,10 @@ $(document).ready(function() {
     <div class="col-md-4">
       <h1>Multiple select boxes</h1>
 
+      <p>
+        <select class="js-states" multiple="multiple"></select>
+      </p>
+
       <p>
         Select2 also supports multi-value select boxes. The select below is declared with the <code>multiple</code> attribute.
       </p>

+ 16 - 1
src/js/select2/core.js

@@ -68,10 +68,20 @@ define([
       });
     });
 
+    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);
 
@@ -120,18 +130,23 @@ define([
     // 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 = $(
       '<span class="select2 select2-container select2-theme-default">' +

+ 9 - 1
src/js/select2/dropdown/search.js

@@ -8,7 +8,7 @@ define([
 
     var $search = $(
       '<span class="search">' +
-        '<input type="search" name="search" />' +
+        '<input type="search" name="search" tabindex="-1" role="textbox" />' +
       '</span>'
     );
 
@@ -31,6 +31,14 @@ define([
       });
     });
 
+    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);

+ 39 - 0
src/js/select2/keys.js

@@ -0,0 +1,39 @@
+define([
+
+], 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;
+});

+ 57 - 12
src/js/select2/results.js

@@ -12,7 +12,7 @@ define([
 
   Results.prototype.render = function () {
     var $results = $(
-      '<ul class="options"></ul>'
+      '<ul class="options" role="listbox"></ul>'
     );
 
     this.$results = $results;
@@ -52,28 +52,42 @@ define([
         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 = $(
-      '<li class="option highlightable selectable"></li>'
+      '<li class="option" role="option" aria-selected="false"></li>'
     );
 
     if (data.children) {
-      $option.addClass('group').removeClass('highlightable selectable');
+      $option
+        .addClass('group')
+        .removeAttr('aria-selected');
 
       var $label = $('<strong class="group-label"></strong>');
       $label.html(data.text);
@@ -99,11 +113,13 @@ define([
     }
 
     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);
@@ -135,11 +151,40 @@ define([
       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
@@ -154,7 +199,7 @@ define([
       });
     });
 
-    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');
     });

+ 1 - 1
src/js/select2/selection/multiple.js

@@ -58,7 +58,7 @@ define([
   MultipleSelection.prototype.selectionContainer = function () {
     var $container = $(
       '<li class="choice">' +
-        '<span class="remove">&times;</span>' +
+        '<span class="remove" role="presentation">&times;</span>' +
       '</li>'
     );
 

+ 28 - 3
src/js/select2/selection/single.js

@@ -1,7 +1,8 @@
 define([
   './base',
-  '../utils'
-], function (BaseSelection, Utils) {
+  '../utils',
+  '../keys'
+], function (BaseSelection, Utils, KEYS) {
   function SingleSelection () {
     SingleSelection.__super__.constructor.apply(this, arguments);
   }
@@ -10,11 +11,13 @@ define([
 
   SingleSelection.prototype.render = function () {
     var $selection = $(
-      '<span class="single-select">' +
+      '<span class="single-select" tabindex="0">' +
         '<span class="rendered-selection"></span>' +
       '</span>'
     );
 
+    $selection.attr('title', this.$element.attr('title'));
+
     this.$selection = $selection;
 
     return $selection;
@@ -36,6 +39,28 @@ define([
       });
     });
 
+    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);
     });

+ 4 - 1
src/scss/_dropdown.scss

@@ -36,11 +36,14 @@
         padding: 0;
 
         .option {
-          cursor: pointer;
           padding: 6px;
 
           user-select: none;
           -webkit-user-select: none;
+
+          &[aria-selected] {
+              cursor: pointer;
+          }
         }
       }
     }

+ 3 - 3
src/scss/theme/default/layout.scss

@@ -93,15 +93,15 @@
             }
           }
 
-          &.disabled {
+          &[aria-disabled=true] {
             color: #666;
           }
 
-          &.selected {
+          &[aria-selected=true] {
             background-color: #ddd;
           }
 
-          &.highlightable.highlighted {
+          &[aria-selected].highlighted {
             background-color: #5897fb;
             color: white;
           }

部分文件因为文件数量过多而无法显示