that ng-transclude generates
element.append(angular.element('').append(element.contents()));
- element.attr('tabindex', attr.tabindex || '0');
+ element.attr('tabindex', attrs.tabindex || '0');
- if (!hasDefinedValue(attr)) {
+ if (!hasDefinedValue(attrs)) {
element.attr('md-option-empty', '');
}
return postLink;
}
- function hasDefinedValue(attr) {
- var value = attr.value;
- var ngValue = attr.ngValue;
+ /**
+ * @param {Object} attrs list of attributes from the compile function
+ * @return {string|undefined|null} if defined and non-empty, return the value of the option's
+ * value attribute, otherwise return the value of the option's ng-value attribute.
+ */
+ function hasDefinedValue(attrs) {
+ var value = attrs.value;
+ var ngValue = attrs.ngValue;
return value || ngValue;
}
- function postLink(scope, element, attr, ctrls) {
+ function postLink(scope, element, attrs, ctrls) {
var optionCtrl = ctrls[0];
- var selectCtrl = ctrls[1];
+ var selectMenuCtrl = ctrls[1];
$mdTheming(element);
- if (selectCtrl.isMultiple) {
+ if (selectMenuCtrl.isMultiple) {
element.addClass('md-checkbox-enabled');
element.prepend(CHECKBOX_SELECTION_INDICATOR.clone());
}
- if (angular.isDefined(attr.ngValue)) {
- scope.$watch(attr.ngValue, setOptionValue);
- } else if (angular.isDefined(attr.value)) {
- setOptionValue(attr.value);
+ if (angular.isDefined(attrs.ngValue)) {
+ scope.$watch(attrs.ngValue, function (newValue, oldValue) {
+ setOptionValue(newValue, oldValue);
+ element.removeAttr('aria-checked');
+ });
+ } else if (angular.isDefined(attrs.value)) {
+ setOptionValue(attrs.value);
} else {
scope.$watch(function() {
return element.text().trim();
}, setOptionValue);
}
- attr.$observe('disabled', function(disabled) {
+ attrs.$observe('disabled', function(disabled) {
if (disabled) {
element.attr('tabindex', '-1');
} else {
@@ -1040,27 +1183,17 @@ function OptionDirective($mdButtonInkRipple, $mdUtil, $mdTheming) {
}
});
- scope.$$postDigest(function() {
- attr.$observe('selected', function(selected) {
- if (!angular.isDefined(selected)) return;
- if (typeof selected == 'string') selected = true;
- if (selected) {
- if (!selectCtrl.isMultiple) {
- selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
- }
- selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
- } else {
- selectCtrl.deselect(optionCtrl.hashKey);
- }
- selectCtrl.refreshViewValue();
- });
- });
-
$mdButtonInkRipple.attach(scope, element);
configureAria();
+ /**
+ * @param {*} newValue the option's new value
+ * @param {*=} oldValue the option's previous value
+ * @param {boolean=} prevAttempt true if this had to be attempted again due to an undefined
+ * hashGetter on the selectCtrl, undefined otherwise.
+ */
function setOptionValue(newValue, oldValue, prevAttempt) {
- if (!selectCtrl.hashGetter) {
+ if (!selectMenuCtrl.hashGetter) {
if (!prevAttempt) {
scope.$$postDigest(function() {
setOptionValue(newValue, oldValue, true);
@@ -1068,49 +1201,64 @@ function OptionDirective($mdButtonInkRipple, $mdUtil, $mdTheming) {
}
return;
}
- var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
- var newHashKey = selectCtrl.hashGetter(newValue, scope);
+ var oldHashKey = selectMenuCtrl.hashGetter(oldValue, scope);
+ var newHashKey = selectMenuCtrl.hashGetter(newValue, scope);
optionCtrl.hashKey = newHashKey;
optionCtrl.value = newValue;
- selectCtrl.removeOption(oldHashKey, optionCtrl);
- selectCtrl.addOption(newHashKey, optionCtrl);
+ selectMenuCtrl.removeOption(oldHashKey, optionCtrl);
+ selectMenuCtrl.addOption(newHashKey, optionCtrl);
}
scope.$on('$destroy', function() {
- selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
+ selectMenuCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
});
function configureAria() {
var ariaAttrs = {
- 'role': 'option',
- 'aria-selected': 'false'
+ 'role': 'option'
};
+ // We explicitly omit the `aria-selected` attribute from single-selection, unselected
+ // options. Including the `aria-selected="false"` attributes adds a significant amount of
+ // noise to screen-reader users without providing useful information.
+ if (selectMenuCtrl.isMultiple) {
+ ariaAttrs['aria-selected'] = 'false';
+ }
+
if (!element[0].hasAttribute('id')) {
ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
}
element.attr(ariaAttrs);
}
}
+}
- function OptionController($element) {
- this.selected = false;
- this.setSelected = function(isSelected) {
- if (isSelected && !this.selected) {
- $element.attr({
- 'selected': 'selected',
- 'aria-selected': 'true'
- });
- } else if (!isSelected && this.selected) {
- $element.removeAttr('selected');
+function OptionController($element) {
+ /**
+ * @param {boolean} isSelected
+ * @param {boolean=} isMultiple
+ */
+ this.setSelected = function(isSelected, isMultiple) {
+ if (isSelected) {
+ $element.attr({
+ 'selected': 'true',
+ 'aria-selected': 'true'
+ });
+ } else if (!isSelected) {
+ $element.removeAttr('selected');
+
+ if (isMultiple) {
$element.attr('aria-selected', 'false');
+ } else {
+ // We explicitly omit the `aria-selected` attribute from single-selection, unselected
+ // options. Including the `aria-selected="false"` attributes adds a significant amount of
+ // noise to screen-reader users without providing useful information.
+ $element.removeAttr('aria-selected');
}
- this.selected = isSelected;
- };
- }
-
+ }
+ };
}
/**
@@ -1166,7 +1314,7 @@ function OptgroupDirective() {
restrict: 'E',
compile: compile
};
- function compile(el, attrs) {
+ function compile(element, attrs) {
// If we have a select header element, we don't want to add the normal label
// header.
if (!hasSelectHeader()) {
@@ -1174,18 +1322,20 @@ function OptgroupDirective() {
}
function hasSelectHeader() {
- return el.parent().find('md-select-header').length;
+ return element.parent().find('md-select-header').length;
}
function setupLabelElement() {
- var labelElement = el.find('label');
+ var labelElement = element.find('label');
if (!labelElement.length) {
labelElement = angular.element('