diff --git a/packages/components/src/components/context-menu/_context-menu.scss b/packages/components/src/components/context-menu/_context-menu.scss
index a9d0506d4c53..d73855050098 100644
--- a/packages/components/src/components/context-menu/_context-menu.scss
+++ b/packages/components/src/components/context-menu/_context-menu.scss
@@ -24,6 +24,7 @@
}
.#{$prefix}--context-menu--open {
+ position: fixed;
display: block;
}
@@ -46,6 +47,10 @@
z-index: 1;
}
+ .#{$prefix}--context-menu-option > .#{$prefix}--context-menu--reverse {
+ left: -100%;
+ }
+
.#{$prefix}--context-menu-option:hover > .#{$prefix}--context-menu,
.#{$prefix}--context-menu-option:focus-within > .#{$prefix}--context-menu {
display: block;
diff --git a/packages/react/src/components/ContextMenu/ContextMenu-story.js b/packages/react/src/components/ContextMenu/ContextMenu-story.js
index 973c461d53ff..3c724d330cb4 100644
--- a/packages/react/src/components/ContextMenu/ContextMenu-story.js
+++ b/packages/react/src/components/ContextMenu/ContextMenu-story.js
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { FolderShared16, Edit16, TrashCan16 } from '@carbon/icons-react';
import ContextMenu, {
@@ -22,26 +22,49 @@ export default {
},
};
-export const _ContextMenu = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
+export const _ContextMenu = () => {
+ const [open, setOpen] = useState(true);
+ const [position, setPosition] = useState([0, 0]);
+
+ function openContextMenu(e) {
+ e.preventDefault();
+
+ const { x, y } = e;
+
+ setPosition([x, y]);
+ setOpen(true);
+ }
+
+ useEffect(() => {
+ document.addEventListener('contextmenu', openContextMenu);
+
+ return () => {
+ document.removeEventListener('contextmenu', openContextMenu);
+ };
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
_ContextMenu.storyName = 'ContextMenu';
diff --git a/packages/react/src/components/ContextMenu/ContextMenu.js b/packages/react/src/components/ContextMenu/ContextMenu.js
index b72fa99a3bff..6293b7e43046 100644
--- a/packages/react/src/components/ContextMenu/ContextMenu.js
+++ b/packages/react/src/components/ContextMenu/ContextMenu.js
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { settings } from 'carbon-components';
@@ -16,8 +16,18 @@ import ContextMenuRadioGroup from './ContextMenuRadioGroup';
const { prefix } = settings;
-const ContextMenu = function ContextMenu({ children, open, ...rest }) {
+const contextMenuWidth = 208; // in px
+
+const ContextMenu = function ContextMenu({
+ children,
+ open,
+ level = 1,
+ x = 0,
+ y = 0,
+ ...rest
+}) {
const rootRef = useRef(null);
+ const [shouldReverse, setShouldReverse] = useState(false);
function resetFocus() {
Array.from(
@@ -27,11 +37,14 @@ const ContextMenu = function ContextMenu({ children, open, ...rest }) {
});
}
- function focusNode(node) {
+ function focusNode(node, focus = true) {
if (node) {
resetFocus();
node.tabIndex = 0;
- node.focus();
+
+ if (focus) {
+ node.focus();
+ }
}
}
@@ -107,16 +120,47 @@ const ContextMenu = function ContextMenu({ children, open, ...rest }) {
}
}
+ function willFit() {
+ if (rootRef?.current) {
+ const bodyWidth = document.body.clientWidth;
+
+ const reverseMap = [...Array(level)].reduce(
+ (acc) => {
+ const endX = acc.lastX + contextMenuWidth * acc.direction;
+ const fits =
+ acc.direction === 1 ? endX < bodyWidth : endX > contextMenuWidth;
+
+ const newDirection = fits ? acc.direction : acc.direction * -1;
+ const newLastX = fits
+ ? endX
+ : acc.lastX + contextMenuWidth * newDirection;
+
+ return {
+ direction: newDirection,
+ lastX: newLastX,
+ map: [...acc.map, newDirection === 1],
+ };
+ },
+ { direction: 1, lastX: x, map: [] }
+ );
+
+ return reverseMap.map[level - 1];
+ }
+
+ return true;
+ }
+
useEffect(() => {
const topLevelNodes = getValidNodes(rootRef?.current);
if (topLevelNodes && topLevelNodes.length > 0) {
- focusNode(topLevelNodes[0].firstChild);
+ focusNode(topLevelNodes[0].firstChild, false);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [open]);
- const someNodesHaveIcons = children.some(
+ setShouldReverse(!willFit());
+ }, [open, x, y]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const someNodesHaveIcons = React.Children.toArray(children).some(
(node) =>
node.props.renderIcon ||
node.type === SelectableContextMenuOption ||
@@ -127,17 +171,25 @@ const ContextMenu = function ContextMenu({ children, open, ...rest }) {
if (React.isValidElement(node)) {
return React.cloneElement(node, {
indented: someNodesHaveIcons,
+ level: level,
+ menuX: x,
});
}
});
const classes = classnames(`${prefix}--context-menu`, {
[`${prefix}--context-menu--open`]: open,
+ [`${prefix}--context-menu--reverse`]: shouldReverse,
});
return (
- // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
-
+
);
@@ -149,10 +201,25 @@ ContextMenu.propTypes = {
*/
children: PropTypes.node,
+ /**
+ * Internal: keeps track of the nesting level of the menu
+ */
+ level: PropTypes.number,
+
/**
* Specify whether the ContextMenu is currently open
*/
open: PropTypes.bool,
+
+ /**
+ * Specify the x position where this menu is rendered
+ */
+ x: PropTypes.number,
+
+ /**
+ * Specify the y position where this menu is rendered
+ */
+ y: PropTypes.number,
};
export default ContextMenu;
diff --git a/packages/react/src/components/ContextMenu/ContextMenuOption.js b/packages/react/src/components/ContextMenu/ContextMenuOption.js
index 5b3a1d37a3c1..49f6046f2e28 100644
--- a/packages/react/src/components/ContextMenu/ContextMenuOption.js
+++ b/packages/react/src/components/ContextMenu/ContextMenuOption.js
@@ -47,6 +47,8 @@ function ContextMenuOption({
shortcut,
renderIcon,
indented,
+ level,
+ menuX,
...rest
}) {
const subOptions = React.Children.map(children, (node) => {
@@ -60,7 +62,7 @@ function ContextMenuOption({
});
return (
- -
+
-
{subOptions ? (
<>
}
indented={indented}
/>
- {subOptions}
+
+ {subOptions}
+
>
) : (