-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,6 +132,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
element.append(angular.element('<md-content>').append(element.contents())); | ||
} | ||
|
||
|
||
// Add progress spinner for md-options-loading | ||
if (attr.mdOnOpen) { | ||
|
||
|
@@ -173,17 +174,18 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
// Use everything that's left inside element.contents() as the contents of the menu | ||
var multiple = angular.isDefined(attr.multiple) ? 'multiple' : ''; | ||
var selectTemplate = '' + | ||
'<div class="md-select-menu-container">' + | ||
'<div class="md-select-menu-container" aria-hidden="true">' + | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
marcysutton
Contributor
|
||
'<md-select-menu {0}>{1}</md-select-menu>' + | ||
'</div>'; | ||
|
||
selectTemplate = $mdUtil.supplant(selectTemplate, [multiple, element.html()]); | ||
element.empty().append(valueEl); | ||
element.append(selectTemplate); | ||
|
||
attr.tabindex = attr.tabindex || '0'; | ||
|
||
return function postLink(scope, element, attr, ctrls) { | ||
var isDisabled; | ||
var isDisabled, ariaLabelBase; | ||
|
||
// Remove event ngModel's blur listener for touched and untouched | ||
// we will do it ourself. | ||
|
@@ -218,7 +220,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
|
||
var selectContainer, selectScope, selectMenuCtrl; | ||
|
||
createSelect(); | ||
getSelect(); | ||
$mdTheming(element); | ||
|
||
if (attr.name && formCtrl) { | ||
|
@@ -241,6 +243,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
ngModelCtrl.$render = function() { | ||
originalRender(); | ||
syncLabelText(); | ||
syncAriaLabel(); | ||
inputCheckValue(); | ||
}; | ||
|
||
|
@@ -290,15 +293,17 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
}; | ||
|
||
scope.$$postDigest(function() { | ||
setAriaLabel(); | ||
initAriaLabel(); | ||
syncLabelText(); | ||
syncAriaLabel(); | ||
}); | ||
|
||
function setAriaLabel() { | ||
var labelText = element.attr('placeholder'); | ||
function initAriaLabel() { | ||
var labelText = element.attr('aria-label') || element.attr('placeholder'); | ||
if (!labelText && containerCtrl && containerCtrl.label) { | ||
labelText = containerCtrl.label.text(); | ||
} | ||
ariaLabelBase = labelText; | ||
$mdAria.expect(element, 'aria-label', labelText); | ||
} | ||
|
||
|
@@ -311,6 +316,11 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
} | ||
} | ||
|
||
function syncAriaLabel() { | ||
if (!ariaLabelBase) return; | ||
element.attr('aria-label', ariaLabelBase + ': ' + selectMenuCtrl.selectedLabels({mode: 'aria'})); | ||
This comment has been minimized.
Sorry, something went wrong.
marcysutton
Contributor
|
||
} | ||
|
||
var deregisterWatcher; | ||
attr.$observe('ngMultiple', function(val) { | ||
if (deregisterWatcher) deregisterWatcher(); | ||
|
@@ -324,12 +334,14 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
} else { | ||
element.removeAttr('multiple'); | ||
} | ||
element.attr('aria-multiselectable', multiple ? 'true' : 'false'); | ||
if (selectContainer) { | ||
selectMenuCtrl.setMultiple(multiple); | ||
originalRender = ngModelCtrl.$render; | ||
ngModelCtrl.$render = function() { | ||
originalRender(); | ||
syncLabelText(); | ||
syncAriaLabel(); | ||
inputCheckValue(); | ||
}; | ||
ngModelCtrl.$render(); | ||
|
@@ -365,9 +377,11 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
|
||
|
||
var ariaAttrs = { | ||
role: 'combobox', | ||
'aria-expanded': 'false' | ||
role: 'listbox', | ||
'aria-expanded': 'false', | ||
'aria-multiselectable': attr.multiple !== undefined && !attr.ngMultiple ? 'true' : 'false' | ||
}; | ||
|
||
if (!element[0].hasAttribute('id')) { | ||
ariaAttrs.id = 'select_' + $mdUtil.nextUid(); | ||
} | ||
|
@@ -377,10 +391,6 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
$mdSelect | ||
.destroy() | ||
.finally(function() { | ||
if ( selectContainer ) { | ||
selectContainer.remove(); | ||
} | ||
|
||
if (containerCtrl) { | ||
containerCtrl.setFocused(false); | ||
containerCtrl.setHasValue(false); | ||
|
@@ -398,18 +408,15 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
} | ||
|
||
// Create a fake select to find out the label value | ||
function createSelect() { | ||
selectContainer = angular.element(selectTemplate); | ||
var selectEl = selectContainer.find('md-select-menu'); | ||
selectEl.data('$ngModelController', ngModelCtrl); | ||
selectEl.data('$mdSelectController', mdSelectCtrl); | ||
selectScope = scope.$new(); | ||
$mdTheming.inherit(selectContainer, element); | ||
function getSelect() { | ||
selectContainer = angular.element( | ||
element[0].querySelector('.md-select-menu-container') | ||
); | ||
selectScope = selectContainer.scope(); | ||
if (element.attr('md-container-class')) { | ||
var value = selectContainer[0].getAttribute('class') + ' ' + element.attr('md-container-class'); | ||
selectContainer[0].setAttribute('class', value); | ||
} | ||
selectContainer = $compile(selectContainer)(selectScope); | ||
selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu'); | ||
} | ||
|
||
|
@@ -434,8 +441,9 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
} | ||
} | ||
|
||
function openSelect() { | ||
function openSelect(e) { | ||
selectScope.isOpen = true; | ||
element.attr('aria-expanded', 'true'); | ||
|
||
$mdSelect.show({ | ||
scope: selectScope, | ||
|
@@ -449,6 +457,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par | |
loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false | ||
}).finally(function() { | ||
selectScope.isOpen = false; | ||
element.attr('aria-expanded', 'false'); | ||
ngModelCtrl.$setTouched(); | ||
}); | ||
} | ||
|
@@ -460,7 +469,8 @@ function SelectMenuDirective($parse, $mdUtil, $mdTheming) { | |
|
||
return { | ||
restrict: 'E', | ||
require: ['mdSelectMenu', '?ngModel'], | ||
require: ['mdSelectMenu', '^ngModel'], | ||
scope: true, | ||
controller: SelectMenuController, | ||
link: {pre: preLink} | ||
}; | ||
|
@@ -475,15 +485,6 @@ function SelectMenuDirective($parse, $mdUtil, $mdTheming) { | |
element.on('click', clickListener); | ||
element.on('keypress', keyListener); | ||
if (ngModel) selectCtrl.init(ngModel); | ||
configureAria(); | ||
|
||
function configureAria() { | ||
element.attr({ | ||
'id': 'select_menu_' + $mdUtil.nextUid(), | ||
'role': 'listbox', | ||
'aria-multiselectable': (selectCtrl.isMultiple ? 'true' : 'false') | ||
}); | ||
} | ||
|
||
function keyListener(e) { | ||
if (e.keyCode == 13 || e.keyCode == 32) { | ||
|
@@ -623,12 +624,19 @@ function SelectMenuDirective($parse, $mdUtil, $mdTheming) { | |
self.setMultiple(self.isMultiple); | ||
}; | ||
|
||
self.selectedLabels = function() { | ||
self.selectedLabels = function(opts) { | ||
opts = opts || {}; | ||
var mode = opts.mode || 'html'; | ||
var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]')); | ||
if (selectedOptionEls.length) { | ||
return selectedOptionEls.map(function(el) { | ||
return el.innerHTML; | ||
}).join(', '); | ||
var mapFn; | ||
|
||
if (mode == 'html') { | ||
mapFn = function(el) { return el.innerHTML; }; | ||
} else if (mode == 'aria') { | ||
mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; }; | ||
} | ||
return selectedOptionEls.map(mapFn).join(', '); | ||
} else { | ||
return ''; | ||
} | ||
|
@@ -844,7 +852,7 @@ function SelectProvider($$interimElementProvider) { | |
}); | ||
|
||
/* @ngInject */ | ||
function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate) { | ||
function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) { | ||
var ERRROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!"; | ||
var animator = $mdUtil.dom.animator; | ||
|
||
|
@@ -869,7 +877,7 @@ function SelectProvider($$interimElementProvider) { | |
// For navigation $destroy events, do a quick, non-animated removal, | ||
// but for normal closes (from clicks, etc) animate the removal | ||
|
||
return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean ); | ||
return (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement ); | ||
|
||
/** | ||
* For normal closes (eg clicks), animate the removal. | ||
|
@@ -881,14 +889,14 @@ function SelectProvider($$interimElementProvider) { | |
} | ||
|
||
/** | ||
* Detach the element and cleanup prior changes | ||
* Clean the element up to a closed state | ||
*/ | ||
function detachAndClean() { | ||
configureAria(opts.target, false); | ||
function cleanElement() { | ||
|
||
element.attr('opacity', 0); | ||
element.removeClass('md-active'); | ||
detachElement(element, opts); | ||
element.attr('aria-hidden', 'true'); | ||
element[0].style.display = 'none'; | ||
|
||
announceClosed(opts); | ||
|
||
|
@@ -906,12 +914,12 @@ function SelectProvider($$interimElementProvider) { | |
|
||
watchAsyncLoad(); | ||
sanitizeAndConfigure(scope, opts); | ||
configureAria(opts.target); | ||
|
||
opts.hideBackdrop = showBackdrop(scope, element, opts); | ||
|
||
return showDropDown(scope, element, opts) | ||
.then(function(response) { | ||
element.attr('aria-hidden', 'false'); | ||
opts.alreadyOpen = true; | ||
opts.cleanupInteraction = activateInteraction(); | ||
opts.cleanupResizing = activateResizing(); | ||
|
@@ -998,7 +1006,7 @@ function SelectProvider($$interimElementProvider) { | |
if (options.disableParentScroll) options.restoreScroll(); | ||
|
||
delete options.restoreScroll; | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
|
@@ -1059,7 +1067,7 @@ function SelectProvider($$interimElementProvider) { | |
// Disable resizing handlers | ||
window.off('resize', debouncedOnResize); | ||
window.off('orientationchange', debouncedOnResize); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
|
@@ -1078,7 +1086,7 @@ function SelectProvider($$interimElementProvider) { | |
delete opts.loadingAsync; | ||
}).then(function() { | ||
$$rAF(positionAndFocusMenu); | ||
}) | ||
}); | ||
} | ||
} | ||
|
||
|
@@ -1099,12 +1107,12 @@ function SelectProvider($$interimElementProvider) { | |
// Escape to close | ||
// Cycling of options, and closing on enter | ||
dropDown.on('keydown', onMenuKeyDown); | ||
dropDown.on('mouseup', checkCloseMenu); | ||
dropDown.on('click', checkCloseMenu); | ||
|
||
return function cleanupInteraction() { | ||
opts.backdrop && opts.backdrop.off('click', onBackdropClick); | ||
dropDown.off('keydown', onMenuKeyDown); | ||
dropDown.off('mouseup', checkCloseMenu); | ||
dropDown.off('click', checkCloseMenu); | ||
|
||
element.removeClass('md-clickable'); | ||
opts.isRemoved = true; | ||
|
@@ -1123,14 +1131,14 @@ function SelectProvider($$interimElementProvider) { | |
|
||
function onMenuKeyDown(ev) { | ||
var keyCodes = $mdConstant.KEY_CODE; | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
|
||
switch (ev.keyCode) { | ||
case keyCodes.UP_ARROW: | ||
return focusPrevOption(); | ||
break; | ||
case keyCodes.DOWN_ARROW: | ||
return focusNextOption(); | ||
break; | ||
case keyCodes.SPACE: | ||
case keyCodes.ENTER: | ||
var option = $mdUtil.getClosest(ev.target, 'md-option'); | ||
|
@@ -1190,11 +1198,13 @@ function SelectProvider($$interimElementProvider) { | |
} | ||
|
||
function checkCloseMenu(ev) { | ||
if (ev && ( ev.type == 'mouseup') && (ev.currentTarget != dropDown[0])) return; | ||
if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return; | ||
if ( mouseOnScrollbar() ) return; | ||
|
||
var option = $mdUtil.getClosest(ev.target, 'md-option'); | ||
if (option && option.hasAttribute && !option.hasAttribute('disabled')) { | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
if (!selectCtrl.isMultiple) { | ||
opts.restoreFocus = true; | ||
|
||
|
@@ -1224,14 +1234,6 @@ function SelectProvider($$interimElementProvider) { | |
|
||
} | ||
|
||
/** | ||
* | ||
*/ | ||
function configureAria(element, isExpanded) { | ||
isExpanded = angular.isUndefined(isExpanded) ? 'true' : 'false'; | ||
element && element.attr('aria-expanded', isExpanded); | ||
} | ||
|
||
/** | ||
* To notify listeners that the Select menu has closed, | ||
* trigger the [optional] user-defined expression | ||
|
@@ -1245,23 +1247,15 @@ function SelectProvider($$interimElementProvider) { | |
} | ||
} | ||
|
||
/** | ||
* Use browser to remove this element without triggering a $destroy event | ||
*/ | ||
function detachElement(element, opts) { | ||
if (element[0].parentNode === opts.parent[0]) { | ||
opts.parent[0].removeChild(element[0]); | ||
} | ||
} | ||
|
||
/** | ||
* Calculate the | ||
*/ | ||
function calculateMenuPositions(scope, element, opts) { | ||
var optionNodes, | ||
var | ||
containerNode = element[0], | ||
targetNode = opts.target[0].firstElementChild, // target the label | ||
parentNode = opts.parent[0], | ||
targetNode = opts.target[0].children[1], // target the label | ||
parentNode = $document[0].body, | ||
selectNode = opts.selectEl[0], | ||
contentNode = opts.contentEl[0], | ||
parentRect = parentNode.getBoundingClientRect(), | ||
|
@@ -1329,6 +1323,7 @@ function SelectProvider($$interimElementProvider) { | |
opts.focusedNode = focusedNode; | ||
|
||
// Get the selectMenuRect *after* max-width is possibly set above | ||
containerNode.style.display = 'block'; | ||
var selectMenuRect = selectNode.getBoundingClientRect(); | ||
var centeredRect = getOffsetRect(centeredNode); | ||
|
||
|
@@ -1367,7 +1362,7 @@ function SelectProvider($$interimElementProvider) { | |
} else { | ||
left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2; | ||
top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 - | ||
centeredRect.top + contentNode.scrollTop) + 2; | ||
centeredRect.top + contentNode.scrollTop) + 5; | ||
|
||
transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' + | ||
(centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px'; | ||
|
What is this hiding?