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); + }); + } +});