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/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/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/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..8e65ac229 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); @@ -191,17 +192,14 @@ export default class GroupRows extends Module{ } virtualRenderFill(){ - 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; + const layout = this.layoutMode(); + if ( + layout === "fitDataFill" + || layout === "fitDataStretch" + || layout === "fitColumns" + ) { + this.table.rowManager.tableElement.style.minWidth = + this.table.columnManager.getWidth() + "px"; } } diff --git a/test/e2e/group-scroll.html b/test/e2e/group-scroll.html new file mode 100644 index 000000000..aed821ead --- /dev/null +++ b/test/e2e/group-scroll.html @@ -0,0 +1,55 @@ + + + + + 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..d6e5993d4 --- /dev/null +++ b/test/e2e/group-scroll.spec.js @@ -0,0 +1,90 @@ +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); + }); + + 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); + }); +}); 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); + }); + } +});