Skip to content

Commit defe232

Browse files
authored
Mark highlighted results with aria-selected (select2#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.
1 parent 00aa2a5 commit defe232

5 files changed

Lines changed: 48 additions & 43 deletions

File tree

src/js/select2/results.js

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ define([
9999

100100
Results.prototype.highlightFirstItem = function () {
101101
var $options = this.$results
102-
.find('.select2-results__option[aria-selected]');
102+
.find('.select2-results__option--selectable');
103103

104-
var $selected = $options.filter('[aria-selected=true]');
104+
var $selected = $options.filter('.select2-results__option--selected');
105105

106106
// Check if there are any selected options
107107
if ($selected.length > 0) {
@@ -125,7 +125,7 @@ define([
125125
});
126126

127127
var $options = self.$results
128-
.find('.select2-results__option[aria-selected]');
128+
.find('.select2-results__option--selectable');
129129

130130
$options.each(function () {
131131
var $option = $(this);
@@ -137,8 +137,10 @@ define([
137137

138138
if ((item.element != null && item.element.selected) ||
139139
(item.element == null && selectedIds.indexOf(id) > -1)) {
140+
this.classList.add('select2-results__option--selected');
140141
$option.attr('aria-selected', 'true');
141142
} else {
143+
this.classList.remove('select2-results__option--selected');
142144
$option.attr('aria-selected', 'false');
143145
}
144146
});
@@ -168,11 +170,11 @@ define([
168170

169171
Results.prototype.option = function (data) {
170172
var option = document.createElement('li');
171-
option.className = 'select2-results__option';
173+
option.classList.add('select2-results__option');
174+
option.classList.add('select2-results__option--selectable');
172175

173176
var attrs = {
174-
'role': 'option',
175-
'aria-selected': 'false'
177+
'role': 'option'
176178
};
177179

178180
var matches = window.Element.prototype.matches ||
@@ -181,12 +183,14 @@ define([
181183

182184
if ((data.element != null && matches.call(data.element, ':disabled')) ||
183185
(data.element == null && data.disabled)) {
184-
delete attrs['aria-selected'];
185186
attrs['aria-disabled'] = 'true';
187+
188+
option.classList.remove('select2-results__option--selectable');
189+
option.classList.add('select2-results__option--disabled');
186190
}
187191

188192
if (data.id == null) {
189-
delete attrs['aria-selected'];
193+
option.classList.remove('select2-results__option--selectable');
190194
}
191195

192196
if (data._resultId != null) {
@@ -200,7 +204,9 @@ define([
200204
if (data.children) {
201205
attrs.role = 'group';
202206
attrs['aria-label'] = data.text;
203-
delete attrs['aria-selected'];
207+
208+
option.classList.remove('select2-results__option--selectable');
209+
option.classList.add('select2-results__option--group');
204210
}
205211

206212
for (var attr in attrs) {
@@ -215,7 +221,6 @@ define([
215221
var label = document.createElement('strong');
216222
label.className = 'select2-results__group';
217223

218-
var $label = $(label);
219224
this.template(data, label);
220225

221226
var $children = [];
@@ -334,7 +339,7 @@ define([
334339

335340
var data = Utils.GetData($highlighted[0], 'data');
336341

337-
if ($highlighted.attr('aria-selected') == 'true') {
342+
if ($highlighted.hasClass('select2-results__option--selected')) {
338343
self.trigger('close', {});
339344
} else {
340345
self.trigger('select', {
@@ -346,7 +351,7 @@ define([
346351
container.on('results:previous', function () {
347352
var $highlighted = self.getHighlightedResults();
348353

349-
var $options = self.$results.find('[aria-selected]');
354+
var $options = self.$results.find('.select2-results__option--selectable');
350355

351356
var currentIndex = $options.index($highlighted);
352357

@@ -381,7 +386,7 @@ define([
381386
container.on('results:next', function () {
382387
var $highlighted = self.getHighlightedResults();
383388

384-
var $options = self.$results.find('[aria-selected]');
389+
var $options = self.$results.find('.select2-results__option--selectable');
385390

386391
var currentIndex = $options.index($highlighted);
387392

@@ -410,6 +415,7 @@ define([
410415

411416
container.on('results:focus', function (params) {
412417
params.element[0].classList.add('select2-results__option--highlighted');
418+
params.element[0].setAttribute('aria-selected', 'true');
413419
});
414420

415421
container.on('results:message', function (params) {
@@ -441,13 +447,13 @@ define([
441447
});
442448
}
443449

444-
this.$results.on('mouseup', '.select2-results__option[aria-selected]',
450+
this.$results.on('mouseup', '.select2-results__option--selectable',
445451
function (evt) {
446452
var $this = $(this);
447453

448454
var data = Utils.GetData(this, 'data');
449455

450-
if ($this.attr('aria-selected') === 'true') {
456+
if ($this.hasClass('select2-results__option--selected')) {
451457
if (self.options.get('multiple')) {
452458
self.trigger('unselect', {
453459
originalEvent: evt,
@@ -466,12 +472,13 @@ define([
466472
});
467473
});
468474

469-
this.$results.on('mouseenter', '.select2-results__option[aria-selected]',
475+
this.$results.on('mouseenter', '.select2-results__option--selectable',
470476
function (evt) {
471477
var data = Utils.GetData(this, 'data');
472478

473479
self.getHighlightedResults()
474-
.removeClass('select2-results__option--highlighted');
480+
.removeClass('select2-results__option--highlighted')
481+
.attr('aria-selected', 'false');
475482

476483
self.trigger('results:focus', {
477484
data: data,
@@ -498,7 +505,7 @@ define([
498505
return;
499506
}
500507

501-
var $options = this.$results.find('[aria-selected]');
508+
var $options = this.$results.find('.select2-results__option--selectable');
502509

503510
var currentIndex = $options.index($highlighted);
504511

src/scss/_dropdown.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131

3232
user-select: none;
3333
-webkit-user-select: none;
34+
}
3435

35-
&[aria-selected] {
36-
cursor: pointer;
37-
}
36+
.select2-results__option--selectable {
37+
cursor: pointer;
3838
}
3939

4040
.select2-container--open .select2-dropdown {

src/scss/theme/classic/layout.scss

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,15 @@
3737
overflow-y: auto;
3838
}
3939

40-
.select2-results__option {
41-
&[role=group] {
42-
padding: 0;
43-
}
40+
.select2-results__option--group {
41+
padding: 0;
42+
}
4443

45-
&[aria-disabled=true] {
46-
color: $results-choice-fg-unselectable-color;
47-
}
44+
.select2-results__option--disabled {
45+
color: $results-choice-fg-unselectable-color;
4846
}
4947

50-
.select2-results__option--highlighted[aria-selected] {
48+
.select2-results__option--highlighted.select2-results__option--selectable {
5149
background-color: $results-choice-bg-hover-color;
5250
color: $results-choice-fg-hover-color;
5351
}

src/scss/theme/default/layout.scss

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,6 @@
3838
}
3939

4040
.select2-results__option {
41-
&[role=group] {
42-
padding: 0;
43-
}
44-
45-
&[aria-disabled=true] {
46-
color: #999;
47-
}
48-
49-
&[aria-selected=true] {
50-
background-color: #ddd;
51-
}
52-
5341
.select2-results__option {
5442
padding-left: 1em;
5543

@@ -84,7 +72,19 @@
8472
}
8573
}
8674

87-
.select2-results__option--highlighted[aria-selected] {
75+
.select2-results__option--group {
76+
padding: 0;
77+
}
78+
79+
.select2-results__option--disabled {
80+
color: #999;
81+
}
82+
83+
.select2-results__option--selected {
84+
background-color: #ddd;
85+
}
86+
87+
.select2-results__option--highlighted.select2-results__option--selectable {
8888
background-color: #5897fb;
8989
color: white;
9090
}

tests/results/option-tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ test('options are not selected by default', function (assert) {
7171
element: $option[0]
7272
});
7373

74-
assert.equal(option.getAttribute('aria-selected'), 'false');
74+
assert.notOk(option.classList.contains('select2-results__option--selected'));
7575
});
7676

7777
test('options with children are given the group role', function(assert) {

0 commit comments

Comments
 (0)