diff --git a/config/build.config.js b/config/build.config.js index db117589ee1..cc748cf001f 100644 --- a/config/build.config.js +++ b/config/build.config.js @@ -4,7 +4,7 @@ var fs = require('fs'); var versionFile = __dirname + '/../dist/commit'; module.exports = { - ngVersion: '1.3.2', + ngVersion: '1.3.15', version: pkg.version, repository: pkg.repository.url .replace(/^git/,'https') diff --git a/docs/app/img/icons/sets/communication-icons.svg b/docs/app/img/icons/sets/communication-icons.svg new file mode 100644 index 00000000000..1e2fbb0a403 --- /dev/null +++ b/docs/app/img/icons/sets/communication-icons.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/app/img/icons/sets/core-icons.svg b/docs/app/img/icons/sets/core-icons.svg index 5f52b2d8100..fff04153670 100644 --- a/docs/app/img/icons/sets/core-icons.svg +++ b/docs/app/img/icons/sets/core-icons.svg @@ -23,4 +23,5 @@ - \ No newline at end of file + + diff --git a/docs/app/img/icons/sets/device-icons.svg b/docs/app/img/icons/sets/device-icons.svg new file mode 100644 index 00000000000..cc5bfd7f69d --- /dev/null +++ b/docs/app/img/icons/sets/device-icons.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 8ad06b2bd04..ed5de4a881f 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -103,7 +103,7 @@ function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, ngModelCtrl.$render = render; function keypressHandler(ev) { - if(ev.which === $mdConstant.KEY_CODE.SPACE) { + if(ev.which === $mdConstant.KEY_CODE.SPACE || ev.which === $mdConstant.KEY_CODE.ENTER) { ev.preventDefault(); listener(ev); } diff --git a/src/components/icon/icon-theme.scss b/src/components/icon/icon-theme.scss index 7042242fc00..ca6a0d09a14 100644 --- a/src/components/icon/icon-theme.scss +++ b/src/components/icon/icon-theme.scss @@ -1,4 +1,5 @@ md-icon.md-THEME_NAME-theme { + color: '{{foreground-2}}'; &.md-primary { color: '{{primary-color}}'; diff --git a/src/components/list/demoBasicUsage/index.html b/src/components/list/demoBasicUsage/index.html index 8dfd50311f5..94a57d1acad 100644 --- a/src/components/list/demoBasicUsage/index.html +++ b/src/components/list/demoBasicUsage/index.html @@ -1,25 +1,32 @@
- - - - -
- {{item.who}} -
-
-

{{item.what}}

-

{{item.who}}

-

- {{item.notes}} -

-
-
+ 3 line item + + {{item.who}} +
+

{{ item.who }}

+

{{ item.what }}

+

{{ item.notes }}

+
+
+ 2 line item + + {{todos[0].who}} +
+

{{ todos[0].who }}

+

Secondary text

+
+
+ 3 line item, long paragraph + + {{todos[0].who}} +
+

{{ todos[0].who }}

+

Secondary line text Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam massa quam.

+
-
-
diff --git a/src/components/list/demoBasicUsage/script.js b/src/components/list/demoBasicUsage/script.js index 555e145ca59..c10d2fc965e 100644 --- a/src/components/list/demoBasicUsage/script.js +++ b/src/components/list/demoBasicUsage/script.js @@ -4,35 +4,35 @@ angular.module('listDemo1', ['ngMaterial']) .controller('AppCtrl', function($scope) { $scope.todos = [ { - face : '/img/list/60.jpeg', + face : 'http://lorempixel.com/50/50/people', what: 'Brunch this weekend?', who: 'Min Li Chan', when: '3:08PM', notes: " I'll be in your neighborhood doing errands" }, { - face : '/img/list/60.jpeg', + face : 'http://lorempixel.com/50/50/people', what: 'Brunch this weekend?', who: 'Min Li Chan', when: '3:08PM', notes: " I'll be in your neighborhood doing errands" }, { - face : '/img/list/60.jpeg', + face : 'http://lorempixel.com/50/50/people', what: 'Brunch this weekend?', who: 'Min Li Chan', when: '3:08PM', notes: " I'll be in your neighborhood doing errands" }, { - face : '/img/list/60.jpeg', + face : 'http://lorempixel.com/50/50/people', what: 'Brunch this weekend?', who: 'Min Li Chan', when: '3:08PM', notes: " I'll be in your neighborhood doing errands" }, { - face : '/img/list/60.jpeg', + face : 'http://lorempixel.com/50/50/people', what: 'Brunch this weekend?', who: 'Min Li Chan', when: '3:08PM', diff --git a/src/components/list/demoListControls/index.html b/src/components/list/demoListControls/index.html new file mode 100644 index 00000000000..2dcf9143bb7 --- /dev/null +++ b/src/components/list/demoListControls/index.html @@ -0,0 +1,29 @@ + + Single Action Checkboxes + +

{{ topping.name }}

+ +
+ Clickable Items with Secondary Controls + + +

{{ setting.name }}

+ +
+ + +

Data Usage

+
+ Checkbox with Secondary Action + + +

{{message.title}}

+ +
+ Avatar with Secondary Action Icon + + avatar +

{{ person.name }}

+ +
+
diff --git a/src/components/list/demoListControls/script.js b/src/components/list/demoListControls/script.js new file mode 100644 index 00000000000..3fc8982e7f3 --- /dev/null +++ b/src/components/list/demoListControls/script.js @@ -0,0 +1,48 @@ +angular.module('listDemo2', ['ngMaterial']) +.config(function($mdIconProvider) { + $mdIconProvider + .iconSet('social', 'img/icons/sets/social-icons.svg', 24) + .iconSet('device', 'img/icons/sets/device-icons.svg', 24) + .iconSet('communication', 'img/icons/sets/communication-icons.svg', 24) + .defaultIconSet('img/icons/sets/core-icons.svg', 24); +}) +.controller('ListCtrl', function($scope) { + $scope.toppings = [ + { name: 'Pepperoni', wanted: true }, + { name: 'Sausage', wanted: false }, + { name: 'Black Olives', wanted: true }, + { name: 'Green Peppers', wanted: false } + ]; + + $scope.settings = [ + { name: 'Wi-Fi', extraScreen: 'Wi-fi menu', icon: 'device:network-wifi', enabled: true }, + { name: 'Bluetooth', extraScreen: 'Bluetooth menu', icon: 'device:bluetooth', enabled: false }, + ]; + + $scope.messages = [ + {id: 1, title: "Message A", selected: false}, + {id: 2, title: "Message B", selected: true}, + {id: 3, title: "Message C", selected: true}, + ]; + + + + $scope.people = [ + { name: 'Janet Perkins', newMessage: true }, + { name: 'Mary Johnson', newMessage: false }, + { name: 'Peter Carlsson', newMessage: false } + ]; + + $scope.goToPerson = function(person) { + alert('Inspect ' + person); + }; + + $scope.navigateTo = function(to) { + alert('Imagine being taken to ' + to); + }; + + $scope.doSecondaryAction = function() { + alert('Seconday action clicked'); + }; + +}); diff --git a/src/components/list/demoListControls/style.css b/src/components/list/demoListControls/style.css new file mode 100644 index 00000000000..d872104a2d4 --- /dev/null +++ b/src/components/list/demoListControls/style.css @@ -0,0 +1,3 @@ +md-icon-placeholder { + height: 24px; +} diff --git a/src/components/list/list-theme.scss b/src/components/list/list-theme.scss new file mode 100644 index 00000000000..c08b10ac7fb --- /dev/null +++ b/src/components/list/list-theme.scss @@ -0,0 +1,27 @@ +md-list.md-THEME_NAME-theme { + md-item.md-double-line .md-item-text, + md-item.md-triple-line .md-item-text { + h3, h4 { + color: '{{foreground-1}}'; + } + p { + color: '{{foreground-2}}'; + } + } + .md-proxy-focus.md-focused div.md-no-style, + .md-secondary:focus, + .md-no-style:focus { + background-color: '{{background-100}}'; + } + + md-item > md-icon { + color: '{{foreground-2}}'; + + &.md-highlight { + color: '{{primary-color}}'; + &.md-accent { + color: '{{accent-color}}'; + } + } + } +} diff --git a/src/components/list/list.js b/src/components/list/list.js index ad7180a40cd..94d5bb0a31a 100644 --- a/src/components/list/list.js +++ b/src/components/list/list.js @@ -45,11 +45,12 @@ angular.module('material.components.list', [ * * */ -function mdListDirective() { +function mdListDirective($mdTheming) { return { restrict: 'E', - compile: function(element) { - element[0].setAttribute('role', 'list'); + compile: function(tEl) { + tEl[0].setAttribute('role', 'list'); + return $mdTheming; } }; } @@ -74,11 +75,150 @@ function mdListDirective() { * * */ -function mdItemDirective() { +function mdItemDirective($document, $log, $mdUtil, $mdAria) { + var proxiedTypes = ['md-checkbox', 'md-switch']; return { restrict: 'E', - compile: function(element) { - element[0].setAttribute('role', 'listitem'); + compile: function(tEl, tAttrs) { + // Check for proxy controls (no ng-click on parent, and a control inside) + var secondaryItem = tEl[0].querySelector('.md-secondary'); + var hasProxiedElement; + var proxyElement; + + tEl[0].setAttribute('role', 'listitem'); + + if (!tAttrs.ngClick) { + for (var i = 0, type; type = proxiedTypes[i]; ++i) { + if (proxyElement = tEl[0].querySelector(type)) { + hasProxiedElement = true; + break; + } + } + if (hasProxiedElement) { + wrapIn('div'); + } else { + tEl.addClass('md-no-style'); + } + } else { + wrapIn('button'); + } + setupToggleAria(); + + + function setupToggleAria() { + var toggleTypes = ['md-switch', 'md-checkbox']; + var toggle; + + for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) { + if (toggle = tEl.find(toggleType)[0]) { + if (!toggle.hasAttribute('aria-label')) { + var p = tEl.find('p')[0]; + if (!p) return; + toggle.setAttribute('aria-label', 'Toggle ' + p.textContent); + } + } + } + } + + + function wrapIn(type) { + var container; + if (type == 'div') { + container = angular.element('
'); + container.append(tEl.contents()); + tEl.addClass('md-proxy-focus'); + } else { + container = angular.element(''); + container[0].setAttribute('ng-click', tEl[0].getAttribute('ng-click')); + tEl[0].removeAttribute('ng-click'); + container.children().eq(0).append(tEl.contents()); + } + + tEl[0].setAttribute('tabindex', '-1'); + tEl.append(container); + + if (secondaryItem && secondaryItem.hasAttribute('ng-click')) { + $mdAria.expect(secondaryItem, 'aria-label'); + } + + // Check for a secondary item and move it outside + if ( secondaryItem && ( + secondaryItem.hasAttribute('ng-click') || + ( tAttrs.ngClick && + isProxiedElement(secondaryItem) ) + )) { + tEl.addClass('md-with-secondary'); + tEl.append(secondaryItem); + } + } + + function isProxiedElement(el) { + return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1; + } + + return postLink; + + function postLink($scope, $element, $attr) { + + var proxies = []; + + computeProxies(); + computeClickable(); + + if ($element.hasClass('md-proxy-focus') && proxies.length) { + angular.forEach(proxies, function(proxy) { + proxy = angular.element(proxy); + proxy.on('focus', function() { + $element.addClass('md-focused'); + proxy.on('blur', function() { + $element.removeClass('md-focused'); + proxy.off('blur'); + }); + }); + }); + } + + function computeProxies() { + if (!$element.children()[0].hasAttribute('ng-click')) { + angular.forEach(proxiedTypes, function(type) { + angular.forEach($element[0].firstElementChild.querySelectorAll(type), function(child) { + proxies.push(child); + }); + }); + } + } + function computeClickable() { + if (proxies.length || $element[0].firstElementChild.hasAttribute('ng-click')) { + $element.addClass('md-clickable'); + } + } + + if (!$element[0].firstElementChild.hasAttribute('ng-click') && !proxies.length) { + $element[0].firstElementChild.addEventListener('keypress', function(e) { + if (e.keyCode == 13 || e.keyCode == 32) { + $element[0].firstElementChild.click(); + e.preventDefault(); + e.stopPropagation(); + } + }); + } + + $element.off('click'); + $element.off('keypress'); + + if (proxies.length) { + $element.children().eq(0).on('click', function(e) { + if ($element[0].firstElementChild.contains(e.target)) { + angular.forEach(proxies, function(proxy) { + if (e.target !== proxy && !proxy.contains(e.target)) { + angular.element(proxy).triggerHandler('click'); + proxy.focus(); + } + }); + } + }); + } + } } }; } diff --git a/src/components/list/list.scss b/src/components/list/list.scss index 3171da485fe..d87e9f12bc3 100644 --- a/src/components/list/list.scss +++ b/src/components/list/list.scss @@ -1,11 +1,12 @@ -$list-h3-font-size: 1.1em !default; -$list-h3-margin: 0 0 3px 0 !default; +$list-h3-font-size: 1em !default; +$list-h3-margin: 0 0 6px 0 !default; $list-h3-font-weight: 400 !default; -$list-h4-font-size: 0.9em !default; +$list-h4-font-size: 0.875em !default; $list-h4-font-weight: 400 !default; -$list-h4-margin: 0 0 3px 0 !default; +$list-h4-margin: 10px 0 5px 0 !default; $list-p-font-size: 0.75em !default; -$list-p-margin: 0 0 3px 0 !default; +$list-p-margin: 0 0 0px 0 !default; +$list-p-line-height: 1.6em !default; $list-padding-top: $baseline-grid !default; $list-padding-right: 0px !default; @@ -16,63 +17,165 @@ $item-padding-top: 0px !default; $item-padding-right: 0px !default; $item-padding-left: 0px !default; $item-padding-bottom: 0px !default; +$list-item-padding-vertical: 0px !default; +$list-item-padding-horizontal: $baseline-grid * 2 !default; +$list-item-primary-width: $baseline-grid * 7 !default; +$list-item-primary-avatar-width: $baseline-grid * 5 !default; +$list-item-primary-icon-width: $baseline-grid * 3 !default; +$list-item-secondary-left-margin: $baseline-grid * 2 !default; +$list-item-text-padding-top: $baseline-grid * 2 !default; +$list-item-text-padding-bottom: $baseline-grid * 2.5 !default; md-list { + display: block; padding: $list-padding-top $list-padding-right $list-padding-bottom $list-padding-left; + + .md-subheader { + font-weight: 500; + font-size: 0.875em; + } } md-item { - + &.md-no-style, + .md-no-style { + padding: $list-item-padding-vertical $list-item-padding-horizontal; + flex: 1; + &:focus { + outline: none + } + } + &.md-with-secondary { + padding-right: $list-item-padding-horizontal; + } + &.md-clickable:hover { + cursor: pointer; + } } -md-item-content { +md-item, md-item .md-item-inner { display: flex; + justify-content: flex-start; align-items: center; - flex-direction: row; - box-sizing: border-box; - position: relative; + // Layout for controls in primary or secondary divs, or auto-infered first child + & > div.md-primary > md-icon, + & > div.md-secondary > md-icon, + & > md-icon:first-child, + > md-icon.md-secondary { + width: $list-item-primary-icon-width; + margin-top: 12px; + margin-bottom: 12px; + box-sizing: content-box; + &:focus { + border-radius: 2px; + outline: none; + padding: 6px; + margin-top: 6px; + margin-bottom: 6px; + } + } + & > div.md-primary > md-checkbox, + & > div.md-secondary > md-checkbox, + & > md-checkbox:first-child, + md-checkbox.md-secondary { + position: relative; + top: -2px; + margin-top: 14px; + margin-bottom: 12px; + .md-label { display: none; } + &:not(md-checkbox:first-child):focus { + padding: 6px; + margin-right: -6px; + margin-top: 8px; + margin-bottom: 6px; + } + } - padding: $item-padding-top $item-padding-right $item-padding-bottom $item-padding-left; -} + & > md-icon:first-child { + margin-right: $list-item-primary-width - $list-item-primary-icon-width; + } + & > md-checkbox:first-child { + width: 3 * $baseline-grid; + margin-left: 3px; + margin-right: 29px; + } + & > .md-avatar:first-child { + width: $list-item-primary-avatar-width; + height: $list-item-primary-avatar-width; + margin-top: $baseline-grid; + margin-bottom: $baseline-grid; + margin-right: $list-item-primary-width - $list-item-primary-avatar-width; + border-radius: 50%; + box-sizing: content-box; + } + + md-checkbox.md-secondary { + margin-right: 0px; + } + md-switch.md-secondary { + margin: 0; + position: relative; + right: -9px; + } + .md-secondary { + margin-left: $list-item-secondary-left-margin; + } -/** - * The left tile for a list item. - */ -.md-tile-left { - min-width: 56px; - margin-right: -16px; + & > p, & > .md-item-inner > p { + flex: 1; + margin: 0; + } } -/** - * The center content tile for a list item. - */ -.md-tile-content { - flex: 1; - padding: $baseline-grid * 2; +md-item.md-2-line, +md-item.md-2-line > .md-no-style, +md-item.md-3-line, +md-item.md-3-line > .md-no-style { + align-items: flex-start; - text-overflow: ellipsis; + .md-item-text { + flex: 1; + padding: $baseline-grid * 2 0; + text-overflow: ellipsis; - h3 { - margin: $list-h3-margin; - font-weight: $list-h3-font-weight; - font-size: $list-h3-font-size; + h3 { + margin: $list-h3-margin; + font-weight: $list-h3-font-weight; + font-size: $list-h3-font-size; + line-height: 0.75em; + } + h4 { + margin: $list-h4-margin; + font-weight: $list-h4-font-weight; + font-size: $list-h4-font-size; + line-height: 0.75em; + } + p { + margin: $list-p-margin; + font-size: $list-p-font-size; + line-height: $list-p-line-height; + } } - h4 { - margin: $list-h4-margin; - font-weight: $list-h4-font-weight; - font-size: $list-h4-font-size; +} + +md-item.md-2-line, +md-item.md-2-line > .md-no-style { + > md-icon:first-child, + > .md-avatar:first-child { + margin-top: $baseline-grid * 1.5; } - p { - margin: $list-p-margin; - font-size: $list-p-font-size; + .md-item-text { + flex: 1; + padding-top: ($baseline-grid * 2.5) - 1; } } -/** - * The right tile for a list item. - */ -.md-tile-right { - padding-right: $item-padding-right; +md-item.md-3-line, +md-item.md-3-line > .md-no-style { + > md-icon:first-child, + > .md-avatar:first-child { + margin-top: $baseline-grid * 2; + } } diff --git a/src/components/list/list.spec.js b/src/components/list/list.spec.js index 6bc0fa793d2..3b51f5d601c 100644 --- a/src/components/list/list.spec.js +++ b/src/components/list/list.spec.js @@ -1,11 +1,42 @@ -describe('mdList directive', function() { - function setup(attrs) { - module('material.components.list'); +describe('mdItem directive', function() { + beforeEach(module('material.components.list', 'material.components.checkbox', 'material.components.switch')); + + function setup(html) { var el; inject(function($compile, $rootScope) { - el = $compile('')($rootScope.$new()); + el = $compile(html)($rootScope); $rootScope.$apply(); }); return el; } + + it('forwards click events for md-checkbox', inject(function($rootScope) { + var listItem = setup(''); + listItem[0].querySelector('div').click(); + expect($rootScope.modelVal).toBe(true); + })); + + it('forwards click events for md-switch', inject(function($rootScope) { + var listItem = setup(''); + listItem[0].querySelector('div').click(); + expect($rootScope.modelVal).toBe(true); + })); + + it('creates buttons when used with ng-click', function() { + var listItem = setup('

Hello world

'); + var firstChild = listItem.children()[0]; + expect(firstChild.nodeName).toBe('BUTTON'); + expect(firstChild.childNodes[0].nodeName).toBe('DIV'); + expect(firstChild.childNodes[0].childNodes[0].nodeName).toBe('P'); + }); + + it('moves md-secondary items outside of the button', function() { + var listItem = setup('

Hello World

'); + var firstChild = listItem.children()[0]; + expect(firstChild.nodeName).toBe('BUTTON'); + expect(firstChild.childNodes.length).toBe(1); + var secondChild = listItem.children()[1]; + expect(secondChild.nodeName).toBe('MD-ICON'); + }); + }); diff --git a/src/components/subheader/subheader.js b/src/components/subheader/subheader.js index 6dc35c75676..233f06beb0f 100644 --- a/src/components/subheader/subheader.js +++ b/src/components/subheader/subheader.js @@ -33,7 +33,9 @@ angular.module('material.components.subheader', [ * @restrict E * * @description - * The `` directive is a subheader for a section + * The `` directive is a subheader for a section. By default it is sticky. + * You can make it not sticky by applying the `md-no-sticky` class to the subheader. + * * * @usage * @@ -66,12 +68,14 @@ function MdSubheaderDirective($mdSticky, $compile, $mdTheming) { // Create another clone, that uses the outer and inner contents // of the element, that will be 'stickied' as the user scrolls. - transclude(scope, function(clone) { - var stickyClone = $compile(angular.element(outerHTML))(scope); - $mdTheming(stickyClone); - getContent(stickyClone).append(clone); - $mdSticky(scope, element, stickyClone); - }); + if (!element.hasClass('md-no-sticky')) { + transclude(scope, function(clone) { + var stickyClone = $compile(angular.element(outerHTML))(scope); + $mdTheming(stickyClone); + getContent(stickyClone).append(clone); + $mdSticky(scope, element, stickyClone); + }); + } }; } }; diff --git a/src/core/services/aria/aria.js b/src/core/services/aria/aria.js index f7aa3948c12..eb67f04da7f 100644 --- a/src/core/services/aria/aria.js +++ b/src/core/services/aria/aria.js @@ -19,7 +19,7 @@ function AriaService($$rAF, $log, $window) { * @param {optional} defaultValue What to set the attr to if no value is found */ function expect(element, attrName, defaultValue) { - var node = element[0]; + var node = element[0] || element; // if node exists and neither it nor its children have the attribute if (node && diff --git a/src/core/style/structure.scss b/src/core/style/structure.scss index ca39ef58cec..f35fde72f57 100644 --- a/src/core/style/structure.scss +++ b/src/core/style/structure.scss @@ -17,6 +17,15 @@ body { padding: 10px; } +button.md-no-style { + font-weight: normal; + background-color: inherit; + text-align: left; + border: none; + padding: 0px; + margin: 0px; +} + select, button, textarea,