How do I stop propagation on a modal popup with keydown events?

442 Views Asked by At

I do not know if this is a stupid question but I just learned about stop propagation and am still getting used to how they work.

I am using a Headless UI dialog element, but I do not want key events to occur in the background. For example, I have made a program that updates the count whenever you press the "a" key. However, when the dialog popup shows up, I do not want the count to update in the background. I have added e.stopPropagation in the Dialog element to prevent key down events. It kind of works because the button is focused when you open the modal. When the button loses focus, the problem happens again and "a" will update the count.

Here is that implementation: https://codesandbox.io/p/sandbox/ecstatic-grass-896xzd

I have another count that updates whenever you press the escape key. There is a conflict because the escape key also hides the dialog, but I do not want the count to update when this happens.

I think I found a similar problem here and I tried to transfer that solution to my own problem, I do not know if I did it wrong or if the problems are comparable: Prevent event propagation on row click and dialog in material ui

I do know of a solution that puts the Dialog element "outside", like this: https://github.com/facebook/react/issues/11387#issuecomment-340019419

I am fine with this solution, but I am still curious if there is something more convenient.

1

There are 1 best solutions below

2
On

in handleKeyPress you need to check if the modal is open. If it is, return and do not proceed further.

to be able to check if the modal is open in KeyPressComponent, move isOpen above to App and pass it down to both components as props.

import React, { useState, useEffect, Fragment } from "react";
import { createRoot } from "react-dom/client";
import { Dialog, Transition } from "@headlessui/react";

function App() {
  let [isOpen, setIsOpen] = useState(true);

  return (
    <>
      <KeyPressComponent isOpen={isOpen} />
      <MyModal isOpen={isOpen} setIsOpen={setIsOpen} />
    </>
  );
}

function KeyPressComponent(props) {
  const { isOpen } = props;
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useEffect(() => {
    const handleKeyPress = (event) => {
      if (isOpen) return;
      if (event.key === "a") {
        setCount((count) => count + 1);
      }
      if (event.key === "Escape") {
        setCount2((count2) => count2 + 1);
      }
    };

    window.addEventListener("keydown", handleKeyPress);

    // Cleanup function to remove the event listener when the component unmounts
    return () => {
      window.removeEventListener("keydown", handleKeyPress);
    };
  }, [isOpen]); // rerun when isOpen changes

  return (
    <div>
      <p>Press the a key.</p>
      <p>Count: {count}</p>

      <p>Press the escape key.</p>
      <p>Count: {count2}</p>
    </div>
  );
}

function MyModal(props) {
  const { isOpen, setIsOpen } = props;

  function closeModal() {
    setIsOpen(false);
  }

  function openModal() {
    setIsOpen(true);
  }

  return (
    <>
      <div className="fixed inset-0 flex items-center justify-center">
        <button
          type="button"
          onClick={openModal}
          className="rounded-md bg-black/20 px-4 py-2 text-sm font-medium text-white hover:bg-black/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75"
        >
          Open dialog
        </button>
      </div>

      <Transition
        appear
        show={isOpen}
        as={Fragment}
        onKeyDown={(e) => e.stopPropagation()}
      >
        <Dialog as="div" className="relative z-10" onClose={closeModal}>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black/25" />
          </Transition.Child>

          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <Transition.Child
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <Dialog.Title
                    as="h3"
                    className="text-lg font-medium leading-6 text-gray-900"
                  >
                    Payment successful
                  </Dialog.Title>
                  <div className="mt-2">
                    <p className="text-sm text-gray-500">
                      Your payment has been successfully submitted. We’ve sent
                      you an email with all of the details of your order.
                    </p>
                  </div>

                  <div className="mt-4">
                    <button
                      type="button"
                      className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      onClick={closeModal}
                    >
                      Got it, thanks!
                    </button>
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  );
}

const root = createRoot(document.querySelector("#app"));
root.render(<App />);

demo: https://livecodes.io/?x=id/84rcv8trn5k