diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js
index 34fc5cbda6e..ea5d3bc7d80 100644
--- a/src/components/checkbox/checkbox.js
+++ b/src/components/checkbox/checkbox.js
@@ -46,9 +46,8 @@ angular.module('material.components.checkbox', [
*
*
*/
-function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, $mdTheming) {
+function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant, $mdTheming, $mdUtil) {
inputDirective = inputDirective[0];
-
var CHECKED_CSS = 'md-checked';
return {
@@ -74,18 +73,10 @@ function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant,
tElement.attr('role', tAttrs.type);
return function postLink(scope, element, attr, ngModelCtrl) {
+ ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel();
var checked = false;
$mdTheming(element);
- // Create a mock ngModel if the user doesn't provide one
- ngModelCtrl = ngModelCtrl || {
- $setViewValue: function(value) {
- this.$viewValue = value;
- },
- $parsers: [],
- $formatters: []
- };
-
$mdAria.expectWithText(tElement, 'aria-label');
// Reuse the original input[type=checkbox] directive from Angular core.
@@ -96,7 +87,11 @@ function MdCheckboxDirective(inputDirective, $mdInkRipple, $mdAria, $mdConstant,
0: {}
}, attr, [ngModelCtrl]);
- element.on('click', listener);
+ // Used by switch. in Switch, we don't want click listeners; we have more granular
+ // touchup/touchdown listening.
+ if (!attr.mdNoClick) {
+ element.on('click', listener);
+ }
element.on('keypress', keypressHandler);
ngModelCtrl.$render = render;
diff --git a/src/components/radioButton/radioButton.js b/src/components/radioButton/radioButton.js
index c248f31e80b..1ea19c30bf5 100644
--- a/src/components/radioButton/radioButton.js
+++ b/src/components/radioButton/radioButton.js
@@ -61,10 +61,8 @@ function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming) {
function linkRadioGroup(scope, element, attr, ctrls) {
$mdTheming(element);
- var rgCtrl = ctrls[0],
- ngModelCtrl = ctrls[1] || {
- $setViewValue: angular.noop
- };
+ var rgCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
function keydownListener(ev) {
if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW || ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW) {
diff --git a/src/components/switch/_switch.scss b/src/components/switch/_switch.scss
index 674f666819d..d123e2ed4f9 100644
--- a/src/components/switch/_switch.scss
+++ b/src/components/switch/_switch.scss
@@ -1,42 +1,80 @@
-$switch-width: $baseline-grid * 8;
+$switch-width: 36px !default;
+$switch-height: $baseline-grid * 3 !default;
+$switch-bar-height: 14px !default;
+$switch-thumb-size: 20px !default;
md-switch {
- display: block;
- position: relative;
- height: $baseline-grid * 3;
- margin: $baseline-grid;
display: flex;
align-items: center;
- .md-switch-bar {
+ .md-container {
+ width: $switch-width;
+ height: $switch-height;
+ position: relative;
+ user-select: none;
+ margin-right: 8px;
+ }
+
+ .md-text {
+ border: 1px solid transparent;
+ }
+
+ .md-bar {
+ left: 1px;
+ width: $switch-width - 2px;
+ top: $switch-height / 2 - $switch-bar-height / 2;
+ height: $switch-bar-height;
+ border-radius: 8px;
position: absolute;
- left: $baseline-grid * 2;
- top: $baseline-grid * 1.5;
- width: $baseline-grid * 4;
- height: 1px;
- pointer-events: none;
}
- /* used also in _radio-button.scss */
- .md-switch-thumb {
+ .md-thumb-container {
+ top: $switch-height / 2 - $switch-thumb-size / 2;
+ left: 0;
+ width: $switch-width - $switch-thumb-size;
+ position: absolute;
+ transform: translate3d(0,0,0);
+ z-index: 1;
+ }
+ &.md-checked .md-thumb-container {
+ transform: translate3d(100%,0,0);
+ }
+
+ .md-thumb {
position: absolute;
margin: 0;
left: 0;
top: 0;
outline: none;
+ height: $switch-thumb-size;
+ width: $switch-thumb-size;
+ border-radius: 50%;
+ box-shadow: $whiteframe-shadow-z1;
- .md-container {
+ .md-ripple-container {
position: absolute;
- transition: transform 0.2s linear;
- transform: translate3d(0,0,0);
- }
- &.md-checked .md-container {
- transform: translate3d($switch-width - 16,0,0);
+ display: block;
+ width: auto;
+ height: auto;
+ left: -$switch-thumb-size;
+ top: -$switch-thumb-size;
+ right: -$switch-thumb-size;
+ bottom: -$switch-thumb-size;
}
+ }
- .md-label {
- margin-left: $baseline-grid * 9;
+ &.transition {
+ .md-bar,
+ .md-thumb-container,
+ .md-thumb {
+ transition: $swift-ease-in-out;
+ transition-property: transform, background-color;
+ }
+ .md-bar,
+ .md-thumb {
+ transition-delay: 0.05s;
}
}
+
}
diff --git a/src/components/switch/demoBasicUsage/index.html b/src/components/switch/demoBasicUsage/index.html
index c400d520568..cceb0f8279a 100644
--- a/src/components/switch/demoBasicUsage/index.html
+++ b/src/components/switch/demoBasicUsage/index.html
@@ -1,4 +1,4 @@
-
+
Switch 1: {{ data.cb1 }}
@@ -11,7 +11,7 @@
Switch (Disabled)
-
+
Switch (Disabled, Active)
diff --git a/src/components/switch/demoBasicUsage/script.js b/src/components/switch/demoBasicUsage/script.js
index 90489e17770..afbe39b0a57 100644
--- a/src/components/switch/demoBasicUsage/script.js
+++ b/src/components/switch/demoBasicUsage/script.js
@@ -1 +1,7 @@
-angular.module('switchDemo1', ['ngMaterial']);
+angular.module('switchDemo1', ['ngMaterial'])
+.controller('SwitchDemoCtrl', function($scope) {
+ $scope.data = {
+ cb1: true,
+ cb4: true
+ };
+});
diff --git a/src/components/switch/switch-theme.scss b/src/components/switch/switch-theme.scss
index e0f461631c9..a7b1aa8b140 100644
--- a/src/components/switch/switch-theme.scss
+++ b/src/components/switch/switch-theme.scss
@@ -1,13 +1,46 @@
-$switch-color: $foreground-secondary-color !default;
-$switch-focus-color: map-get($foreground-color-palette, '1000');
+$switch-color-palette: $primary-color-palette !default;
+$switch-off-color-palette: $foreground-color-palette !default;
+
+$switch-on-color: map-get($switch-color-palette, '500') !default;
+$switch-on-bar-color: rgba($switch-on-color, 0.5) !default;
+
+$switch-off-color: map-get($switch-off-color-palette, '50') !default;
+$switch-off-bar-color: map-get($switch-off-color-palette, '500') !default;
+
+$switch-disabled-color: map-get($switch-off-color-palette, '400') !default;
+$switch-disabled-bar-color: rgba(#000, 0.12);
md-switch.md-#{$theme-name}-theme {
- .md-switch-bar {
- background-color: $switch-color;
+ .md-thumb {
+ background-color: $switch-off-color;
+ }
+ .md-bar {
+ background-color: $switch-off-bar-color;
+ }
+
+ &.md-checked {
+ .md-thumb {
+ background-color: $switch-on-color;
+ }
+ .md-bar {
+ background-color: $switch-on-bar-color;
+ }
}
- .md-switch-thumb {
- &:focus .md-label {
- border: 1px dotted black;
+
+ &[disabled] {
+ .md-thumb {
+ background-color: $switch-disabled-color;
+ }
+ .md-bar {
+ background-color: $switch-disabled-bar-color;
}
}
+
+ &:focus {
+ .md-text {
+ border-color: black;
+ border-style: dotted;
+ }
+ }
+
}
diff --git a/src/components/switch/switch.js b/src/components/switch/switch.js
index b4ec806a0fc..8a3bb0ef34e 100644
--- a/src/components/switch/switch.js
+++ b/src/components/switch/switch.js
@@ -9,8 +9,7 @@
angular.module('material.components.switch', [
'material.core',
- 'material.components.checkbox',
- 'material.components.radioButton'
+ 'material.components.checkbox'
])
.directive('mdSwitch', MdSwitch);
@@ -47,30 +46,90 @@ angular.module('material.components.switch', [
*
*
*/
-function MdSwitch(mdCheckboxDirective, mdRadioButtonDirective, $mdTheming) {
+function MdSwitch(mdCheckboxDirective, $mdTheming, $mdUtil, $document, $mdConstant, $parse, $$rAF) {
var checkboxDirective = mdCheckboxDirective[0];
- var radioButtonDirective = mdRadioButtonDirective[0];
return {
restrict: 'E',
transclude: true,
template:
- '' +
- '' +
- radioButtonDirective.template +
+ '
' +
+ '
' +
'
',
require: '?ngModel',
compile: compile
};
function compile(element, attr) {
- var thumb = angular.element(element[0].querySelector('.md-switch-thumb'));
- var checkboxLink = checkboxDirective.compile(thumb, attr);
+ var checkboxLink = checkboxDirective.compile(element, attr);
- return function (scope, element, attr, ngModelCtrl) {
- $mdTheming(element);
- return checkboxLink(scope, thumb, attr, ngModelCtrl);
+ return function (scope, element, attr, ngModel) {
+ ngModel = ngModel || $mdUtil.fakeNgModel();
+ var disabledGetter = $parse(attr.ngDisabled);
+ var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container'));
+ var switchContainer = angular.element(element[0].querySelector('.md-container'));
+
+ // no transition on initial load
+ $$rAF(function() {
+ element.addClass('transition');
+ });
+
+ // Tell the checkbox we don't want a click listener.
+ // Our drag listener tells us everything, using more granular events.
+ attr.mdNoClick = true;
+ checkboxLink(scope, element, attr, ngModel);
+
+ $mdUtil.attachDragBehavior(scope, switchContainer);
+
+ // These events are triggered by setup drag
+ switchContainer.on('$md.dragstart', onPanStart)
+ .on('$md.drag', onPan)
+ .on('$md.dragend', onPanEnd);
+
+ function onPanStart(ev, drag) {
+ // Don't go if ng-disabled===true
+ if (disabledGetter(scope)) return ev.preventDefault();
+
+ drag.width = thumbContainer.prop('offsetWidth');
+ element.removeClass('transition');
+ }
+ function onPan(ev, drag) {
+ var percent = drag.distance / drag.width;
+
+ //if checked, start from right. else, start from left
+ var translate = ngModel.$viewValue ? 1 - percent : -percent;
+ // Make sure the switch stays inside its bounds, 0-1%
+ translate = Math.max(0, Math.min(1, translate));
+
+ thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)');
+ drag.translate = translate;
+ }
+ function onPanEnd(ev, drag) {
+ if (disabledGetter(scope)) return false;
+
+ element.addClass('transition');
+ thumbContainer.css($mdConstant.CSS.TRANSFORM, '');
+
+ // We changed if there is no distance (this is a click a click),
+ // or if the drag distance is >50% of the total.
+ var isChanged = Math.abs(drag.distance || 0) < 5 ||
+ (ngModel.$viewValue ? drag.translate < 0.5 : drag.translate > 0.5);
+ if (isChanged) {
+ scope.$apply(function() {
+ ngModel.$setViewValue(!ngModel.$viewValue);
+ ngModel.$render();
+ });
+ }
+ }
};
}
+
+
}
+
})();
diff --git a/src/components/switch/switch.spec.js b/src/components/switch/switch.spec.js
index 3d78a50418f..4a4f1efd1f2 100644
--- a/src/components/switch/switch.spec.js
+++ b/src/components/switch/switch.spec.js
@@ -2,8 +2,7 @@ describe('
', function() {
var CHECKED_CSS = 'md-checked';
beforeEach(TestUtil.mockRaf);
- beforeEach(module('ngAria'));
- beforeEach(module('material.components.switch'));
+ beforeEach(module('ngAria', 'material.components.switch'));
it('should set checked css class and aria-checked attributes', inject(function($compile, $rootScope) {
var element = $compile('' +
@@ -18,33 +17,85 @@ describe('
', function() {
$rootScope.green = true;
});
- var cbElements = angular.element(element[0].querySelectorAll('.md-switch-thumb'));
+ var switches = angular.element(element[0].querySelectorAll('md-switch'));
- expect(cbElements.eq(0).hasClass(CHECKED_CSS)).toEqual(false);
- expect(cbElements.eq(1).hasClass(CHECKED_CSS)).toEqual(true);
- // expect(cbElements.eq(0).attr('aria-checked')).toEqual('false');
- // expect(cbElements.eq(1).attr('aria-checked')).toEqual('true');
- expect(cbElements.eq(0).attr('role')).toEqual('checkbox');
+ expect(switches.eq(0).hasClass(CHECKED_CSS)).toEqual(false);
+ expect(switches.eq(1).hasClass(CHECKED_CSS)).toEqual(true);
+ expect(switches.eq(0).attr('aria-checked')).toEqual('false');
+ expect(switches.eq(1).attr('aria-checked')).toEqual('true');
+ expect(switches.eq(0).attr('role')).toEqual('checkbox');
+
+ $rootScope.$apply(function(){
+ $rootScope.blue = true;
+ $rootScope.green = false;
+ });
+
+ expect(switches.eq(1).hasClass(CHECKED_CSS)).toEqual(false);
+ expect(switches.eq(0).hasClass(CHECKED_CSS)).toEqual(true);
+ expect(switches.eq(1).attr('aria-checked')).toEqual('false');
+ expect(switches.eq(0).attr('aria-checked')).toEqual('true');
+ expect(switches.eq(1).attr('role')).toEqual('checkbox');
}));
- it('should be disabled with disabled attr', inject(function($compile, $rootScope) {
- var element = $compile('' +
- '' +
- '' +
- '
')($rootScope);
+ it('should change on panstart/panend if distance < 5', inject(function($compile, $rootScope) {
+ var element = $compile('')($rootScope);
+ var switchContainer = angular.element(element[0].querySelector('.md-container'));
+
+ $rootScope.$apply('banana = false');
+
+ expect($rootScope.banana).toBe(false);
+ expect(element.hasClass(CHECKED_CSS)).toBe(false);
+
+ switchContainer.triggerHandler('$md.dragstart', {});
+ switchContainer.triggerHandler('$md.dragend', {distance: 3});
+
+ expect($rootScope.banana).toBe(true);
+ expect(element.hasClass(CHECKED_CSS)).toBe(true);
+
+ switchContainer.triggerHandler('$md.dragstart', {});
+ switchContainer.triggerHandler('$md.dragend', {distance: 15});
+
+ expect($rootScope.banana).toBe(true);
+ expect(element.hasClass(CHECKED_CSS)).toBe(true);
+
+ switchContainer.triggerHandler('$md.dragstart', {});
+ switchContainer.triggerHandler('$md.dragend', {distance: -4});
+
+ expect($rootScope.banana).toBe(false);
+ expect(element.hasClass(CHECKED_CSS)).toBe(false);
+ }));
+
+ it('should check on panend if translate > 50%', inject(function($compile, $rootScope) {
+ var element = $compile('')($rootScope);
+ var switchContainer = angular.element(element[0].querySelector('.md-container'));
+ var drag;
+
+ drag = { distance: -55 };
+ switchContainer.triggerHandler('$md.dragstart', {});
+ drag.width = 100;
+ switchContainer.triggerHandler('$md.drag', drag);
+ switchContainer.triggerHandler('$md.dragend', drag);
- var switchThumb = angular.element(element[0].querySelectorAll('.md-switch-thumb'));
+ expect($rootScope.banana).toBe(true);
+ expect(element.hasClass(CHECKED_CSS)).toBe(true);
- $rootScope.$apply('blue = false');
- switchThumb.attr('disabled', 'true');
+ drag = { distance: 45 };
+ switchContainer.triggerHandler('$md.dragstart', {});
+ drag.width = 100;
+ switchContainer.triggerHandler('$md.drag', drag);
+ switchContainer.triggerHandler('$md.dragend', drag);
- switchThumb.triggerHandler('click');
- expect($rootScope.blue).toBe(false);
+ expect($rootScope.banana).toBe(true);
+ expect(element.hasClass(CHECKED_CSS)).toBe(true);
- switchThumb.removeAttr('disabled');
+ drag = { distance: 85 };
+ switchContainer.triggerHandler('$md.dragstart', {});
+ drag.width = 100;
+ switchContainer.triggerHandler('$md.drag', drag);
+ switchContainer.triggerHandler('$md.dragend', drag);
- switchThumb.triggerHandler('click');
- expect($rootScope.blue).toBe(true);
+ expect($rootScope.banana).toBe(false);
+ expect(element.hasClass(CHECKED_CSS)).toBe(false);
}));
});
diff --git a/src/core/services/ripple/ripple.js b/src/core/services/ripple/ripple.js
index 0f397cc83f5..1834f20d8d8 100644
--- a/src/core/services/ripple/ripple.js
+++ b/src/core/services/ripple/ripple.js
@@ -347,7 +347,12 @@ function InkRippleService($window, $timeout) {
*/
function isRippleAllowed() {
var parent = node.parentNode;
- return !node.hasAttribute('disabled') && !(parent && parent.hasAttribute('disabled'));
+ var grandparent = parent && parent.parentNode;
+ var ancestor = grandparent && grandparent.parentNode;
+ return !node.hasAttribute('disabled') &&
+ !(parent && parent.hasAttribute('disabled')) &&
+ !(grandparent && grandparent.hasAttribute('disabled')) &&
+ !(ancestor && ancestor.hasAttribute('disabled'));
}
}
}
@@ -355,7 +360,7 @@ function InkRippleService($window, $timeout) {
/**
* noink/nobar/nostretch directive: make any element that has one of
- * these attributes be given a controller, so that other directives can
+ * these attributes be given a controller, so that other directives can
* `require:` these and see if there is a `no` parent attribute.
*
* @usage
diff --git a/src/core/util/util.js b/src/core/util/util.js
index a25954843fa..ab5883567b3 100644
--- a/src/core/util/util.js
+++ b/src/core/util/util.js
@@ -1,26 +1,40 @@
(function() {
'use strict';
-/*
+/*
* This var has to be outside the angular factory, otherwise when
* there are multiple material apps on the same page, each app
- * will create its own instance of this array and the app's IDs
+ * will create its own instance of this array and the app's IDs
* will not be unique.
*/
var nextUniqueId = ['0','0','0'];
angular.module('material.core')
-.factory('$mdUtil', ['$cacheFactory', function($cacheFactory) {
+.factory('$mdUtil', function($cacheFactory, $document) {
var Util;
return Util = {
now: window.performance ? angular.bind(window.performance, window.performance.now) : Date.now,
+ attachDragBehavior: attachDragBehavior,
+
/**
* Publish the iterator facade to easily support iteration and accessors
* @see iterator below
*/
iterator: iterator,
+ fakeNgModel: function() {
+ return {
+ $setViewValue: function(value) {
+ this.$viewValue = value;
+ this.$render(value);
+ },
+ $parsers: [],
+ $formatters: [],
+ $render: angular.noop
+ };
+ },
+
/**
* @see cacheFactory below
*/
@@ -291,7 +305,7 @@ angular.module('material.core')
}
/*
- * Find the next item. If reloop is true and at the end of the list, it will
+ * Find the next item. If reloop is true and at the end of the list, it will
* go back to the first item. If given ,the `validate` callback will be used
* determine whether the next item is valid. If not valid, it will try to find the
* next item again.
@@ -313,7 +327,7 @@ angular.module('material.core')
}
/*
- * Find the previous item. If reloop is true and at the beginning of the list, it will
+ * Find the previous item. If reloop is true and at the beginning of the list, it will
* go back to the last item. If given ,the `validate` callback will be used
* determine whether the previous item is valid. If not valid, it will try to find the
* previous item again.
@@ -351,6 +365,84 @@ angular.module('material.core')
}
}
+ function attachDragBehavior(scope, element, options) {
+ // The state of the current drag
+ var drag;
+ // Whether the pointer is currently down on this element.
+ var pointerIsDown;
+ var START_EVENTS = 'mousedown touchstart pointerdown';
+ var MOVE_EVENTS = 'mousemove touchmove pointermove';
+ var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
+
+ // Listen to move and end events on document. End events especially could have bubbled up
+ // from the child.
+ element.on(START_EVENTS, startDrag);
+ $document.on(MOVE_EVENTS, doDrag)
+ .on(END_EVENTS, endDrag);
+
+ scope.$on('$destroy', cleanup);
+
+ return cleanup;
+
+ function cleanup() {
+ if (cleanup.called) return;
+ cleanup.called = true;
+
+ element.off(START_EVENTS, startDrag);
+ $document.off(MOVE_EVENTS, doDrag)
+ .off(END_EVENTS, endDrag);
+ drag = pointerIsDown = false;
+ }
+
+ function startDrag(ev) {
+ if (pointerIsDown) return;
+ pointerIsDown = true;
+
+ drag = {
+ // Restrict this drag to whatever started it: if a mousedown started the drag,
+ // don't let anything but mouse events continue it.
+ pointerType: ev.type.charAt(0),
+ startX: getPosition(ev),
+ startTime: Util.now()
+ };
+
+ element.one('$md.dragstart', function(ev) {
+ // Allow user to cancel by preventing default
+ if (ev.defaultPrevented) drag = null;
+ });
+ element.triggerHandler('$md.dragstart', drag);
+ }
+ function doDrag(ev) {
+ if (!drag || !isProperEventType(ev)) return;
+
+ updateDragState(ev);
+ element.triggerHandler('$md.drag', drag);
+ }
+ function endDrag(ev) {
+ pointerIsDown = false;
+ if (!drag || !isProperEventType(ev)) return;
+
+ updateDragState(ev);
+ element.triggerHandler('$md.dragend', drag);
+ drag = null;
+ }
+
+ function updateDragState(ev) {
+ var x = getPosition(ev);
+ drag.distance = drag.startX - x;
+ drag.direction = drag.distance > 0 ? 'left' : (drag.distance < 0 ? 'right' : '');
+ drag.time = drag.startTime - Util.now();
+ drag.velocity = Math.abs(drag.distance) / drag.time;
+ }
+ function getPosition(ev) {
+ ev = ev.originalEvent || ev; //support jQuery events
+ return (ev.touches ? ev.touches[0] : ev).pageX;
+ }
+ function isProperEventType(ev) {
+ return drag && ev && (ev.type || '').charAt(0) === drag.pointerType;
+ }
+ }
+
/*
* Angular's $cacheFactory doesn't have a keys() method,
* so we add one ourself.
@@ -376,9 +468,10 @@ angular.module('material.core')
return cache;
}
-}]);
-/*
+});
+
+/*
* Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
*
* We need to add `element.focus()`, because it's testable unlike `element[0].focus`.