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"
type="submit"
>
<Spinner />
<Spinner
className="spinner"
color="blue"
/>
</Button>
] : [
<Button

View file

@ -15,7 +15,7 @@
.spinner {
margin: 0 .8rem;
width: 1rem;
height: 1rem;
vertical-align: middle;
}
.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 */
}
.studio-selector-button {
display: flex;
position: relative;
transition: all .5s;
margin: .21875rem .21875rem;
border-radius: .5rem;
background-color: $ui-white;
@ -102,6 +102,7 @@
height: 2.5rem;
box-sizing: border-box;
justify-content: space-between;
}
.studio-selector-button-text {
@ -164,30 +165,30 @@
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;
height: 1.4rem;
transform-origin: center;
}
.studio-status-icon--img {
width: 1.4rem;
height: 1.4rem;
.studio-status-icon-with-animation {
animation-name: bump;
animation-duration: .25s;
animation-timing-function: cubic-bezier(.3, -3, .6, 3);
animation-iteration-count: 1;
}
.action-button-text .spinner-smooth {
margin: .2125rem auto;
width: 1.875rem;
height: 1rem;
}
.studio-status-icon .spinner-smooth {
position: unset; /* don't understand why neither relative nor absolute work */
}
.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 */
@keyframes bump {
0% {
transform: scale(0);
opacity: 0;
-webkit-transform: scale(0);
}
100% {
transform: scale(1);
opacity: 1;
-webkit-transform: scale(1);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -75,7 +75,7 @@ class StudentCompleteRegistration extends React.Component {
}
handleRegister (formData) {
this.setState({waiting: true});
formData = defaults({}, formData || {}, this.state.formData);
const submittedData = {
birth_month: formData.user.birth.month,
@ -87,7 +87,7 @@ class StudentCompleteRegistration extends React.Component {
if (this.props.must_reset_password) {
submittedData.password = formData.user.password;
}
api({
host: '',
uri: '/classes/student_update_registration/',

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