Merge pull request #2073 from LiFaytheGoblin/2005/animate-add-to-studio-buttons

Animate add-to-studio-buttons on preview project page
This commit is contained in:
Linda 2018-09-14 12:28:44 -04:00 committed by GitHub
commit 42f0ed16e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 184 additions and 169 deletions

View file

@ -75,7 +75,10 @@ class Login extends React.Component {
key="submitButton" key="submitButton"
type="submit" type="submit"
> >
<Spinner /> <Spinner
className="spinner"
color="blue"
/>
</Button> </Button>
] : [ ] : [
<Button <Button

View file

@ -15,7 +15,7 @@
.spinner { .spinner {
margin: 0 .8rem; margin: 0 .8rem;
width: 1rem; width: 1rem;
height: 1rem; vertical-align: middle;
} }
.submit-button { .submit-button {

View file

@ -0,0 +1,48 @@
const React = require('react');
const PropTypes = require('prop-types');
/**
* Higher-order component for building an animated studio button
* it is used to decorate the onToggleStudio function with noticing
* when the button has first been clicked.
* This is needed so the buttons don't play the animation when they are
* first rendered but when they are first clicked.
* @param {React.Component} Component a studio button component
* @return {React.Component} a wrapped studio button component
*/
const AnimateHOC = Component => {
class WrappedComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
wasClicked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick () {
this.setState({ // else tell the state that the button has been clicked
wasClicked: true
}, () => this.props.onClick(this.props.id)); // callback after state has been updated
}
render () {
const {wasClicked} = this.state;
return (<Component
{...this.props}
wasClicked={wasClicked}
onClick={this.handleClick}
/>);
}
}
WrappedComponent.propTypes = {
id: PropTypes.number,
onClick: PropTypes.func
};
return WrappedComponent;
};
module.exports = AnimateHOC;

View file

@ -89,10 +89,10 @@
pointer-events: none; /* pass clicks through to buttons underneath */ pointer-events: none; /* pass clicks through to buttons underneath */
} }
.studio-selector-button { .studio-selector-button {
display: flex; display: flex;
position: relative; position: relative;
transition: all .5s;
margin: .21875rem .21875rem; margin: .21875rem .21875rem;
border-radius: .5rem; border-radius: .5rem;
background-color: $ui-white; background-color: $ui-white;
@ -102,6 +102,7 @@
height: 2.5rem; height: 2.5rem;
box-sizing: border-box; box-sizing: border-box;
justify-content: space-between; justify-content: space-between;
} }
.studio-selector-button-text { .studio-selector-button-text {
@ -164,30 +165,30 @@
background-color: $ui-blue; background-color: $ui-blue;
} }
.studio-status-icon-plus-img { .studio-status-icon-plus-img,
.studio-status-icon-checkmark-img {
animation-direction: normal;
width: 1.4rem; width: 1.4rem;
height: 1.4rem; height: 1.4rem;
transform-origin: center;
} }
.studio-status-icon--img { .studio-status-icon-with-animation {
width: 1.4rem; animation-name: bump;
height: 1.4rem; animation-duration: .25s;
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
animation-iteration-count: 1;
} }
.action-button-text .spinner-smooth { @keyframes bump {
margin: .2125rem auto; 0% {
width: 1.875rem; transform: scale(0);
height: 1rem; opacity: 0;
} -webkit-transform: scale(0);
}
.studio-status-icon .spinner-smooth { 100% {
position: unset; /* don't understand why neither relative nor absolute work */ transform: scale(1);
} opacity: 1;
-webkit-transform: scale(1);
.studio-status-icon .spinner-smooth .circle { }
/* overlay spinner on circle */
position: absolute;
margin: .1875rem; /* stay within boundaries of circle */
width: 75%; /* stay within boundaries of circle */
height: 75%; /* stay within boundaries of circle */
} }

View file

@ -31,7 +31,7 @@ const AddToStudioModalPresentation = ({
includesProject={studio.includesProject} includesProject={studio.includesProject}
key={studio.id} key={studio.id}
title={studio.title} title={studio.title}
onToggleStudio={onToggleStudio} onClick={onToggleStudio}
/> />
)); ));
@ -83,7 +83,7 @@ const AddToStudioModalPresentation = ({
type="submit" type="submit"
> >
<div className="action-button-text"> <div className="action-button-text">
<Spinner mode="smooth" /> <Spinner />
<FormattedMessage id="addToStudio.finishing" /> <FormattedMessage id="addToStudio.finishing" />
</div> </div>
</Button> </Button>

View file

@ -1,28 +1,36 @@
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const classNames = require('classnames'); const classNames = require('classnames');
const Spinner = require('../../spinner/spinner.jsx'); const Spinner = require('../../spinner/spinner.jsx');
const AnimateHOC = require('./animate-hoc.jsx');
require('./modal.scss'); require('./modal.scss');
const StudioButton = ({ const StudioButton = ({
hasRequestOutstanding, hasRequestOutstanding,
id,
includesProject, includesProject,
title, title,
onToggleStudio onClick,
wasClicked
}) => { }) => {
const checkmark = ( const checkmark = (
<img <img
alt="checkmark-icon" alt="checkmark-icon"
className="studio-status-icon-checkmark-img" className={classNames(
'studio-status-icon-checkmark-img',
{'studio-status-icon-with-animation': wasClicked}
)}
src="/svgs/modal/confirm.svg" src="/svgs/modal/confirm.svg"
/> />
); );
const plus = ( const plus = (
<img <img
alt="plus-icon" alt="plus-icon"
className="studio-status-icon-plus-img" className={classNames(
'studio-status-icon-plus-img',
{'studio-status-icon-with-animation': wasClicked}
)}
src="/svgs/modal/add.svg" src="/svgs/modal/add.svg"
/> />
); );
@ -34,8 +42,7 @@ const StudioButton = ({
{'studio-selector-button-selected': {'studio-selector-button-selected':
includesProject && !hasRequestOutstanding} includesProject && !hasRequestOutstanding}
)} )}
data-id={id} onClick={onClick}
onClick={onToggleStudio}
> >
<div <div
className={classNames( className={classNames(
@ -50,11 +57,11 @@ const StudioButton = ({
<div <div
className={classNames( className={classNames(
'studio-status-icon', 'studio-status-icon',
{'studio-status-icon-unselected': !includesProject} {'studio-status-icon-unselected': !includesProject && !hasRequestOutstanding}
)} )}
> >
{(hasRequestOutstanding ? {(hasRequestOutstanding ?
(<Spinner mode="smooth" />) : <Spinner /> :
(includesProject ? checkmark : plus))} (includesProject ? checkmark : plus))}
</div> </div>
</div> </div>
@ -63,10 +70,10 @@ const StudioButton = ({
StudioButton.propTypes = { StudioButton.propTypes = {
hasRequestOutstanding: PropTypes.bool, hasRequestOutstanding: PropTypes.bool,
id: PropTypes.number,
includesProject: PropTypes.bool, includesProject: PropTypes.bool,
onToggleStudio: PropTypes.func, onClick: PropTypes.func,
title: PropTypes.string title: PropTypes.string,
wasClicked: PropTypes.bool
}; };
module.exports = StudioButton; module.exports = AnimateHOC(StudioButton);

View file

@ -224,7 +224,7 @@ class ReportModal extends React.Component {
> >
{isWaiting ? ( {isWaiting ? (
<div className="action-button-text"> <div className="action-button-text">
<Spinner mode="smooth" /> <Spinner />
<FormattedMessage id="report.sending" /> <FormattedMessage id="report.sending" />
</div> </div>
) : ( ) : (

View file

@ -1,29 +1,28 @@
const range = require('lodash.range');
const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const PropTypes = require('prop-types');
const classNames = require('classnames');
require('./spinner.scss'); require('./spinner.scss');
// Adapted from http://tobiasahlin.com/spinkit/ // Adapted from http://tobiasahlin.com/spinkit/
const Spinner = ({ const Spinner = ({
mode className,
}) => { color
const spinnerClassName = (mode === 'smooth' ? 'spinner-smooth' : 'spinner'); }) => (
const spinnerDivCount = (mode === 'smooth' ? 24 : 12); <img
return ( alt="loading animation"
<div className={spinnerClassName}> className={classNames('studio-status-icon-spinner', className)}
{range(1, spinnerDivCount + 1).map(id => ( src={`/svgs/modal/spinner-${color}.svg`}
<div />
className={`circle${id} circle`} );
key={`circle${id}`}
/> Spinner.defaultProps = {
))} color: 'white'
</div>
);
}; };
Spinner.propTypes = { Spinner.propTypes = {
mode: PropTypes.string className: PropTypes.string,
color: PropTypes.oneOf(['white', 'blue', 'transparent-gray'])
}; };
module.exports = Spinner; module.exports = Spinner;

View file

@ -1,118 +1,44 @@
@import "../../colors"; .studio-status-icon-spinner {
/* This class can be used on an icon that should spin.
.spinner { It first plays the intro animation, then spins forever. */
position: relative; animation-name: intro, spin;
margin: 0 auto; animation-duration: .25s, .5s;
width: 20px; animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
height: 20px; animation-delay: 0s, .25s;
animation-iteration-count: 1, infinite;
.circle { animation-direction: normal;
position: absolute; width: 1.4rem; /* standard is 1.4 rem but can be overwritten by parent */
top: 0; height: 1.4rem;
left: 0; -webkit-animation-name: intro, spin;
width: 100%; -webkit-animation-duration: .25s, .5s;
height: 100%; -webkit-animation-iteration-count: 1, infinite;
-webkit-animation-delay: 0s, .25s;
&:before { -webkit-animation-timing-function: cubic-bezier(.3, -3, .6, 3), linear;
display: block; transform-origin: center;
animation: circleFadeDelay 1.2s infinite ease-in-out both;
margin: 0 auto;
border-radius: 100%;
background-color: $ui-gray;
width: 15%;
height: 15%;
content: "";
.white & {
background-color: $ui-blue-dark;
}
}
}
@for $i from 1 through 12 {
$rotation: 30deg * ($i - 1);
$delay: -1.3s + $i * .1;
.circle#{$i} {
transform: rotate($rotation);
&:before {
animation-delay: $delay;
}
}
}
} }
@keyframes circleFadeDelay { @keyframes intro {
0%, 0% {
39%, transform: scale(0);
opacity: 0;
-webkit-transform: scale(0);
}
100% { 100% {
opacity: 0; transform: scale(1);
}
40% {
opacity: 1; opacity: 1;
-webkit-transform: scale(1);
} }
} }
@keyframes spin {
/*********************/ 0% {
/* type === "smooth" */ transform: rotate(0);
/*********************/ -webkit-transform: rotate(0);
.spinner-smooth {
position: relative;
margin: 0 auto;
width: 20px;
height: 20px;
.circle {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
&:before {
display: block;
animation: circleFadeDelaySmooth 1.8s infinite ease-in-out both;
margin: 0 auto;
border-radius: 100%;
background-color: $ui-white;
width: 30%;
height: 20%;
content: "";
.white & {
background-color: darken($ui-blue, 8%);
}
}
} }
@for $i from 1 through 24 { 100% {
$rotation: 15deg * ($i - 1); transform: rotate(359deg);
$delay: -1.9s + $i * .075; -webkit-transform: rotate(359deg);
.circle#{$i} {
transform: rotate($rotation);
&:before {
animation-delay: $delay;
}
}
}
}
@keyframes circleFadeDelaySmooth {
0%,
35% {
opacity: 0;
},
40% {
opacity: 1;
} }
} }

View file

@ -35,8 +35,10 @@ const Components = () => (
<Box title="Carousel component in a box!"> <Box title="Carousel component in a box!">
<Carousel /> <Carousel />
</Box> </Box>
<h1>This is a Spinner</h1> <h1>This is a blue Spinner</h1>
<Spinner /> <Spinner
color="blue"
/>
<h1>Colors</h1> <h1>Colors</h1>
<div className="colors"> <div className="colors">
<span className="ui-blue">$ui-blue</span> <span className="ui-blue">$ui-blue</span>

View file

@ -208,8 +208,8 @@ class Preview extends React.Component {
); );
} }
} }
handleToggleStudio (event) { handleToggleStudio (id) {
const studioId = parseInt(event.currentTarget.dataset.id, 10); const studioId = parseInt(id, 10);
if (isNaN(studioId)) { // sanity check in case event had no integer data-id if (isNaN(studioId)) { // sanity check in case event had no integer data-id
return; return;
} }

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>spinner-blue</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner-blue" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M15,10 C15,7.23857625 12.7614237,5 10,5 C7.23857625,5 5,7.23857625 5,10 C5,12.7614237 7.23857625,15 10,15" id="Oval-2" stroke="#4D97FF" stroke-width="2.5"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 686 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>spinner-transparent-gray</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner-transparent-gray" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.15">
<path d="M15,10 C15,7.23857625 12.7614237,5 10,5 C7.23857625,5 5,7.23857625 5,10 C5,12.7614237 7.23857625,15 10,15" id="Oval-2" stroke="#000000" stroke-width="2.5"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 732 B

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<title>spinner-white</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner-white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M15,10 C15,7.23857625 12.7614237,5 10,5 C7.23857625,5 5,7.23857625 5,10 C5,12.7614237 7.23857625,15 10,15" id="Oval-2" stroke="#FFFFFF" stroke-width="2.5"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 688 B

View file

@ -1 +0,0 @@
<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M15 10a5 5 0 1 0-5 5" stroke="#FFF" stroke-width="2.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 213 B