From 00680fb776fdab40158fccb49f33879546504f7d Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Mon, 19 Jan 2026 13:09:06 +0000 Subject: [PATCH] refactor: optimize context menu positioning logic Improvements: - change position to `fixed` so menu stays on top and doesn't get cut off by the sidebar's container - use `getBoundingClientRect` to calc exact position relative to the screen, better when sidebar collapsed - fix rtl alignment - off-screen fix add a safety check to make sure the menu never slides off the left edge of the window --- frappe/public/js/frappe/ui/menu.js | 54 +++++++++++------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/frappe/public/js/frappe/ui/menu.js b/frappe/public/js/frappe/ui/menu.js index e3c0d1298e..c09b382992 100644 --- a/frappe/public/js/frappe/ui/menu.js +++ b/frappe/public/js/frappe/ui/menu.js @@ -179,48 +179,27 @@ frappe.ui.menu = class ContextMenu { show(event) { this.make(); - const offset = $(this.parent).offset(); - const height = $(this.parent).outerHeight(); - this.left_offset = 0; + const parent_rect = this.parent.get(0).getBoundingClientRect(); this.gap = 4; + let top, left; if (this.opts.nested && this.opts.parent_menu) { - let top = - this.parent.get(0).getBoundingClientRect().bottom - - this.parent.get(0).getBoundingClientRect().height; - let dropdown = frappe.menu_map[this.opts.parent_menu].template; - let width = dropdown.outerWidth(); - let offset = $(dropdown).offset(); - let left = offset.left; + let parent_menu_el = frappe.menu_map[this.opts.parent_menu].template; + let parent_menu_rect = parent_menu_el.get(0).getBoundingClientRect(); + top = parent_rect.top; if (frappe.utils.is_rtl()) { - left = left - width - this.gap; + left = parent_menu_rect.left - this.template.outerWidth() - this.gap; } else { - left = left + width + this.gap; + left = parent_menu_rect.right + this.gap; } - this.template.css({ - display: "block", - position: "absolute", - top: top + "px", - left: left + "px", - }); } else { - this.template.css({ - display: "block", - position: "absolute", - top: offset.top + height + this.gap + "px", - left: offset.left, - }); + top = parent_rect.bottom + this.gap; + left = parent_rect.left; + if (this.open_on_left || frappe.utils.is_rtl()) { + left = parent_rect.right - this.template.outerWidth(); + } } - if (this.open_on_left) { - this.left_offset = this.parent.get(0).getBoundingClientRect().width; - this.template.css({ - left: - offset.left - - this.template.get(0).getBoundingClientRect().width + - this.left_offset + - "px", - }); - } + if (left < 0) left = 10; if (this.opts.right_click) { this.template.css({ @@ -229,6 +208,13 @@ frappe.ui.menu = class ContextMenu { }); } + this.template.css({ + display: "block", + position: "fixed", + top: top + "px", + left: left + "px", + }); + this.visible = true; frappe.visible_menus.push(this); }