0

My App.js is as below

export const App = () => {
  const [toggled, setToggled] = useState(false);
  const [ordering, setOrdering] = useState(false);

  const handleColorModeClick = () => {
    setToggled((s) => !s);
  };

  const handleOrdering = () => {
    setOrdering((s) => !s);
  };

  return (
    <Ordering.Provider value={{ ordering: ordering }}>
      <div className={`app ${toggled ? "theme-dark" : "theme-light"}`}>
        <Switch>
          <Route path="/" exact>
            <HeaderComponent toggled={toggled} onClick={handleColorModeClick} />
            <div>components2</div>
            <EateryInfo toggled={toggled} />
            {/* <CategoryItems toggled={toggled} /> */}
            <MenuButton toggled={toggled} />
          </Route>
          <Route path="/menu">
            <HeaderComponent toggled={toggled} onClick={handleColorModeClick} />
            <CategoryItems toggled={toggled} />
            <CheckBox
              text="Start Ordering"
              standAlone={true}
              handleOrdering={handleOrdering}
            />
            <MenuButton toggled={toggled} />
          </Route>
        </Switch>
      </div>
    </Ordering.Provider>
  );
};

I set the state of ordering variable using a checkbox

Then I use this to conditionally render the QuantityChange component like so

export const MenuEntry = ({ mealData, toggled }: MenuEntryProps) => {
  const orderingEnabled = useContext(Ordering);

  return (
    <div className="menu-entry">
      <MenuItem oneMenuItem={mealData} toggled={toggled} />
      {orderingEnabled.ordering ? <QuantityChange toggled={toggled} /> : ""}
    </div>
  );
};

All this works fine & the component is render as desired.

I want to have a smooth transition of entry & exit of this component. The animation on entry works just fine but I am not able to figure out how to get the exit animation working.

The video is what is happening now can be found in the video here https://youtu.be/5kl1wCBwR_U (the checkbox is at the right bottom hand corner)

I looked at several online forums to find an answer to this but I am unable to figure it out.

I tried usiing react-transition-group as well but no luck

export const QuantityChange = ({ toggled }: QuantityChangeProps) => {
  const orderingEnabled = useContext(Ordering);
  const duration = 500;
  return (
    <Transition in={orderingEnabled.ordering} timeout={duration} appear>
      {(status) => (
        <div
          className={`quantity-change flex ${
            toggled ? "theme-dark" : "theme-light"
          } fade-${status}`}
        >
          <span className="add-quantity">+</span>
          <span className="quantity">0</span>
          <span className="subtract-quantity">-</span>
        </div>
      )}
    </Transition>
  );
};

I looked at onAnimationEnd but was unable to figure it out.

moys
  • 7,747
  • 2
  • 11
  • 42
  • You could try out to have a css class that does the animation and then adding useEffect with cleanup part (`return () => doStuff()`) that would get triggered before component will unmount and you could use setTimeout within this cleanup function that would set this css class – JSEvgeny Jan 17 '22 at 10:22
  • @JSEvgeny I think you are referring to something like in this question https://stackoverflow.com/questions/61428958/react-js-how-to-animate-conditionally-rendered-components I tried that & it does not work. The reason is that the state change in happening on the `checkBox` and the animation has to happen on a separate component. – moys Jan 17 '22 at 10:30

1 Answers1

1

Looks like you need a simple Accordion thingy. You could try something like that (snippet below).

One of the main moments here is setting the height to the auto value. It allows the content to change, and it won't strict its dimensions.

AccordionItem conditionally renders its children. If it should be closed and the animation is over, then no children will be rendered.

const AccordionItem = (props) => {
  const { className, headline, open, children } = props

  const [height, setHeight] = React.useState(0)
  const [isOver, setOver] = React.useState(false)
  const bodyRef = React.useRef(null)

  const getDivHeight = React.useCallback(() => {
    const { height } = bodyRef.current ? bodyRef.current.getBoundingClientRect() : {}

    return height || 0
  }, [])

  // set `auto` to allow an inner content to change
  const handleTransitionEnd = React.useCallback(
    (e) => {
      if (e.propertyName === 'height') {
        setHeight(open ? 'auto' : 0)
        if (!open) {
          setOver(true)
        }
      }
    },
    [open]
  )

  React.useEffect(() => {
    setHeight(getDivHeight())
    setOver(false)
    
    if (!open) {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => setHeight(0))
      })
    }

  }, [getDivHeight, open])
  
  const shouldHide = !open && isOver

  return (
    <div style={{overflow: 'hidden'}}>
      <div
        style={{ height, transition: "all 2s" }}
        onTransitionEnd={handleTransitionEnd}
      >
        <div ref={bodyRef}>
          {shouldHide ? null : children}
        </div>
      </div>
    </div>
  )
}



const App = () => {
  const [open, setOpen] = React.useState(false)

  return (
    <div>          
      <button onClick={() => setOpen(isOpen => !isOpen)}>toggle</button>
      
      <table style={{width: '100%'}}>
        <tr>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
        </tr>
         <tr>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
        </tr>
         <tr>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
          <td>
            Hot Pongal
            <AccordionItem open={open}>
              <button>-</button>
              <input  />
              <button>+</button>
             </AccordionItem> 
          </td>
        </tr>
      </table>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>


<div id="root"></div>
Ivan Burnaev
  • 2,690
  • 18
  • 27
  • Updated my snippet. Now the `AccordionItem` conditionally renders its children. If it should be closed and the animation is over, then no children will be rendered. – Ivan Burnaev Jan 17 '22 at 11:19
  • Ivan, even in your code, only entry of the children in automated, the exit is however, abrupt. I want the exit to be also smooth. – moys Jan 17 '22 at 11:57
  • Ah, hold on... looks like it doesn't for as it should in Chrome. Let me fix it. – Ivan Burnaev Jan 17 '22 at 12:13
  • @moys, it's done. I had to wrap the "closing part" to extra `requestAnimationFrame` cuz Chrome and Safari manage the order of a script calling differently a bit. – Ivan Burnaev Jan 17 '22 at 12:27