react-router-transition
react-router-transition copied to clipboard
Transition without `position: absolute;`
In demos,
<Lorem />
Component's position is set to absolute
, So transition seems to work well.
https://github.com/maisano/react-router-transition/blob/master/demos/index.html#L36
but If you can't set router's position to absolute, (like each router's height are different and it can be changed) transition wouldn't work well.
I've tried to set position: absolute
on atLeave
only, but it didn't work.
Also, I couldn't figure out how to handle it using mapStyes
.
Any ideas? :disappointed:
I achieved this by applying the css to the inner div (created in renderRoute). To do this I added the css through mapStyles in my presets (see noTransition below):
const fadeTransitionConfig = { stiffness: 200, damping: 22 };
const popTransitionConfig = { stiffness: 360, damping: 25 };
const slideTransitionConfig = { stiffness: 330, damping: 30 };
noTransition = {
atEnter: {
opacity: 1,
scale: 1,
offset: 0
},
atLeave: {
opacity: spring(1, fadeTransitionConfig),
scale: spring(1, popTransitionConfig),
offset: spring(0, slideTransitionConfig)
},
atActive: {
opacity: spring(1, fadeTransitionConfig),
scale: spring(1, popTransitionConfig),
offset: spring(0, slideTransitionConfig)
},
mapStyles(styles) {
return {
position: 'absolute',
boxSizing: 'border-box',
width: '100%',
height: '100%',
opacity: styles.opacity,
transform: 'translateX(' + styles.offset + '%) scale(' + styles.scale + ')'
}
}
};
fadeTransition = {
atEnter: Object.assign({}, noTransition.atEnter, { opacity: 0 }),
atLeave: Object.assign({}, noTransition.atLeave, { opacity: spring(0, fadeTransitionConfig) }),
atActive: Object.assign({}, noTransition.atLeave, { opacity: spring(1, fadeTransitionConfig) }),
mapStyles: noTransition.mapStyles
};
popTransition = {
atEnter: Object.assign({}, noTransition.atEnter, { scale: 0.8 }),
atLeave: Object.assign({}, noTransition.atLeave, { scale: spring(0.8, popTransitionConfig) }),
atActive: Object.assign({}, noTransition.atLeave, { scale: spring(1, popTransitionConfig) }),
mapStyles: noTransition.mapStyles
};
slideLeftTransition = {
atEnter: Object.assign({}, noTransition.atEnter, { offset: 100 }),
atLeave: Object.assign({}, noTransition.atLeave, { offset: spring(-100, slideTransitionConfig) }),
atActive: Object.assign({}, noTransition.atLeave, { offset: spring(0, slideTransitionConfig) }),
mapStyles: noTransition.mapStyles
};
slideRightTransition = {
atEnter: Object.assign({}, noTransition.atEnter, { offset: -100 }),
atLeave: Object.assign({}, noTransition.atLeave, { offset: spring(100, slideTransitionConfig) }),
atActive: Object.assign({}, noTransition.atLeave, { offset: spring(0, slideTransitionConfig) }),
mapStyles: noTransition.mapStyles
};
Implementation:
<RouteTransition pathname={this.props.location.pathname} {...this.state.transition}>
{
React.cloneElement(this.props.children, {setTransition: this.setTransition})
}
</RouteTransition>
From the child I pass the preset when I click on the react-router link for example, and set whatever transition state I want on RouteTransition
@maisano - I ended up doing the above (having a base i.e. 'noTransition') because
- My app base has a RouteTransition but I may not want to transition so basically that is the default. Avoids having to change the structure of the container
- More importantly, I found that if I only mapped the styles for the particular transition and I switched between "different ones" i.e. fade and slide it wouldn't transition correctly. I also avoided React complaining that I mutated the props because they are different ;) This fixed it and it seems to work well now
Would like to hear your thoughts on this approach i.e. performance considerations interpolating props that don't actually change. I suspect it shouldn't be an issue but not 100% sure.
Thanks for this wrapper - excellent work!
@arkist: hm, i haven't given this a ton of thought–to be honest, most of the times i have animated routes, they've been wrapped in such a way where height was not an issue (they made up the entire page or the overflow was scrollable, etc). can you give some more background into your layout/how you're using this?
@abelovic: i'm glad you're enjoying the project! thanks for the feedback. you're correct in your approach if you want to dynamically toggle between different transition settings–react-motion
requires the same keys to interpolate, and introducing new ones/removing old ones won't work.
as per your question on performance, i don't see this being an issue. the api recently changed and i haven't looked at the internals since, though i recall TransitionMotion
only needing to interpolate keys when their destination values change. in this example, nothing changes so you should be fine.
one small thing i did notice is that you're cloning children to pass in the setTransition
callback–if you wanted to avoid this you could use state from history instead, pulling it out in your root component as needed, e.g. using a Link
with a state
prop (<Link to={path} state={{transition: 'slideLeft'}}>
) where you reference that state in your handler via this.props.history.state.transition
.
I think that the issue he's referring (it's just a guess) is this: https://gfycat.com/JadedUnrealisticBoto Without an absolute position, this will happen. (This is using the presets.pop)
@amorino i debated for a while whether to add absolute positions to the presets, or even include the presets at all. it probably makes more sense to add position: absolute
to the mapStyles
of each preset. it's easy enough to override and they're sort of broken without. feel free to pr or i can update this sometime over the weekend.
@amorino exactly. that's the what I encountered. @maisano I'm a newbie in motion/transitioning so don't know this issue is common or not. see below codes, please.
What I learn from @abelovic's code is set position: absolute; width: 100%; height: 100%
and put all the Component inside of <RouteTransition />
works fine. thanks! and noTransition-default strategy looks good.
but I think it seems to be still a problem: how about nested Component's transition? (not about path)
// <Header/> and <Footer/> don't need transition (always there)
// so they're on outside of <RouteTransition />.
// in this case, you can't set `position: absolute` to route because of Header&Footer.
<Header />
<RouteTransition
pathname={this.props.location.pathname}
atEnter={{opacity: 0, position: 'relative'}}
atLeave={{opacity: 0, position: 'absolute'}}
atActive={{opacity: 1, position: 'relative'}}
mapStyles={styles => {
return {
position: styles.position, // so I wanna something like this but it is impossible! :(
opacity: styles.opacity
};
}}
>
{this.props.children}
</RouteTransition>
<Footer />
@arkist: ah, got it. so the issue with the code posted is that the objects that get passed to atEnter
, atLeave
and atActive
get passed to react-motion for numeric interpolation–this won't work with strings.
i'm not currently sure the best way to handle dynamic, unbounded route heights, though i'm sure this is a common use case. i'll have to give it some more thought.
@maisano good :) I'll try to find a way, too. thanks for this project!
@maisano - thanks for the tip that is a much cleaner way to do it :) The API of react router changed in 2.0 so if anyone is interested this is what I did:
The mixin ContextRoute is deprecated so you will have to get the location object by passing context (its explained in the 2.0.0 change docs)
@arkist - I actually transition my header so the label and buttons etc... are encapsulated inside the react component (my view). If you do this one trick I did is to make the container color the same color as the header otherwise you will see the white background (or whatever color your container is) as the transition occurs.
However it sounds like you would rather have a fixed header where the content is the same for all views you transition to i.e. a common menu or application header
Could you not just do the following (untested)?
.header { height: 50px top: 0 position: absolute }
/* this is passed using mapStyles / .route-transition { height: calc(100% - 100px) / height of header and footer - since it is app specific probably ok to just add it to your presets? */ top: 50px position: absolute }
.footer { height: 50px bottom: 0 position: absolute }
Sorry code snippet got removed
<Link to={{ pathname: '/Home', state: {transition: slideRight} }} />
@abelovic thanks for the awesome tip! :) browser support is a problem only. http://caniuse.com/#search=calc
Well looks like you would mainly loose Opera mini (Blackberry I think) and IE 8 support but look at this:
https://facebook.github.io/react/blog/2016/01/12/discontinuing-ie8-support.html
For me this is not a problem and I use calc all the time. If you do need to support these browsers however you might be able to use something like:
https://github.com/souporserious/react-measure
and just do the calculations in react :)
Just my $0.02:
Instead of percentage I am using:
height:calc(100vh - 50px);
in my mapStyles() function instead of percentage. This makes it so that you don't have to worry about the geometry of the parent component. (100vh is the height of the window in pixels, 50px is the height of my header)
This may not be the best solution - but I managed to get @arkist's desired effect by swapping the non-numeric values when one of the animating properties reached a desired value inside the mapStyles function:
<RouteTransition
pathname={this.props.location.pathname}
atEnter={{ opacity: 0 }}
atLeave={{ opacity: 0 }}
atActive={{ opacity: 1 }}
mapStyles={(styles) => {
return {
position: (styles.opacity === 1) ? undefined: 'absolute',
width: (styles.opacity === 1) ? undefined : '100%',
height: (styles.opacity === 1) ? undefined : '100%',
opacity: styles.opacity,
}
}}>
{this.props.children}
</RouteTransition>
Now I don't need to worry about my transitioning elements blowing out the layout, but also as soon as they're done animating in I can remove the style and they'll revert to their original css.
I have a similar solution, it just fades only the appearing div, and hides the older one:
<RouteTransition
pathname={this.props.location.pathname}
atEnter={{ opacity: 0 }}
atLeave={{ opacity: 2 }}
atActive={{ opacity: 1 }}
mapStyles={styles => {
if(styles.opacity > 1){
return { display: 'none'}
}
return { opacity: styles.opacity}
}}
>
{this.props.children}
</RouteTransition>
I had issues with this too, but even with some of the workarounds here, couldn't get it working properly.
So here's my amendment to one of the workarounds. Notice I do the switch to absolute positioning just before the div disappears.
<RouteTransition
pathname={this.props.location.pathname}
atEnter={{ opacity: 0 }}
atLeave={{ opacity: 0 }}
atActive={{ opacity: 1 }}
mapStyles={(styles) => {
return {
position: (styles.opacity > 0.3) ? 'relative': 'absolute',
boxSizing: 'border-box',
width: '100%',
height: '100%',
opacity: styles.opacity,
transform: 'translateX(' + styles.offset + '%) scale(' + styles.scale + ')'
}
}}
>
Hello,
@akeelm The fade effect works quite well with your code. However do you think we could achieve the same for slide effect without absolute positionning ?
The solution from @akeelm is very nice, but I still had a problem with the footer of the page to toggle up and down. Using code below this is fixed.
<RouteTransition
pathname={this.props.location.pathname}
atEnter={{ opacity: 0, foo: 0 }}
atLeave={{ opacity: 0, foo: 2 }}
atActive={{ opacity: 1, foo: 1 }}
mapStyles={(styles) => {
return {
position: (styles.foo <= 1) ? 'relative': 'absolute',
width: '100%',
height: '100%',
opacity: styles.opacity
}
}}>
I only still have the footer problem when I toggle routes really quick.
I have modified @JurJean 's solution with a pop config. Works great. Same issue with fast route transitions, though (which is a shame, as one of the motivations for react-motion was to improve UX for cancelled transitions).
const popConfig = { stiffness: 360, damping: 25 };
export const pop = {
atEnter: {
transitionIndex: 0,
scale: 0.8,
opacity: 0,
},
atLeave: {
scale: spring(0.8, popConfig),
opacity: spring(0, popConfig),
transitionIndex: 2,
},
atActive: {
scale: spring(1, popConfig),
opacity: 1,
transitionIndex: 1,
},
mapStyles: styles => ({
position: styles.transitionIndex <= 1 ? 'relative' : 'absolute',
width: '100%',
height: '100%',
transform: `scale(${styles.scale})`,
opacity: styles.opacity,
}),
};
It would be so helpful to have a codepen example for some of these. I've switched from easy-transition to react-motion to react-router-transition but transitionX fails for all of them. I know this can be fixed. Has someone tried setting the height of the children components\pages with JS or using a css property that does not require position:absolute?
I have tried @JurJean solution works great. Except for when switching route back before the first animation is done. Then you have both elements having position absolute, which means the container collapses until the animations are done. Any solution for this?
For those looking for an answer to this problem, look no further! I have found a solution of making the transitions overlap without using position:absolute or floats.
How did I do it? Simple, use CSS Grid on the parent, then have both of the childs use the same grid-area. This will make both of the elements use the same area, allowing them to overlap while keeping the layout completely functional as before.
The only drawback is IE and it's partial support for CSS Grids, but if you live in the present like most of us then it shouldn't be a big of an issue. Otherwise, you can simply disable the transition for the unsupported browsers.
Hey @Xerios 👋
Could you share some code with your solution?
On my project I'm using flexbox
everywhere and I have some sort of "jumping" when I use @aakarim solution.
Unfortunately, I didn't save my code because I had changed my mind about using transitions, but you should be able to do it by doing something like:
// Name of the container, forgot the name
.container {
display: grid;
grid-template-areas:"MyCustomAreaName";
}
// Forces all children under container to be in the same space/area
.container > div{
grid-area: MyCustomAreaName;
}
Note that I don't remember the names of the classes, so I suggest you check them using the Inspector, the child class should be the one that has opacity and all the stuff, while the parent is the container.
Also if you want a wider browser support, you should do the same thing without using "area" feature. ( e.g. grid-template-columns
and things like that )
good idea. I am now using grid if support. I check for grid with css feature detection and fallback to position absolute/relative if not supported. Thanks
Thanks @akeelm For the solution. I face slightly different type of problem my Carousel was not working and after applying your following code
mapStyles={(styles) => { return { position: (styles.opacity > 0.3) ? 'relative': 'absolute', boxSizing: 'border-box', width: '100%', height: '100%', opacity: styles.opacity, transform: 'translateX(' + styles.offset + '%) scale(' + styles.scale + ')' } }}
here's a very simple example to get the basics
https://github.com/ueeieiie/simple-route-transitions