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]