toss / overlay-kit

A declarative way to manage overlays in React.
https://overlay-kit.slash.page
MIT License
146 stars 9 forks source link

[Question] Could we access open state from outside? #19

Open ojj1123 opened 4 days ago

ojj1123 commented 4 days ago

Problem

If using useOverlay hook of slash, we couldn't handle this use case:

function Component() {
  const overlay = useOverlay()

  // HOW could I access overlay state such as open/close state?

  return (
    <button onClick={() => overlay.open({ isOpen, close } => <Dialog isOpen={isOpen} close={close} />)}>open overlay</button>
  )
}

overlay-kit does not provide useOverlay hook, but it has the same interface as overlay object returned by useOverlay. That is, overlay state is only known inside Dialog component, and there is no way to know the state outside the callback function of overlay.open function. This can cause the following problem:

function Component() {
  return (
    <button onClick={() => overlay.open({ isOpen, close } => <Dialog isOpen={isOpen} close={close} />)}>open overlay</button>
    {/** UI according to overlay state but.. I couldn't do that */}
  )
}

There are cases where you need to change the UI, not the Dialog, depending on overlay state(open/close). I don't think this use case is uncommon. I've been in this situation too.

Question

Has this use case ever been required in Toss products? If so, can't we use the API provided by overlay-kit?

jungpaeng commented 4 days ago

Thank you for your interest in overlay-kit and for your question!

First, it seems that manipulating the UI outside the overlay based on the open state of the overlay is not a common scenario, especially given the nature of mobile products. Therefore, this was not a significant consideration when use-overlay was created, and it was designed so that each useOverlay manages a single overlay.

Overlay-kit aims to address this issue more effectively. It normalizes and stores the state of open overlays, which should meet these requirements.

Although it is currently unavailable, we will soon provide an API that allows accessing the overlay state from outside the overlay. Thank you.

ojj1123 commented 4 days ago

based on the open state of the overlay is not a common scenario, especially given the nature of mobile products.

I got why useOverlay deals with a single overlay. Thx!

It normalizes and stores the state of open overlays, which should meet these requirements.

I'm wondering what you are thinking about overlay interface handling this requirement. If you have the idea, please share with me! And if I have, I will do that too :)

jungpaeng commented 4 days ago

Before proceeding with the task, I have a question.

The following example modifies the code in the examples/framer-motion/src/demo.tsx file for testing:

function DemoWithEsOverlay() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <p>Demo with overlay-kit isOpen: {isOpen ? 'true' : 'false'}</p>
      <button
        onClick={() => {
          setIsOpen(true);

          overlay.open(({ isOpen, close, unmount }) => {
            function closeModal() {
              setIsOpen(false);
              close();
            }

            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                <div>
                  <p>MODAL CONTENT</p>
                  <button onClick={closeModal}>close modal</button>
                </div>
              </Modal>
            );
          });
        }}
      >
        open modal
      </button>
    </div>
  );
}

By working in this manner, the setIsOpen function operates together when opening and closing the overlay, allowing the open state of the overlay to be checked through an external state.

Are there any issues that cannot be resolved using this method?

jungpaeng commented 4 days ago

I'm wondering what you are thinking about overlay interface handling this requirement.

Separately, the method I have in mind is as follows.

Currently, overlay.open returns overlayId and also passes overlayId through a callback function. Therefore, by using overlay.open and setState, you can get the ID of the currently triggered overlay.

In code, it would look like this:

function DemoWithEsOverlay() {
  const [overlayId, setOverlayId] = useState<string>();

  return (
    <div>
      <p>overlayId: {overlayId}</p>
      <button
        onClick={() => {
          const overlayId = overlay.open(() => <Modal />);
          setOverlayId(overlayId);
        }}
      >
        open modal
      </button>
    </div>
  );
}

Additionally, the OverlayProvider has a value overlayState.overlayData that can check the isOpen status of the currently managed overlay. I planned to access this using useContext.

By doing so, I thought that by using the useOverlayData() hook and the overlayId value obtained above, it would be possible to know the isOpen status of the corresponding overlay.

const overlayData = useOverlayData();
const [overlayId, setOverlayId] = useState<string>();

...

overlayData[overlayId].isOpen; // Check the isOpen status of the overlay with the given overlayId
ojj1123 commented 3 days ago

By working in this manner, the setIsOpen function operates together when opening and closing the overlay, allowing the open state of the overlay to be checked through an external state.

I think this workaround is good. I've tried already but I was considering the use case below:

Using the multiple overlays

If using the multiple overlays and it needs to access those states outside, maybe we need to do this:

function DemoWithEsOverlay() {
  // if we want to access those states, we should define the state each other.
  const [isOpen1, setIsOpen1] = useState(false);
  const [isOpen2, setIsOpen2] = useState(false);

  return (
    <div>
      <p>Demo with overlay-kit isOpen1: {isOpen1 ? 'true' : 'false'}</p>
      <p>Demo with overlay-kit isOpen2: {isOpen2 ? 'true' : 'false'}</p>
      <button
        onClick={() => {
          setIsOpen1(true);

          overlay.open(({ isOpen, close, unmount }) => {
            function closeModal() {
              setIsOpen1(false);
              close();
            }

            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                <div>
                  <p>MODAL CONTENT</p>
                  <button onClick={closeModal}>close modal</button>
                </div>
              </Modal>
            );
          });
        }}
      >
        open modal1
      </button>

      <button
        onClick={() => {
          setIsOpen2(true);

          overlay.open(({ isOpen, close, unmount }) => {
            function closeModal() {
              setIsOpen2(false);
              close();
            }

            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                <div>
                  <p>MODAL CONTENT</p>
                  <button onClick={closeModal}>close modal</button>
                </div>
              </Modal>
            );
          });
        }}
      >
        open modal2
      </button>
    </div>
  );
}

If we want to access those open state, we need to define those states using useState and then do write the same code that sync those overlay state. So if this case is common, I hope the feature that let our access the overlay state, is provided by overlay-kit. But I don't know this case is common. So If this use case is required in Toss products enough, Please let me know later.

jungpaeng commented 1 day ago

I was thinking about how to handle the overlay interface for this requirement and wanted to share a different approach I have in mind.

We could add an isActive function to the overlay object and modify the overlay.open function to accept a custom ID as a second argument. This way, we can effectively check if a specific modal is open without managing additional state.

Here’s how it could be implemented:

function DemoWithEsOverlay() {
  return (
    <div>
      <p>overlay1 isOpen: {overlay.isActive('overlay-1') ? 'true' : 'false'}</p>
      <p>overlay2 isOpen: {overlay.isActive('overlay-2') ? 'true' : 'false'}</p>
      <button
        onClick={() => {
          overlay.open(({ isOpen, close, unmount }) => {
            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                <div>
                  <p>MODAL CONTENT</p>
                  <button onClick={close}>close modal</button>
                </div>
              </Modal>
            );
          }, { overlayId: 'overlay-1' });
        }}
      >
        open modal1
      </button>

      <button
        onClick={() => {
          overlay.open(({ isOpen, close, unmount }) => {
            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                <div>
                  <p>MODAL CONTENT</p>
                  <button onClick={close}>close modal</button>
                </div>
              </Modal>
            );
          }, { overlayId: 'overlay-2' });
        }}
      >
        open modal2
      </button>
    </div>
  );
}

This should help streamline the process and eliminate the need for declaring and managing state manually.

I wanted to get your thoughts on this approach and see if there are any concerns before we move forward with the implementation.

ojj1123 commented 20 hours ago

I think custom id is good. But is it possible to re-render the component(DemoWithEsOverlay above) using overlay.isActive()?

As your demo code, if opening the modal with overlay.open() in DemoWithEsOverlay, it couldn't re-render this component because there are no states that trigger to re-render. So I'm wondering it is possible to know the open states with overlay.isActive(). I feel like that it needs to access overlayData to trigger the re-rendering:

function DemoWithEsOverlay() {
  // If overlay.open() is called, overlayData is changed and then trigger to re-render.
  const overlayData = useOverlayData();

  return (
    <div>
      <p>overlay1 isOpen: {overlayData['overlay-1'].isOpen ? 'true' : 'false'}</p>
      <p>overlay2 isOpen: {overlayData['overlay-2'].isOpen ? 'true' : 'false'}</p>
      <button
        onClick={() => {
          overlay.open(({ isOpen, close, unmount }) => {
            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                ...
              </Modal>
            );
          }, { overlayId: 'overlay-1' });
        }}
      >
        open modal1
      </button>

      <button
        onClick={() => {
          overlay.open(({ isOpen, close, unmount }) => {
            return (
              <Modal isOpen={isOpen} onExit={unmount}>
                ...
              </Modal>
            );
          }, { overlayId: 'overlay-2' });
        }}
      >
        open modal2
      </button>
    </div>
  );
}