{
+ const lang = useLang();
+
+ const itemRef = useRef
();
+ const closeTimeoutRef = useRef();
+ const openTimeoutRef = useRef();
+ const submenuId = useUniqueId();
+ const isClosingRef = useRef(false);
+
+ const [isSubmenuOpen, openSubmenu, closeSubmenu] = useFlag(false);
+
+ useUnmountCleanup(() => {
+ clearTimeout(closeTimeoutRef.current);
+ clearTimeout(openTimeoutRef.current);
+ });
+
+ const [submenuAnchor, setSubmenuAnchor] = useState();
+ const { isResizing } = useWindowSize();
+
+ const updateAnchor = useLastCallback(() => {
+ requestMeasure(() => {
+ if (!itemRef.current) return;
+
+ const rect = itemRef.current.getBoundingClientRect();
+ const overlap = REM;
+
+ setSubmenuAnchor({
+ x: lang.isRtl ? rect.left + overlap : rect.right - overlap,
+ y: rect.top,
+ width: rect.width - overlap * 2,
+ height: rect.height,
+ });
+ });
+ });
+
+ useEffect(() => {
+ if (isSubmenuOpen && !isResizing) {
+ updateAnchor();
+ }
+ }, [isSubmenuOpen, lang.isRtl, updateAnchor, isResizing]);
+
+ const cancelOpen = useLastCallback(() => {
+ clearTimeout(openTimeoutRef.current);
+ openTimeoutRef.current = undefined;
+ });
+
+ const cancelClose = useLastCallback(() => {
+ clearTimeout(closeTimeoutRef.current);
+ closeTimeoutRef.current = undefined;
+ });
+
+ const scheduleOpen = useLastCallback(() => {
+ cancelClose();
+ cancelOpen();
+ openTimeoutRef.current = window.setTimeout(() => {
+ openTimeoutRef.current = undefined;
+ // Don't open if the parent menu is closing
+ const parentBubble = itemRef.current?.closest('.bubble');
+ if (parentBubble?.classList.contains('closing')) return;
+ openSubmenu();
+ }, OPEN_TIMEOUT);
+ });
+
+ const scheduleClose = useLastCallback(() => {
+ cancelOpen();
+ cancelClose();
+ closeTimeoutRef.current = window.setTimeout(() => {
+ closeSubmenu();
+ closeTimeoutRef.current = undefined;
+ }, CLOSE_TIMEOUT);
+ });
+
+ const handleMouseEnter = useLastCallback(() => {
+ if (disabled) return;
+ scheduleOpen();
+ });
+
+ const handleSubmenuMouseEnter = useLastCallback(() => {
+ cancelOpen();
+ cancelClose();
+ });
+
+ const handleSubmenuMouseLeave = useLastCallback(() => {
+ scheduleClose();
+ });
+
+ const closeParentMenu = useLastCallback(() => {
+ const parentMenu = itemRef.current?.closest('.Menu');
+ if (parentMenu) {
+ const backdrop = parentMenu.querySelector('.backdrop') as HTMLElement;
+ if (backdrop) {
+ const event = new MouseEvent('mousedown', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ });
+ backdrop.dispatchEvent(event);
+ }
+ }
+ });
+
+ const handleSubmenuClose = useLastCallback(() => {
+ if (isClosingRef.current) return;
+ isClosingRef.current = true;
+
+ cancelOpen();
+ cancelClose();
+ closeSubmenu();
+
+ closeParentMenu();
+
+ // Reset after a short delay
+ setTimeout(() => {
+ isClosingRef.current = false;
+ }, 100);
+ });
+
+ const getTriggerElement = useLastCallback(() => itemRef.current);
+ const getRootElement = useLastCallback(() => document.body);
+ const getMenuElement = useLastCallback(
+ () => document.getElementById(submenuId)?.querySelector('.bubble') as HTMLElement | undefined,
+ );
+ const getLayout = useLastCallback(() => ({ withPortal: true }));
+
+ const handleClick = useLastCallback((e: React.SyntheticEvent) => {
+ e.stopPropagation();
+ if (disabled || isSubmenuOpen) return;
+ openSubmenu();
+ });
+
+ return (
+
+
+
+ );
+};
+
+export default NestedMenuItem;
diff --git a/src/hooks/useMenuPosition.ts b/src/hooks/useMenuPosition.ts
index 6f8a1fdbb..ea89cb03a 100644
--- a/src/hooks/useMenuPosition.ts
+++ b/src/hooks/useMenuPosition.ts
@@ -132,6 +132,8 @@ function processDynamically(
let { x, y } = anchor;
const anchorX = x;
const anchorY = y;
+ const anchorWidth = anchor.width || 0;
+ const anchorHeight = anchor.height || 0;
const menuEl = getMenuElement();
const rootEl = getRootElement();
@@ -161,9 +163,9 @@ function processDynamically(
if (isDense || (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left)) {
x += 3;
positionX = 'left';
- } else if (x - menuRect.width - rootRect.left > 0) {
+ } else if (x - anchorWidth - menuRect.width - rootRect.left > 0) {
positionX = 'right';
- x -= 3;
+ x = x - anchorWidth - 3;
} else {
positionX = 'left';
x = 16;
@@ -178,6 +180,7 @@ function processDynamically(
y = yWithTopShift;
} else {
positionY = 'bottom';
+ y = y + anchorHeight;
if (y - menuRect.height < rootRect.top + extraTopPadding) {
y = rootRect.top + rootRect.height;
diff --git a/src/types/index.ts b/src/types/index.ts
index 7c485d217..e40d23600 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -163,6 +163,8 @@ export interface AccountSettings {
export type IAnchorPosition = {
x: number;
y: number;
+ width?: number;
+ height?: number;
};
export interface ShippingOption {
diff --git a/src/types/language.d.ts b/src/types/language.d.ts
index f13accbe1..5c43801b8 100644
--- a/src/types/language.d.ts
+++ b/src/types/language.d.ts
@@ -1235,6 +1235,7 @@ export interface LangPair {
'MenuArchivedChats': undefined;
'MenuContacts': undefined;
'MenuSettings': undefined;
+ 'MenuMore': undefined;
'MenuNightMode': undefined;
'AriaMenuEnableNightMode': undefined;
'AriaMenuDisableNightMode': undefined;