Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to handle closing animations? #9

Closed
asnaeb opened this issue Aug 17, 2024 · 4 comments · Fixed by #10
Closed

How to handle closing animations? #9

asnaeb opened this issue Aug 17, 2024 · 4 comments · Fixed by #10
Labels
enhancement New feature or request

Comments

@asnaeb
Copy link

asnaeb commented Aug 17, 2024

Libraries like Radix put a data attribute on their components based on open/closed state so that you can apply css animations accordingly. Whit this library, calling .end, causes the component to be immediately unmounted so the only way I can think of to apply closing animations is performing the animation via javascript, then calling .end on animation end. Is this how it should be done or am I missing something?

I really loved the idea behind this library and to use it according to my needs, I ended up writing my own version which is as follows

import {ReactNode, useEffect, useState} from "react";

interface Openable<Result> {
  resolve(value?: Result): void;
  state: {
    open: boolean;
    onOpenChange(value: boolean): void;
  };
}

function createOpenable<
  Args = void,
  Result = void,
  Props extends object = object,
>(UserComponent: (args: Props & {openable: Openable<Result>; args: Args}) => ReactNode, timeout = 0) {
  let $resolve: ((value?: Result) => void) | null = null;
  let $setOpen: ((value: boolean) => void) | null = null;
  let $setArgs: ((value: Args) => void) | null = null;

  return {
    Component(props: Props) {
      const [open, setOpen] = useState(false);
      const [args, setArgs] = useState({} as Args);

      useEffect(() => {
        $setOpen = setOpen;
        $setArgs = setArgs;
        return () => {
          $setOpen = null;
          $setArgs = null;
          $resolve = null;
        };
      }, []);

      const openable: Openable<Result> = {
        state: {
          open,
          onOpenChange(value) {
            if (!value) {
              $resolve?.();
              setTimeout(setArgs.bind(null, {} as Args), timeout);
            }
            setOpen(value);
          }
        },
        resolve(value: Result) {
          if (typeof $resolve !== "function") {
            throw Error();
          }
          $resolve(value);
          setOpen(false);
          setTimeout(setArgs.bind(null, {} as Args), timeout);
        }
      };

      return <UserComponent {...props} openable={openable} args={args as Args}/>;
    },
    open(args: Args) {
      if (typeof $setOpen !== "function") {
        throw Error();
      }
      if (args) {
        if (typeof $setArgs !== "function") {
          throw Error();
        }
        $setArgs(args);
      }
      $setOpen(true);
      return new Promise<Result | undefined>(resolve => $resolve = resolve);
    }
  };
}

export {createOpenable};

I don't need the component to stack, nor to be unmounted but I just want to open and close it imperatively. I would love to know if there are plans to support such a thing and if I can make anything to contribute!

@desko27 desko27 added the enhancement New feature or request label Aug 18, 2024
@desko27
Copy link
Owner

desko27 commented Aug 20, 2024

Hi @asnaeb! Thanks for your request and glad you liked the idea!

I'm really interested in supporting animations since they're pretty common nowadays. Here are a couple of constraints to consider though, both being important commitments to react-call:

  • 📦 Keeping a small bundle size (around 500B)
  • 🌀 Keeping flexibility for any use case

I like the call stack approach because it actually covers a variety of scenarios. Even if visual stacked elements are not desired, imagine nested modals, or even opening a new dialog when the closing animation from the previous one is still not finished.

Still, you're completely right: the component gets unmounted immediately which makes closing animations impossible. Please take a look at #10 in which I tried to cover all that's been discussed with:

  • An unmountingDelay argument: the end() method immediately resolves, but the component remains.
  • A call.ended boolean that may be used to place a className.

The bad news is that the bundle size exceeds 500B and I can see that it's gonna be hard to impossible to reduce it enough to fit in. But I still managed to get <550B, which may be all right considering how usual animations are on the web.

@desko27
Copy link
Owner

desko27 commented Aug 20, 2024

I've published react-call@next as a pre-release for testing purposes. Could you please give it a try and let me know?

Of course, any idea on how to reduce the bundle size is more than welcome!

@asnaeb
Copy link
Author

asnaeb commented Aug 20, 2024

I tested it and I've been able to achieve what I wanted by setting the delay to the animation duration. A minimal example using Radix Dialog component would be

import * as Dialog from "@radix-ui/react-dialog";

const CallableDialog = createCallable(({call}) => (
  <Dialog.Root open={!call.ended} onOpenChange={value => !value && call.end()}>
    <Dialog.Content className="data-[state=closed]:animate-my-animation">
      <button onClick={() => call.end()}>close</button>
    <Dialog.Content>
  </Dialog.Root>
), 200);

Setting a delay was the same conclusion I had thought of in my take when I came across the issue that new args got cleaned up before the animation ended (just updated to show it). So this looks good to me. Plus, I don't think that 500 or 550B would be that much of a difference honestly.

@desko27
Copy link
Owner

desko27 commented Aug 22, 2024

This will get delivered in v1.3.0. @asnaeb thanks for contributing!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants