I've got 2 components that are conditionally shown or hidden based on a string value stored in useState as showModal
{showModal === 'SIGNIN' && <SignIn />}
{showModal === 'JOIN' && <Join />}
I want to fade in one component, then when the state changes, fade it out and fade in the other component.
Can this be done with react transition group?
I tried
<TransitionGroup>
<CSSTransition in={showModal === 'SIGNIN' ? true : false} classNames='fade' timeout={220} key={showModal}>
<div>
<SignIn />
</div>
</CSSTransition>
<CSSTransition in={showModal === 'JOIN' ? true : false} classNames='fade' timeout={220} key={showModal}>
<div>
<Join />
</div>
</CSSTransition>
</TransitionGroup>
I don't get any error, one component is shown, changing showModal from 'SIGNIN' to 'JOIN' does nothing. Inspecting the divs with the timeout set to 22000 shows that no new classes have been added.
SwitchTransition from react transition group might help.
Example
const { useState, useEffect } = React;
const { SwitchTransition, CSSTransition } = ReactTransitionGroup;
const SignIn = () => <div className="block sign-in">Sign In</div>;
const Join = () => <div className="block join">Join</div>;
const App = () => {
const [showModal, setModal] = useState("SIGNIN");
useEffect(() => {
let handle;
const loop = () => {
setModal(state => state === "JOIN" ? "SIGNIN" : "JOIN");
handle = setTimeout(loop, 2500);
};
handle = setTimeout(loop, 1000);
return () => {
clearTimeout(handle);
}
}, []);
const addEndListener = (node, done) => {
node.addEventListener("transitionend", done, false);
}
return <div>
<SwitchTransition mode="out-in">
<CSSTransition
key={showModal === "SIGNIN"}
addEndListener={addEndListener}
classNames="fade">
{showModal === "SIGNIN" ? <SignIn/> : <Join/>}
</CSSTransition>
</SwitchTransition>
</div>;
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
body {
margin: 0;
overflow: hidden;
font-family: Georgia, serif;
}
.block {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px;
color: white;
width: 200px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
}
.sign-in {
background: #0984e3;
}
.join {
background: #6c5ce7;
}
.fade-enter {
opacity: 0;
transform: translate(-100%, -50%);
}
.fade-exit {
opacity: 1;
transform: translate(-50%, -50%);
}
.fade-enter-active {
opacity: 1;
transform: translate(-50%, -50%);
}
.fade-exit-active {
opacity: 0;
transform: translate(100%, -50%);
}
.fade-enter-active,
.fade-exit-active {
transition: opacity 500ms, transform 500ms;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-transition-group#4.4.2
/dist/react-transition-group.min.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
I currently have an app with UIWebView that has a scrollable div with the webkit-overflow-scrolling:touch property.
When the side menu is open, I place an overlay (another div) on top of the content to give a dimming effect.
The problem is that when the menu is open (and the overlay in place) when the user pans, the scrollable div actually scrolls when the overlay should be stopping this form happening.
Now, in iOS7, the solution was to add the webkit-overflow-scrolling:touch; to the overlay. That works like a charm, but in iOS8 it doesn't.
Here is a link to an example of the problem. If run on iOS 7 it works as expected, if run on iOS 8 the content in the back will scroll.
.scrollable {
width:100%;
height:200px;
-webkit-overflow-scrolling:touch;
overflow:scroll;
}
.overlay {
position:absolute;
width:100%;
overflow:scroll;
height:200px;
-webkit-overflow-scrolling:touch;
background-color:black;
opacity:.5;
transform: translate3d(0,0,0);
-webkit-transform: translate3d(0,0,0);
z-index:10;
}
https://jsfiddle.net/SergioM/57f2da87/9/
I also tried setting the overflow-x property of the scrollable div to hidden/auto when the menu is opened but that adds a terrible flicker.
Any suggestion will be much appreciated.
Thanks.
Well after trying many different options I came up with a work around that is good enough for now.
I added a div inside the overlay that is vertically 1% bigger. That now warranties that the event is handled by the overlay and not passed on to the container at the back.
This also allows me to listen to events natively such as pan (horizontally), the vertical ones wont come up but that is ok for now.
.overlayInner {
color:red;
height:101%;
margin-left:30px;
}
here is a link to the fiddle.The margin is unnecessary, just to avoid the number to overlap.
http://jsfiddle.net/SergioM/57f2da87/15/
i don't think this is possible by only using css in the current safari/webkit-version of iOS8.
But you should be able to prevent the scrolling with javascript.
$( ".showHide" ).click( function() {
$( ".overlay" ).toggle();
});
$( ".overlay" ).on( 'touchstart touchmove', function( event ) {
if ( $( this ).is( ":visible" ) ) {
event.preventDefault();
}
});
Update:
i played a little around and came up with another possible solution that could be helpful for you.
HTML:
<button class="showHide">show/hide overlay</button>
<br/>
<br/>
<div class="scrollable">
<div class="overlay"></div>
1
<br/>2
<br/>3
<br/>4
<br/>5
<br/>6
<br/>7
<br/>8
<br/>9
<br/>10
<br/>11
<br/>12
<br/>13
<br/>14
<br/>15
<br/>16
<br/>17
</div>
CSS:
.scrollable {
width:100%;
height:200px;
-webkit-overflow-scrolling:touch;
overflow:scroll;
position: relative
}
.overlay {
content: ' ';
position:absolute;
top: 0;
left: 0;
width:100%;
height:100%;
background-color:black;
opacity:.5;
transform: translate3d(0,0,0);
-webkit-transform: translate3d(0,0,0);
transition: opacity 300ms ease;
z-index:10;
}
Javascript:
$( ".showHide" ).click( function () {
$( ".overlay" ).toggle( { duration: 0, complete: function() {
if ( $( ".overlay" ).is( ":visible" ) ) {
$( ".overlay" ).css( 'top', $( ".scrollable" ).scrollTop() );
$( ".scrollable" ).css( 'overflow', 'hidden' );
} else {
$( ".scrollable" ).css( 'overflow', 'scroll' );
}
}});
});
Example: JSFiddle
I am doing a jQuery animation of 3 boxes, the effect is like the jsfiddle link below:
Fiddle
Basically when clicked on each color box the corresponding box will enlarge to width 100% taking up all width. And when clicking close (yes, it is not functioning well) the box will shrink back to its original position.
What I encountered now is
When enlarging the pink or the blue box, sometimes the yellow box on RHS will jump to next row (but just a very quick jump, i dunno why). It happens only in Chrome, looks perfect in Firefox.
When clicking the close button, it will trigger the yellow box to
enlarge (supposed it has taken up 0% of width? Why the bind click
still works?
Could someone advise how I should solve these and is there a better way for me to achieve the same results?
HTML
<div class="animateWrap">
<div id="btn_howtojoin" class="btn_group">
<div class="btn_close"></div>
<div class="content">Content</div>
</div>
<div id="btn_judgecriteria" class="btn_group">
<div class="btn_close"></div>
<div class="content">Content</div>
</div>
<div id="btn_prizeinfo" class="btn_group">
<div class="btn_close">Close</div>
<div class="content">Content</div>
</div>
<div class="clear00"></div>
JS
$(function () {
$('#btn_judgecriteria').bind('click', function () {
$(this).animate({
width: "100%"
}, function () {
$(this).animate({
height: "400px"
});
$('.btn_close').show();
});
$('#btn_prizeinfo').animate({
width: "0%"
});
$('#btn_howtojoin').animate({
width: "0%"
});
});
$('#btn_howtojoin').bind('click', function () {
$(this).animate({
width: "100%"
}, function () {
$(this).animate({
height: "400px"
});
$(this).find('.content').animate({
top: "40px"
});
$('.btn_close').show();
});
$('#btn_prizeinfo').animate({
width: "0%"
});
$('#btn_judgecriteria').animate({
width: "0%"
});
});
$('#btn_prizeinfo').bind('click', function () {
$(this).animate({
width: "100%"
}, function () {
$(this).animate({
height: "400px"
});
$('.btn_close').show();
});
$('#btn_howtojoin').animate({
width: "0%"
});
$('#btn_judgecriteria').animate({
width: "0%"
});
});
$('.btn_close').bind('click', function () {
$('.btn_group').animate({
width: "33.333%",
height: "220px"
});
$('.btn_group .content').hide();
$('.btn_close').hide();
});
});
CSS
.btn_group {
width: 33.3%;
height: 220px;
float: left;
cursor: pointer;
position:relative;
}
#btn_howtojoin {
background: #fcb2b2 ;
width: 33.4%;
}
#btn_judgecriteria {
background: #7acccb ;
}
#btn_prizeinfo {
background: #f1c348;
}
.btn_group .content {
position:absolute;
top:-400px;
left:40px;
}
.btn_close {
position:absolute;
top:20px;
right:20px;
color:#FFF;
width: 40px;
height:62px;
display:none;
}
Problem 1 maybe due to rounding up calculation of 33.333% which make the total width of 3 container slightly larger than 100%. Changing them to pixel might help.
Problem 2 is due to event bubbling, it will trigger click event of .btn_group after .btn_close
Add event.stopPropagation(); will help solving this.
$('.btn_close').bind('click', function () {
$('.btn_group').animate({
width: "33.333%",
height: "220px"
});
$('.btn_group .content').hide();
$('.btn_close').hide();
event.stopPropagation();
});
So according to MDN (it makes sense) the AnimationEvent has the bubble argument but how can one set it to true? Considering the event is fired from a CSS animation.
Ok, it turns out that CSS does bubble events, for example:
HTML:
<ul id="ul">
<li>
<a id="a">Some text!</a>
</li>
</ul>
CSS:
a
{
color: red;
}
a:hover
{
-webkit-animation-duration: 1s;
-webkit-animation-name: change-color;
}
#-webkit-keyframes change-color {
from
{
color: red;
}
to
{
color: blue;
}
}
JS:
var a = document.getElementById( 'a' );
var ul = document.getElementById( 'ul' );
a.addEventListener( 'webkitAnimationEnd', handleEnd );
ul.addEventListener( 'webkitAnimationEnd', handleEnd );
function handleEnd(e)
{
console.log(e);
}
You'll see two events fired. The problem I had was that I was using jQuery's bind() and this binds to a specific selector rather than listening to bubbled events (I think).
I have two elements on a page that I'd like to animate via CSS (specifically, -webkit-animation). The animation itself simply bounces an element up and down. One element is always shown and bouncing, the other is not animated until mouse-over (hover).
My question is: Is it possible to sync (have both elements reach their apex at the same time, etc) the animation across both elements regardless of when the 2nd element's animation is started?
Here's my HTML:
<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>
and my CSS:
#-webkit-keyframes bounce {
0% {-webkit-transform: translateY(0px);}
25% {-webkit-transform: translateY(-2px);}
50% {-webkit-transform: translateY(-4px);}
75% {-webkit-transform: translateY(-2px);}
100% {-webkit-transform: translateY(0px);}
}
#bouncy01,
#bouncy02 {
margin:10px;
float: left;
background: #ff0000;
padding: 10px;
border: 1px solid #777;
}
#bouncy01 {
-webkit-animation:bounce 0.25s ease-in-out infinite alternate;
}
#bouncy02 {
background: #ffff00;
}
#bouncy02:hover {
-webkit-animation:bounce 0.25s ease-in-out infinite alternate;
}
and finally, a working demo of the issue: http://jsfiddle.net/7ZLmq/2/
(to see the problem, mouse-over the yellow block)
I don't think its possible natively, but you can actually hack similar functionality by using a bouncing wrapper and some position altering
html:
<div id="bouncywrap">
<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>
<div>
CSS:
#-webkit-keyframes bounce {
0% { padding-top:1px;}
/* using padding as it does not affect position:relative of sublinks
* using 0 instead of 0 b/c of a box-model issue,
* on kids wiht margin, but parents without margin, just try out
*/
50% { padding-top:5px;} /*desired value +1*/
100% { padding-top:1px;}
}
#bouncy01,
#bouncy02 {
margin:10px;
background: #ff0000;
padding: 10px;
border: 1px solid #777;
width:30px;
position:absolute;
}
#bouncywrap {
-webkit-animation:bounce 0.125s ease-in-out infinite;
position:relative;
width:140px;
height:50px;
/* background:grey; /*debug*/
}
#bouncy02 {
background: #ffff00;
left:60px;
top:2px; /*half of desired value, just a fix for the optic*/
}
#bouncy02:hover {
position:relative; /*here happens the magic*/
top:0px;
}
demo http://jsfiddle.net/A92pU/1/
The Web Animations API now allows to control animations very precisely and quite easily.
There are various ways to declare a Web Animation, but since here we started with CSS, here is how to hook to it:
// when the animation starts
document.querySelector("#bouncy02")
.addEventListener("animationstart", (evt) => {
// double check it the animation we wanted
if (evt.animationName === "bounce") {
// retrieve both Animation objects
const myAnim = findAnimByName(evt.target, "bounce");
const otherAnim = findAnimByName(document.querySelector("#bouncy01"), "bounce");
// update mine to act as if it started
// at the same time as the first one
myAnim.startTime = otherAnim.startTime;
}
});
// simple helper to find an Animation by animationName
function findAnimByName(elem, name) {
// get all the active animations on this element
const anims = elem.getAnimations();
// return the first one with the expected animationName
return anims.find((anim) => anim.animationName === name);
}
#keyframes bounce {
0% {transform: translateY(0px);}
25% {transform: translateY(-2px);}
50% {transform: translateY(-4px);}
75% {transform: translateY(-2px);}
100% {transform: translateY(0px);}
}
#bouncy01,
#bouncy02 {
margin:10px;
float: left;
background: #ff0000;
padding: 10px;
border: 1px solid #777;
}
#bouncy01 {
animation:bounce 0.25s ease-in-out infinite alternate;
}
#bouncy02 {
background: #ffff00;
}
#bouncy02:hover {
animation:bounce 0.25s ease-in-out infinite alternate;
}
<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>
Note that while it's surprisingly not yet that propular, this API actually exists for some times now, and its browser support (all except IE) is quite good.
I was looking for an alternative solution to those proposed here because:
I am animating a background color - which can't use the positioning magic in the accepted answer.
I wanted to avoid calculations for a very simple animation in my app.
After further research I came across this module by bealearts.
It exposes a very neat API that lets you keep an animation in sync across the app by referring to it's name:
import sync from 'css-animation-sync';
sync('spinner');
Since this seemed a little too good to be true, I tested the library (which is a single short file) in this fiddle and am happy to report it works (hover on the third image and see that I quickly syncs to the second image's animation) :).
Credit: I used the animation from this fiddle by Simurai as a basis for my fiddle.
If anyone wants to replicate the mechanism behind this synchronisation, the code is open, but in it's essence, it uses events listeners for the animation itself as sync points:
window.addEventListener('animationstart', animationStart, true);
window.addEventListener('animationiteration', animationIteration, true);
Hope this helps the next person looking for a solution to this problem.
Calculate and add a delay before adding the class:
function getTime (seconds) {
const msDuration = (seconds * 1000).toFixed(0);
const currentTime = (new Date()).getTime();
const msDelay = msDuration - (currentTime % msDuration);
return (msDelay / 1000).toFixed(4);
}
$('div').css({animationDelay: getTime(0.6) + "s"}).addClass('animating');
https://codepen.io/s-flhti/pen/GRoVXZw
Looks like you can just stack two of the yellow ones and switch visibility on :hover through a parent element.
You need the animation to always be running otherwise you'll run into the sync issue you've got.
I modified your code a bit to get this.
You could use a setInterval to maintain the animation state of the first animation and give the other animation a negative delay to seek to its matching keyframe on mouse-over.
Read about the state-maintaining-interval-thing here, at the "Manipulating CSS Animations" section; read about the negative delay to seek here.
This was my little quest in synchronizing animations for different elements and pseudo-elements, thanks to the ideas above, the solution turned out to be very simple. I hope this small code helps someone.
window.addEventListener('animationstart', e =>
e.animationName == 'rgb' && e.target.getAnimations({subtree: true}).forEach(e => e.startTime = 0), true)
on mouse hover:
remove animation classes from both elements
use requestAnimationFrame(() => { ... add here "bounce" class to both elements })
Should sync nicely.
You could set a class on the root element which set the altertating state, and then alternate the class using a timer
CSS
.alt .bouncy {
padding-top:5px !important;
}
.bouncy {
padding-top: 1px;
transition: padding-top ease 500ms;
}
HTML
<div class="container">
<div class="bouncy">Drip</div>
<div class="bouncy">droP</div>
<div>
Javascript
$(function () {
setInterval(() => $(".container").toggleClass("alt"), 1000)
})
In this way transition and timer do the work of css animation, but controlled by a single master switch (the container).
With css animation sync lib by bealearts, you can easly synchonize animations. But in version 0.4.1 (lastest today), it had the bugs:
Sync gets lost, when all the sync-animation elements are stopped they animation (for example display:none )
First animation starts from non-zero frame after restarting, that may be critical.
Some time after the start of the first animation, it flashes.
Not working with pseudo-elements :before, :after
To fix all theese bugs (but 4) , you can fix library code:
-Add animation-cancel callback
function animationCancel(event) {
if (shouldSync(event)) {
elements.delete(event.target);
}
}
window.addEventListener('animationcancel', animationCancel, true);
-Modify animation-start callback to process first animation
function animationStart(event) {
if (shouldSync(event)) {
const { target: element, timeStamp } = event;
elements.add(element);
let diff;
if (elements.size == 1){
diff = 0;
lastIterationTimestamp = timeStamp;
}else diff = timeStamp - lastIterationTimestamp;
element.style.setProperty('animation-delay', `-${diff}ms`);
}
}
-And empty the body of init() method.
Here is the fixed using sample:
//Sample demo code
jQuery(function($){
window.cssAnimationSync('pulse-visible');
let animateGroup = function(selector){
let hideNext = function(){
let next = $(selector + ':visible:first');
if (next.length){
next.fadeOut();
setTimeout(hideNext, 200 + Math.random()*200);
}else setTimeout(showNext, 200 + Math.random()*200);
}
let showNext = function(){
let next = $(selector + ':hidden:first');
if (next.length){
next.fadeIn();
setTimeout(showNext, 200 + Math.random()*200);
}else setTimeout(hideNext, 200 + Math.random()*200);
};
showNext();
};
animateGroup('.pulsar_sync');
animateGroup('.pulsar');
});
//Fixed library code
/** #see https://github.com/bealearts/css-animation-sync */
window.cssAnimationSync = function(animationNameOrNames) {
const animationNames = new Set(
Array.isArray(animationNameOrNames) ? animationNameOrNames : [animationNameOrNames]
);
const elements = new Set();
let animationDuration;
let isPaused = false;
let lastIterationTimestamp = 0;
const api = {
getElements() {
return elements;
},
free() {
window.removeEventListener('animationiteration', animationIteration, true);
window.removeEventListener('animationstart', animationStart, true);
this.start();
elements.clear();
},
start() {
elements.forEach((el) => {
if (validate(el)) {
if (isPaused) {
el.style.removeProperty('animation-play-state');
} else {
el.style.removeProperty('animation');
}
}
});
isPaused = false;
},
stop() {
isPaused = false;
elements.forEach((el) => {
if (validate(el)) {
el.style.setProperty('animation', 'none');
}
});
},
pause() {
isPaused = true;
elements.forEach((el) => {
if (validate(el)) {
el.style.setProperty('animation-play-state', 'paused');
}
});
}
};
function shouldSync(event) {
return animationNames.has(event.animationName);
}
function validate(el) {
const isValid = document.body.contains(el);
if (!isValid) {
elements.delete(el);
}
return isValid;
}
function init() {
//setTimeout(restart, animationDuration);
}
function restart() {
api.stop();
setTimeout(api.start, 50);
}
function animationStart(event) {
if (shouldSync(event)) {
const { target: element, timeStamp } = event;
elements.add(element);
let diff;
if (elements.size == 1){
diff = 0;
lastIterationTimestamp = timeStamp;
}else diff = timeStamp - lastIterationTimestamp;
element.style.setProperty('animation-delay', `-${diff}ms`);
}
}
function cssToMs(time) {
const num = parseFloat(time);
let unit = time.match(/m?s/);
if (!unit) return 0;
[unit] = unit;
switch (unit) {
case 's':
return num * 1000;
case 'ms':
return num;
default:
return 0;
}
}
function animationIteration(event) {
if (shouldSync(event)) {
const { target: element, timeStamp } = event;
elements.add(element);
lastIterationTimestamp = timeStamp;
if (!animationDuration) {
animationDuration = cssToMs(window.getComputedStyle(element).animationDuration);
init();
}
}
}
function animationCancel(event) {
if (shouldSync(event)) {
elements.delete(event.target);
}
}
window.addEventListener('animationiteration', animationIteration, true);
window.addEventListener('animationstart', animationStart, true);
window.addEventListener('animationcancel', animationCancel, true);
return api;
};
#keyframes pulse-visible {
0% { opacity: 0.85;}
30% { opacity: 0.85;}
40% { opacity: 0.55;}
45% { opacity: 0;}
85% { opacity: 0;}
90% { opacity: 0.55;}
100% { opacity: 0.85;}
}
#keyframes pulse-visible-copy {
0% { opacity: 0.85;}
30% { opacity: 0.85;}
40% { opacity: 0.55;}
45% { opacity: 0;}
85% { opacity: 0;}
90% { opacity: 0.55;}
100% { opacity: 0.85;}
}
.pulsar
{
animation-name: pulse-visible-copy;
}
.pulsar_sync
{
animation-name: pulse-visible;
}
.pulsar, .pulsar_sync
{
animation-duration: 0.7s;
animation-iteration-count: infinite;
animation-timing-function: linear;
/*styles not depending on animation*/
display: inline-block;
width: 30px;
height: 30px;
margin: 5px;
border: 3px solid red;
border-radius: 25%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
With cssAnimationSync
<div style='min-height:50px;'>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
<div class="pulsar_sync" style="display: none;"></div>
</div>
Without
<div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
<div class="pulsar" style="display: none;"></div>
</div>