Skip to content

Instantly share code, notes, and snippets.

@traviskaufman
Last active August 10, 2016 21:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save traviskaufman/4dc363038520fdbbac6ad6d360909ac3 to your computer and use it in GitHub Desktop.
Save traviskaufman/4dc363038520fdbbac6ad6d360909ac3 to your computer and use it in GitHub Desktop.
(Probably Bad) MDL v2 Ripple Prototype (alpha.2)
<section>
<button type="button" class="btn">A Thing</button>
<button type="button">Something else that can be focused</button>
</section>
const MATCHES = [
'matches', 'webkitMatchesSelector', 'msMatchesSelector'
].filter(function(p) {
return p in HTMLElement.prototype;
}).pop();
const SUPPORTS_CUSTOM_PROPS =
window.CSS && typeof window.CSS.supports === 'function' &&
window.CSS.supports('--custom-properties', 'yes');
if (!SUPPORTS_CUSTOM_PROPS) {
console.warn(`Your browser does not support custom properties.
Ripples will not be activated.`);
}
function animateWithClass(el, cls, endEvent = 'transitionend') {
const listener = () => {
el.classList.remove(cls);
el.removeEventListener(endEvent, listener);
};
el.addEventListener(endEvent, listener);
el.classList.add(cls);
}
// TODO: Ripple not as root element, but as surface.
class MDLRipple {
constructor(root) {
this.root = root;
this.frame_ = null;
this.pointerDownRecognized_ = false;
this.nonPointerFocused_ = false;
this.wasElementMadeActive_ = false;
this.listenerInfos_ = [
{
activate: 'mousedown',
deactivate: 'mouseup',
cancel: 'mouseout'
},
{
activate: 'touchstart',
deactivate: 'touchend',
cancel: 'touchcancel'
},
{
activate: 'keydown',
deactivate: 'keyup',
cancel: null
},
{
activate: 'focus',
deactivate: 'blur',
cancel: null
}
];
this.listeners_ = {
activate: (e) => this.animateActivation(e),
deactivate: (e) => this.animateDeactivation(e),
cancel: (e) => this.animateCancellation(e)
};
}
initialize() {
if (!SUPPORTS_CUSTOM_PROPS) {
return;
}
this.root.classList.add('mdl-ripple');
this.addEventListeners();
this.layout();
}
destroy() {
if (!SUPPORTS_CUSTOM_PROPS) {
return;
}
this.removeEventListeners();
}
layout() {
this.frame_ = this.root.getBoundingClientRect();
const maxDim = Math.max(this.frame_.height, this.frame_.width);
// Sqrt(2) * square length == diameter
const maxRadius = Math.sqrt(2) * maxDim / 2;
requestAnimationFrame(() => {
this.root.style.setProperty('--mdl-ripple-surface-width', `${this.frame_.width}px`);
this.root.style.setProperty('--mdl-ripple-surface-height', `${this.frame_.height}px`);
this.root.style.setProperty('--mdl-ripple-fg-size', `${maxRadius * 2}px`);
});
}
addEventListeners() {
this.listenerInfos_.forEach(info => {
Object.keys(info).forEach(k => {
this.root.addEventListener(info[k], this.listeners_[k]);
});
});
}
removeEventListeners() {
this.listenerInfos_.forEach(info => {
Object.keys(info).forEach(k => {
this.root.removeEventListener(info[k], this.listeners_[k]);
});
});
}
animateActivation(e) {
const isPointerEvent = e.type === 'mousedown' || e.type === 'touchstart';
if (isPointerEvent) {
this.pointerDownRecognized_ = true;
}
if (e.type === 'focus') {
if (!this.pointerDownRecognized_) {
this.nonPointerFocused_ = true;
}
}
// This needs to be wrapped in an rAF call b/c web browsers
// report active states inconsistently when they're called within
// event handling code:
// - https://bugs.chromium.org/p/chromium/issues/detail?id=635971 // - https://bugzilla.mozilla.org/show_bug.cgi?id=1293741
requestAnimationFrame(() => {
this.wasElementMadeActive_ = this.root[MATCHES](':active');
if (this.wasElementMadeActive_ || this.nonPointerFocused_) {
this.root.classList.add('mdl-ripple--background-active');
}
});
}
animateDeactivation(e) {
const isPointerEvent = e.type === 'mouseup' || e.type === 'touchend';
const pointerWasRecognized = this.pointerDownRecognized_;
let startPoint;
if (isPointerEvent) {
this.pointerDownRecognized_ = false;
startPoint = getNormalizedEventCoords(e, this.root);
} else {
startPoint = {
left: this.frame_.width / 2,
top: this.frame_.height / 2
};
}
requestAnimationFrame(() => {
if (this.root[MATCHES](':active')) {
return;
}
const {left, top} = startPoint;
this.root.style.setProperty('--mdl-ripple-left', `${left}px`);
this.root.style.setProperty('--mdl-ripple-top', `${top}px`);
if (e.type === 'blur') {
this.root.classList.remove('mdl-ripple--background-active');
} else if (this.wasElementMadeActive_) {
this.root.classList.remove('mdl-ripple--background-active');
animateWithClass(this.root, 'mdl-ripple--background-bounded-active-fill');
animateWithClass(this.root, 'mdl-ripple--foreground-bounded-active-fill', 'animationend');
}
this.wasElementMadeActive_ = false;
this.nonPointerFocused_ = false;
});
}
animateCancellation(e) {
}
}
function getNormalizedEventCoords(ev, container) {
const rect = container.getBoundingClientRect();
const documentLeft = window.pageXOffset + rect.left;
const documentTop = window.pageYOffset + rect.top;
let normalizedLeft;
let normalizedTop;
// Determine touch point relative to the ripple container.
if (ev.type === 'touchstart') {
normalizedLeft = ev.touches[0].pageX - documentLeft;
normalizedTop = ev.touches[0].pageY - documentTop;
} else {
normalizedLeft = ev.pageX - documentLeft;
normalizedTop = ev.pageY - documentTop;
}
return {left: normalizedLeft, top: normalizedTop};
}
new MDLRipple(document.querySelector('.btn')).initialize();
@mixin mdl-ripple-base() {
// Custom props used by ripple
--mdl-ripple-left: 0px;
--mdl-ripple-top: 0px;
--mdl-ripple-fg-size: 0px;
--mdl-ripple-surface-height: 0px;
--mdl-ripple-surface-width: 0px;
}
@mixin mdl-ripple-bg-base($color, $radius: 100%) {
position: absolute;
left: calc(50% - #{$radius});
top: calc(50% - #{$radius});
width: $radius * 2;
height: $radius * 2;
transform: scale(1);
transition: opacity 480ms linear;
border-radius: 50%;
background-color: rgba(0, 0, 0, .06);
opacity: 0;
pointer-events: none;
}
@mixin mdl-ripple-bg($color: rgba(black, .06), $pseudo: null) {
@if ($pseudo) {
&#{$pseudo} {
@include mdl-ripple-bg-base($color);
}
} @else {
@include mdl-ripple-bg-base($color);
}
&.mdl-ripple--background-active#{$pseudo} {
transition: opacity 600ms linear;
opacity: .99999;
}
&.mdl-ripple--background-bounded-active-fill#{$pseudo} {
transition-duration: 120ms;
opacity: 1;
}
}
@mixin mdl-ripple-fg-base($color) {
position: absolute;
left: calc(var(--mdl-ripple-left) - var(--mdl-ripple-fg-size)/2);
top: calc(var(--mdl-ripple-top) - var(--mdl-ripple-fg-size)/2);
background-color: $color;
width: var(--mdl-ripple-fg-size);
height: var(--mdl-ripple-fg-size);
border-radius: 50%;
opacity: 0;
pointer-events: none;
transform-origin: center center;
}
@mixin mdl-ripple-fg($color: rgba(black, .06), $pseudo: null) {
@if ($pseudo) {
&#{$pseudo} {
@include mdl-ripple-fg-base($color);
}
} @else {
@include mdl-ripple-fg-base($color);
}
&.mdl-ripple--foreground-bounded-active-fill#{$pseudo} {
animation-fill-mode: forwards;
animation: 800ms mdl-ripple-fg-radius-in, 400ms mdl-ripple-fg-opacity-out;
}
}
@keyframes mdl-ripple-fg-radius-in {
0% {
transform: scale(0);
animation-timing-function: cubic-bezier(.157, .72, .386, .987);
}
100% {
// This differs slightly from spec.
transform: scale(1.5);
}
}
@keyframes mdl-ripple-fg-opacity-out {
from {
opacity: 1;
animation-timing-function: linear;
}
to {
opacity: 0;
}
}
// Demo Stuff Below
section {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.btn.mdl-ripple {
@include mdl-ripple-base;
@include mdl-ripple-bg(rgba(black, .06), '::before');
@include mdl-ripple-fg(rgba(black, .06), '::after');
overflow: hidden;
&::after {
content: '';
}
}
.btn {
position: relative;
padding: 2rem;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2),
0px 2px 2px 0px rgba(0, 0, 0, 0.14),
0px 1px 5px 0px rgba(0, 0, 0, 0.12);
width: 200px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: none;
border: none;
appearance: none;
outline: none;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: opacity 120ms cubic-bezier(0, 0, .2, 1);
border-radius: inherit;
background: currentColor;
content: "";
opacity: 0;
}
&:focus::before {
opacity: .12;
}
&:active::before {
opacity: .18;
}
&:active {
outline: none;
}
&:hover {
cursor: pointer;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment