This is an automated email from the ASF dual-hosted git repository.

tiagobento pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git


The following commit(s) were added to refs/heads/main by this push:
     new e23410b150e NO-ISSUE: DMN Editor: Boxed Expression context menu 
renders behind other panels (#2350)
e23410b150e is described below

commit e23410b150e402cad9210638baacad396f50e02c
Author: Daniel José dos Santos <[email protected]>
AuthorDate: Wed May 22 19:50:01 2024 -0300

    NO-ISSUE: DMN Editor: Boxed Expression context menu renders behind other 
panels (#2350)
---
 .../ExpressionDefinitionLogicTypeSelector.tsx      | 355 +++++++++++++--------
 .../table/BeeTable/BeeTableContextMenuHandler.tsx  |  25 +-
 2 files changed, 237 insertions(+), 143 deletions(-)

diff --git 
a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx
 
b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx
index c9a926ba1bd..85ce74868c7 100644
--- 
a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx
+++ 
b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx
@@ -33,11 +33,10 @@ import { RebootingIcon } from 
"@patternfly/react-icons/dist/js/icons/rebooting-i
 import { ResourcesAlmostEmptyIcon } from 
"@patternfly/react-icons/dist/js/icons/resources-almost-empty-icon";
 import { ResourcesFullIcon } from 
"@patternfly/react-icons/dist/js/icons/resources-full-icon";
 import * as React from "react";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } 
from "react";
 import { BoxedExpression } from "../../api";
 import { useCustomContextMenuHandler } from "../../contextMenu";
 import { MenuItemWithHelp } from "../../contextMenu/MenuWithHelp";
-import { PopoverMenu } from "../../contextMenu/PopoverMenu";
 import { useBoxedExpressionEditorI18n } from "../../i18n";
 import { useNestedExpressionContainer } from 
"../../resizing/NestedExpressionContainerContext";
 import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from 
"../../BoxedExpressionEditorContext";
@@ -76,11 +75,13 @@ export interface ExpressionDefinitionLogicTypeSelectorProps 
{
   hideDmn14BoxedExpressions?: boolean;
 }
 
+const DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT = 550;
+const LOGIC_TYPE_SELECTOR_BOTTOM_MARGIN = 5;
+
 export function ExpressionDefinitionLogicTypeSelector({
   expression,
   onLogicTypeSelected,
   onLogicTypeReset,
-  getPlacementRef,
   isResetSupported,
   isNested,
   parentElementId,
@@ -90,6 +91,27 @@ export function ExpressionDefinitionLogicTypeSelector({
     () => (isNested ? new Set([undefined]) : new Set([undefined, 
"functionDefinition"])),
     [isNested]
   );
+  const { i18n } = useBoxedExpressionEditorI18n();
+  const { setCurrentlyOpenContextMenu, widthsById, scrollableParentRef } = 
useBoxedExpressionEditor();
+  const [isOpen, setOpen] = useState(false);
+  const [position, setPosition] = useState({ x: 0, y: 0 });
+
+  const showSelectExpression = useCallback(
+    (e: React.MouseEvent<HTMLDivElement>) => {
+      setPosition({
+        x:
+          e.pageX +
+          (scrollableParentRef.current?.offsetLeft ?? 0) -
+          (scrollableParentRef.current?.getBoundingClientRect().left ?? 0),
+        y:
+          e.pageY +
+          (scrollableParentRef.current?.offsetTop ?? 0) -
+          (scrollableParentRef.current?.getBoundingClientRect().top ?? 0),
+      });
+      setOpen(true);
+    },
+    [scrollableParentRef]
+  );
 
   const selectableLogicTypes = useMemo<Array<BoxedExpression["__$$element"]>>(
     () => [
@@ -109,9 +131,7 @@ export function ExpressionDefinitionLogicTypeSelector({
     [hideDmn14BoxedExpressions, isNested]
   );
 
-  const { i18n } = useBoxedExpressionEditorI18n();
-
-  const { setCurrentlyOpenContextMenu, editorRef, widthsById } = 
useBoxedExpressionEditor();
+  const selectLogicTypeContainer = useRef<HTMLDivElement>(null);
 
   const renderExpression = useMemo(() => {
     const logicType = expression?.__$$element;
@@ -152,20 +172,14 @@ export function ExpressionDefinitionLogicTypeSelector({
     }
   }, [expression, isNested, parentElementId]);
 
-  const getPopoverArrowPlacement = useCallback(() => {
-    return getPlacementRef() as HTMLDivElement;
-  }, [getPlacementRef]);
-
-  const getPopoverContainer = useCallback(() => {
-    return editorRef?.current ?? getPopoverArrowPlacement;
-  }, [getPopoverArrowPlacement, editorRef]);
-
   const selectLogicType = useCallback(
-    (_: React.MouseEvent, itemId?: string | number) => {
+    (mouseEvent: React.MouseEvent, itemId?: string | number) => {
       onLogicTypeSelected(itemId as BoxedExpression["__$$element"] | 
undefined);
       setCurrentlyOpenContextMenu(undefined);
       setPasteExpressionError("");
       setVisibleHelp("");
+      setOpen(false);
+      mouseEvent.stopPropagation();
     },
     [onLogicTypeSelected, setCurrentlyOpenContextMenu]
   );
@@ -195,6 +209,27 @@ export function ExpressionDefinitionLogicTypeSelector({
     return isResetContextMenuOpen && expression && isResetSupported;
   }, [isResetContextMenuOpen, isResetSupported, expression]);
 
+  const selectExpressionMenuContainerRef = React.useRef<HTMLDivElement>(null);
+
+  const shouldRenderSelectExpressionContextMenu = useMemo(() => {
+    return isOpen && !expression;
+  }, [isOpen, expression]);
+
+  const hide = useCallback((e: MouseEvent) => {
+    if (e.target != selectExpressionMenuContainerRef.current) {
+      setOpen(false);
+    }
+  }, []);
+
+  useEffect(() => {
+    if (isOpen) {
+      document.addEventListener("click", hide);
+    }
+    return () => {
+      document.removeEventListener("click", hide);
+    };
+  }, [hide, isOpen]);
+
   const logicTypeIcon = useCallback((logicType: BoxedExpression["__$$element"] 
| undefined) => {
     switch (logicType) {
       case undefined:
@@ -454,135 +489,185 @@ export function ExpressionDefinitionLogicTypeSelector({
     setVisibleHelp((previousHelp) => (previousHelp !== help ? help : ""));
   }, []);
 
-  return (
-    <>
-      <div
-        className={cssClass}
-        ref={resetContextMenuContainerRef}
-        style={
-          !expression && nestedExpressionContainer.resizingWidth
-            ? { width: `${nestedExpressionContainer.resizingWidth?.value}px` }
-            : {}
+  useLayoutEffect(() => {
+    if (selectLogicTypeContainer.current) {
+      const boundingClientRect = 
selectLogicTypeContainer.current?.getBoundingClientRect();
+      if (boundingClientRect) {
+        const yPos = boundingClientRect.top;
+        const availableHeight = document.documentElement.clientHeight;
+        if (DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT + yPos > availableHeight) {
+          const offset =
+            DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT + yPos - availableHeight - 
LOGIC_TYPE_SELECTOR_BOTTOM_MARGIN;
+          selectLogicTypeContainer.current.style.height = 
DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT - offset + "px";
+          selectLogicTypeContainer.current.style.overflowY = "scroll";
+        } else {
+          selectLogicTypeContainer.current.style.height = 
DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT + "px";
+          selectLogicTypeContainer.current.style.overflowY = "visible";
         }
-      >
-        {expression ? (
-          <>
-            {showExpressionHeader && (
-              <div className={"logic-type-selected-header"}>
-                <Dropdown
-                  data-testid={"logic-type-selected-header"}
-                  isPlain={true}
-                  isOpen={isDropdownOpen}
-                  onKeyDown={(e) => {
-                    if (NavigationKeysUtils.isEsc(e.key)) {
-                      setDropdownOpen(false);
-                    }
-                  }}
-                  toggle={
-                    <DropdownToggle
-                      data-testid={"logic-type-button-test-id"}
-                      icon={<>{logicTypeIcon(expression.__$$element)}</>}
-                      style={{ padding: 0 }}
-                      onToggle={setDropdownOpen}
-                      tabIndex={-1}
-                    >
-                      {getLogicTypeLabel(expression?.__$$element)}
-                      {expression.__$$element === "functionDefinition" && ` 
(${expression["@_kind"]})`}
-                    </DropdownToggle>
-                  }
+      } else {
+        selectLogicTypeContainer.current.style.height = 
DEFAULT_LOGIC_TYPE_SELECTOR_HEIGHT + "px";
+        selectLogicTypeContainer.current.style.overflowY = "visible";
+      }
+    }
+  });
+
+  const getRenderExpressionElement = useCallback(() => {
+    return (
+      <>
+        {showExpressionHeader && expression && (
+          <div className={"logic-type-selected-header"}>
+            <Dropdown
+              data-testid={"logic-type-selected-header"}
+              isPlain={true}
+              isOpen={isDropdownOpen}
+              onKeyDown={(e) => {
+                if (NavigationKeysUtils.isEsc(e.key)) {
+                  setDropdownOpen(false);
+                }
+              }}
+              toggle={
+                <DropdownToggle
+                  data-testid={"logic-type-button-test-id"}
+                  icon={<>{logicTypeIcon(expression.__$$element)}</>}
+                  style={{ padding: 0 }}
+                  onToggle={setDropdownOpen}
+                  tabIndex={-1}
                 >
-                  <Menu className="table-context-menu" style={{ width: 
"200px", fontSize: "larger" }}>
-                    <>{contextMenuItems}</>
-                  </Menu>
-                </Dropdown>
-              </div>
-            )}
-            {renderExpression}
-          </>
-        ) : (
-          i18n.selectExpression
+                  {getLogicTypeLabel(expression?.__$$element)}
+                  {expression.__$$element === "functionDefinition" && ` 
(${expression["@_kind"]})`}
+                </DropdownToggle>
+              }
+            >
+              <Menu className="table-context-menu" style={{ width: "200px", 
fontSize: "larger" }}>
+                <>{contextMenuItems}</>
+              </Menu>
+            </Dropdown>
+          </div>
         )}
+        {renderExpression}
+      </>
+    );
+  }, [contextMenuItems, expression, isDropdownOpen, logicTypeIcon, 
renderExpression, showExpressionHeader]);
 
-        {!expression && (
-          <PopoverMenu
-            onHide={() => {
-              setPasteExpressionError("");
-              setVisibleHelp("");
-            }}
-            arrowPlacement={getPopoverArrowPlacement}
-            appendTo={getPopoverContainer()}
-            className="logic-type-popover"
-            hasAutoWidth={true}
-            body={
-              <>
-                <Menu onSelect={selectLogicType}>
-                  <MenuGroup className="menu-with-help">
-                    <MenuList>
-                      <>
-                        {selectableLogicTypes.map((key) => {
-                          const label = getLogicTypeLabel(key);
-                          return (
-                            <MenuItemWithHelp
-                              key={key}
-                              menuItemKey={key}
-                              menuItemHelp={logicTypeHelp(key)}
-                              menuItemIcon={logicTypeIcon(key)}
-                              menuItemCustomText={label}
-                              menuItemIconStyle={menuIconContainerStyle}
-                              setVisibleHelp={toggleVisibleHelp}
-                              visibleHelp={visibleHelp}
-                            />
-                          );
-                        })}
-                        <Divider style={{ padding: "16px" }} />
-                      </>
-                    </MenuList>
-                  </MenuGroup>
-                </Menu>
-                <Menu>
-                  <MenuList>
-                    <MenuItem
-                      className={pasteExpressionError ? 
"paste-from-clipboard-error" : ""}
-                      description={pasteExpressionError ? "Paste operation was 
not successful" : ""}
-                      onClick={pasteExpression}
-                      icon={
-                        <div style={menuIconContainerStyle}>
-                          <PasteIcon />
-                        </div>
-                      }
-                    >
-                      {i18n.terms.paste}
-                    </MenuItem>
-                  </MenuList>
-                </Menu>
-              </>
-            }
-          />
-        )}
+  const getSelectExpressionElement = useCallback(() => {
+    return (
+      <div
+        className="context-menu-container"
+        style={{
+          top: position.y,
+          left: position.x,
+          opacity: 1,
+          width: "200px",
+        }}
+        ref={selectLogicTypeContainer}
+      >
+        <Menu onSelect={selectLogicType}>
+          <MenuGroup className="menu-with-help" style={{ paddingBottom: "16px" 
}}>
+            <MenuList>
+              {selectableLogicTypes.map((key) => {
+                const label = getLogicTypeLabel(key);
+                return (
+                  <MenuItemWithHelp
+                    key={key}
+                    menuItemKey={key}
+                    menuItemHelp={logicTypeHelp(key)}
+                    menuItemIcon={logicTypeIcon(key)}
+                    menuItemCustomText={label}
+                    menuItemIconStyle={menuIconContainerStyle}
+                    setVisibleHelp={toggleVisibleHelp}
+                    visibleHelp={visibleHelp}
+                  />
+                );
+              })}
+              <Divider style={{ padding: "16px" }} />
+              <MenuItem
+                className={pasteExpressionError ? "paste-from-clipboard-error" 
: ""}
+                description={pasteExpressionError ? "Paste operation was not 
successful" : ""}
+                onClick={pasteExpression}
+                icon={
+                  <div style={menuIconContainerStyle}>
+                    <PasteIcon />
+                  </div>
+                }
+              >
+                {i18n.terms.paste}
+              </MenuItem>
+            </MenuList>
+          </MenuGroup>
+        </Menu>
+      </div>
+    );
+  }, [
+    i18n.terms.paste,
+    logicTypeHelp,
+    logicTypeIcon,
+    menuIconContainerStyle,
+    pasteExpression,
+    pasteExpressionError,
+    position.x,
+    position.y,
+    selectLogicType,
+    selectableLogicTypes,
+    toggleVisibleHelp,
+    visibleHelp,
+  ]);
+
+  const getResetExpressionElement = useCallback(() => {
+    return (
+      <div
+        className="context-menu-container"
+        style={{
+          top: resetContextMenuYPos,
+          left: resetContextMenuXPos,
+          opacity: 1,
+          minWidth: "150px",
+        }}
+        onKeyDown={(e) => {
+          if (NavigationKeysUtils.isEsc(e.key)) {
+            setDropdownOpen(false);
+          }
+        }}
+      >
+        <Menu className="table-context-menu">
+          <MenuGroup 
label={`${getLogicTypeLabel(expression?.__$$element).toLocaleUpperCase()} 
EXPRESSION`}></MenuGroup>
+          {contextMenuItems}
+        </Menu>
       </div>
-      {shouldRenderResetContextMenu && (
+    );
+  }, [contextMenuItems, expression?.__$$element, resetContextMenuXPos, 
resetContextMenuYPos]);
+
+  return (
+    <>
+      {!expression && (
         <div
-          className="context-menu-container"
-          style={{
-            top: resetContextMenuYPos,
-            left: resetContextMenuXPos,
-            opacity: 1,
-            minWidth: "150px",
-          }}
-          onKeyDown={(e) => {
-            if (NavigationKeysUtils.isEsc(e.key)) {
-              setDropdownOpen(false);
-            }
-          }}
+          className={cssClass}
+          ref={selectExpressionMenuContainerRef}
+          onClick={showSelectExpression}
+          style={
+            !expression && nestedExpressionContainer.resizingWidth
+              ? { width: `${nestedExpressionContainer.resizingWidth?.value}px` 
}
+              : {}
+          }
+        >
+          {i18n.selectExpression}
+          {!expression && shouldRenderSelectExpressionContextMenu && 
getSelectExpressionElement()}
+        </div>
+      )}
+
+      {expression && (
+        <div
+          className={cssClass}
+          ref={resetContextMenuContainerRef}
+          style={
+            !expression && nestedExpressionContainer.resizingWidth
+              ? { width: `${nestedExpressionContainer.resizingWidth?.value}px` 
}
+              : {}
+          }
         >
-          <Menu className="table-context-menu">
-            <MenuGroup
-              
label={`${getLogicTypeLabel(expression?.__$$element).toLocaleUpperCase()} 
EXPRESSION`}
-            ></MenuGroup>
-            {contextMenuItems}
-          </Menu>
+          {expression ? getRenderExpressionElement() : i18n.selectExpression}
         </div>
       )}
+      {shouldRenderResetContextMenu && getResetExpressionElement()}
     </>
   );
 }
diff --git 
a/packages/boxed-expression-component/src/table/BeeTable/BeeTableContextMenuHandler.tsx
 
b/packages/boxed-expression-component/src/table/BeeTable/BeeTableContextMenuHandler.tsx
index 15f49546ee4..76ed7239fe4 100644
--- 
a/packages/boxed-expression-component/src/table/BeeTable/BeeTableContextMenuHandler.tsx
+++ 
b/packages/boxed-expression-component/src/table/BeeTable/BeeTableContextMenuHandler.tsx
@@ -485,7 +485,7 @@ export function BeeTableContextMenuHandler({
     );
   }, [insertMultipleRowColumnsValue, onMinus, onChange, onPlus]);
 
-  const contextMenuContainerDiv = React.createRef<HTMLDivElement>();
+  const contextMenuContainer = React.createRef<HTMLDivElement>();
 
   const { xPos, yPos, isOpen } = useCustomContextMenuHandler(tableRef);
 
@@ -511,21 +511,30 @@ export function BeeTableContextMenuHandler({
   }, [xPos, yPos]);
 
   useLayoutEffect(() => {
-    if (contextMenuContainerDiv.current) {
-      const bounds = contextMenuContainerDiv.current.getBoundingClientRect();
-      const contextMenuHeight = menuHeights[activeMenuId];
+    if (contextMenuContainer.current) {
+      const bounds = contextMenuContainer.current.getBoundingClientRect();
+      let contextMenuHeight = menuHeights[activeMenuId];
       const availableHeight = document.documentElement.clientHeight;
+      if (contextMenuHeight + yPos >= availableHeight) {
+        const offset = contextMenuHeight + yPos - availableHeight;
+        contextMenuHeight = contextMenuHeight - offset;
+        contextMenuContainer.current.style.height = contextMenuHeight + "px";
+        contextMenuContainer.current.style.overflowY = "scroll";
+      } else {
+        contextMenuContainer.current.style.overflowY = "visible";
+      }
+
       if (contextMenuHeight <= availableHeight && contextMenuHeight + yPos > 
availableHeight) {
         const offset = contextMenuHeight + yPos - availableHeight;
-        contextMenuContainerDiv.current.style.top = yPos - offset + "px";
-        contextMenuContainerDiv.current.style.left = xPos + 2 + "px";
+        contextMenuContainer.current.style.top = yPos - offset + "px";
+        contextMenuContainer.current.style.left = xPos + 2 + "px";
       }
 
       const contextMenuWidth = bounds.width;
       const availableWidth = document.documentElement.clientWidth;
       if (contextMenuWidth <= availableWidth && contextMenuWidth + xPos > 
availableWidth) {
         const offset = contextMenuWidth + xPos - availableWidth;
-        contextMenuContainerDiv.current.style.left = xPos - offset - 2 + "px";
+        contextMenuContainer.current.style.left = xPos - offset - 2 + "px";
       }
     }
   });
@@ -662,7 +671,7 @@ export function BeeTableContextMenuHandler({
           className="context-menu-container"
           style={style}
           onMouseDown={(e) => e.stopPropagation()}
-          ref={contextMenuContainerDiv}
+          ref={contextMenuContainer}
         >
           <Menu
             ouiaId="expression-table-context-menu"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to