From 0948ed37b308ae62c89c86b2d3b7b61219ab68bd Mon Sep 17 00:00:00 2001 From: "cristobalchao@google.com" Date: Tue, 24 Jan 2017 16:35:10 -0800 Subject: [PATCH] feat(ripple): Implement new ripple sizing requirements Resolves #187 --- demos/ripple.html | 2 +- packages/mdc-ripple/_keyframes.scss | 6 +- packages/mdc-ripple/_mixins.scss | 16 ++-- packages/mdc-ripple/constants.js | 5 +- packages/mdc-ripple/foundation.js | 44 +++++----- .../mdc-ripple/foundation-activation.test.js | 52 +----------- .../foundation-deactivation.test.js | 10 ++- test/unit/mdc-ripple/foundation.test.js | 85 ++++++++++++++----- 8 files changed, 105 insertions(+), 115 deletions(-) diff --git a/demos/ripple.html b/demos/ripple.html index a5d88682e53..d575f794369 100644 --- a/demos/ripple.html +++ b/demos/ripple.html @@ -40,7 +40,7 @@ -webkit-user-select: none; } - .demo-surface[data-mdc-ripple-is-unbounded] { + .mdc-ripple-surface[data-mdc-ripple-is-unbounded] { width: 40px; height: 40px; padding: 0; diff --git a/packages/mdc-ripple/_keyframes.scss b/packages/mdc-ripple/_keyframes.scss index a8a86bee22d..1c5c2dead64 100644 --- a/packages/mdc-ripple/_keyframes.scss +++ b/packages/mdc-ripple/_keyframes.scss @@ -20,12 +20,12 @@ @keyframes mdc-ripple-fg-radius-in { from { - transform: scale(0); + transform: scale(1); animation-timing-function: $mdc-ripple-easing-fn; } to { - transform: scale(1.5); + transform: scale(var(--mdc-ripple-fg-scale)); } } @@ -56,6 +56,6 @@ } to { - transform: scale(1.01); + transform: scale(var(--mdc-ripple-fg-scale)); } } diff --git a/packages/mdc-ripple/_mixins.scss b/packages/mdc-ripple/_mixins.scss index b1c17142683..5db22808cee 100644 --- a/packages/mdc-ripple/_mixins.scss +++ b/packages/mdc-ripple/_mixins.scss @@ -1,17 +1,17 @@ -// +// // Copyright 2016 Google Inc. All Rights Reserved. -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // http://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// +// @import "@material/theme/variables"; @import "./keyframes"; @@ -78,7 +78,7 @@ left: calc(50% - #{$radius}); width: $radius * 2; height: $radius * 2; - transform: scale(1); + transform: scale(var(--mdc-ripple-fg-scale)); transition: opacity 200ms linear; border-radius: 50%; opacity: 0; @@ -163,7 +163,7 @@ &.mdc-ripple-upgraded--unbounded#{$pseudo} { top: var(--mdc-ripple-top); left: var(--mdc-ripple-left); - transform-origin: var(--mdc-ripple-xfo-x) var(--mdc-ripple-xfo-y); + transform-origin: center center; } &.mdc-ripple-upgraded--foreground-bounded-active-fill#{$pseudo} { @@ -172,7 +172,7 @@ } &.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded--foreground-unbounded-activation#{$pseudo} { - transform: scale(1); + transform: scale(var(--mdc-ripple-fg-scale)); transition: opacity 110ms linear, transform var(--mdc-ripple-fg-unbounded-transform-duration) linear 80ms; diff --git a/packages/mdc-ripple/constants.js b/packages/mdc-ripple/constants.js index 16ab14d4c09..f3d7361b0bf 100644 --- a/packages/mdc-ripple/constants.js +++ b/packages/mdc-ripple/constants.js @@ -38,9 +38,8 @@ export const strings = { VAR_FG_UNBOUNDED_TRANSFORM_DURATION: `--${ROOT}-fg-unbounded-transform-duration`, VAR_LEFT: `--${ROOT}-left`, VAR_TOP: `--${ROOT}-top`, - VAR_XF_ORIGIN_X: `--${ROOT}-xfo-x`, - VAR_XF_ORIGIN_Y: `--${ROOT}-xfo-y`, VAR_FG_APPROX_XF: `--${ROOT}-fg-approx-xf`, + VAR_FG_SCALE: `--${ROOT}-fg-scale`, }; export const numbers = { @@ -49,4 +48,6 @@ export const numbers = { ACTIVE_OPACITY_DURATION_MS: 110, MIN_OPACITY_DURATION_MS: 200, UNBOUNDED_TRANSFORM_DURATION_MS: 200, + PADDING: 10, + INITIAL_ORIGIN_SCALE: 0.6, }; diff --git a/packages/mdc-ripple/foundation.js b/packages/mdc-ripple/foundation.js index f4a30446bba..50ee55727ab 100644 --- a/packages/mdc-ripple/foundation.js +++ b/packages/mdc-ripple/foundation.js @@ -72,7 +72,8 @@ export default class MDCRippleFoundation extends MDCFoundation { this.frame_ = {width: 0, height: 0}; this.activationState_ = this.defaultActivationState_(); this.xfDuration_ = 0; - this.maxRadius = 0; + this.initialSize_ = 0; + this.maxRadius_ = 0; this.listenerInfos_ = [ {activate: 'touchstart', deactivate: 'touchend'}, {activate: 'pointerdown', deactivate: 'pointerup'}, @@ -99,6 +100,7 @@ export default class MDCRippleFoundation extends MDCFoundation { left: 0, top: 0, }; + this.fgScale_ = 0; } defaultActivationState_() { @@ -193,22 +195,6 @@ export default class MDCRippleFoundation extends MDCFoundation { animateUnboundedActivation_() { const {FG_UNBOUNDED_ACTIVATION} = MDCRippleFoundation.cssClasses; - let startPoint; - if (this.activationState_.wasActivatedByPointer) { - startPoint = getNormalizedEventCoords( - this.activationState_.activationEvent, this.adapter_.getWindowPageOffset(), - this.adapter_.computeBoundingRect() - ); - } else { - startPoint = { - left: this.frame_.width / 2, - top: this.frame_.height / 2, - }; - } - const {left, top} = startPoint; - const {VAR_XF_ORIGIN_X, VAR_XF_ORIGIN_Y} = MDCRippleFoundation.strings; - this.adapter_.updateCssVariable(VAR_XF_ORIGIN_X, `${left - this.unboundedCoords_.left}px`); - this.adapter_.updateCssVariable(VAR_XF_ORIGIN_Y, `${top - this.unboundedCoords_.top}px`); this.adapter_.addClass(FG_UNBOUNDED_ACTIVATION); } @@ -285,7 +271,8 @@ export default class MDCRippleFoundation extends MDCFoundation { let approxCurScale = 0; if (msElapsed > FG_TRANSFORM_DELAY_MS) { - approxCurScale = Math.min((msElapsed - FG_TRANSFORM_DELAY_MS) / this.xfDuration_, 1); + const percentComplete = Math.min((msElapsed - FG_TRANSFORM_DELAY_MS) / this.xfDuration_, 1); + approxCurScale = percentComplete * this.fgScale_; } const transformDuration = UNBOUNDED_TRANSFORM_DURATION_MS; @@ -368,30 +355,37 @@ export default class MDCRippleFoundation extends MDCFoundation { this.frame_ = this.adapter_.computeBoundingRect(); const maxDim = Math.max(this.frame_.height, this.frame_.width); + const surfaceDiameter = Math.sqrt(Math.pow(this.frame_.width, 2) + Math.pow(this.frame_.height, 2)); - // Sqrt(2) * square length == diameter - this.maxRadius_ = Math.sqrt(2) * maxDim / 2; + // 60% of the largest dimension of the surface + this.initialSize_ = maxDim * MDCRippleFoundation.numbers.INITIAL_ORIGIN_SCALE; + + // Diameter of the surface + 10px + this.maxRadius_ = surfaceDiameter + MDCRippleFoundation.numbers.PADDING; + this.fgScale_ = this.maxRadius_ / this.initialSize_; this.xfDuration_ = 1000 * Math.sqrt(this.maxRadius_ / 1024); this.updateLayoutCssVars_(); } updateLayoutCssVars_() { - const fgSize = this.maxRadius_ * 2; const { VAR_SURFACE_WIDTH, VAR_SURFACE_HEIGHT, VAR_FG_SIZE, - VAR_FG_UNBOUNDED_TRANSFORM_DURATION, VAR_LEFT, VAR_TOP, + VAR_FG_UNBOUNDED_TRANSFORM_DURATION, VAR_LEFT, VAR_TOP, VAR_FG_SCALE, } = MDCRippleFoundation.strings; this.adapter_.updateCssVariable(VAR_SURFACE_WIDTH, `${this.frame_.width}px`); this.adapter_.updateCssVariable(VAR_SURFACE_HEIGHT, `${this.frame_.height}px`); - this.adapter_.updateCssVariable(VAR_FG_SIZE, `${fgSize}px`); + this.adapter_.updateCssVariable(VAR_FG_SIZE, `${this.initialSize_}px`); this.adapter_.updateCssVariable(VAR_FG_UNBOUNDED_TRANSFORM_DURATION, `${this.xfDuration_}ms`); + this.adapter_.updateCssVariable(VAR_FG_SCALE, this.fgScale_); if (this.adapter_.isUnbounded()) { this.unboundedCoords_ = { - left: Math.round(-(fgSize / 2) + (this.frame_.width / 2)), - top: Math.round(-(fgSize / 2) + (this.frame_.height / 2)), + left: Math.round((this.frame_.width / 2) - (this.initialSize_ / 2)), + top: Math.round((this.frame_.height / 2) - (this.initialSize_ / 2)), }; + + this.adapter_.updateCssVariable(VAR_LEFT, `${this.unboundedCoords_.left}px`); this.adapter_.updateCssVariable(VAR_TOP, `${this.unboundedCoords_.top}px`); } diff --git a/test/unit/mdc-ripple/foundation-activation.test.js b/test/unit/mdc-ripple/foundation-activation.test.js index f3cc0758502..3a13985c908 100644 --- a/test/unit/mdc-ripple/foundation-activation.test.js +++ b/test/unit/mdc-ripple/foundation-activation.test.js @@ -17,7 +17,7 @@ import td from 'testdouble'; import {testFoundation, captureHandlers} from './helpers'; -import {cssClasses, strings} from '../../../packages/mdc-ripple/constants'; +import {cssClasses} from '../../../packages/mdc-ripple/constants'; testFoundation(`adds ${cssClasses.BG_ACTIVE} on mousedown`, (t) => { const {foundation, adapter, mockRaf} = t.data; @@ -131,53 +131,3 @@ testFoundation('displays the foreground ripple on activation when unbounded', (t t.end(); }); -testFoundation('sets unbounded FG xf origin to the coords within surface on pointer activation, ' + - 'accounting for FG ripple offset', (t) => { - const {foundation, adapter, mockRaf} = t.data; - const handlers = captureHandlers(adapter); - td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 100, left: 50, top: 50}); - td.when(adapter.isUnbounded()).thenReturn(true); - foundation.init(); - mockRaf.flush(); - - handlers.mousedown({pageX: 100, pageY: 75}); - mockRaf.flush(); - - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_XF_ORIGIN_X, '71px'))); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_XF_ORIGIN_Y, '46px'))); - t.end(); -}); - -testFoundation('takes scroll offset into account when computing transform origin', (t) => { - const {foundation, adapter, mockRaf} = t.data; - const handlers = captureHandlers(adapter); - td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 100, left: 25, top: 25}); - td.when(adapter.getWindowPageOffset()).thenReturn({x: 25, y: 25}); - td.when(adapter.isUnbounded()).thenReturn(true); - foundation.init(); - mockRaf.flush(); - - handlers.mousedown({pageX: 100, pageY: 75}); - mockRaf.flush(); - - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_XF_ORIGIN_X, '71px'))); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_XF_ORIGIN_Y, '46px'))); - t.end(); -}); - -testFoundation('sets unbounded FG xf origin to center on non-pointer activation', (t) => { - const {foundation, adapter, mockRaf} = t.data; - const handlers = captureHandlers(adapter); - td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 100, left: 50, top: 50}); - td.when(adapter.isUnbounded()).thenReturn(true); - td.when(adapter.isSurfaceActive()).thenReturn(true); - foundation.init(); - mockRaf.flush(); - - handlers.keydown(); - mockRaf.flush(); - - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_XF_ORIGIN_X, '71px'))); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_XF_ORIGIN_Y, '71px'))); - t.end(); -}); diff --git a/test/unit/mdc-ripple/foundation-deactivation.test.js b/test/unit/mdc-ripple/foundation-deactivation.test.js index eff94cc67d6..b59a1debe73 100644 --- a/test/unit/mdc-ripple/foundation-deactivation.test.js +++ b/test/unit/mdc-ripple/foundation-deactivation.test.js @@ -195,8 +195,9 @@ testFoundation('triggers unbounded deactivation based on time it took to activat const clock = lolex.install(); const {foundation, adapter, mockRaf} = t.data; const handlers = captureHandlers(adapter); + const size = 100; td.when(adapter.isUnbounded()).thenReturn(true); - td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 100, left: 0, top: 0}); + td.when(adapter.computeBoundingRect()).thenReturn({width: size, height: size, left: 0, top: 0}); foundation.init(); mockRaf.flush(); @@ -210,10 +211,13 @@ testFoundation('triggers unbounded deactivation based on time it took to activat handlers.mouseup(); mockRaf.flush(); - const maxRadius = Math.sqrt(2) * 50; + const surfaceDiameter = Math.sqrt(Math.pow(size, 2) + Math.pow(size, 2)); + const initialSize = size * numbers.INITIAL_ORIGIN_SCALE; + const maxRadius = surfaceDiameter + numbers.PADDING; + const fgScale = maxRadius / initialSize; const xfDuration = 1000 * Math.sqrt(maxRadius / 1024); - const scaleVal = baseElapsedTime / xfDuration; + const scaleVal = baseElapsedTime / xfDuration * fgScale; t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_FG_APPROX_XF, `scale(${scaleVal})`))); t.doesNotThrow( () => td.verify( diff --git a/test/unit/mdc-ripple/foundation.test.js b/test/unit/mdc-ripple/foundation.test.js index d8aac24a2a4..ffb7aa0723b 100644 --- a/test/unit/mdc-ripple/foundation.test.js +++ b/test/unit/mdc-ripple/foundation.test.js @@ -112,23 +112,26 @@ testFoundation(`#init sets ${strings.VAR_SURFACE_HEIGHT} css variable to the cli testFoundation(`#init sets ${strings.VAR_FG_SIZE} to the circumscribing circle's diameter`, (t) => { const {foundation, adapter, mockRaf} = t.data; - td.when(adapter.computeBoundingRect()).thenReturn({width: 200, height: 100}); + const size = 200; + td.when(adapter.computeBoundingRect()).thenReturn({width: size, height: size / 2}); foundation.init(); mockRaf.flush(); + const initialSize = size * numbers.INITIAL_ORIGIN_SCALE; - const expectedDiameter = Math.sqrt(2) * 200; - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_FG_SIZE, `${expectedDiameter}px`))); + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_FG_SIZE, `${initialSize}px`))); t.end(); }); testFoundation(`#init sets ${strings.VAR_FG_UNBOUNDED_TRANSFORM_DURATION} based on the max radius`, (t) => { const {foundation, adapter, mockRaf} = t.data; - td.when(adapter.computeBoundingRect()).thenReturn({width: 200, height: 100}); + const width = 200; + const height = 100; + td.when(adapter.computeBoundingRect()).thenReturn({width, height}); foundation.init(); mockRaf.flush(); - const expectedDiameter = Math.sqrt(2) * 200; - const expectedRadius = expectedDiameter / 2; + const expectedDiameter = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); + const expectedRadius = expectedDiameter + numbers.PADDING; const expectedDuration = 1000 * Math.sqrt(expectedRadius / 1024); const {VAR_FG_UNBOUNDED_TRANSFORM_DURATION: expectedCssVar} = strings; t.doesNotThrow(() => td.verify(adapter.updateCssVariable(expectedCssVar, `${expectedDuration}ms`))); @@ -137,15 +140,20 @@ testFoundation(`#init sets ${strings.VAR_FG_UNBOUNDED_TRANSFORM_DURATION} based testFoundation(`#init centers via ${strings.VAR_LEFT} and ${strings.VAR_TOP} when unbounded`, (t) => { const {foundation, adapter, mockRaf} = t.data; - td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 200}); + const width = 200; + const height = 100; + const maxSize = Math.max(width, height); + const initialSize = maxSize * numbers.INITIAL_ORIGIN_SCALE; + + td.when(adapter.computeBoundingRect()).thenReturn({width, height}); td.when(adapter.isUnbounded()).thenReturn(true); foundation.init(); mockRaf.flush(); - const expectedDiameter = Math.sqrt(2) * 200; - const offset = (expectedDiameter / 2); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_LEFT, `${Math.round(-offset + 50)}px`))); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_TOP, `${Math.round(-offset + 100)}px`))); + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_LEFT, + `${Math.round((width / 2) - (initialSize / 2))}px`))); + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_TOP, + `${Math.round((height / 2) - (initialSize / 2))}px`))); t.end(); }); @@ -254,24 +262,52 @@ testFoundation(`#layout sets ${strings.VAR_SURFACE_HEIGHT} css variable to the c testFoundation(`#layout sets ${strings.VAR_FG_SIZE} to the circumscribing circle's diameter`, (t) => { const {foundation, adapter, mockRaf} = t.data; - td.when(adapter.computeBoundingRect()).thenReturn({width: 200, height: 100}); + const width = 200; + const height = 100; + const maxSize = Math.max(width, height); + const initialSize = maxSize * numbers.INITIAL_ORIGIN_SCALE; + + td.when(adapter.computeBoundingRect()).thenReturn({width, height}); + foundation.layout(); + mockRaf.flush(); + + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_FG_SIZE, `${initialSize}px`))); + t.end(); +}); + +testFoundation(`#layout sets ${strings.VAR_FG_SCALE} based on the difference between the ` + + 'proportion of the max radius and the initial size', (t) => { + const {foundation, adapter, mockRaf} = t.data; + const width = 200; + const height = 100; + + td.when(adapter.computeBoundingRect()).thenReturn({width, height}); foundation.layout(); mockRaf.flush(); - const expectedDiameter = Math.sqrt(2) * 200; - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_FG_SIZE, `${expectedDiameter}px`))); + const maxSize = Math.max(width, height); + const initialSize = maxSize * numbers.INITIAL_ORIGIN_SCALE; + const surfaceDiameter = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); + const maxRadius = surfaceDiameter + numbers.PADDING; + const fgScale = maxRadius / initialSize; + + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_FG_SCALE, fgScale))); t.end(); }); testFoundation(`#layout sets ${strings.VAR_FG_UNBOUNDED_TRANSFORM_DURATION} based on the max radius`, (t) => { const {foundation, adapter, mockRaf} = t.data; - td.when(adapter.computeBoundingRect()).thenReturn({width: 200, height: 100}); + const width = 200; + const height = 100; + + td.when(adapter.computeBoundingRect()).thenReturn({width, height}); foundation.layout(); mockRaf.flush(); - const expectedDiameter = Math.sqrt(2) * 200; - const expectedRadius = expectedDiameter / 2; + const expectedDiameter = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); + const expectedRadius = expectedDiameter + numbers.PADDING; const expectedDuration = 1000 * Math.sqrt(expectedRadius / 1024); + const {VAR_FG_UNBOUNDED_TRANSFORM_DURATION: expectedCssVar} = strings; t.doesNotThrow(() => td.verify(adapter.updateCssVariable(expectedCssVar, `${expectedDuration}ms`))); t.end(); @@ -279,15 +315,20 @@ testFoundation(`#layout sets ${strings.VAR_FG_UNBOUNDED_TRANSFORM_DURATION} base testFoundation(`#layout centers via ${strings.VAR_LEFT} and ${strings.VAR_TOP} when unbounded`, (t) => { const {foundation, adapter, mockRaf} = t.data; - td.when(adapter.computeBoundingRect()).thenReturn({width: 100, height: 200}); + const width = 200; + const height = 100; + const maxSize = Math.max(width, height); + const initialSize = maxSize * numbers.INITIAL_ORIGIN_SCALE; + + td.when(adapter.computeBoundingRect()).thenReturn({width, height}); td.when(adapter.isUnbounded()).thenReturn(true); foundation.layout(); mockRaf.flush(); - const expectedDiameter = Math.sqrt(2) * 200; - const offset = (expectedDiameter / 2); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_LEFT, `${Math.round(-offset + 50)}px`))); - t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_TOP, `${Math.round(-offset + 100)}px`))); + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_LEFT, + `${Math.round((width / 2) - (initialSize / 2))}px`))); + t.doesNotThrow(() => td.verify(adapter.updateCssVariable(strings.VAR_TOP, + `${Math.round((height / 2) - (initialSize / 2))}px`))); t.end(); });