From 2d4f04e55d44861f7e54ade30f4a04a67ea0fc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=A4rtel?= Date: Mon, 13 Apr 2026 20:47:03 +0700 Subject: [PATCH 1/9] fix: scrolling errors when all groups are collapsed (#4219, #4525) - fix horizontal scrollbar showing on initial display when there are groups - fix tableholder not scrolling to bottom when there are groups - fix tableholder jumping to top every time a group is collapsed/expanded Implements Group.calcHeight() so group row height is non-zero, and re-runs GroupRows.virtualRenderFill() on table-layout so minWidth is recalculated after fitColumns has scaled the columns. fixes #4219 fixes #4525 --- src/js/modules/GroupRows/Group.js | 4 +++- src/js/modules/GroupRows/GroupRows.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/modules/GroupRows/Group.js b/src/js/modules/GroupRows/Group.js index db1b86f99..c152f3f59 100644 --- a/src/js/modules/GroupRows/Group.js +++ b/src/js/modules/GroupRows/Group.js @@ -592,7 +592,9 @@ export default class Group{ reinitializeHeight(){} - calcHeight(){} + calcHeight(){ + this.outerHeight = this.element.offsetHeight; + } setCellHeight(){} diff --git a/src/js/modules/GroupRows/GroupRows.js b/src/js/modules/GroupRows/GroupRows.js index 2b50a5884..e861933c1 100644 --- a/src/js/modules/GroupRows/GroupRows.js +++ b/src/js/modules/GroupRows/GroupRows.js @@ -70,6 +70,7 @@ export default class GroupRows extends Module{ this.subscribe("rows-sample", this.rowSample.bind(this)); this.subscribe("render-virtual-fill", this.virtualRenderFill.bind(this)); + this.subscribe("table-layout", this.virtualRenderFill.bind(this)); this.registerDisplayHandler(this.displayHandler, 20); From eb84f4367058cadac33207349d764ca7b4a4f37f Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 25 May 2026 14:25:00 +0700 Subject: [PATCH 2/9] test(e2e): add grouped-table scrolling regression tests (#4219, #4525) Covers the two headline symptoms fixed by this PR: - #4219: virtual-DOM scroll never reaches the bottom with many collapsed groups - #4525: expanding a group below the fold resets the scroll position to the top Both require a real browser (group rows report a zero height only after layout), so they live in the Playwright e2e suite. Verified red without the fix and green with it. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/e2e/group-scroll.html | 57 ++++++++++++++++++++++++++ test/e2e/group-scroll.spec.js | 77 +++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 test/e2e/group-scroll.html create mode 100644 test/e2e/group-scroll.spec.js diff --git a/test/e2e/group-scroll.html b/test/e2e/group-scroll.html new file mode 100644 index 000000000..0bd596c56 --- /dev/null +++ b/test/e2e/group-scroll.html @@ -0,0 +1,57 @@ + + + + + Tabulator Group Scroll Test + + + + + +

Tabulator Group Scroll Test

+
+ + + + diff --git a/test/e2e/group-scroll.spec.js b/test/e2e/group-scroll.spec.js new file mode 100644 index 000000000..7d099abb7 --- /dev/null +++ b/test/e2e/group-scroll.spec.js @@ -0,0 +1,77 @@ +import { test, expect } from "@playwright/test"; +import { join } from "path"; + +const holderSel = ".tabulator-tableholder"; + +// - #4219 (cannot scroll to the bottom) +// - #4525 (expanding a group resets the scroll position) +test.describe("Grouped table virtual scrolling (#4219, #4525)", () => { + test.beforeEach(async ({ page }) => { + await page.goto(`file://${join(__dirname, "group-scroll.html")}`); + await page.waitForSelector(".tabulator-row"); + // position the pointer over the table so wheel events scroll it + await page.locator(holderSel).hover(); + }); + + async function atBottom(page) { + return page.evaluate((sel) => { + const holder = document.querySelector(sel); + return holder.scrollTop >= holder.scrollHeight - holder.clientHeight - 1; + }, holderSel); + } + + test("scrolls to the bottom through many collapsed groups (#4219)", async ({ + page, + }) => { + // Scroll down with the mouse wheel until we reach the bottom or give up. + for (let i = 0; i < 250; i++) { + if (await atBottom(page)) { + break; + } + await page.mouse.wheel(0, 300); + await page.waitForTimeout(12); + } + + const result = await page.evaluate((sel) => { + const holder = document.querySelector(sel); + return { + scrollTop: holder.scrollTop, + maxScroll: holder.scrollHeight - holder.clientHeight, + }; + }, holderSel); + + // Before the fix the view snapped back to the top after a few hundred + // pixels and never reached the bottom of a ~25,000px tall table. + expect(result.scrollTop).toBeGreaterThan(result.maxScroll * 0.9); + }); + + test("expanding a group below the fold keeps the scroll position (#4525)", async ({ + page, + }) => { + // Wheel into the middle of the table, away from the top. + for (let i = 0; i < 45; i++) { + await page.mouse.wheel(0, 300); + await page.waitForTimeout(12); + } + await page.waitForTimeout(150); + + const result = await page.evaluate((sel) => { + const holder = document.querySelector(sel); + const before = holder.scrollTop; + + const groups = window.testTable.getGroups(); + groups[Math.floor(groups.length / 2) + 2].toggle(); + + return new Promise((resolve) => { + setTimeout( + () => resolve({ before, after: holder.scrollTop }), + 200, + ); + }); + }, holderSel); + + // Before the fix expanding a group reset the scroll position to 0. + expect(result.before).toBeGreaterThan(0); + expect(result.after).toBeGreaterThan(result.before * 0.8); + }); +}); From 72aed52aa7152d4962bb11d7186909f704ad37d4 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sun, 31 May 2026 20:12:35 +0700 Subject: [PATCH 3/9] shrinking group rows test --- test/e2e/group-scroll.html | 24 +++++++++++------------- test/e2e/group-scroll.spec.js | 20 ++++++++++++++++---- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/test/e2e/group-scroll.html b/test/e2e/group-scroll.html index 0bd596c56..aed821ead 100644 --- a/test/e2e/group-scroll.html +++ b/test/e2e/group-scroll.html @@ -38,19 +38,17 @@

Tabulator Group Scroll Test

return data; } - document.addEventListener("DOMContentLoaded", () => { - window.testTable = new Tabulator("#test-table", { - data: generateData(), - columns: [ - { title: "Name", field: "name" }, - { title: "Category", field: "category" }, - { title: "Value", field: "value", sorter: "number" }, - ], - height: "300px", - layout: "fitColumns", - groupBy: "category", - groupStartOpen: false, - }); + window.testTable = new Tabulator("#test-table", { + data: generateData(), + columns: [ + { title: "Name", field: "name" }, + { title: "Category", field: "category" }, + { title: "Value", field: "value", sorter: "number" }, + ], + height: "300px", + layout: "fitColumns", + groupBy: "category", + groupStartOpen: false, }); diff --git a/test/e2e/group-scroll.spec.js b/test/e2e/group-scroll.spec.js index 7d099abb7..6df043dd5 100644 --- a/test/e2e/group-scroll.spec.js +++ b/test/e2e/group-scroll.spec.js @@ -63,10 +63,7 @@ test.describe("Grouped table virtual scrolling (#4219, #4525)", () => { groups[Math.floor(groups.length / 2) + 2].toggle(); return new Promise((resolve) => { - setTimeout( - () => resolve({ before, after: holder.scrollTop }), - 200, - ); + setTimeout(() => resolve({ before, after: holder.scrollTop }), 200); }); }, holderSel); @@ -74,4 +71,19 @@ test.describe("Grouped table virtual scrolling (#4219, #4525)", () => { expect(result.before).toBeGreaterThan(0); expect(result.after).toBeGreaterThan(result.before * 0.8); }); + + test("should not shrink group headers (#4877)", async ({ page }) => { + await page.evaluate(() => { + window.testTable.getGroups()[0].toggle(); + return new Promise((resolve) => setTimeout(resolve, 500)); + }); + await page.mouse.wheel(0, 400); + const [tableWidth, groupWidth] = await page.evaluate(async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + const group = window.testTable.getGroups()[18].getElement(); + return [ window.testTable.element.offsetWidth, group.offsetWidth ]; + }); + const borderOffset = 2; + expect(groupWidth).toBe(tableWidth - borderOffset); + }); }); From 805cec735443361055e71716f22487b00fc3646d Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 1 Jun 2026 08:47:15 +0700 Subject: [PATCH 4/9] cleanup stale code This condition is always true because of the initialize method --- src/js/modules/GroupRows/GroupRows.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/js/modules/GroupRows/GroupRows.js b/src/js/modules/GroupRows/GroupRows.js index e861933c1..90e309204 100644 --- a/src/js/modules/GroupRows/GroupRows.js +++ b/src/js/modules/GroupRows/GroupRows.js @@ -195,15 +195,11 @@ export default class GroupRows extends Module{ var el = this.table.rowManager.tableElement; var rows = this.table.rowManager.getVisibleRows(); - if(this.table.options.groupBy){ - rows = rows.filter((row) => { - return row.type !== "group"; - }); - - el.style.minWidth = !rows.length ? this.table.columnManager.getWidth() + "px" : ""; - }else{ - return rows; - } + rows = rows.filter((row) => { + return row.type !== "group"; + }); + + el.style.minWidth = !rows.length ? this.table.columnManager.getWidth() + "px" : ""; } rowAddingIndex(row, index, top){ From eda418913f2074375e89c97351df06ec82c031d2 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 1 Jun 2026 09:43:14 +0700 Subject: [PATCH 5/9] refactor test --- package-lock.json | 26 +++++++++++++------------- package.json | 2 +- test/e2e/group-scroll.spec.js | 23 ++++++++++++----------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff7cc79af..c8c09c5a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,12 @@ "packages": { "": { "name": "tabulator-tables", - "version": "6.3.1", + "version": "6.4.0", "license": "MIT", "devDependencies": { "@babel/core": "^7.26.9", "@babel/preset-env": "^7.26.9", - "@playwright/test": "^1.50.1", + "@playwright/test": "^1.60.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "@types/node": "^22.13.5", @@ -2733,13 +2733,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", - "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.1" + "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -7305,13 +7305,13 @@ } }, "node_modules/playwright": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", - "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.1" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -7324,9 +7324,9 @@ } }, "node_modules/playwright-core": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", - "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index a025d01c0..06408551a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@babel/core": "^7.26.9", "@babel/preset-env": "^7.26.9", - "@playwright/test": "^1.50.1", + "@playwright/test": "^1.60.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "@types/node": "^22.13.5", diff --git a/test/e2e/group-scroll.spec.js b/test/e2e/group-scroll.spec.js index 6df043dd5..d6e5993d4 100644 --- a/test/e2e/group-scroll.spec.js +++ b/test/e2e/group-scroll.spec.js @@ -72,18 +72,19 @@ test.describe("Grouped table virtual scrolling (#4219, #4525)", () => { expect(result.after).toBeGreaterThan(result.before * 0.8); }); - test("should not shrink group headers (#4877)", async ({ page }) => { - await page.evaluate(() => { - window.testTable.getGroups()[0].toggle(); - return new Promise((resolve) => setTimeout(resolve, 500)); - }); - await page.mouse.wheel(0, 400); - const [tableWidth, groupWidth] = await page.evaluate(async () => { - await new Promise((resolve) => setTimeout(resolve, 500)); - const group = window.testTable.getGroups()[18].getElement(); - return [ window.testTable.element.offsetWidth, group.offsetWidth ]; - }); + test("should not shrink group rows and their content horizontally when scrolling down (#4877)", async ({ page }) => { const borderOffset = 2; + + await page.evaluate(() => window.testTable.getGroups()[0].toggle()); + await page.waitForTimeout(200); + + await page.mouse.wheel(0, 400); + await page.waitForTimeout(200); + + const [tableWidth, groupWidth] = await page.evaluate(() => [ + window.testTable.element.offsetWidth, + window.testTable.getGroups()[18].getElement().offsetWidth, + ]); expect(groupWidth).toBe(tableWidth - borderOffset); }); }); From bb279e76a054036ae1fd17cd789d935189441657 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 1 Jun 2026 09:48:54 +0700 Subject: [PATCH 6/9] fix shrinking grouped rows when scrolling --- src/js/core/CoreFeature.js | 1 + src/js/modules/GroupRows/GroupRows.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/js/core/CoreFeature.js b/src/js/core/CoreFeature.js index 7e19be5d8..89253f986 100644 --- a/src/js/core/CoreFeature.js +++ b/src/js/core/CoreFeature.js @@ -45,6 +45,7 @@ export default class CoreFeature{ //////////////// Layout ///////////////// ////////////////////////////////////////// + /** @returns {("fitData" | "fitDataFill" | "fitDataTable" | "fitDataStretch" | "fitColumns")} */ layoutMode(){ return this.table.modules.layout.getMode(); } diff --git a/src/js/modules/GroupRows/GroupRows.js b/src/js/modules/GroupRows/GroupRows.js index 90e309204..8e65ac229 100644 --- a/src/js/modules/GroupRows/GroupRows.js +++ b/src/js/modules/GroupRows/GroupRows.js @@ -192,14 +192,15 @@ export default class GroupRows extends Module{ } virtualRenderFill(){ - var el = this.table.rowManager.tableElement; - var rows = this.table.rowManager.getVisibleRows(); - - rows = rows.filter((row) => { - return row.type !== "group"; - }); - - el.style.minWidth = !rows.length ? this.table.columnManager.getWidth() + "px" : ""; + const layout = this.layoutMode(); + if ( + layout === "fitDataFill" + || layout === "fitDataStretch" + || layout === "fitColumns" + ) { + this.table.rowManager.tableElement.style.minWidth = + this.table.columnManager.getWidth() + "px"; + } } rowAddingIndex(row, index, top){ From 7865b52861254ea9664df1865297109553419fa0 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 12 May 2026 09:21:17 +0700 Subject: [PATCH 7/9] fix overflowing menu (cherry picked from commit c4ac9c55fcbe999e0092fe2e21dada68e51886e8) --- playwright.config.js | 4 +-- src/js/core/tools/Popup.js | 11 +++++-- test/e2e/basic.spec.js | 4 +-- test/e2e/menu.html | 61 ++++++++++++++++++++++++++++++++++++++ test/e2e/menu.spec.js | 47 +++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 test/e2e/menu.html create mode 100644 test/e2e/menu.spec.js diff --git a/playwright.config.js b/playwright.config.js index 9aec529a7..68a450b18 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -43,8 +43,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'npx serve test -p 3000', - port: 3000, + command: 'npx serve . -p 9876', + port: 9876, timeout: 60 * 1000, reuseExistingServer: !process.env.CI, }, diff --git a/src/js/core/tools/Popup.js b/src/js/core/tools/Popup.js index d2ec9b3f4..bde739b97 100644 --- a/src/js/core/tools/Popup.js +++ b/src/js/core/tools/Popup.js @@ -203,13 +203,18 @@ 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 newTop = y - this.element.offsetHeight; + if(newTop < 0){ + newTop = 0; + this.element.style.height = offsetHeight + "px"; + } + this.element.style.top = newTop + "px"; } } } diff --git a/test/e2e/basic.spec.js b/test/e2e/basic.spec.js index 3a24096a4..69a1b4a0b 100644 --- a/test/e2e/basic.spec.js +++ b/test/e2e/basic.spec.js @@ -1,11 +1,9 @@ // @ts-check import { test, expect } from "@playwright/test"; -import { join } from "path"; test.describe("Tabulator functionality", () => { test.beforeEach(async ({ page }) => { - const htmlPath = join(__dirname, "index.html"); - await page.goto(`file://${htmlPath}`); + await page.goto("/test/e2e/index.html"); await page.waitForSelector(".tabulator"); }); 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..293512a45 --- /dev/null +++ b/test/e2e/menu.spec.js @@ -0,0 +1,47 @@ +// @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 corners = [ + { 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 }, + ]; + + for (const corner of corners) { + test(`menu stays inside viewport when opened near ${corner.name}`, async ({ page }) => { + const viewport = page.viewportSize(); + const x = corner.offsetX < 0 ? viewport.width + corner.offsetX : corner.offsetX; + const y = corner.offsetY < 0 ? viewport.height + corner.offsetY : corner.offsetY; + + 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); + }); + } +}); From 97e70d0ffbf94287fc91798c81dfac87b5d99a16 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 12 May 2026 09:37:43 +0700 Subject: [PATCH 8/9] add e2e test for menu viewport overflow and refine fit logic Popup._fitToScreen now flips a popup upward when it would overflow the container bottom, anchoring the menu's bottom to the container when a flip-up would push it past the top edge. Only falls back to the full-height + scroll behavior when the menu itself is taller than the container, which avoids stretching the menu across the viewport for mid-screen triggers. Co-Authored-By: Claude Opus 4.7 (1M context) (cherry picked from commit c7e1321a5d6995d3f0deffeb0b99deae0b977675) --- playwright.config.js | 2 +- src/js/core/tools/Popup.js | 13 +++++++++---- test/e2e/menu.spec.js | 13 +++++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 68a450b18..606c74daa 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -27,7 +27,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + baseURL: 'http://127.0.0.1:9876', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', diff --git a/src/js/core/tools/Popup.js b/src/js/core/tools/Popup.js index bde739b97..796a51b7b 100644 --- a/src/js/core/tools/Popup.js +++ b/src/js/core/tools/Popup.js @@ -209,12 +209,17 @@ export default class Popup extends CoreFeature{ } }else{ - let newTop = y - this.element.offsetHeight; - if(newTop < 0){ - newTop = 0; + 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"; } - this.element.style.top = newTop + "px"; } } } diff --git a/test/e2e/menu.spec.js b/test/e2e/menu.spec.js index 293512a45..7fe897846 100644 --- a/test/e2e/menu.spec.js +++ b/test/e2e/menu.spec.js @@ -8,18 +8,23 @@ test.describe("Context menu viewport bounds", () => { }); const margin = 5; - const corners = [ + 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 corners) { + for (const corner of positions) { test(`menu stays inside viewport when opened near ${corner.name}`, async ({ page }) => { const viewport = page.viewportSize(); - const x = corner.offsetX < 0 ? viewport.width + corner.offsetX : corner.offsetX; - const y = corner.offsetY < 0 ? viewport.height + corner.offsetY : corner.offsetY; + 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); From 216d2a737173926772e037febbc8c75ac04d07a1 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 5 Jun 2026 11:06:25 +0700 Subject: [PATCH 9/9] restore unnecessary changes --- playwright.config.js | 6 +++--- test/e2e/basic.spec.js | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 606c74daa..9aec529a7 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -27,7 +27,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://127.0.0.1:9876', + // baseURL: 'http://127.0.0.1:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -43,8 +43,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'npx serve . -p 9876', - port: 9876, + command: 'npx serve test -p 3000', + port: 3000, timeout: 60 * 1000, reuseExistingServer: !process.env.CI, }, diff --git a/test/e2e/basic.spec.js b/test/e2e/basic.spec.js index 69a1b4a0b..3a24096a4 100644 --- a/test/e2e/basic.spec.js +++ b/test/e2e/basic.spec.js @@ -1,9 +1,11 @@ // @ts-check import { test, expect } from "@playwright/test"; +import { join } from "path"; test.describe("Tabulator functionality", () => { test.beforeEach(async ({ page }) => { - await page.goto("/test/e2e/index.html"); + const htmlPath = join(__dirname, "index.html"); + await page.goto(`file://${htmlPath}`); await page.waitForSelector(".tabulator"); });