react-router-transition icon indicating copy to clipboard operation
react-router-transition copied to clipboard

Transition without `position: absolute;`

Open arkist opened this issue 9 years ago • 26 comments

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:

arkist avatar Feb 12 '16 14:02 arkist

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

  1. 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
  2. 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!

abelovic avatar Feb 12 '16 16:02 abelovic

@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.

maisano avatar Feb 12 '16 22:02 maisano

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 avatar Feb 13 '16 01:02 amorino

@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.

maisano avatar Feb 13 '16 02:02 maisano

@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 avatar Feb 13 '16 02:02 arkist

@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 avatar Feb 13 '16 03:02 maisano

@maisano good :) I'll try to find a way, too. thanks for this project!

arkist avatar Feb 13 '16 13:02 arkist

@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 }

abelovic avatar Feb 17 '16 07:02 abelovic

Sorry code snippet got removed

<Link to={{ pathname: '/Home', state: {transition: slideRight} }} />

abelovic avatar Feb 17 '16 07:02 abelovic

@abelovic thanks for the awesome tip! :) browser support is a problem only. http://caniuse.com/#search=calc

arkist avatar Feb 17 '16 07:02 arkist

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 :)

abelovic avatar Feb 17 '16 18:02 abelovic

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)

JoeMattie avatar Feb 21 '16 15:02 JoeMattie

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.

haustraliaer avatar Jun 29 '16 01:06 haustraliaer

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>

albermav avatar Jul 05 '16 21:07 albermav

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 + ')'
      }
    }}
  >

akeelm avatar Feb 02 '17 15:02 akeelm

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 ?

jaybe78 avatar Feb 16 '17 23:02 jaybe78

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.

JurJean avatar Apr 15 '17 13:04 JurJean

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,
  }),
};

aakarim avatar Apr 28 '17 12:04 aakarim

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?

uxlayouts avatar Jun 17 '17 21:06 uxlayouts

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?

markusv avatar Jul 08 '17 09:07 markusv

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.

Xerios avatar Aug 04 '17 14:08 Xerios

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.

RomainLanz avatar Sep 07 '17 14:09 RomainLanz

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 )

Xerios avatar Sep 07 '17 16:09 Xerios

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

markusv avatar Sep 14 '17 19:09 markusv

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 + ')' } }}

sazedulhaque avatar Oct 24 '17 05:10 sazedulhaque

here's a very simple example to get the basics

https://github.com/ueeieiie/simple-route-transitions

ueeieiie avatar Dec 26 '17 17:12 ueeieiie