Add ReusableContextMenu component
authorAjay Bura <ajbura@gmail.com>
Tue, 11 Jan 2022 15:13:40 +0000 (20:43 +0530)
committerAjay Bura <ajbura@gmail.com>
Tue, 11 Jan 2022 15:13:40 +0000 (20:43 +0530)
Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/atoms/context-menu/ReusableContextMenu.jsx [new file with mode: 0644]
src/util/common.js

diff --git a/src/app/atoms/context-menu/ReusableContextMenu.jsx b/src/app/atoms/context-menu/ReusableContextMenu.jsx
new file mode 100644 (file)
index 0000000..f3a3c0d
--- /dev/null
@@ -0,0 +1,84 @@
+import React, { useState, useEffect, useRef } from 'react';
+
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+
+import ContextMenu from './ContextMenu';
+
+let key = null;
+function ReusableContextMenu() {
+  const [data, setData] = useState(null);
+  const openerRef = useRef(null);
+
+  const closeMenu = () => {
+    key = null;
+    if (data) openerRef.current.click();
+  };
+
+  useEffect(() => {
+    if (data) {
+      const { cords } = data;
+      openerRef.current.style.transform = `translate(${cords.x}px, ${cords.y}px)`;
+      openerRef.current.style.width = `${cords.width}px`;
+      openerRef.current.style.height = `${cords.height}px`;
+      openerRef.current.click();
+    }
+    const handleContextMenuOpen = (placement, cords, render) => {
+      if (key) {
+        closeMenu();
+        return;
+      }
+      setData({ placement, cords, render });
+    };
+    navigation.on(cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED, handleContextMenuOpen);
+    return () => {
+      navigation.removeListener(
+        cons.events.navigation.REUSABLE_CONTEXT_MENU_OPENED,
+        handleContextMenuOpen,
+      );
+    };
+  }, [data]);
+
+  const handleAfterToggle = (isVisible) => {
+    if (isVisible) {
+      key = Math.random();
+      return;
+    }
+    if (setData) setData(null);
+
+    if (key === null) return;
+    const copyKey = key;
+    setTimeout(() => {
+      if (key === copyKey) key = null;
+    }, 200);
+  };
+
+  return (
+    <ContextMenu
+      afterToggle={handleAfterToggle}
+      placement={data?.placement || 'right'}
+      content={data?.render(closeMenu) ?? ''}
+      render={(toggleMenu) => (
+        <input
+          ref={openerRef}
+          onClick={toggleMenu}
+          type="button"
+          style={{
+            width: '32px',
+            height: '32px',
+            backgroundColor: 'transparent',
+            position: 'fixed',
+            top: 0,
+            left: 0,
+            padding: 0,
+            border: 'none',
+            visibility: 'hidden',
+            appearance: 'none',
+          }}
+        />
+      )}
+    />
+  );
+}
+
+export default ReusableContextMenu;
index c3745838227f08e5a60bf871864ab53f61a7a053..914242088ceb990f1ab8674971448c4564e0414d 100644 (file)
@@ -21,11 +21,28 @@ export function isInSameDay(dt2, dt1) {
   );
 }
 
-export function getEventCords(ev) {
-  const boxInfo = ev.target.getBoundingClientRect();
+/**
+ * @param {Event} ev
+ * @param {string} [targetSelector] element selector for Element.matches([selector])
+ */
+export function getEventCords(ev, targetSelector) {
+  let boxInfo;
+
+  const path = ev.nativeEvent.composedPath();
+  const target = targetSelector
+    ? path.find((element) => element.matches?.(targetSelector))
+    : null;
+  if (target) {
+    boxInfo = target.getBoundingClientRect();
+  } else {
+    boxInfo = ev.target.getBoundingClientRect();
+  }
+
   return {
     x: boxInfo.x,
     y: boxInfo.y,
+    width: boxInfo.width,
+    height: boxInfo.height,
     detail: ev.detail,
   };
 }