diff --git a/docs/app/js/preload.js b/docs/app/js/preload.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/components/input/input.js b/src/components/input/input.js index 65beb9e577c..f97a4cfa7e0 100644 --- a/src/components/input/input.js +++ b/src/components/input/input.js @@ -98,7 +98,7 @@ function labelDirective() { restrict: 'E', require: '^?mdInputContainer', link: function(scope, element, attr, containerCtrl) { - if (!containerCtrl || attr.mdNoFloat) return; + if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; containerCtrl.label = element; scope.$on('$destroy', function() { diff --git a/src/components/input/input.scss b/src/components/input/input.scss index 7d3eb74e10a..69799d7dde7 100644 --- a/src/components/input/input.scss +++ b/src/components/input/input.scss @@ -87,14 +87,14 @@ md-input-container { -ms-flex-preferred-size: auto; //IE fix } - label { + label:not(.md-container-ignore) { position: absolute; bottom: 100%; left: 0; } - label:not(.md-no-float), + label:not(.md-no-float):not(.md-container-ignore), .md-placeholder { order: 1; pointer-events: none; diff --git a/src/components/select/select.js b/src/components/select/select.js index 81e0e958882..da45fe21bfe 100755 --- a/src/components/select/select.js +++ b/src/components/select/select.js @@ -132,6 +132,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par element.append(angular.element('').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 = '' + - '
' + + ''; 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(); + findSelectContainer(); $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,12 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par } } + function syncAriaLabel() { + if (!ariaLabelBase) return; + var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'}); + element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase); + } + var deregisterWatcher; attr.$observe('ngMultiple', function(val) { if (deregisterWatcher) deregisterWatcher(); @@ -324,12 +335,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 +378,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 +392,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); @@ -397,19 +408,15 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput); } - // 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 findSelectContainer() { + 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, @@ -443,10 +451,13 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par skipCompile: true, element: selectContainer, target: element[0], + preserveElement: true, + parent: element, hasBackdrop: true, loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false }).finally(function() { selectScope.isOpen = false; + element.attr('aria-expanded', 'false'); ngModelCtrl.$setTouched(); }); } @@ -458,7 +469,8 @@ function SelectMenuDirective($parse, $mdUtil, $mdTheming) { return { restrict: 'E', - require: ['mdSelectMenu', '?ngModel'], + require: ['mdSelectMenu', '^ngModel'], + scope: true, controller: SelectMenuController, link: {pre: preLink} }; @@ -473,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) { @@ -621,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 ''; } @@ -830,6 +840,7 @@ function OptgroupDirective() { labelElement = angular.element('