diff --git a/config/build.config.js b/config/build.config.js index 53d86cf3195..a7738830345 100644 --- a/config/build.config.js +++ b/config/build.config.js @@ -69,9 +69,8 @@ module.exports = { // Ink Components 'src/components/animate/effects.js', - 'src/components/animate/canvas/rippler.js', 'src/components/animate/noEffect.js', - 'src/components/animate/inkRipple.js', + 'src/components/animate/inkCssRipple.js', // Components 'src/components/buttons/buttons.js', diff --git a/docs/app/index.html b/docs/app/index.html index 5a544ffc0e0..8b5a8d0c172 100644 --- a/docs/app/index.html +++ b/docs/app/index.html @@ -25,18 +25,18 @@

@@ -88,7 +88,7 @@

+ Improve This Doc diff --git a/src/base/_layout.scss b/src/base/_layout.scss index 509d8373093..3abd1fc58ec 100644 --- a/src/base/_layout.scss +++ b/src/base/_layout.scss @@ -114,6 +114,10 @@ display: none; } +[show] { + display: inherit; +} + @mixin layout-presets($attr) { [#{$attr}] { @@ -186,6 +190,10 @@ display: inline-block; } + [show-#{$suffix}] { + display: inherit; + } + [hide-#{$suffix}] { display: none; } diff --git a/src/base/_mixins.scss b/src/base/_mixins.scss index f245dc5947d..a97ebccd742 100644 --- a/src/base/_mixins.scss +++ b/src/base/_mixins.scss @@ -51,6 +51,14 @@ -ms-user-select: $val; user-select: $val; } +@mixin keyframes($name) { + @-webkit-keyframes #{$name} { + @content; + } + @keyframes #{$name} { + @content; + } +} @mixin margin-selectors($before:1em, $after:1em, $start:0px, $end:0px) { -webkit-margin-before: $before; diff --git a/src/components/animate/_effects.scss b/src/components/animate/_effects.scss index b6c64d666cc..c21412efb93 100644 --- a/src/components/animate/_effects.scss +++ b/src/components/animate/_effects.scss @@ -5,12 +5,54 @@ material-ink-bar { margin-top: -2px; } -canvas.material-ink-ripple { - pointer-events: none !important; +// Button ripple: keep same opacity, but expand to 0.75 scale. +// Then, fade out and expand to 2.0 scale. +@include keyframes(inkRippleButton) { + 0% { + @include transform(scale(0)); + opacity: 0.15; + } + 50% { + @include transform(scale(0.75)); + opacity: 0.15; + } + 100% { + @include transform(scale(2.0)); + opacity: 0; + } +} + +// Checkbox ripple: fully expand, then fade out. +@include keyframes(inkRippleCheckbox) { + 0% { + @include transform(scale(0)); + opacity: 0.4; + } + 50% { + @include transform(scale(2.7)); + opacity: 0.4; + } + 100% { + @include transform(scale(2.7)); + opacity: 0; + } +} + +/* + * A container inside of a rippling element (eg a button), + * which contains all of the individual ripples + */ +.material-ripple-container { + pointer-events: none; position: absolute; - top: 0; + overflow: hidden; left: 0; + top: 0; + width: 100%; + height: 100%; } -material-tabs .material-ink-ripple { - color: #f0f495; + +.material-ripple { + z-index: 0; + position: absolute; } diff --git a/src/components/animate/canvas/rippler.js b/src/components/animate/canvas/rippler.js deleted file mode 100644 index 40c28c5e54b..00000000000 --- a/src/components/animate/canvas/rippler.js +++ /dev/null @@ -1,459 +0,0 @@ -angular.module('material.animations') - .service('$ripple', [ - '$$rAF', - MaterialRippleService - ]); -/** - * Port of the Polymer Paper-Ripple code - * This service returns a reference to the Ripple class - * - * @group Paper Elements - * @element paper-ripple - * @homepage github.io - */ - -function MaterialRippleService($$rAF) { - var now = Date.now; - - if (window.performance && performance.now) { - now = performance.now.bind(performance); - } - - /** - * Unlike angular.extend() will always overwrites destination, - * mixin() only overwrites the destination if it is undefined; so - * pre-existing destination values are **not** modified. - */ - var mixin = function (dst) { - angular.forEach(arguments, function(obj) { - if (obj !== dst) { - angular.forEach(obj, function(value, key) { - // Only mixin if destination value is undefined - if ( angular.isUndefined(dst[key]) ) - { - dst[key] = value; - } - }); - } - }); - return dst; - }; - - // ********************************************************** - // Ripple Class - // ********************************************************** - - return (function(){ - - /** - * Ripple creates a `paper-ripple` which is a visual effect that other quantum paper elements can - * use to simulate a rippling effect emanating from the point of contact. The - * effect can be visualized as a concentric circle with motion. - */ - function Ripple(canvas, options) { - - this.canvas = canvas; - this.waves = []; - this.cAF = undefined; - - return angular.extend(this, mixin(options || { }, { - onComplete: angular.noop, // Completion hander/callback - initialOpacity: 0.6, // The initial default opacity set on the wave. - opacityDecayVelocity: 1.7, // How fast (opacity per second) the wave fades out. - backgroundFill: true, - pixelDensity: 1 - })); - } - - /** - * Define class methods - */ - Ripple.prototype = { - - /** - * - */ - createAt : function (startAt) { - var canvas = this.adjustBounds(this.canvas); - var width = canvas.width / this.pixelDensity; - var height = canvas.height / this.pixelDensity; - var recenter = this.canvas.classList.contains("recenteringTouch"); - - // Auto center ripple if startAt is not defined... - startAt = startAt || { x: Math.round(width / 2), y: Math.round(height / 2) }; - - this.waves.push( createWave(canvas, width, height, startAt, recenter ) ); - this.cancelled = false; - - this.animate(); - }, - - /** - * - */ - draw : function (done) { - this.onComplete = done; - - for (var i = 0; i < this.waves.length; i++) { - // Declare the next wave that has mouse down to be mouse'ed up. - var wave = this.waves[i]; - if (wave.isMouseDown) { - wave.isMouseDown = false - wave.mouseDownStart = 0; - wave.tUp = 0.0; - wave.mouseUpStart = now(); - break; - } - } - this.animate(); - }, - - /** - * - */ - cancel : function () { - this.cancelled = true; - return this; - }, - - /** - * Stop or start rendering waves for the next animation frame - */ - animate : function (active) { - if (angular.isUndefined(active)) active = true; - - if (active === false) { - if (angular.isDefined(this.cAF)) { - this._loop = null; - this.cAF(); - - // Notify listeners [via callback] of animation completion - this.onComplete(); - } - } else { - if (!this._loop) { - this._loop = angular.bind(this, function () { - var ctx = this.canvas.getContext('2d'); - ctx.scale(this.pixelDensity, this.pixelDensity); - - this.onAnimateFrame(ctx); - }); - } - this.cAF = $$rAF(this._loop); - } - }, - - /** - * - */ - onAnimateFrame : function (ctx) { - // Clear the canvas - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - var deleteTheseWaves = []; - // wave animation values - var anim = { - initialOpacity: this.initialOpacity, - opacityDecayVelocity: this.opacityDecayVelocity, - height: ctx.canvas.height, - width: ctx.canvas.width - }; - - for (var i = 0; i < this.waves.length; i++) { - var wave = this.waves[i]; - - if ( !this.cancelled ) { - - if (wave.mouseDownStart > 0) { - wave.tDown = now() - wave.mouseDownStart; - } - if (wave.mouseUpStart > 0) { - wave.tUp = now() - wave.mouseUpStart; - } - - // Obtain the instantaneous size and alpha of the ripple. - // Determine whether there is any more rendering to be done. - - var radius = waveRadiusFn(wave.tDown, wave.tUp, anim); - var maximumWave = waveAtMaximum(wave, radius, anim); - var waveDissipated = waveDidFinish(wave, radius, anim); - var shouldKeepWave = !waveDissipated || !maximumWave; - - if (!shouldKeepWave) { - - deleteTheseWaves.push(wave); - - } else { - - - drawWave( wave, angular.extend( anim, { - radius : radius, - backgroundFill : this.backgroundFill, - ctx : ctx - })); - - } - } - } - - if ( this.cancelled ) { - // Clear all waves... - deleteTheseWaves = deleteTheseWaves.concat( this.waves ); - } - for (var i = 0; i < deleteTheseWaves.length; ++i) { - removeWave( deleteTheseWaves[i], this.waves ); - } - - if (!this.waves.length) { - // If there is nothing to draw, clear any drawn waves now because - // we're not going to get another requestAnimationFrame any more. - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - - // stop animations - this.animate(false); - - } else if (!waveDissipated && !maximumWave) { - this.animate(); - } - - return this; - }, - - /** - * - */ - adjustBounds : function (canvas) { - // Default to parent container to define bounds - var self = this, - src = canvas.parentNode.getBoundingClientRect(), // read-only - bounds = { width: src.width, height: src.height }; - - angular.forEach("width height".split(" "), function (style) { - var value = (self[style] != "auto") ? self[style] : undefined; - - // Allow CSS to explicitly define bounds (instead of parent container - if (angular.isDefined(value)) { - bounds[style] = sanitizePosition(value); - canvas.setAttribute(style, bounds[style] * self.pixelDensity + "px"); - } - - }); - - // NOTE: Modified from polymer implementation - canvas.setAttribute('width', bounds.width * this.pixelDensity + "px"); - canvas.setAttribute('height', bounds.height * this.pixelDensity + "px"); - - function sanitizePosition(style) { - var val = style.replace('px', ''); - return val; - } - - return canvas; - } - - }; - - // Return class reference - - return Ripple; - })(); - - - - - // ********************************************************** - // Private Wave Methods - // ********************************************************** - - /** - * - */ - function waveRadiusFn(touchDownMs, touchUpMs, anim) { - // Convert from ms to s. - var waveMaxRadius = 150; - var touchDown = touchDownMs / 1000; - var touchUp = touchUpMs / 1000; - var totalElapsed = touchDown + touchUp; - var ww = anim.width, hh = anim.height; - // use diagonal size of container to avoid floating point math sadness - var waveRadius = Math.min(Math.sqrt(ww * ww + hh * hh), waveMaxRadius) * 1.1 + 5; - var duration = 1.1 - .2 * (waveRadius / waveMaxRadius); - var tt = (totalElapsed / duration); - - var size = waveRadius * (1 - Math.pow(80, -tt)); - return Math.abs(size); - } - - /** - * - */ - function waveOpacityFn(td, tu, anim) { - // Convert from ms to s. - var touchDown = td / 1000; - var touchUp = tu / 1000; - - return (tu <= 0) ? anim.initialOpacity : Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity); - } - - /** - * - */ - function waveOuterOpacityFn(td, tu, anim) { - // Convert from ms to s. - var touchDown = td / 1000; - var touchUp = tu / 1000; - - // Linear increase in background opacity, capped at the opacity - // of the wavefront (waveOpacity). - var outerOpacity = touchDown * 0.3; - var waveOpacity = waveOpacityFn(td, tu, anim); - return Math.max(0, Math.min(outerOpacity, waveOpacity)); - } - - - /** - * Determines whether the wave should be completely removed. - */ - function waveDidFinish(wave, radius, anim) { - var waveMaxRadius = 150; - var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim); - // If the wave opacity is 0 and the radius exceeds the bounds - // of the element, then this is finished. - if (waveOpacity < 0.01 && radius >= Math.min(wave.maxRadius, waveMaxRadius)) { - return true; - } - return false; - }; - - /** - * - */ - function waveAtMaximum(wave, radius, anim) { - var waveMaxRadius = 150; - var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim); - if (waveOpacity >= anim.initialOpacity && radius >= Math.min(wave.maxRadius, waveMaxRadius)) { - return true; - } - return false; - } - - /** - * Wave is created on mouseDown - */ - function createWave(elem, width, height, startAt, recenter ) { - var wave = { - startPosition : startAt, - containerSize : Math.max(width, height), - waveColor: window.getComputedStyle(elem).color, - maxRadius : distanceFromPointToFurthestCorner(startAt, {w: width, h: height}) * 0.75, - - isMouseDown : true, - mouseDownStart : now(), - mouseUpStart : 0.0, - - tDown : 0.0, - tUp : 0.0 - }; - - if ( recenter ) { - wave.endPosition = {x: width / 2, y: height / 2}; - wave.slideDistance = dist(wave.startPosition, wave.endPosition); - } - - return wave; - } - - /** - * - */ - function removeWave(wave, buffer) { - if (buffer && buffer.length) { - var pos = buffer.indexOf(wave); - buffer.splice(pos, 1); - } - } - - function drawWave ( wave, anim ) { - - // Calculate waveColor and alphas; if we do a background - // fill fade too, work out the correct color. - - anim.waveColor = cssColorWithAlpha( - wave.waveColor, - waveOpacityFn(wave.tDown, wave.tUp, anim) - ); - - if ( anim.backgroundFill ) { - anim.backgroundFill = cssColorWithAlpha( - wave.waveColor, - waveOuterOpacityFn(wave.tDown, wave.tUp, anim) - ); - } - - // Position of the ripple. - var x = wave.startPosition.x; - var y = wave.startPosition.y; - - // Ripple gravitational pull to the center of the canvas. - if ( wave.endPosition ) { - - // This translates from the origin to the center of the view based on the max dimension of - var translateFraction = Math.min(1, anim.radius / wave.containerSize * 2 / Math.sqrt(2)); - - x += translateFraction * (wave.endPosition.x - wave.startPosition.x); - y += translateFraction * (wave.endPosition.y - wave.startPosition.y); - } - - // Draw the ripple. - renderRipple(anim.ctx, x, y, anim.radius, anim.waveColor, anim.backgroundFill); - - // Render the ripple on the target canvas 2-D context - function renderRipple(ctx, x, y, radius, innerColor, outerColor) { - if (outerColor) { - ctx.fillStyle = outerColor || 'rgba(252, 252, 158, 1.0)'; - ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height); - } - ctx.beginPath(); - - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.fillStyle = innerColor || 'rgba(252, 252, 158, 1.0)'; - ctx.fill(); - - ctx.closePath(); - } - } - - - /** - * - */ - function cssColorWithAlpha(cssColor, alpha) { - var parts = cssColor ? cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) : null; - if (typeof alpha == 'undefined') { - alpha = 1; - } - if (!parts) { - return 'rgba(255, 255, 255, ' + alpha + ')'; - } - return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')'; - } - - /** - * - */ - function dist(p1, p2) { - return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); - } - - /** - * - */ - function distanceFromPointToFurthestCorner(point, size) { - var tl_d = dist(point, {x: 0, y: 0}); - var tr_d = dist(point, {x: size.w, y: 0}); - var bl_d = dist(point, {x: 0, y: size.h}); - var br_d = dist(point, {x: size.w, y: size.h}); - return Math.max(tl_d, tr_d, bl_d, br_d); - } - -} diff --git a/src/components/animate/effects.js b/src/components/animate/effects.js index 4482b99d0bd..bb2a860eb8a 100644 --- a/src/components/animate/effects.js +++ b/src/components/animate/effects.js @@ -13,7 +13,6 @@ angular.module('material.animations', [ ]) .service('$materialEffects', [ '$animateSequence', - '$ripple', '$rootElement', '$position', '$$rAF', @@ -28,55 +27,52 @@ angular.module('material.animations', [ * * @description * The `$materialEffects` service provides a simple API for various - * Material Design effects: - * - * 1) to animate ink bars and ripple effects, and - * 2) to perform popup open/close effects on any DOM element. + * Material Design effects. * * @returns A `$materialEffects` object with the following properties: - * - `{function(canvas,options)}` `inkRipple` - Renders ripple ink - * waves on the specified canvas * - `{function(element,styles,duration)}` `inkBar` - starts ink bar * animation on specified DOM element * - `{function(element,parentElement,clickElement)}` `popIn` - animated show of element overlayed on parent element * - `{function(element,parentElement)}` `popOut` - animated close of popup overlay * */ -function MaterialEffects($animateSequence, $ripple, $rootElement, $position, $$rAF, $sniffer) { +function MaterialEffects($animateSequence, $rootElement, $position, $$rAF, $sniffer) { var styler = angular.isDefined( $rootElement[0].animate ) ? 'webAnimations' : angular.isDefined( window['TweenMax'] || window['TweenLite'] ) ? 'gsap' : angular.isDefined( window['jQuery'] ) ? 'jQuery' : 'default'; - - var isWebkit = /webkit/i.test($sniffer.vendorPrefix); - var TRANSFORM_PROPERTY = isWebkit ? 'webkitTransform' : 'transform'; - var TRANSITIONEND_EVENT = 'transitionend' + - (isWebkit ? ' webkitTransitionEnd' : ''); + var webkit = /webkit/i.test($sniffer.vendorPrefix); + function vendorProperty(name) { + return webkit ? + ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : + name; + } + var self; // Publish API for effects... - return { - inkRipple: animateInkRipple, + return self = { popIn: popIn, popOut: popOut, /* Constants */ - TRANSFORM_PROPERTY: TRANSFORM_PROPERTY, - TRANSITIONEND_EVENT: TRANSITIONEND_EVENT + TRANSITIONEND_EVENT: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''), + ANIMATIONEND_EVENT: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''), + + TRANSFORM: vendorProperty('transform'), + TRANSITION: vendorProperty('transition'), + TRANSITION_DURATION: vendorProperty('transitionDuration'), + ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'), + ANIMATION_DURATION: vendorProperty('animationDuration'), + ANIMATION_NAME: vendorProperty('animationName'), + ANIMATION_TIMING: vendorProperty('animationTimingFunction'), + ANIMATION_DIRECTION: vendorProperty('animationDirection') }; // ********************************************************** // API Methods // ********************************************************** - /** - * Use the canvas animator to render the ripple effect(s). - */ - function animateInkRipple( canvas, options ) - { - return new $ripple(canvas, options); - } - /** * */ @@ -96,23 +92,23 @@ function MaterialEffects($animateSequence, $ripple, $rootElement, $position, $$r } element - .css(TRANSFORM_PROPERTY, startPos) + .css(self.TRANSFORM, startPos) .css('opacity', 0); $$rAF(function() { $$rAF(function() { element .addClass('active') - .css(TRANSFORM_PROPERTY, '') + .css(self.TRANSFORM, '') .css('opacity', '') - .on(TRANSITIONEND_EVENT, finished); + .on(self.TRANSITIONEND_EVENT, finished); }); }); function finished(ev) { //Make sure this transitionend didn't bubble up from a child if (ev.target === element[0]) { - element.off(TRANSITIONEND_EVENT, finished); + element.off(self.TRANSITIONEND_EVENT, finished); (done || angular.noop)(); } } @@ -129,12 +125,12 @@ function MaterialEffects($animateSequence, $ripple, $rootElement, $position, $$r '-webkit-transform': translateString(endPos.left, endPos.top, 0) + ' scale(0.5)', opacity: 0 }); - element.on(TRANSITIONEND_EVENT, finished); + element.on(self.TRANSITIONEND_EVENT, finished); function finished(ev) { //Make sure this transitionend didn't bubble up from a child if (ev.target === element[0]) { - element.off(TRANSITIONEND_EVENT, finished); + element.off(self.TRANSITIONEND_EVENT, finished); (done || angular.noop)(); } } diff --git a/src/components/animate/inkCssRipple.js b/src/components/animate/inkCssRipple.js new file mode 100644 index 00000000000..09a3184b164 --- /dev/null +++ b/src/components/animate/inkCssRipple.js @@ -0,0 +1,183 @@ + +angular.module('material.animations') + +.directive('inkRipple', [ + '$materialInkRipple', + InkRippleDirective +]) + +.factory('$materialInkRipple', [ + '$window', + '$$rAF', + '$materialEffects', + '$timeout', + InkRippleService +]); + +function InkRippleDirective($materialInkRipple) { + return function(scope, element, attr) { + if (attr.inkRipple == 'checkbox') { + $materialInkRipple.attachCheckboxBehavior(element); + } else { + $materialInkRipple.attachButtonBehavior(element); + } + }; +} + +function InkRippleService($window, $$rAF, $materialEffects, $timeout) { + + return { + attachButtonBehavior: attachButtonBehavior, + attachCheckboxBehavior: attachCheckboxBehavior, + attach: attach, + }; + + function attachButtonBehavior(element) { + return attach(element, { + mousedown: true, + center: false, + animationDuration: 350, + mousedownPauseTime: 175, + animationName: 'inkRippleButton', + animationTimingFunction: 'linear' + }); + } + + function attachCheckboxBehavior(element) { + return attach(element, { + mousedown: true, + center: true, + animationDuration: 300, + mousedownPauseTime: 180, + animationName: 'inkRippleCheckbox', + animationTimingFunction: 'linear' + }); + } + + function attach(element, options) { + options = angular.extend({ + mousedown: true, + hover: true, + focus: true, + center: false, + animationDuration: 300, + mousedownPauseTime: 150, + animationName: '', + animationTimingFunction: 'linear' + }, options || {}); + + var rippleContainer; + var node = element[0]; + + if (options.mousedown) { + enableMousedown(); + } + + function rippleIsAllowed() { + return !element.controller('noink') && !Util.isDisabled(element); + } + + var hasTouch = !!('ontouchend' in document); + function enableMousedown() { + // TODO fix this. doesn't support touch AND click devices (eg chrome pixel) + var POINTERDOWN_EVENT = hasTouch ? 'touchstart' : 'mousedown'; + var POINTERUP_EVENT = hasTouch ? 'touchend touchcancel' : 'mouseup mouseleave'; + + if (!rippleIsAllowed()) return; + + element.on(POINTERDOWN_EVENT, onPointerDown); + + var pointerIsDown; + function onPointerDown(ev) { + if (pointerIsDown) return; + pointerIsDown = true; + + element.one(POINTERUP_EVENT, function() { + pointerIsDown = false; + }); + + var rippleEl = createRippleFromEvent(ev); + + var pointerCheckTimeout = $timeout( + pauseRippleIfPointerDown, + options.mousedownPauseTime, + false + ); + + rippleEl.on('$destroy', cancelPointerCheck); + + function pauseRippleIfPointerDown() { + if (pointerIsDown) { + rippleEl.css($materialEffects.ANIMATION_PLAY_STATE, 'paused'); + element.one(POINTERUP_EVENT, function() { + rippleEl.css($materialEffects.ANIMATION_PLAY_STATE, 'running'); + }); + } + } + function cancelPointerCheck() { + $timeout.cancel(pointerCheckTimeout); + } + } + } + + function createRippleFromEvent(ev) { + ev = ev.touches ? ev.touches[0] : ev; + return createRipple(ev.pageX, ev.pageY, true); + } + function createRipple(left, top, positionsAreAbsolute) { + var elementRect = node.getBoundingClientRect(); + var elementStyle = $window.getComputedStyle(node); + var finalSize = elementRect.width; + + var rippleEl = angular.element('
') + .css( + $materialEffects.ANIMATION_DURATION, + options.animationDuration + 'ms' + ) + .css( + $materialEffects.ANIMATION_NAME, + options.animationName + ) + .css( + $materialEffects.ANIMATION_TIMING, + options.animationTimingFunction + ) + .on($materialEffects.ANIMATIONEND_EVENT, function() { + rippleEl.remove(); + }); + + if (!rippleContainer) { + rippleContainer = angular.element('
'); + element.append(rippleContainer); + } + rippleContainer.append(rippleEl); + + if (options.center) { + left = rippleContainer.prop('offsetWidth') / 2; + top = rippleContainer.prop('offsetHeight') / 2; + } else if (positionsAreAbsolute) { + left -= elementRect.left; + top -= elementRect.top; + } + + var rippleStyle = $window.getComputedStyle(rippleEl[0]); + + // TODO don't use px, make setRippleCss fix that + var css = { + 'background-color': rippleStyle.color || elementStyle.color, + 'border-radius': (finalSize / 2) + 'px', + + left: (left - finalSize / 2) + 'px', + width: finalSize + 'px', + + top: (top - finalSize / 2) + 'px', + height: finalSize + 'px', + }; + css[$materialEffects.ANIMATION_DURATION] = options.fadeoutDuration + 'ms'; + rippleEl.css(css); + + return rippleEl; + } + } + +} diff --git a/src/components/animate/inkRipple.js b/src/components/animate/inkRipple.js deleted file mode 100644 index a7fc62d304b..00000000000 --- a/src/components/animate/inkRipple.js +++ /dev/null @@ -1,183 +0,0 @@ -(function() { - - angular.module('material.animations') - .directive('materialRipple', [ - '$materialEffects', - '$interpolate', - '$throttle', - MaterialRippleDirective - ]); - - /** - * @ngdoc directive - * @name materialRipple - * @module material.components.animate - * - * @restrict E - * - * @description - * The `` directive implements the Material Design ripple ink effects within a specified - * parent container. - * - * @param {string=} start Indicates where the wave ripples should originate in the parent container area. - * 'center' will force the ripples to always originate in the horizontal and vertical. - * @param {number=} initial-opacity Value indicates the initial opacity of each ripple wave - * @param {number=} opacity-decay-velocity Value indicates the speed at which each wave will fade out - * - * @usage - * ``` - * - * - * - * ``` - */ - function MaterialRippleDirective($materialEffects, $interpolate, $throttle) { - return { - restrict: 'E', - require: '^?noink', - compile: compileWithCanvas - }; - - /** - * Use Javascript and Canvas to render ripple effects - * - * Note: attribute start="" has two (2) options: `center` || `pointer`; which - * defines the start of the ripple center. - * - * @param element - * @returns {Function} - */ - function compileWithCanvas( element, attrs ) { - var RIGHT_BUTTON = 2; - - var options = calculateOptions(element, attrs); - var tag = - '' + - ''; - - element.replaceWith( - angular.element( $interpolate(tag)(options) ) - ); - - return function postLink( scope, element, attrs, noinkCtrl ) { - if ( noinkCtrl ) return; - - var ripple, watchMouse, - parent = element.parent(), - makeRipple = $throttle({ - start : function() { - ripple = ripple || $materialEffects.inkRipple( element[0], options ); - watchMouse = watchMouse || buildMouseWatcher(parent, makeRipple); - - // Ripples start with left mouseDowns (or taps) - parent.on('mousedown', makeRipple); - }, - throttle : function(e, done) { - if ( !Util.isDisabled(element) ) { - switch(e.type) { - case 'mousedown' : - // If not right- or ctrl-click... - if (!e.ctrlKey && (e.button !== RIGHT_BUTTON)) - { - watchMouse(true); - ripple.createAt( options.forceToCenter ? null : localToCanvas(e) ); - } - break; - - default: - watchMouse(false); - - // Draw of each wave/ripple in the ink only occurs - // on mouseup/mouseleave - - ripple.draw( done ); - break; - } - } else { - done(); - } - }, - end : function() { - watchMouse(false); - } - })(); - - - // ********************************************************** - // Utility Methods - // ********************************************************** - - /** - * Build mouse event listeners for the specified element - * @param element Angular element that will listen for bubbling mouseEvents - * @param handlerFn Function to be invoked with the mouse event - * @returns {Function} - */ - function buildMouseWatcher(element, handlerFn) { - // Return function to easily toggle on/off listeners - return function watchMouse(active) { - angular.forEach("mouseup,mouseleave".split(","), function(eventType) { - var fn = active ? element.on : element.off; - fn.apply(element, [eventType, handlerFn]); - }); - }; - } - - /** - * Convert the mouse down coordinates from `parent` relative - * to `canvas` relative; needed since the event listener is on - * the parent [e.g. tab element] - */ - function localToCanvas(e) - { - var canvas = element[0].getBoundingClientRect(); - - return { - x : e.clientX - canvas.left, - y : e.clientY - canvas.top - }; - } - - }; - - function calculateOptions(element, attrs) - { - return angular.extend( getBounds(element), { - classList : (attrs.class || ""), - forceToCenter : (attrs.start == "center"), - initialOpacity : getFloatValue( attrs, "initialOpacity" ), - opacityDecayVelocity : getFloatValue( attrs, "opacityDecayVelocity" ) - }); - - function getBounds(element) { - var node = element[0]; - var styles = node.ownerDocument.defaultView.getComputedStyle( node, null ) || { }; - - return { - left : (styles.left == "auto" || !styles.left) ? "0px" : styles.left, - top : (styles.top == "auto" || !styles.top) ? "0px" : styles.top, - width : getValue( styles, "width" ), - height : getValue( styles, "height" ) - }; - } - - function getFloatValue( map, key, defaultVal ) - { - return angular.isDefined( map[key] ) ? +map[key] : defaultVal; - } - - function getValue( map, key, defaultVal ) - { - var val = map[key]; - return (angular.isDefined( val ) && (val !== "")) ? map[key] : defaultVal; - } - } - - } - - } - - -})(); diff --git a/src/components/buttons/_buttons.scss b/src/components/buttons/_buttons.scss index 23f0a8cf4f2..7a36b7a7933 100644 --- a/src/components/buttons/_buttons.scss +++ b/src/components/buttons/_buttons.scss @@ -14,16 +14,15 @@ material-button { @include user-select(none); - display: inline-block; position: relative; //for child absolute-positioned - padding: 4px; + display: inline-block; outline: none; border: 0; + padding: 0; + margin: 0; border-radius: $button-border-radius; - background-color: transparent; - background-image: none; text-align: center; // Always uppercase buttons @@ -38,25 +37,17 @@ material-button { cursor: pointer; overflow: hidden; // for ink containment - &.material-button-colored { - color: $theme-light; - fill: $theme-light; - } - - &.material-button-cornered { - border-radius: 0; - } - - &:hover, - &:focus { - background-color: $button-hover-background; - } - // reserved for inner buttons and inner hrefs .material-button-inner { + display: block; + left: 0; + top: 0; + width: 100%; + height: 100%; background: transparent; border: none; color: inherit; + outline: none; text-transform: inherit; font-weight: inherit; font-style: inherit; @@ -64,8 +55,31 @@ material-button { font-size: inherit; font-family: inherit; line-height: inherit; - margin: 0; - padding: 0; + padding: 4px; + } + + // If we have a href and an inner anchor, let the clicks pass through to the anchor + &[href] { + pointer-events: none; + .material-button-inner { + pointer-events: auto; + } + } + + + &.material-button-colored { + color: $theme-light; + fill: $theme-light; + } + + &.material-button-cornered { + border-radius: 0; + } + + &:hover:not([disabled]), + // Uses a .focus class because the child button/a is getting the actual focus + &.focus { + background-color: $button-hover-background; } &[disabled] { @@ -82,10 +96,16 @@ material-button { &.material-button-raised { background-color: $button-raised-background; + -webkit-transition: 0.2s linear; + -webkit-transition-property: box-shadow, -webkit-transform, background-color; + transition: 0.2s linear transform; + transition-property: box-shadow, transform, background-color; + @include transform-translate3d(0, 0, 0); + @extend .material-shadow-bottom-z-1; - &:hover, - &:focus { + &:hover:not([disabled]), + &.focus { background-color: $button-raised-hover-background; } @@ -93,8 +113,8 @@ material-button { background-color: $button-raised-colored-background; color: $button-raised-colored-color; - &:hover, - &:focus { + &:hover:not([disabled]), + &.focus { background-color: $button-raised-colored-hover-background; } } @@ -108,21 +128,36 @@ material-button { @include fab-position(top-left, $button-fab-height/2, auto, auto, $button-fab-width/2); z-index: $z-index-fab; - padding: $button-fab-padding; width: $button-fab-width; height: $button-fab-height; border-radius: $button-fab-border-radius; - box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.258824); + @extend .material-shadow-bottom-z-1; border-radius: 50%; overflow: hidden; @include transform-translate3d(0,0,0); - -webkit-transition: -webkit-transform 0.2s linear; + -webkit-transition: 0.2s linear; + -webkit-transition-property: -webkit-transform, box-shadow; transition: transform 0.2s linear; + transition-property: transform, box-shadow; + + .material-button-inner { + @include flex-display(); + @include flex-align-items(center); + @include flex-justify-content(center); + } + } + &.material-button-raised, + &.material-button-fab { + &.focus, + &:hover:not([disabled]) { + @include transform-translate3d(0, -1px, 0); + @extend .material-shadow-bottom-z-2; + } } } @@ -130,12 +165,20 @@ material-button { material-button.material-button-fab-top-left, material-button.material-button-fab-top-right { @include transform-translate3d(0, $button-fab-toast-offset, 0); + &.focus, + &:hover:not([disabled]) { + @include transform-translate3d(0, $button-fab-toast-offset - 1, 0); + } } } .material-toast-open-bottom { material-button.material-button-fab-bottom-left, material-button.material-button-fab-bottom-right { @include transform-translate3d(0, -$button-fab-toast-offset, 0); + &.focus, + &:hover { + @include transform-translate3d(0, -$button-fab-toast-offset - 1, 0); + } } } diff --git a/src/components/buttons/buttons.js b/src/components/buttons/buttons.js index c97e89a79b7..c00d86b98e2 100644 --- a/src/components/buttons/buttons.js +++ b/src/components/buttons/buttons.js @@ -12,6 +12,7 @@ angular.module('material.components.button', [ .directive('materialButton', [ 'ngHrefDirective', '$expectAria', + '$materialInkRipple', MaterialButtonDirective ]); @@ -45,21 +46,20 @@ angular.module('material.components.button', [ * * */ -function MaterialButtonDirective(ngHrefDirectives, $expectAria) { +function MaterialButtonDirective(ngHrefDirectives, $expectAria, $materialInkRipple) { var ngHrefDirective = ngHrefDirectives[0]; return { restrict: 'E', compile: function(element, attr) { + var innerElement; + var attributesToCopy; // Add an inner anchor if the element has a `href` or `ngHref` attribute, // so this element can be clicked like a normal ``. - var innerElement; - var attributesToCopy; if (attr.ngHref || attr.href) { innerElement = angular.element(''); attributesToCopy = ['ng-href', 'href', 'rel', 'target']; - // Otherwise, just add an inner button element (for form submission etc) } else { innerElement = angular.element('