Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 538f460

Browse files
committed
fix(select): add attribute "selected" for select[multiple]
This helps screen readers identify the selected options, see #14419
1 parent 4385e12 commit 538f460

File tree

2 files changed

+33
-15
lines changed

2 files changed

+33
-15
lines changed

src/ng/directive/select.js

+25-15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
var noopNgModelController = { $setViewValue: noop, $render: noop };
66

7+
function setOptionSelectedStatus(optionEl, value) {
8+
optionEl.prop('selected', value); // needed for IE
9+
/**
10+
* When unselecting an option, setting the property to null / false should be enough
11+
* However, screenreaders might react to the selected attribute instead, see
12+
* https://github.com/angular/angular.js/issues/14419
13+
* Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false
14+
* or null
15+
*/
16+
optionEl.attr('selected', value);
17+
}
18+
719
/**
820
* @ngdoc type
921
* @name select.SelectController
@@ -44,14 +56,14 @@ var SelectController =
4456
var unknownVal = self.generateUnknownOptionValue(val);
4557
self.unknownOption.val(unknownVal);
4658
$element.prepend(self.unknownOption);
47-
setOptionAsSelected(self.unknownOption);
59+
setOptionSelectedStatus(self.unknownOption, true);
4860
$element.val(unknownVal);
4961
};
5062

5163
self.updateUnknownOption = function(val) {
5264
var unknownVal = self.generateUnknownOptionValue(val);
5365
self.unknownOption.val(unknownVal);
54-
setOptionAsSelected(self.unknownOption);
66+
setOptionSelectedStatus(self.unknownOption, true);
5567
$element.val(unknownVal);
5668
};
5769

@@ -66,7 +78,7 @@ var SelectController =
6678
self.selectEmptyOption = function() {
6779
if (self.emptyOption) {
6880
$element.val('');
69-
setOptionAsSelected(self.emptyOption);
81+
setOptionSelectedStatus(self.emptyOption, true);
7082
}
7183
};
7284

@@ -102,7 +114,7 @@ var SelectController =
102114
// Make sure to remove the selected attribute from the previously selected option
103115
// Otherwise, screen readers might get confused
104116
var currentlySelectedOption = $element[0].options[$element[0].selectedIndex];
105-
if (currentlySelectedOption) currentlySelectedOption.removeAttribute('selected');
117+
if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false);
106118

107119
if (self.hasOption(value)) {
108120
self.removeUnknownOption();
@@ -112,7 +124,7 @@ var SelectController =
112124

113125
// Set selected attribute and property on selected option for screen readers
114126
var selectedOption = $element[0].options[$element[0].selectedIndex];
115-
setOptionAsSelected(jqLite(selectedOption));
127+
setOptionSelectedStatus(jqLite(selectedOption), true);
116128
} else {
117129
if (value == null && self.emptyOption) {
118130
self.removeUnknownOption();
@@ -292,11 +304,6 @@ var SelectController =
292304
}
293305
});
294306
};
295-
296-
function setOptionAsSelected(optionEl) {
297-
optionEl.prop('selected', true); // needed for IE
298-
optionEl.attr('selected', true);
299-
}
300307
}];
301308

302309
/**
@@ -611,11 +618,14 @@ var selectDirective = function() {
611618
includes(value, selectCtrl.selectValueMap[option.value]));
612619
var currentlySelected = option.selected;
613620

614-
// IE and Edge will de-select selected options when you set the selected property again, e.g.
615-
// when you add to the selection via shift+click/UP/DOWN
616-
// Therefore, only set it if necessary
617-
if ((shouldBeSelected && !currentlySelected) || (!shouldBeSelected && currentlySelected)) {
618-
option.selected = shouldBeSelected;
621+
// IE and Edge, adding options to the selection via shift+click/UP/DOWN,
622+
// will de-select already selected options if "selected" on those options was set
623+
// more than once (i.e. when the options were already selected)
624+
// So we only modify the selected property if neccessary.
625+
// Note: this behavior cannot be replicated via unit tests because it only shows in the
626+
// actual user interface.
627+
if (shouldBeSelected !== currentlySelected) {
628+
setOptionSelectedStatus(jqLite(option), shouldBeSelected);
619629
}
620630

621631
});

test/ng/directive/selectSpec.js

+8
Original file line numberDiff line numberDiff line change
@@ -1098,13 +1098,21 @@ describe('select', function() {
10981098
scope.selection = ['A'];
10991099
});
11001100

1101+
var optionElements = element.find('option');
1102+
11011103
expect(element).toEqualSelect(['A'], 'B');
1104+
expect(optionElements[0]).toBeMarkedAsSelected();
1105+
expect(optionElements[1]).not.toBeMarkedAsSelected();
11021106

11031107
scope.$apply(function() {
11041108
scope.selection.push('B');
11051109
});
11061110

1111+
optionElements = element.find('option');
1112+
11071113
expect(element).toEqualSelect(['A'], ['B']);
1114+
expect(optionElements[0]).toBeMarkedAsSelected();
1115+
expect(optionElements[1]).toBeMarkedAsSelected();
11081116
});
11091117

11101118
it('should work with optgroups', function() {

0 commit comments

Comments
 (0)