diff --git a/src/components/VBtn/VBtn.ts b/src/components/VBtn/VBtn.ts index bd9bcd25784..8306919b873 100644 --- a/src/components/VBtn/VBtn.ts +++ b/src/components/VBtn/VBtn.ts @@ -5,6 +5,7 @@ import '../../stylus/components/_buttons.styl' import { VNode, VNodeChildren } from 'vue' import { PropValidator } from 'vue/types/options' import mixins from '../../util/mixins' +import { RippleOptions } from '../../directives/ripple' // Components import VProgressCircular from '../VProgressCircular' @@ -43,7 +44,7 @@ export default mixins( outline: Boolean, ripple: { type: [Boolean, Object], - default: true + default: null }, round: Boolean, small: Boolean, @@ -83,6 +84,11 @@ export default mixins( 'v-btn--top': this.top, ...this.themeClasses } + }, + computedRipple (): RippleOptions | boolean { + const defaultRipple = this.icon || this.fab ? { circle: true } : true + if (this.disabled) return false + else return this.ripple !== null ? this.ripple : defaultRipple } }, diff --git a/src/directives/ripple.ts b/src/directives/ripple.ts index 2eb93033a8e..ff543ac032f 100644 --- a/src/directives/ripple.ts +++ b/src/directives/ripple.ts @@ -1,13 +1,42 @@ import { VNodeDirective } from 'vue' -function style (el: HTMLElement, value: string) { +function transform (el: HTMLElement, value: string) { el.style['transform'] = value el.style['webkitTransform'] = value } -interface RippleOptions { +function opacity (el: HTMLElement, value: number) { + el.style['opacity'] = value.toString() +} + +export interface RippleOptions { class?: string center?: boolean + circle?: boolean +} + +const calculate = (e: MouseEvent, el: HTMLElement, value: RippleOptions = {}) => { + const offset = el.getBoundingClientRect() + const localX = e.clientX - offset.left + const localY = e.clientY - offset.top + + let radius = 0 + let scale = 0.3 + if (el._ripple && el._ripple.circle) { + scale = 0.15 + radius = el.clientWidth / 2 + radius = value.center ? radius : radius + Math.sqrt((localX - radius)**2 + (localY - radius)**2) / 4 + } else { + radius = Math.sqrt(el.clientWidth**2 + el.clientHeight**2) / 2 + } + + const centerX = `${(el.clientWidth - (radius * 2)) / 2}px` + const centerY = `${(el.clientHeight - (radius * 2)) / 2}px` + + const x = value.center ? centerX : `${localX - radius}px` + const y = value.center ? centerY : `${localY - radius}px` + + return { radius, scale, x, y, centerX, centerY } } const ripple = { @@ -26,31 +55,33 @@ const ripple = { container.className += ` ${value.class}` } - const size = ( - Math.min(el.clientWidth, el.clientHeight) * - (value.center ? 1 : el.clientWidth / el.clientHeight * 1.6) - ) - const halfSize = size / 2 + const { radius, scale, x, y, centerX, centerY } = calculate(e, el, value) + animation.className = 'v-ripple__animation' - animation.style.width = `${size}px` - animation.style.height = `${size}px` + animation.style.width = `${radius * 2}px` + animation.style.height = animation.style.width el.appendChild(container) const computed = window.getComputedStyle(el) if (computed.position !== 'absolute' && computed.position !== 'fixed') el.style.position = 'relative' - const offset = el.getBoundingClientRect() - const x = value.center ? 0 : e.clientX - offset.left - halfSize - const y = value.center ? 0 : e.clientY - offset.top - halfSize - animation.classList.add('v-ripple__animation--enter') animation.classList.add('v-ripple__animation--visible') - style(animation, `translate(${x}px, ${y}px) scale3d(0.5, 0.5, 0.5)`) + transform(animation, `translate(${x}, ${y}) scale3d(${scale},${scale},${scale})`) + opacity(animation, 0) animation.dataset.activated = String(performance.now()) setTimeout(() => { animation.classList.remove('v-ripple__animation--enter') - style(animation, `translate(${x}px, ${y}px) scale3d(1, 1, 1)`) + animation.classList.add('v-ripple__animation--in') + transform(animation, `translate(${centerX}, ${centerY}) scale3d(1,1,1)`) + opacity(animation, 0.25) + + setTimeout(() => { + animation.classList.remove('v-ripple__animation--in') + animation.classList.add('v-ripple__animation--out') + opacity(animation, 0) + }, 300) }, 0) }, @@ -66,10 +97,10 @@ const ripple = { else animation.dataset.isHiding = 'true' const diff = performance.now() - Number(animation.dataset.activated) - const delay = Math.max(300 - diff, 0) + const delay = Math.max(200 - diff, 0) setTimeout(() => { - animation.classList.remove('v-ripple__animation--visible') + animation.classList.remove('v-ripple__animation--out') setTimeout(() => { const ripples = el.getElementsByClassName('v-ripple__animation') @@ -113,6 +144,9 @@ function updateRipple (el: HTMLElement, binding: VNodeDirective, wasEnabled: boo if (value.class) { el._ripple.class = binding.value.class } + if (value.circle) { + el._ripple.circle = value.circle + } if (enabled && !wasEnabled) { if ('ontouchstart' in window) { el.addEventListener('touchend', rippleHide, false) diff --git a/src/globals.d.ts b/src/globals.d.ts index 33fb530f81d..722e293a87b 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -37,6 +37,7 @@ declare global { enabled?: boolean centered?: boolean class?: string + circle?: boolean } _onScroll?: { callback: EventListenerOrEventListenerObject diff --git a/src/mixins/routable.ts b/src/mixins/routable.ts index 79b6deea1e6..5f71fdae4aa 100644 --- a/src/mixins/routable.ts +++ b/src/mixins/routable.ts @@ -1,7 +1,7 @@ import Vue, { VNodeData } from 'vue' import { PropValidator } from 'vue/types/options' -import Ripple from '../directives/ripple' +import Ripple, { RippleOptions } from '../directives/ripple' export default Vue.extend({ name: 'routable', @@ -28,6 +28,12 @@ export default Vue.extend({ target: String }, + computed: { + computedRipple (): RippleOptions | boolean { + return (this.ripple && !this.disabled) ? this.ripple : false + } + }, + methods: { /* eslint-disable-next-line no-unused-vars */ click (e: MouseEvent): void { /**/ }, @@ -41,7 +47,7 @@ export default Vue.extend({ props: {}, directives: [{ name: 'ripple', - value: (this.ripple && !this.disabled) ? this.ripple : false + value: this.computedRipple }] as any, // TODO [this.to ? 'nativeOn' : 'on']: { ...this.$listeners, diff --git a/src/stylus/components/_ripples.styl b/src/stylus/components/_ripples.styl index dee52198245..4c67ae4ae57 100644 --- a/src/stylus/components/_ripples.styl +++ b/src/stylus/components/_ripples.styl @@ -20,7 +20,6 @@ border-radius: 50% background: currentColor opacity: 0 - transition: $ripple-animation-transition pointer-events: none overflow: hidden will-change: transform, opacity @@ -28,5 +27,8 @@ &--enter transition: none - &--visible - opacity: $ripple-animation-visible-opacity + &--in + transition: $ripple-animation-transition-in + + &--out + transition: $ripple-animation-transition-out diff --git a/src/stylus/components/_selection-controls.styl b/src/stylus/components/_selection-controls.styl index f20c64fb62e..b4f9e239ec9 100644 --- a/src/stylus/components/_selection-controls.styl +++ b/src/stylus/components/_selection-controls.styl @@ -53,6 +53,7 @@ rtl(v-selection-control-rtl, "v-input--selection-controls") user-select: none &__ripple + border-radius: 50% cursor: pointer height: 48px position: absolute @@ -62,7 +63,7 @@ rtl(v-selection-control-rtl, "v-input--selection-controls") top: calc(50% - 24px) &:before - border-radius: 50% + border-radius: inherit bottom: 0 content: '' position: absolute diff --git a/src/stylus/settings/_variables.styl b/src/stylus/settings/_variables.styl index 1a586e79b0b..249d1c4c038 100644 --- a/src/stylus/settings/_variables.styl +++ b/src/stylus/settings/_variables.styl @@ -300,5 +300,6 @@ $text-field-active-label-height := 12px // ============================================================ // Ripple animation -$ripple-animation-transition := .4s $transition.linear-out-slow-in +$ripple-animation-transition-in := transform .25s $transition.fast-out-slow-in, opacity .1s $transition.fast-out-slow-in +$ripple-animation-transition-out := opacity .3s $transition.fast-out-slow-in $ripple-animation-visible-opacity := .15