瀏覽代碼

Mark highlighted results with aria-selected (#5841)

* Switch CSS to use BEM over attribute selectors

Previously the CSS was using a combination of very specific BEM
class selectors and attribue selectors based off of ARIA attributes
that were expected to be present on the elements. This creates odd
specificity battles that people have been complaining about for
years, so now we're solving a few of the issues by switching to
BEM classes instead of using the mix.

This is a breaking change within any applications that override
the Select2 CSS through creating higher specificity selectors. While
the attributes are still present on the elements, we are no longer
going to be treating adjustements to them as breaking changes. This
is important becuase accessibility is a changing field and we know
we are going to have to make adjustments going forward.

* Mark highlighted results with aria-selected

This is a breaking change from past expectations where the options
within the results that were selected within the dataset were
previously marked as `aria-selected=true`.

When Select2 was first implementing the `aria-selected` attribute,
the interpretation that we followed was that the "selected" state
was supposed to represent the idea that the option was "selected"
similar to what is done within a standard dropdown. This would make
sense and would align with the interpretation of the WAI-ARIA
Authoring Practices 1.0 where it explicitly says that it represents
the selected state of the option. We later found out, after Select2
had been released, that this intepretation was incorrect and the
large majority of assistive technologies did not align with this
interpretation.

The `aria-selected` attribute should represent the location of
focus within the component. This allows screen readers to read the
contents out load and also detect changes within the focus location.
In later revisions of the WAI-ARIA spec, this was made more clear
that the `aria-selected` attribute forces the focus to be moved to
the element containing that attribute, which is in line with the
behaviour that was encountered during testing.

This should fix a bug that has been around for a while where using
VoiceOver in the Safari browser would result in the currently
focused option not being read aloud.

* Fix failing test

There was a test which was checking the selected status of an option
based on the `aria-selected` attribute. Instead this has been
switched over to check to make sure the new class is not present.
Kevin Brown 5 年之前
父節點
當前提交
defe232bf6

+ 25 - 18
src/js/select2/results.js

@@ -99,9 +99,9 @@ define([
 
   Results.prototype.highlightFirstItem = function () {
     var $options = this.$results
-      .find('.select2-results__option[aria-selected]');
+      .find('.select2-results__option--selectable');
 
-    var $selected = $options.filter('[aria-selected=true]');
+    var $selected = $options.filter('.select2-results__option--selected');
 
     // Check if there are any selected options
     if ($selected.length > 0) {
@@ -125,7 +125,7 @@ define([
       });
 
       var $options = self.$results
-        .find('.select2-results__option[aria-selected]');
+        .find('.select2-results__option--selectable');
 
       $options.each(function () {
         var $option = $(this);
@@ -137,8 +137,10 @@ define([
 
         if ((item.element != null && item.element.selected) ||
             (item.element == null && selectedIds.indexOf(id) > -1)) {
+          this.classList.add('select2-results__option--selected');
           $option.attr('aria-selected', 'true');
         } else {
+          this.classList.remove('select2-results__option--selected');
           $option.attr('aria-selected', 'false');
         }
       });
@@ -168,11 +170,11 @@ define([
 
   Results.prototype.option = function (data) {
     var option = document.createElement('li');
-    option.className = 'select2-results__option';
+    option.classList.add('select2-results__option');
+    option.classList.add('select2-results__option--selectable');
 
     var attrs = {
-      'role': 'option',
-      'aria-selected': 'false'
+      'role': 'option'
     };
 
     var matches = window.Element.prototype.matches ||
@@ -181,12 +183,14 @@ define([
 
     if ((data.element != null && matches.call(data.element, ':disabled')) ||
         (data.element == null && data.disabled)) {
-      delete attrs['aria-selected'];
       attrs['aria-disabled'] = 'true';
+
+      option.classList.remove('select2-results__option--selectable');
+      option.classList.add('select2-results__option--disabled');
     }
 
     if (data.id == null) {
-      delete attrs['aria-selected'];
+      option.classList.remove('select2-results__option--selectable');
     }
 
     if (data._resultId != null) {
@@ -200,7 +204,9 @@ define([
     if (data.children) {
       attrs.role = 'group';
       attrs['aria-label'] = data.text;
-      delete attrs['aria-selected'];
+
+      option.classList.remove('select2-results__option--selectable');
+      option.classList.add('select2-results__option--group');
     }
 
     for (var attr in attrs) {
@@ -215,7 +221,6 @@ define([
       var label = document.createElement('strong');
       label.className = 'select2-results__group';
 
-      var $label = $(label);
       this.template(data, label);
 
       var $children = [];
@@ -334,7 +339,7 @@ define([
 
       var data = Utils.GetData($highlighted[0], 'data');
 
-      if ($highlighted.attr('aria-selected') == 'true') {
+      if ($highlighted.hasClass('select2-results__option--selected')) {
         self.trigger('close', {});
       } else {
         self.trigger('select', {
@@ -346,7 +351,7 @@ define([
     container.on('results:previous', function () {
       var $highlighted = self.getHighlightedResults();
 
-      var $options = self.$results.find('[aria-selected]');
+      var $options = self.$results.find('.select2-results__option--selectable');
 
       var currentIndex = $options.index($highlighted);
 
@@ -381,7 +386,7 @@ define([
     container.on('results:next', function () {
       var $highlighted = self.getHighlightedResults();
 
-      var $options = self.$results.find('[aria-selected]');
+      var $options = self.$results.find('.select2-results__option--selectable');
 
       var currentIndex = $options.index($highlighted);
 
@@ -410,6 +415,7 @@ define([
 
     container.on('results:focus', function (params) {
       params.element[0].classList.add('select2-results__option--highlighted');
+      params.element[0].setAttribute('aria-selected', 'true');
     });
 
     container.on('results:message', function (params) {
@@ -441,13 +447,13 @@ define([
       });
     }
 
-    this.$results.on('mouseup', '.select2-results__option[aria-selected]',
+    this.$results.on('mouseup', '.select2-results__option--selectable',
       function (evt) {
       var $this = $(this);
 
       var data = Utils.GetData(this, 'data');
 
-      if ($this.attr('aria-selected') === 'true') {
+      if ($this.hasClass('select2-results__option--selected')) {
         if (self.options.get('multiple')) {
           self.trigger('unselect', {
             originalEvent: evt,
@@ -466,12 +472,13 @@ define([
       });
     });
 
-    this.$results.on('mouseenter', '.select2-results__option[aria-selected]',
+    this.$results.on('mouseenter', '.select2-results__option--selectable',
       function (evt) {
       var data = Utils.GetData(this, 'data');
 
       self.getHighlightedResults()
-          .removeClass('select2-results__option--highlighted');
+          .removeClass('select2-results__option--highlighted')
+          .attr('aria-selected', 'false');
 
       self.trigger('results:focus', {
         data: data,
@@ -498,7 +505,7 @@ define([
       return;
     }
 
-    var $options = this.$results.find('[aria-selected]');
+    var $options = this.$results.find('.select2-results__option--selectable');
 
     var currentIndex = $options.index($highlighted);
 

+ 3 - 3
src/scss/_dropdown.scss

@@ -31,10 +31,10 @@
 
   user-select: none;
   -webkit-user-select: none;
+}
 
-  &[aria-selected] {
-    cursor: pointer;
-  }
+.select2-results__option--selectable {
+  cursor: pointer;
 }
 
 .select2-container--open .select2-dropdown {

+ 6 - 8
src/scss/theme/classic/layout.scss

@@ -37,17 +37,15 @@
     overflow-y: auto;
   }
 
-  .select2-results__option {
-    &[role=group] {
-      padding: 0;
-    }
+  .select2-results__option--group {
+    padding: 0;
+  }
 
-    &[aria-disabled=true] {
-      color: $results-choice-fg-unselectable-color;
-    }
+  .select2-results__option--disabled {
+    color: $results-choice-fg-unselectable-color;
   }
 
-  .select2-results__option--highlighted[aria-selected] {
+  .select2-results__option--highlighted.select2-results__option--selectable {
     background-color: $results-choice-bg-hover-color;
     color: $results-choice-fg-hover-color;
   }

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

@@ -38,18 +38,6 @@
   }
 
   .select2-results__option {
-    &[role=group] {
-      padding: 0;
-    }
-
-    &[aria-disabled=true] {
-      color: #999;
-    }
-
-    &[aria-selected=true] {
-      background-color: #ddd;
-    }
-
     .select2-results__option {
       padding-left: 1em;
 
@@ -84,7 +72,19 @@
     }
   }
 
-  .select2-results__option--highlighted[aria-selected] {
+  .select2-results__option--group {
+    padding: 0;
+  }
+
+  .select2-results__option--disabled {
+    color: #999;
+  }
+
+  .select2-results__option--selected {
+    background-color: #ddd;
+  }
+
+  .select2-results__option--highlighted.select2-results__option--selectable {
     background-color: #5897fb;
     color: white;
   }

+ 1 - 1
tests/results/option-tests.js

@@ -71,7 +71,7 @@ test('options are not selected by default', function (assert) {
     element: $option[0]
   });
 
-  assert.equal(option.getAttribute('aria-selected'), 'false');
+  assert.notOk(option.classList.contains('select2-results__option--selected'));
 });
 
 test('options with children are given the group role', function(assert) {