) => event.stopPropagation()}
+ >
+
+ {label}
+
+
+
+ {/* rest is sent from the ContextMenu element */}
+ {cloneChildren(children, rest as MenuItemExternalProps)}
+
+
+ );
+};
+
+export default SubMenu;
diff --git a/src/components/index.ts b/src/components/index.ts
index c44a072..3451eee 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,3 +1,4 @@
export { default as ContextMenu } from './ContextMenu';
export { default as MenuItem } from './MenuItem';
export { default as Separator } from './Separator';
+export { default as SubMenu } from './SubMenu';
diff --git a/src/index.ts b/src/index.ts
index 5a62336..daaa1be 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,3 @@
import './styles.css';
-export { ContextMenu, MenuItem, Separator } from './components';
+export { ContextMenu, MenuItem, Separator, SubMenu } from './components';
diff --git a/src/styles.css b/src/styles.css
index 5770e39..2f402ec 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -51,13 +51,14 @@
}
.react-context-menu__item--clicked {
- animation: react-context-menu__item-clicked 100ms ease-out forwards;
+ animation: react-context-menu__item-clicked 100ms ease-out;
+ animation-iteration-count: 1;
}
/* Component styles */
-.react-context-menu {
- position: fixed;
+.react-context-menu,
+.react-context-menu__submenu {
z-index: var(--react-context-menu-z-index);
padding: var(--react-context-menu-padding-sm);
@@ -70,6 +71,30 @@
min-width: 160px;
}
+.react-context-menu {
+ position: fixed;
+}
+
+.react-context-menu__submenu {
+ position: absolute;
+
+ /* Initial position */
+ left: 100%;
+
+ &:not(.react-context-menu__submenu-bottom) {
+ top: calc(-1 * var(--react-context-menu-padding-sm));
+ }
+}
+
+.react-context-menu__submenu-bottom {
+ top: unset;
+}
+
+.react-context-menu__submenu-right {
+ right: 100%;
+ left: unset;
+}
+
.react-context-menu__separator {
border: 0;
margin-block: 0;
@@ -90,12 +115,20 @@
user-select: none;
-webkit-user-select: none;
+ &:has(.react-context-menu__submenu) {
+ position: relative;
+ }
+
&:not(.react-context-menu__item--disabled) {
cursor: pointer;
&:hover {
color: var(--react-context-menu-item-hover-color);
background-color: var(--react-context-menu-item-hover-background-color);
+
+ .react-context-menu__arrow {
+ border-color: var(--react-context-menu-item-hover-color);
+ }
}
}
}
@@ -105,3 +138,21 @@
color: var(--react-context-menu-item-hover-disabled-color);
}
+
+.react-context-menu__label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.react-context-menu__arrow {
+ transform: rotate(-45deg);
+
+ width: 5px;
+ height: 5px;
+ padding: 3px;
+
+ border-style: solid;
+ border-width: 0 2px 2px 0;
+ border-color: var(--react-context-menu-item-color);
+}
diff --git a/src/utils.ts b/src/utils.ts
index fb5ec40..049bf50 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -12,7 +12,7 @@ export const getCursorPosition = (e: MouseEvent): Position => {
return position;
};
-export const validateWindowPosition = (position: Position, element: HTMLDivElement | null) => {
+export const validateMenuPosition = (position: Position, element: HTMLDivElement | null) => {
if (!element) return position;
let { x, y } = position;
@@ -26,7 +26,7 @@ export const validateWindowPosition = (position: Position, element: HTMLDivEleme
return { x, y };
};
-export const cloneChildren = (children: ReactNode, props: MenuItemExternalProps) => {
+export const cloneChildren = (children: ReactNode, props?: MenuItemExternalProps) => {
const filteredItems = Children.toArray(children).filter(Boolean);
return filteredItems.map((item) => cloneElement(item as ReactElement