diff --git a/src/js/core/tools/Popup.js b/src/js/core/tools/Popup.js
index d2ec9b3f4..796a51b7b 100644
--- a/src/js/core/tools/Popup.js
+++ b/src/js/core/tools/Popup.js
@@ -203,13 +203,23 @@ export default class Popup extends CoreFeature{
case "bottom":
this.element.style.top = (parseInt(this.element.style.top) - this.element.offsetHeight - parentEl.offsetHeight - 1) + "px";
break;
-
+
default:
this.element.style.top = (parseInt(this.element.style.top) - this.element.offsetHeight + parentEl.offsetHeight + 1) + "px";
}
-
+
}else{
- this.element.style.height = offsetHeight + "px";
+ let menuHeight = this.element.offsetHeight;
+ if(menuHeight > offsetHeight){
+ this.element.style.top = "0px";
+ this.element.style.height = offsetHeight + "px";
+ }else{
+ let newTop = y - menuHeight;
+ if(newTop < 0){
+ newTop = offsetHeight - menuHeight;
+ }
+ this.element.style.top = newTop + "px";
+ }
}
}
}
diff --git a/test/e2e/menu.html b/test/e2e/menu.html
new file mode 100644
index 000000000..4e9bf261e
--- /dev/null
+++ b/test/e2e/menu.html
@@ -0,0 +1,61 @@
+
+
+
+
+ Tabulator Menu Test
+
+
+
+
+
+
+
+
+
+
diff --git a/test/e2e/menu.spec.js b/test/e2e/menu.spec.js
new file mode 100644
index 000000000..7fe897846
--- /dev/null
+++ b/test/e2e/menu.spec.js
@@ -0,0 +1,52 @@
+// @ts-check
+import { test, expect } from "@playwright/test";
+
+test.describe("Context menu viewport bounds", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/test/e2e/menu.html");
+ await page.waitForSelector(".tabulator-row");
+ });
+
+ const margin = 5;
+ const positions = [
+ { name: "top-left", offsetX: margin, offsetY: 50 },
+ { name: "top-right", offsetX: -margin, offsetY: 50 },
+ { name: "bottom-left", offsetX: margin, offsetY: -margin },
+ { name: "bottom-right", offsetX: -margin, offsetY: -margin },
+ { name: "middle", offsetX: 0.5, offsetY: 0.5 },
+ ];
+
+ for (const corner of positions) {
+ test(`menu stays inside viewport when opened near ${corner.name}`, async ({ page }) => {
+ const viewport = page.viewportSize();
+ const resolve = (offset, size) => {
+ if (offset > 0 && offset < 1) return Math.round(size * offset);
+ return offset < 0 ? size + offset : offset;
+ };
+ const x = resolve(corner.offsetX, viewport.width);
+ const y = resolve(corner.offsetY, viewport.height);
+
+ await page.evaluate(({ x, y }) => {
+ const el = document.elementFromPoint(x, y);
+ el.dispatchEvent(new MouseEvent("contextmenu", {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ button: 2,
+ clientX: x,
+ clientY: y,
+ }));
+ }, { x, y });
+
+ const menu = page.locator(".tabulator-menu");
+ await expect(menu).toBeVisible();
+
+ const box = await menu.boundingBox();
+ expect(box).not.toBeNull();
+ expect(box.x).toBeGreaterThanOrEqual(0);
+ expect(box.y).toBeGreaterThanOrEqual(0);
+ expect(box.x + box.width).toBeLessThanOrEqual(viewport.width);
+ expect(box.y + box.height).toBeLessThanOrEqual(viewport.height);
+ });
+ }
+});